UNPKG

printmaker

Version:

Generate PDF documents and from JavaScript objects

200 lines 8.33 kB
import { parseEdges, parseLength } from './box.js'; import { parseColor } from './colors.js'; import { selectFont } from './fonts.js'; import { parseGraphics } from './graphics.js'; import { asArray, asBoolean, asNonNegNumber, asObject, asString, check, getFrom, isObject, optional, pickDefined, typeError, } from './types.js'; const defaultFontSize = 18; const defaultLineHeight = 1.2; export function parseContent(content, defaultStyle) { return content.map((block, idx) => check(block, `content[${idx}]`, () => parseBlock(asObject(block), defaultStyle))); } export function parseBlock(input, defaultAttrs) { if (input.columns) { return parseColumns(input, defaultAttrs); } if (input.rows) { return parseRows(input, defaultAttrs); } return parseParagraph(input, defaultAttrs); } export function parseColumns(input, defaultAttrs) { const mergedAttrs = Object.assign(Object.assign({}, defaultAttrs), parseInheritableAttrs(input)); const parseColumns = (columns) => asArray(columns).map((col, idx) => check(col, `[${idx}]`, () => parseBlock(col, mergedAttrs))); return pickDefined(Object.assign({ columns: getFrom(input, 'columns', parseColumns) }, parseBlockAttrs(input))); } export function parseRows(input, defaultAttrs) { const mergedAttrs = Object.assign(Object.assign({}, defaultAttrs), parseInheritableAttrs(input)); const parseRows = (rows) => asArray(rows).map((col, idx) => check(col, `[${idx}]`, () => parseBlock(col, mergedAttrs))); return pickDefined(Object.assign({ rows: getFrom(input, 'rows', parseRows) }, parseBlockAttrs(input))); } export function parseParagraph(input, defaultAttrs) { const mergedAttrs = Object.assign(Object.assign({}, defaultAttrs), input); const textAttrs = parseTextAttrs(mergedAttrs); const parseTextWithAttrs = (text) => parseText(text, textAttrs); return pickDefined(Object.assign({ text: getFrom(input, 'text', optional(parseTextWithAttrs)), image: getFrom(input, 'image', optional(asString)), graphics: getFrom(input, 'graphics', optional(parseGraphics)), padding: getFrom(input, 'padding', optional(parseEdges)), textAlign: getFrom(mergedAttrs, 'textAlign', optional(asTextAlign)) }, parseBlockAttrs(input))); } function parseBlockAttrs(input) { return pickDefined({ margin: getFrom(input, 'margin', optional(parseEdges)), width: getFrom(input, 'width', optional(parseLength)), height: getFrom(input, 'height', optional(parseLength)), id: getFrom(input, 'id', optional(asString)), }); } export function parseTextAttrs(input) { return pickDefined({ fontFamily: getFrom(input, 'fontFamily', optional(asString)), fontSize: getFrom(input, 'fontSize', optional(asNonNegNumber)), lineHeight: getFrom(input, 'lineHeight', optional(asNonNegNumber)), bold: getFrom(input, 'bold', optional(asBoolean)), italic: getFrom(input, 'italic', optional(asBoolean)), color: getFrom(input, 'color', optional(parseColor)), link: getFrom(input, 'link', optional(asString)), }); } export function parseInheritableAttrs(input) { return pickDefined(Object.assign(Object.assign({}, parseTextAttrs(input)), { textAlign: getFrom(input, 'textAlign', optional(asTextAlign)) })); } export function parseText(text, attrs) { if (Array.isArray(text)) { return text.flatMap((text) => parseText(text, attrs)); } if (typeof text === 'string') { return [{ text, attrs }]; } if (isObject(text) && 'text' in text) { return parseText(text.text, Object.assign(Object.assign({}, attrs), parseTextAttrs(text))); } throw typeError('string, object with text attribute, or array of text', text); } function asTextAlign(input) { if (input === 'left' || input === 'right' || input === 'center') return input; throw typeError("'left', 'right', or 'center'", input); } export function extractTextSegments(textSpans, fonts) { return textSpans.flatMap((span) => { const { text, attrs } = span; const { fontSize = defaultFontSize, lineHeight = defaultLineHeight, color, link } = attrs; const font = selectFont(fonts, attrs); const height = font.heightAtSize(fontSize); return splitChunks(text).map((text) => ({ text, width: font.widthOfTextAtSize(text, fontSize), height, lineHeight, font, fontSize, color, link, })); }); } /** * Split the given text into chunks of subsequent whitespace (`\s`) and non-whitespace (`\S`) * characters. For example, the string `foo bar` would be split into `['foo', ' ', 'bar']`. * Newlines (`\n`) are preserved, each in a chunk of its own. Any whitespace that surrounds * newlines is removed. * * @param text The input string * @returns an array of chunks */ export function splitChunks(text) { var _a; const segments = []; let tail = text; let match = /\s+/.exec(tail); while (match) { if (match.index) { segments.push(tail.slice(0, match.index)); } const wsSegment = tail.slice(match.index, match.index + match[0].length); const newlineCount = (_a = wsSegment.match(/\n/g)) === null || _a === void 0 ? void 0 : _a.length; if (newlineCount) { segments.push(...'\n'.repeat(newlineCount).split('')); } else { segments.push(wsSegment); } tail = tail.slice(match.index + match[0].length); match = /\s+/.exec(tail); } if (tail) { segments.push(tail); } return segments; } export function breakLine(segments, maxWidth) { const breakIdx = findLinebreak(segments, maxWidth); if (breakIdx >= 0) { const head = segments.slice(0, breakIdx); const tail = segments.slice(breakIdx + 1); return tail.length ? [head, tail] : [head]; } return [segments]; } function findLinebreak(segments, maxWidth) { let x = 0; for (const [idx, segment] of segments.entries()) { const { text, width } = segment; if (text === '\n') { return idx; } x += width; if (x > maxWidth) { return findLinebreakOpportunity(segments, idx); } } } /** * Finds the next appropriate segment that allows for a linebreak, starting at the given index, and * returns its index. * * @param segments A list of text segments. * @param index The index of the element to start at. * @returns The index of the next segment that allows for linebreak if found, `undefined` otherwise. */ export function findLinebreakOpportunity(segments, index) { // If the segment at the given index does not allow for a linebreak, look for a previous one. for (let i = index; i >= 0; i--) { if (isLineBreakOpportunity(segments[i])) { return i; } } // If no previous segment allows for a linebreak, find the next one after the index. for (let i = index + 1; i < segments.length; i++) { if (isLineBreakOpportunity(segments[i])) { return i; } } } function isLineBreakOpportunity(segment) { return segment && /^\s+$/.test(segment.text); } /** * Flatten a list of text segments by merging subsequent segments that have identical text * attributes. * * @param segments a list of text segments * @returns a possibly shorter list of text segments */ export function flattenTextSegments(segments) { const result = []; let prev; segments.forEach((segment) => { if (segment.font === (prev === null || prev === void 0 ? void 0 : prev.font) && segment.fontSize === (prev === null || prev === void 0 ? void 0 : prev.fontSize) && segment.lineHeight === (prev === null || prev === void 0 ? void 0 : prev.lineHeight) && segment.color === (prev === null || prev === void 0 ? void 0 : prev.color) && segment.link === (prev === null || prev === void 0 ? void 0 : prev.link)) { prev.text += segment.text; prev.width += segment.width; } else { prev = Object.assign({}, segment); result.push(prev); } }); return result; } //# sourceMappingURL=text.js.map