printmaker
Version:
Generate PDF documents and from JavaScript objects
200 lines • 8.33 kB
JavaScript
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