2977 lines
91 KiB
JavaScript
2977 lines
91 KiB
JavaScript
import { upperFirst, capitalize, parseFloat as parseFloat$1, without, pick, compose, evolve, mapValues, matchPercent, castArray, isNil, omit, asyncCompose } from '@react-pdf/fns';
|
||
import * as P from '@react-pdf/primitives';
|
||
import resolveStyle, { transformColor, flatten } from '@react-pdf/stylesheet';
|
||
import layoutEngine, { fontSubstitution, wordHyphenation, scriptItemizer, textDecoration, justification, linebreaker, bidi, fromFragments } from '@react-pdf/textkit';
|
||
import * as Yoga from 'yoga-layout/load';
|
||
import { loadYoga as loadYoga$1 } from 'yoga-layout/load';
|
||
import emojiRegex from 'emoji-regex';
|
||
import resolveImage from '@react-pdf/image';
|
||
|
||
/**
|
||
* Apply transformation to text string
|
||
*
|
||
* @param {string} text
|
||
* @param {string} transformation type
|
||
* @returns {string} transformed text
|
||
*/
|
||
const transformText = (text, transformation) => {
|
||
switch (transformation) {
|
||
case 'uppercase':
|
||
return text.toUpperCase();
|
||
case 'lowercase':
|
||
return text.toLowerCase();
|
||
case 'capitalize':
|
||
return capitalize(text);
|
||
case 'upperfirst':
|
||
return upperFirst(text);
|
||
default:
|
||
return text;
|
||
}
|
||
};
|
||
|
||
const isTspan = (node) => node.type === P.Tspan;
|
||
const isTextInstance$4 = (node) => node.type === P.TextInstance;
|
||
const engines$1 = {
|
||
bidi,
|
||
linebreaker,
|
||
justification,
|
||
textDecoration,
|
||
scriptItemizer,
|
||
wordHyphenation,
|
||
fontSubstitution,
|
||
};
|
||
const engine$1 = layoutEngine(engines$1);
|
||
const getFragments$1 = (fontStore, instance) => {
|
||
if (!instance)
|
||
return [{ string: '' }];
|
||
const fragments = [];
|
||
const { fill = 'black', fontFamily = 'Helvetica', fontWeight, fontStyle, fontSize = 18, textDecorationColor, textDecorationStyle, textTransform, opacity, } = instance.props;
|
||
const _textDecoration = instance.props.textDecoration;
|
||
const fontFamilies = typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])];
|
||
// Fallback font
|
||
fontFamilies.push('Helvetica');
|
||
const font = fontFamilies.map((fontFamilyName) => {
|
||
const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle };
|
||
const obj = fontStore.getFont(opts);
|
||
return obj?.data;
|
||
});
|
||
const attributes = {
|
||
font,
|
||
opacity,
|
||
fontSize,
|
||
color: fill,
|
||
underlineStyle: textDecorationStyle,
|
||
underline: _textDecoration === 'underline' ||
|
||
_textDecoration === 'underline line-through' ||
|
||
_textDecoration === 'line-through underline',
|
||
underlineColor: textDecorationColor || fill,
|
||
strike: _textDecoration === 'line-through' ||
|
||
_textDecoration === 'underline line-through' ||
|
||
_textDecoration === 'line-through underline',
|
||
strikeStyle: textDecorationStyle,
|
||
strikeColor: textDecorationColor || fill,
|
||
};
|
||
for (let i = 0; i < instance.children.length; i += 1) {
|
||
const child = instance.children[i];
|
||
if (isTextInstance$4(child)) {
|
||
fragments.push({
|
||
string: transformText(child.value, textTransform),
|
||
attributes,
|
||
});
|
||
}
|
||
else if (child) {
|
||
fragments.push(...getFragments$1(fontStore, child));
|
||
}
|
||
}
|
||
return fragments;
|
||
};
|
||
const getAttributedString$1 = (fontStore, instance) => fromFragments(getFragments$1(fontStore, instance));
|
||
const AlmostInfinity = 999999999999;
|
||
const shrinkWhitespaceFactor = { before: -0.5, after: -0.5 };
|
||
const layoutTspan = (fontStore) => (node, xOffset) => {
|
||
const attributedString = getAttributedString$1(fontStore, node);
|
||
const x = node.props.x === undefined ? xOffset : node.props.x;
|
||
const y = node.props?.y || 0;
|
||
const container = { x, y, width: AlmostInfinity, height: AlmostInfinity };
|
||
const hyphenationCallback = node.props.hyphenationCallback ||
|
||
fontStore?.getHyphenationCallback() ||
|
||
null;
|
||
const layoutOptions = { hyphenationCallback, shrinkWhitespaceFactor };
|
||
const lines = engine$1(attributedString, container, layoutOptions).flat();
|
||
return Object.assign({}, node, { lines });
|
||
};
|
||
// Consecutive TSpan elements should be joined with a space
|
||
const joinTSpanLines = (node) => {
|
||
const children = node.children.map((child, index) => {
|
||
if (!isTspan(child))
|
||
return child;
|
||
const textInstance = child.children[0];
|
||
if (child.props.x === undefined &&
|
||
index < node.children.length - 1 &&
|
||
textInstance?.value) {
|
||
return Object.assign({}, child, {
|
||
children: [{ ...textInstance, value: `${textInstance.value} ` }],
|
||
});
|
||
}
|
||
return child;
|
||
}, []);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
const layoutText$1 = (fontStore, node) => {
|
||
if (!node.children)
|
||
return node;
|
||
let currentXOffset = node.props?.x || 0;
|
||
const layoutFn = layoutTspan(fontStore);
|
||
const joinedNode = joinTSpanLines(node);
|
||
const children = joinedNode.children.map((child) => {
|
||
const childWithLayout = layoutFn(child, currentXOffset);
|
||
currentXOffset += childWithLayout.lines[0].xAdvance;
|
||
return childWithLayout;
|
||
});
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
|
||
const isDefs = (node) => node.type === P.Defs;
|
||
const getDefs = (node) => {
|
||
const children = node.children || [];
|
||
const defs = children.find(isDefs);
|
||
const values = defs?.children || [];
|
||
return values.reduce((acc, value) => {
|
||
const id = value.props?.id;
|
||
if (id)
|
||
acc[id] = value;
|
||
return acc;
|
||
}, {});
|
||
};
|
||
|
||
const isNotDefs = (node) => node.type !== P.Defs;
|
||
const detachDefs = (node) => {
|
||
if (!node.children)
|
||
return node;
|
||
const children = node.children.filter(isNotDefs);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
const URL_REGEX = /url\(['"]?#([^'"]+)['"]?\)/;
|
||
const replaceDef = (defs, value) => {
|
||
if (!value)
|
||
return undefined;
|
||
if (!URL_REGEX.test(value))
|
||
return value;
|
||
const match = value.match(URL_REGEX);
|
||
return defs[match[1]];
|
||
};
|
||
const parseNodeDefs = (defs) => (node) => {
|
||
const props = node.props;
|
||
const fill = `fill` in props ? replaceDef(defs, props?.fill) : undefined;
|
||
const clipPath = `clipPath` in props
|
||
? replaceDef(defs, props?.clipPath)
|
||
: undefined;
|
||
const newProps = Object.assign({}, node.props, { fill, clipPath });
|
||
const children = node.children
|
||
? node.children.map(parseNodeDefs(defs))
|
||
: undefined;
|
||
return Object.assign({}, node, { props: newProps, children });
|
||
};
|
||
const parseDefs = (root) => {
|
||
if (!root.children)
|
||
return root;
|
||
const defs = getDefs(root);
|
||
const children = root.children.map(parseNodeDefs(defs));
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
const replaceDefs = (node) => {
|
||
return detachDefs(parseDefs(node));
|
||
};
|
||
|
||
const parseViewbox = (value) => {
|
||
if (!value)
|
||
return null;
|
||
if (typeof value !== 'string')
|
||
return value;
|
||
const values = value.split(/[,\s]+/).map(parseFloat$1);
|
||
if (values.length !== 4)
|
||
return null;
|
||
return { minX: values[0], minY: values[1], maxX: values[2], maxY: values[3] };
|
||
};
|
||
|
||
const getContainer$1 = (node) => {
|
||
const viewbox = parseViewbox(node.props.viewBox);
|
||
if (viewbox) {
|
||
return { width: viewbox.maxX, height: viewbox.maxY };
|
||
}
|
||
if (node.props.width && node.props.height) {
|
||
return {
|
||
width: parseFloat$1(node.props.width),
|
||
height: parseFloat$1(node.props.height),
|
||
};
|
||
}
|
||
return { width: 0, height: 0 };
|
||
};
|
||
|
||
const BASE_SVG_INHERITED_PROPS = [
|
||
'x',
|
||
'y',
|
||
'clipPath',
|
||
'clipRule',
|
||
'opacity',
|
||
'fill',
|
||
'fillOpacity',
|
||
'fillRule',
|
||
'stroke',
|
||
'strokeLinecap',
|
||
'strokeLinejoin',
|
||
'strokeOpacity',
|
||
'strokeWidth',
|
||
'textAnchor',
|
||
'dominantBaseline',
|
||
'color',
|
||
'fontFamily',
|
||
'fontSize',
|
||
'fontStyle',
|
||
'fontWeight',
|
||
'letterSpacing',
|
||
'opacity',
|
||
'textDecoration',
|
||
'lineHeight',
|
||
'textAlign',
|
||
'visibility',
|
||
'wordSpacing',
|
||
];
|
||
// Do not inherit "x" for <tspan> elements from <text> parent
|
||
const TEXT_SVG_INHERITED_PROPS = without(['x'], BASE_SVG_INHERITED_PROPS);
|
||
const SVG_INHERITED_PROPS = {
|
||
[P.Text]: TEXT_SVG_INHERITED_PROPS,
|
||
};
|
||
const getInheritProps = (node) => {
|
||
const props = node.props || {};
|
||
const svgInheritedProps = SVG_INHERITED_PROPS[node.type] ?? BASE_SVG_INHERITED_PROPS;
|
||
return pick(svgInheritedProps, props);
|
||
};
|
||
const inheritProps = (node) => {
|
||
if (!node.children)
|
||
return node;
|
||
const inheritedProps = getInheritProps(node);
|
||
const children = node.children.map((child) => {
|
||
const props = Object.assign({}, inheritedProps, child.props || {});
|
||
const newChild = Object.assign({}, child, { props });
|
||
return inheritProps(newChild);
|
||
});
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
|
||
const parseAspectRatio = (value) => {
|
||
if (typeof value !== 'string')
|
||
return value;
|
||
const match = value
|
||
.replace(/[\s\r\t\n]+/gm, ' ')
|
||
.replace(/^defer\s/, '')
|
||
.split(' ');
|
||
const align = (match[0] || 'xMidYMid');
|
||
const meetOrSlice = (match[1] ||
|
||
'meet');
|
||
return { align, meetOrSlice };
|
||
};
|
||
|
||
const STYLE_PROPS = [
|
||
'width',
|
||
'height',
|
||
'color',
|
||
'stroke',
|
||
'strokeWidth',
|
||
'opacity',
|
||
'fillOpacity',
|
||
'strokeOpacity',
|
||
'fill',
|
||
'fillRule',
|
||
'clipPath',
|
||
'offset',
|
||
'transform',
|
||
'strokeLinejoin',
|
||
'strokeLinecap',
|
||
'strokeDasharray',
|
||
'gradientUnits',
|
||
'gradientTransform',
|
||
];
|
||
const VERTICAL_PROPS = ['y', 'y1', 'y2', 'height', 'cy', 'ry'];
|
||
const HORIZONTAL_PROPS = ['x', 'x1', 'x2', 'width', 'cx', 'rx'];
|
||
const isSvg$3 = (node) => node.type === P.Svg;
|
||
const isText$5 = (node) => node.type === P.Text;
|
||
const isTextInstance$3 = (node) => node.type === P.TextInstance;
|
||
const transformPercent = (container) => (props) => mapValues(props, (value, key) => {
|
||
const match = matchPercent(value);
|
||
if (match && VERTICAL_PROPS.includes(key)) {
|
||
return match.percent * container.height;
|
||
}
|
||
if (match && HORIZONTAL_PROPS.includes(key)) {
|
||
return match.percent * container.width;
|
||
}
|
||
return value;
|
||
});
|
||
const parsePercent = (value) => {
|
||
const match = matchPercent(value);
|
||
return match ? match.percent : parseFloat$1(value);
|
||
};
|
||
const parseTransform = (container) => (value) => {
|
||
return resolveStyle(container, { transform: value }).transform;
|
||
};
|
||
const parseProps = (container) => (node) => {
|
||
let props = transformPercent(container)(node.props);
|
||
props = evolve({
|
||
x: parseFloat$1,
|
||
x1: parseFloat$1,
|
||
x2: parseFloat$1,
|
||
y: parseFloat$1,
|
||
y1: parseFloat$1,
|
||
y2: parseFloat$1,
|
||
r: parseFloat$1,
|
||
rx: parseFloat$1,
|
||
ry: parseFloat$1,
|
||
cx: parseFloat$1,
|
||
cy: parseFloat$1,
|
||
width: parseFloat$1,
|
||
height: parseFloat$1,
|
||
offset: parsePercent,
|
||
fill: transformColor,
|
||
opacity: parsePercent,
|
||
stroke: transformColor,
|
||
stopOpacity: parsePercent,
|
||
stopColor: transformColor,
|
||
transform: parseTransform(container),
|
||
gradientTransform: parseTransform(container),
|
||
}, props);
|
||
return Object.assign({}, node, { props });
|
||
};
|
||
const mergeStyles$1 = (node) => {
|
||
const style = node.style || {};
|
||
const props = Object.assign({}, style, node.props);
|
||
return Object.assign({}, node, { props });
|
||
};
|
||
const removeNoneValues = (node) => {
|
||
const removeNone = (value) => (value === 'none' ? null : value);
|
||
const props = mapValues(node.props, removeNone);
|
||
return Object.assign({}, node, { props });
|
||
};
|
||
const pickStyleProps = (node) => {
|
||
const props = node.props || {};
|
||
const styleProps = pick(STYLE_PROPS, props);
|
||
const style = Object.assign({}, styleProps, node.style || {});
|
||
return Object.assign({}, node, { style });
|
||
};
|
||
const parseSvgProps = (node) => {
|
||
const props = evolve({
|
||
width: parseFloat$1,
|
||
height: parseFloat$1,
|
||
viewBox: parseViewbox,
|
||
preserveAspectRatio: parseAspectRatio,
|
||
}, node.props);
|
||
return Object.assign({}, node, { props });
|
||
};
|
||
const wrapBetweenTspan = (node) => ({
|
||
type: P.Tspan,
|
||
props: {},
|
||
style: {},
|
||
children: [node],
|
||
});
|
||
const addMissingTspan = (node) => {
|
||
if (!isText$5(node))
|
||
return node;
|
||
if (!node.children)
|
||
return node;
|
||
const resolveChild = (child) => isTextInstance$3(child) ? wrapBetweenTspan(child) : child;
|
||
const children = node.children.map(resolveChild);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
const parseText = (fontStore) => (node) => {
|
||
if (isText$5(node))
|
||
return layoutText$1(fontStore, node);
|
||
if (!node.children)
|
||
return node;
|
||
const children = node.children.map(parseText(fontStore));
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
const resolveSvgNode = (container) => compose(parseProps(container), addMissingTspan, removeNoneValues, mergeStyles$1);
|
||
const resolveChildren = (container) => (node) => {
|
||
if (!node.children)
|
||
return node;
|
||
const resolveChild = compose(resolveChildren(container), resolveSvgNode(container));
|
||
const children = node.children.map(resolveChild);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
const buildXLinksIndex = (node) => {
|
||
const idIndex = {};
|
||
const listToExplore = node.children?.slice(0) || [];
|
||
while (listToExplore.length > 0) {
|
||
const child = listToExplore.shift();
|
||
if (child.props && 'id' in child.props) {
|
||
idIndex[child.props.id] = child;
|
||
}
|
||
if (child.children)
|
||
listToExplore.push(...child.children);
|
||
}
|
||
return idIndex;
|
||
};
|
||
const replaceXLinks = (node, idIndex) => {
|
||
if (node.props && 'xlinkHref' in node.props) {
|
||
const linkedNode = idIndex[node.props.xlinkHref.replace(/^#/, '')];
|
||
// No node to extend from
|
||
if (!linkedNode)
|
||
return node;
|
||
const newProps = Object.assign({}, linkedNode.props, node.props);
|
||
delete newProps.xlinkHref;
|
||
return Object.assign({}, linkedNode, { props: newProps });
|
||
}
|
||
const children = node.children?.map((child) => replaceXLinks(child, idIndex));
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
const resolveXLinks = (node) => {
|
||
const idIndex = buildXLinksIndex(node);
|
||
return replaceXLinks(node, idIndex);
|
||
};
|
||
const resolveSvgRoot = (node, fontStore) => {
|
||
const container = getContainer$1(node);
|
||
return compose(replaceDefs, parseText(fontStore), parseSvgProps, pickStyleProps, inheritProps, resolveChildren(container), resolveXLinks)(node);
|
||
};
|
||
/**
|
||
* Pre-process SVG nodes so they can be rendered in the next steps
|
||
*
|
||
* @param node - Root node
|
||
* @param fontStore - Font store
|
||
* @returns Root node
|
||
*/
|
||
const resolveSvg = (node, fontStore) => {
|
||
if (!('children' in node))
|
||
return node;
|
||
const resolveChild = (child) => resolveSvg(child, fontStore);
|
||
const root = isSvg$3(node) ? resolveSvgRoot(node, fontStore) : node;
|
||
const children = root.children?.map(resolveChild);
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
|
||
let instancePromise;
|
||
const loadYoga = async () => {
|
||
// Yoga WASM binaries must be asynchronously compiled and loaded
|
||
// to prevent Event emitter memory leak warnings, Yoga must be loaded only once
|
||
const instance = await (instancePromise ??= loadYoga$1());
|
||
const config = instance.Config.create();
|
||
config.setPointScaleFactor(0);
|
||
const node = { create: () => instance.Node.createWithConfig(config) };
|
||
return { node };
|
||
};
|
||
|
||
const resolveYoga = async (root) => {
|
||
const yoga = await loadYoga();
|
||
return Object.assign({}, root, { yoga });
|
||
};
|
||
|
||
const getZIndex = (node) => node.style.zIndex;
|
||
const shouldSort = (node) => node.type !== P.Document && node.type !== P.Svg;
|
||
const sortZIndex = (a, b) => {
|
||
const za = getZIndex(a);
|
||
const zb = getZIndex(b);
|
||
if (!za && !zb)
|
||
return 0;
|
||
if (!za)
|
||
return 1;
|
||
if (!zb)
|
||
return -1;
|
||
return zb - za;
|
||
};
|
||
/**
|
||
* Sort children by zIndex value
|
||
*
|
||
* @param node
|
||
* @returns Node
|
||
*/
|
||
const resolveNodeZIndex = (node) => {
|
||
if (!node.children)
|
||
return node;
|
||
const sortedChildren = shouldSort(node)
|
||
? node.children.sort(sortZIndex)
|
||
: node.children;
|
||
const children = sortedChildren.map(resolveNodeZIndex);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
/**
|
||
* Sort children by zIndex value
|
||
*
|
||
* @param node
|
||
* @returns Node
|
||
*/
|
||
const resolveZIndex = (root) => resolveNodeZIndex(root);
|
||
|
||
// Caches emoji images data
|
||
const emojis = {};
|
||
const regex = emojiRegex();
|
||
/**
|
||
* When an emoji as no variations, it might still have 2 parts,
|
||
* the canonical emoji and an empty string.
|
||
* ex.
|
||
* (no color) Array.from('❤️') => ["❤", "️"]
|
||
* (w/ color) Array.from('👍🏿') => ["👍", "🏿"]
|
||
*
|
||
* The empty string needs to be removed otherwise the generated
|
||
* url will be incorect.
|
||
*/
|
||
const removeVariationSelectors = (x) => x !== '️';
|
||
const getCodePoints = (string, withVariationSelectors = false) => Array.from(string)
|
||
.filter(withVariationSelectors ? () => true : removeVariationSelectors)
|
||
.map((char) => char.codePointAt(0).toString(16))
|
||
.join('-');
|
||
const buildEmojiUrl = (emoji, source) => {
|
||
if ('builder' in source) {
|
||
return source.builder(getCodePoints(emoji, source.withVariationSelectors));
|
||
}
|
||
const { url, format = 'png', withVariationSelectors } = source;
|
||
return `${url}${getCodePoints(emoji, withVariationSelectors)}.${format}`;
|
||
};
|
||
const fetchEmojis = (string, source) => {
|
||
if (!source)
|
||
return [];
|
||
const promises = [];
|
||
Array.from(string.matchAll(regex)).forEach((match) => {
|
||
const emoji = match[0];
|
||
if (!emojis[emoji] || emojis[emoji].loading) {
|
||
const emojiUrl = buildEmojiUrl(emoji, source);
|
||
emojis[emoji] = { loading: true };
|
||
promises.push(resolveImage({ uri: emojiUrl }).then((image) => {
|
||
emojis[emoji].loading = false;
|
||
emojis[emoji].data = image.data;
|
||
}));
|
||
}
|
||
});
|
||
return promises;
|
||
};
|
||
const embedEmojis = (fragments) => {
|
||
const result = [];
|
||
for (let i = 0; i < fragments.length; i += 1) {
|
||
const fragment = fragments[i];
|
||
let lastIndex = 0;
|
||
Array.from(fragment.string.matchAll(regex)).forEach((match) => {
|
||
const { index } = match;
|
||
const emoji = match[0];
|
||
const emojiSize = fragment.attributes.fontSize;
|
||
const chunk = fragment.string.slice(lastIndex, index + match[0].length);
|
||
// If emoji image was found, we create a new fragment with the
|
||
// correct attachment and object substitution character;
|
||
if (emojis[emoji] && emojis[emoji].data) {
|
||
result.push({
|
||
string: chunk.replace(match[0], String.fromCharCode(0xfffc)),
|
||
attributes: {
|
||
...fragment.attributes,
|
||
attachment: {
|
||
width: emojiSize,
|
||
height: emojiSize,
|
||
yOffset: Math.floor(emojiSize * 0.1),
|
||
image: emojis[emoji].data,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
else {
|
||
// If no emoji data, we try to use emojis in the font
|
||
result.push({
|
||
string: chunk,
|
||
attributes: fragment.attributes,
|
||
});
|
||
}
|
||
lastIndex = index + emoji.length;
|
||
});
|
||
if (lastIndex < fragment.string.length) {
|
||
result.push({
|
||
string: fragment.string.slice(lastIndex),
|
||
attributes: fragment.attributes,
|
||
});
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* Get image source
|
||
*
|
||
* @param node - Image node
|
||
* @returns Image src
|
||
*/
|
||
const getSource = (node) => {
|
||
if (node.props.src)
|
||
return node.props.src;
|
||
if (node.props.source)
|
||
return node.props.source;
|
||
};
|
||
|
||
/**
|
||
* Resolves `src` to `@react-pdf/image` interface.
|
||
*
|
||
* Also it handles factories and async sources.
|
||
*
|
||
* @param src
|
||
* @returns Resolved src
|
||
*/
|
||
const resolveSource = async (src) => {
|
||
const source = typeof src === 'function' ? await src() : await src;
|
||
return typeof source === 'string' ? { uri: source } : source;
|
||
};
|
||
|
||
/**
|
||
* Fetches image and append data to node
|
||
* Ideally this fn should be immutable.
|
||
*
|
||
* @param node
|
||
*/
|
||
const fetchImage = async (node) => {
|
||
const src = getSource(node);
|
||
const { cache } = node.props;
|
||
if (!src) {
|
||
console.warn(false, 'Image should receive either a "src" or "source" prop');
|
||
return;
|
||
}
|
||
try {
|
||
const source = await resolveSource(src);
|
||
if (!source) {
|
||
throw new Error(`Image's "src" or "source" prop returned ${source}`);
|
||
}
|
||
node.image = await resolveImage(source, { cache });
|
||
if (Buffer.isBuffer(source) || source instanceof Blob)
|
||
return;
|
||
node.image.key = 'data' in source ? source.data.toString() : source.uri;
|
||
}
|
||
catch (e) {
|
||
console.warn(e.message);
|
||
}
|
||
};
|
||
|
||
const isImage$2 = (node) => node.type === P.Image;
|
||
/**
|
||
* Get all asset promises that need to be resolved
|
||
*
|
||
* @param fontStore - Font store
|
||
* @param node - Root node
|
||
* @returns Asset promises
|
||
*/
|
||
const fetchAssets = (fontStore, node) => {
|
||
const promises = [];
|
||
const listToExplore = node.children?.slice(0) || [];
|
||
const emojiSource = fontStore ? fontStore.getEmojiSource() : null;
|
||
while (listToExplore.length > 0) {
|
||
const n = listToExplore.shift();
|
||
if (isImage$2(n)) {
|
||
promises.push(fetchImage(n));
|
||
}
|
||
if (fontStore && n.style?.fontFamily) {
|
||
const fontFamilies = castArray(n.style.fontFamily);
|
||
promises.push(...fontFamilies.map((fontFamily) => fontStore.load({
|
||
fontFamily,
|
||
fontStyle: n.style.fontStyle,
|
||
fontWeight: n.style.fontWeight,
|
||
})));
|
||
}
|
||
if (typeof n === 'string') {
|
||
promises.push(...fetchEmojis(n, emojiSource));
|
||
}
|
||
if ('value' in n && typeof n.value === 'string') {
|
||
promises.push(...fetchEmojis(n.value, emojiSource));
|
||
}
|
||
if (n.children) {
|
||
n.children.forEach((childNode) => {
|
||
listToExplore.push(childNode);
|
||
});
|
||
}
|
||
}
|
||
return promises;
|
||
};
|
||
/**
|
||
* Fetch image, font and emoji assets in parallel.
|
||
* Layout process will not be resumed until promise resolves.
|
||
*
|
||
* @param node root node
|
||
* @param fontStore font store
|
||
* @returns Root node
|
||
*/
|
||
const resolveAssets = async (node, fontStore) => {
|
||
const promises = fetchAssets(fontStore, node);
|
||
await Promise.all(promises);
|
||
return node;
|
||
};
|
||
|
||
const isLink$1 = (node) => node.type === P.Link;
|
||
const DEFAULT_LINK_STYLES = {
|
||
color: 'blue',
|
||
textDecoration: 'underline',
|
||
};
|
||
/**
|
||
* Computes styles using stylesheet
|
||
*
|
||
* @param container
|
||
* @param node - Document node
|
||
* @returns Computed styles
|
||
*/
|
||
const computeStyle = (container, node) => {
|
||
let baseStyle = [node.style];
|
||
if (isLink$1(node)) {
|
||
baseStyle = Array.isArray(node.style)
|
||
? [DEFAULT_LINK_STYLES, ...node.style]
|
||
: [DEFAULT_LINK_STYLES, node.style];
|
||
}
|
||
return resolveStyle(container, baseStyle);
|
||
};
|
||
/**
|
||
* Resolves node styles
|
||
*
|
||
* @param container
|
||
* @returns Resolve node styles
|
||
*/
|
||
const resolveNodeStyles = (container) => (node) => {
|
||
const style = computeStyle(container, node);
|
||
if (!node.children)
|
||
return Object.assign({}, node, { style });
|
||
const children = node.children.map(resolveNodeStyles(container));
|
||
return Object.assign({}, node, { style, children });
|
||
};
|
||
/**
|
||
* Resolves page styles
|
||
*
|
||
* @param page Document page
|
||
* @returns Document page with resolved styles
|
||
*/
|
||
const resolvePageStyles = (page) => {
|
||
const dpi = page.props?.dpi || 72;
|
||
const style = page.style;
|
||
const width = page.box?.width || style.width;
|
||
const height = page.box?.height || style.height;
|
||
const orientation = page.props?.orientation || 'portrait';
|
||
const remBase = style?.fontSize || 18;
|
||
const container = { width, height, orientation, dpi, remBase };
|
||
return resolveNodeStyles(container)(page);
|
||
};
|
||
/**
|
||
* Resolves document styles
|
||
*
|
||
* @param root - Document root
|
||
* @returns Document root with resolved styles
|
||
*/
|
||
const resolveStyles = (root) => {
|
||
if (!root.children)
|
||
return root;
|
||
const children = root.children.map(resolvePageStyles);
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
|
||
const getTransformStyle = (s) => (node) => isNil(node.style?.[s]) ? '50%' : node.style?.[s] ?? null;
|
||
/**
|
||
* Get node origin
|
||
*
|
||
* @param node
|
||
* @returns {{ left?: number, top?: number }} node origin
|
||
*/
|
||
const getOrigin = (node) => {
|
||
if (!node.box)
|
||
return null;
|
||
const { left, top, width, height } = node.box;
|
||
const transformOriginX = getTransformStyle('transformOriginX')(node);
|
||
const transformOriginY = getTransformStyle('transformOriginY')(node);
|
||
const percentX = matchPercent(transformOriginX);
|
||
const percentY = matchPercent(transformOriginY);
|
||
const offsetX = percentX ? width * percentX.percent : transformOriginX;
|
||
const offsetY = percentY ? height * percentY.percent : transformOriginY;
|
||
if (isNil(offsetX) || typeof offsetX === 'string')
|
||
throw new Error(`Invalid origin offsetX: ${offsetX}`);
|
||
if (isNil(offsetY) || typeof offsetY === 'string')
|
||
throw new Error(`Invalid origin offsetY: ${offsetY}`);
|
||
return { left: left + offsetX, top: top + offsetY };
|
||
};
|
||
|
||
/**
|
||
* Resolve node origin
|
||
*
|
||
* @param node
|
||
* @returns Node with origin attribute
|
||
*/
|
||
const resolveNodeOrigin = (node) => {
|
||
const origin = getOrigin(node);
|
||
const newNode = Object.assign({}, node, { origin });
|
||
if (!node.children)
|
||
return newNode;
|
||
const children = node.children.map(resolveNodeOrigin);
|
||
return Object.assign({}, newNode, { children });
|
||
};
|
||
/**
|
||
* Resolve document origins
|
||
*
|
||
* @param root - Document root
|
||
* @returns Document root
|
||
*/
|
||
const resolveOrigin = (root) => {
|
||
if (!root.children)
|
||
return root;
|
||
const children = root.children.map(resolveNodeOrigin);
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
|
||
const getBookmarkValue = (bookmark) => {
|
||
return typeof bookmark === 'string'
|
||
? { title: bookmark, fit: false, expanded: false }
|
||
: bookmark;
|
||
};
|
||
const resolveBookmarks = (node) => {
|
||
let refs = 0;
|
||
const children = (node.children || []).slice(0);
|
||
const listToExplore = children.map((value) => ({
|
||
value,
|
||
parent: null,
|
||
}));
|
||
while (listToExplore.length > 0) {
|
||
const element = listToExplore.shift();
|
||
if (!element)
|
||
break;
|
||
const child = element.value;
|
||
let parent = element.parent;
|
||
if (child.props && 'bookmark' in child.props) {
|
||
const bookmark = getBookmarkValue(child.props.bookmark);
|
||
const ref = refs++;
|
||
const newHierarchy = { ref, parent: parent?.ref, ...bookmark };
|
||
child.props.bookmark = newHierarchy;
|
||
parent = newHierarchy;
|
||
}
|
||
if (child.children) {
|
||
child.children.forEach((childNode) => {
|
||
listToExplore.push({ value: childNode, parent });
|
||
});
|
||
}
|
||
}
|
||
return node;
|
||
};
|
||
|
||
const VALID_ORIENTATIONS = ['portrait', 'landscape'];
|
||
/**
|
||
* Get page orientation. Defaults to portrait
|
||
*
|
||
* @param page - Page object
|
||
* @returns Page orientation
|
||
*/
|
||
const getOrientation = (page) => {
|
||
const value = page.props?.orientation || 'portrait';
|
||
return VALID_ORIENTATIONS.includes(value) ? value : 'portrait';
|
||
};
|
||
|
||
/**
|
||
* Return true if page is landscape
|
||
*
|
||
* @param page - Page instance
|
||
* @returns Is page landscape
|
||
*/
|
||
const isLandscape = (page) => getOrientation(page) === 'landscape';
|
||
|
||
// Page sizes for 72dpi. 72dpi is used internally by pdfkit.
|
||
const PAGE_SIZES = {
|
||
'4A0': [4767.87, 6740.79],
|
||
'2A0': [3370.39, 4767.87],
|
||
A0: [2383.94, 3370.39],
|
||
A1: [1683.78, 2383.94],
|
||
A2: [1190.55, 1683.78],
|
||
A3: [841.89, 1190.55],
|
||
A4: [595.28, 841.89],
|
||
A5: [419.53, 595.28],
|
||
A6: [297.64, 419.53],
|
||
A7: [209.76, 297.64],
|
||
A8: [147.4, 209.76],
|
||
A9: [104.88, 147.4],
|
||
A10: [73.7, 104.88],
|
||
B0: [2834.65, 4008.19],
|
||
B1: [2004.09, 2834.65],
|
||
B2: [1417.32, 2004.09],
|
||
B3: [1000.63, 1417.32],
|
||
B4: [708.66, 1000.63],
|
||
B5: [498.9, 708.66],
|
||
B6: [354.33, 498.9],
|
||
B7: [249.45, 354.33],
|
||
B8: [175.75, 249.45],
|
||
B9: [124.72, 175.75],
|
||
B10: [87.87, 124.72],
|
||
C0: [2599.37, 3676.54],
|
||
C1: [1836.85, 2599.37],
|
||
C2: [1298.27, 1836.85],
|
||
C3: [918.43, 1298.27],
|
||
C4: [649.13, 918.43],
|
||
C5: [459.21, 649.13],
|
||
C6: [323.15, 459.21],
|
||
C7: [229.61, 323.15],
|
||
C8: [161.57, 229.61],
|
||
C9: [113.39, 161.57],
|
||
C10: [79.37, 113.39],
|
||
RA0: [2437.8, 3458.27],
|
||
RA1: [1729.13, 2437.8],
|
||
RA2: [1218.9, 1729.13],
|
||
RA3: [864.57, 1218.9],
|
||
RA4: [609.45, 864.57],
|
||
SRA0: [2551.18, 3628.35],
|
||
SRA1: [1814.17, 2551.18],
|
||
SRA2: [1275.59, 1814.17],
|
||
SRA3: [907.09, 1275.59],
|
||
SRA4: [637.8, 907.09],
|
||
EXECUTIVE: [521.86, 756.0],
|
||
FOLIO: [612.0, 936.0],
|
||
LEGAL: [612.0, 1008.0],
|
||
LETTER: [612.0, 792.0],
|
||
TABLOID: [792.0, 1224.0],
|
||
ID1: [153, 243],
|
||
};
|
||
/**
|
||
* Parses scalar value in value and unit pairs
|
||
*
|
||
* @param value - Scalar value
|
||
* @returns Parsed value
|
||
*/
|
||
const parseValue = (value) => {
|
||
if (typeof value === 'number')
|
||
return { value, unit: undefined };
|
||
const match = /^(-?\d*\.?\d+)(in|mm|cm|pt|px)?$/g.exec(value);
|
||
return match
|
||
? { value: parseFloat(match[1]), unit: match[2] || 'pt' }
|
||
: { value, unit: undefined };
|
||
};
|
||
/**
|
||
* Transform given scalar value to 72dpi equivalent of size
|
||
*
|
||
* @param value - Styles value
|
||
* @param inputDpi - User defined dpi
|
||
* @returns Transformed value
|
||
*/
|
||
const transformUnit = (value, inputDpi) => {
|
||
if (!value)
|
||
return 0;
|
||
const scalar = parseValue(value);
|
||
const outputDpi = 72;
|
||
const mmFactor = (1 / 25.4) * outputDpi;
|
||
const cmFactor = (1 / 2.54) * outputDpi;
|
||
if (typeof scalar.value === 'string')
|
||
throw new Error(`Invalid page size: ${value}`);
|
||
switch (scalar.unit) {
|
||
case 'in':
|
||
return scalar.value * outputDpi;
|
||
case 'mm':
|
||
return scalar.value * mmFactor;
|
||
case 'cm':
|
||
return scalar.value * cmFactor;
|
||
case 'px':
|
||
return Math.round(scalar.value * (outputDpi / inputDpi));
|
||
default:
|
||
return scalar.value;
|
||
}
|
||
};
|
||
const transformUnits = ({ width, height }, dpi) => ({
|
||
width: transformUnit(width, dpi),
|
||
height: transformUnit(height, dpi),
|
||
});
|
||
/**
|
||
* Transforms array into size object
|
||
*
|
||
* @param v - Values array
|
||
* @returns Size object with width and height
|
||
*/
|
||
const toSizeObject = (v) => ({
|
||
width: v[0],
|
||
height: v[1],
|
||
});
|
||
/**
|
||
* Flip size object
|
||
*
|
||
* @param v - Size object
|
||
* @returns Flipped size object
|
||
*/
|
||
const flipSizeObject = (v) => ({
|
||
width: v.height,
|
||
height: v.width,
|
||
});
|
||
/**
|
||
* Returns size object from a given string
|
||
*
|
||
* @param v - Page size string
|
||
* @returns Size object with width and height
|
||
*/
|
||
const getStringSize = (v) => {
|
||
return toSizeObject(PAGE_SIZES[v.toUpperCase()]);
|
||
};
|
||
/**
|
||
* Returns size object from a single number
|
||
*
|
||
* @param n - Page size number
|
||
* @returns Size object with width and height
|
||
*/
|
||
const getNumberSize = (n) => toSizeObject([n, n]);
|
||
/**
|
||
* Return page size in an object { width, height }
|
||
*
|
||
* @param page - Page node
|
||
* @returns Size object with width and height
|
||
*/
|
||
const getSize = (page) => {
|
||
const value = page.props?.size || 'A4';
|
||
const dpi = page.props?.dpi || 72;
|
||
let size;
|
||
if (typeof value === 'string') {
|
||
size = getStringSize(value);
|
||
}
|
||
else if (Array.isArray(value)) {
|
||
size = transformUnits(toSizeObject(value), dpi);
|
||
}
|
||
else if (typeof value === 'number') {
|
||
size = transformUnits(getNumberSize(value), dpi);
|
||
}
|
||
else {
|
||
size = transformUnits(value, dpi);
|
||
}
|
||
return isLandscape(page) ? flipSizeObject(size) : size;
|
||
};
|
||
|
||
/**
|
||
* Resolves page size
|
||
*
|
||
* @param page
|
||
* @returns Page with resolved size in style attribute
|
||
*/
|
||
const resolvePageSize = (page) => {
|
||
const size = getSize(page);
|
||
const style = flatten(page.style || {});
|
||
return { ...page, style: { ...style, ...size } };
|
||
};
|
||
/**
|
||
* Resolves page sizes
|
||
*
|
||
* @param root -Document root
|
||
* @returns Document root with resolved page sizes
|
||
*/
|
||
const resolvePageSizes = (root) => {
|
||
if (!root.children)
|
||
return root;
|
||
const children = root.children.map(resolvePageSize);
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
|
||
const isFixed = (node) => {
|
||
if (!node.props)
|
||
return false;
|
||
return 'fixed' in node.props ? node.props.fixed === true : false;
|
||
};
|
||
|
||
/**
|
||
* Get line index at given height
|
||
*
|
||
* @param node
|
||
* @param height
|
||
*/
|
||
const lineIndexAtHeight = (node, height) => {
|
||
let y = 0;
|
||
if (!node.lines)
|
||
return 0;
|
||
for (let i = 0; i < node.lines.length; i += 1) {
|
||
const line = node.lines[i];
|
||
if (y + line.box.height > height)
|
||
return i;
|
||
y += line.box.height;
|
||
}
|
||
return node.lines.length;
|
||
};
|
||
|
||
/**
|
||
* Get height for given text line index
|
||
*
|
||
* @param node
|
||
* @param index
|
||
*/
|
||
const heightAtLineIndex = (node, index) => {
|
||
let counter = 0;
|
||
if (!node.lines)
|
||
return counter;
|
||
for (let i = 0; i < index; i += 1) {
|
||
const line = node.lines[i];
|
||
if (!line)
|
||
break;
|
||
counter += line.box.height;
|
||
}
|
||
return counter;
|
||
};
|
||
|
||
const getLineBreak = (node, height) => {
|
||
const top = node.box?.top || 0;
|
||
const widows = node.props.widows || 2;
|
||
const orphans = node.props.orphans || 2;
|
||
const linesQuantity = node.lines.length;
|
||
const slicedLine = lineIndexAtHeight(node, height - top);
|
||
if (slicedLine === 0) {
|
||
return 0;
|
||
}
|
||
if (linesQuantity < orphans) {
|
||
return linesQuantity;
|
||
}
|
||
if (slicedLine < orphans || linesQuantity < orphans + widows) {
|
||
return 0;
|
||
}
|
||
if (linesQuantity === orphans + widows) {
|
||
return orphans;
|
||
}
|
||
if (linesQuantity - slicedLine < widows) {
|
||
return linesQuantity - widows;
|
||
}
|
||
return slicedLine;
|
||
};
|
||
// Also receives contentArea in case it's needed
|
||
const splitText = (node, height) => {
|
||
const slicedLineIndex = getLineBreak(node, height);
|
||
const currentHeight = heightAtLineIndex(node, slicedLineIndex);
|
||
const nextHeight = node.box.height - currentHeight;
|
||
const current = Object.assign({}, node, {
|
||
box: {
|
||
...node.box,
|
||
height: currentHeight,
|
||
borderBottomWidth: 0,
|
||
},
|
||
style: {
|
||
...node.style,
|
||
marginBottom: 0,
|
||
paddingBottom: 0,
|
||
borderBottomWidth: 0,
|
||
borderBottomLeftRadius: 0,
|
||
borderBottomRightRadius: 0,
|
||
},
|
||
lines: node.lines.slice(0, slicedLineIndex),
|
||
});
|
||
const next = Object.assign({}, node, {
|
||
box: {
|
||
...node.box,
|
||
top: 0,
|
||
height: nextHeight,
|
||
borderTopWidth: 0,
|
||
},
|
||
style: {
|
||
...node.style,
|
||
marginTop: 0,
|
||
paddingTop: 0,
|
||
borderTopWidth: 0,
|
||
borderTopLeftRadius: 0,
|
||
borderTopRightRadius: 0,
|
||
},
|
||
lines: node.lines.slice(slicedLineIndex),
|
||
});
|
||
return [current, next];
|
||
};
|
||
|
||
const getTop$1 = (node) => node.box?.top || 0;
|
||
const hasFixedHeight = (node) => !isNil(node.style?.height);
|
||
const splitNode = (node, height) => {
|
||
if (!node)
|
||
return [null, null];
|
||
const nodeTop = getTop$1(node);
|
||
const current = Object.assign({}, node, {
|
||
box: {
|
||
...node.box,
|
||
borderBottomWidth: 0,
|
||
},
|
||
style: {
|
||
...node.style,
|
||
marginBottom: 0,
|
||
paddingBottom: 0,
|
||
borderBottomWidth: 0,
|
||
borderBottomLeftRadius: 0,
|
||
borderBottomRightRadius: 0,
|
||
},
|
||
});
|
||
current.style.height = height - nodeTop;
|
||
const nextHeight = hasFixedHeight(node)
|
||
? node.box.height - (height - nodeTop)
|
||
: null;
|
||
const next = Object.assign({}, node, {
|
||
box: {
|
||
...node.box,
|
||
top: 0,
|
||
borderTopWidth: 0,
|
||
},
|
||
style: {
|
||
...node.style,
|
||
marginTop: 0,
|
||
paddingTop: 0,
|
||
borderTopWidth: 0,
|
||
borderTopLeftRadius: 0,
|
||
borderTopRightRadius: 0,
|
||
},
|
||
});
|
||
if (nextHeight) {
|
||
next.style.height = nextHeight;
|
||
}
|
||
return [current, next];
|
||
};
|
||
|
||
const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas];
|
||
const getWrap = (node) => {
|
||
if (NON_WRAP_TYPES.includes(node.type))
|
||
return false;
|
||
if (!node.props)
|
||
return true;
|
||
return 'wrap' in node.props ? node.props.wrap : true;
|
||
};
|
||
|
||
const getComputedPadding = (node, edge) => {
|
||
const { yogaNode } = node;
|
||
return yogaNode ? yogaNode.getComputedPadding(edge) : null;
|
||
};
|
||
/**
|
||
* Get Yoga computed paddings. Zero otherwise
|
||
*
|
||
* @param node
|
||
* @returns paddings
|
||
*/
|
||
const getPadding = (node) => {
|
||
const { style, box } = node;
|
||
const paddingTop = getComputedPadding(node, Yoga.Edge.Top) ||
|
||
box?.paddingTop ||
|
||
style?.paddingTop ||
|
||
0;
|
||
const paddingRight = getComputedPadding(node, Yoga.Edge.Right) ||
|
||
box?.paddingRight ||
|
||
style?.paddingRight ||
|
||
0;
|
||
const paddingBottom = getComputedPadding(node, Yoga.Edge.Bottom) ||
|
||
box?.paddingBottom ||
|
||
style?.paddingBottom ||
|
||
0;
|
||
const paddingLeft = getComputedPadding(node, Yoga.Edge.Left) ||
|
||
box?.paddingLeft ||
|
||
style?.paddingLeft ||
|
||
0;
|
||
return { paddingTop, paddingRight, paddingBottom, paddingLeft };
|
||
};
|
||
|
||
const getWrapArea = (page) => {
|
||
const height = page.style?.height;
|
||
const { paddingBottom } = getPadding(page);
|
||
return height - paddingBottom;
|
||
};
|
||
|
||
const getContentArea = (page) => {
|
||
const height = page.style?.height;
|
||
const { paddingTop, paddingBottom } = getPadding(page);
|
||
return height - paddingBottom - paddingTop;
|
||
};
|
||
|
||
const isString = (value) => typeof value === 'string';
|
||
const isNumber = (value) => typeof value === 'number';
|
||
const isBoolean = (value) => typeof value === 'boolean';
|
||
const isFragment = (value) => value && value.type === Symbol.for('react.fragment');
|
||
/**
|
||
* Transforms a react element instance to internal element format.
|
||
*
|
||
* Can return multiple instances in the case of arrays or fragments.
|
||
*
|
||
* @param element - React element
|
||
* @returns Parsed React elements
|
||
*/
|
||
const createInstances = (element) => {
|
||
if (!element)
|
||
return [];
|
||
if (Array.isArray(element)) {
|
||
return element.reduce((acc, el) => acc.concat(createInstances(el)), []);
|
||
}
|
||
if (isBoolean(element)) {
|
||
return [];
|
||
}
|
||
if (isString(element) || isNumber(element)) {
|
||
return [{ type: P.TextInstance, value: `${element}` }];
|
||
}
|
||
if (isFragment(element)) {
|
||
// @ts-expect-error figure out why this is complains
|
||
return createInstances(element.props.children);
|
||
}
|
||
if (!isString(element.type)) {
|
||
// @ts-expect-error figure out why this is complains
|
||
return createInstances(element.type(element.props));
|
||
}
|
||
const { type, props: { style = {}, children, ...props }, } = element;
|
||
const nextChildren = castArray(children).reduce((acc, child) => acc.concat(createInstances(child)), []);
|
||
return [
|
||
{
|
||
type,
|
||
style,
|
||
props,
|
||
children: nextChildren,
|
||
},
|
||
];
|
||
};
|
||
|
||
const getBreak = (node) => 'break' in node.props ? node.props.break : false;
|
||
const getMinPresenceAhead = (node) => 'minPresenceAhead' in node.props ? node.props.minPresenceAhead : 0;
|
||
const getFurthestEnd = (elements) => Math.max(...elements.map((node) => node.box.top + node.box.height));
|
||
const getEndOfMinPresenceAhead = (child) => {
|
||
return (child.box.top +
|
||
child.box.height +
|
||
child.box.marginBottom +
|
||
getMinPresenceAhead(child));
|
||
};
|
||
const getEndOfPresence = (child, futureElements) => {
|
||
const afterMinPresenceAhead = getEndOfMinPresenceAhead(child);
|
||
const endOfFurthestFutureElement = getFurthestEnd(futureElements.filter((node) => !('fixed' in node.props)));
|
||
return Math.min(afterMinPresenceAhead, endOfFurthestFutureElement);
|
||
};
|
||
const shouldBreak = (child, futureElements, height) => {
|
||
if ('fixed' in child.props)
|
||
return false;
|
||
const shouldSplit = height < child.box.top + child.box.height;
|
||
const canWrap = getWrap(child);
|
||
// Calculate the y coordinate where the desired presence of the child ends
|
||
const endOfPresence = getEndOfPresence(child, futureElements);
|
||
// If the child is already at the top of the page, breaking won't improve its presence
|
||
// (as long as react-pdf does not support breaking into differently sized containers)
|
||
const breakingImprovesPresence = child.box.top > child.box.marginTop;
|
||
return (getBreak(child) ||
|
||
(shouldSplit && !canWrap) ||
|
||
(!shouldSplit && endOfPresence > height && breakingImprovesPresence));
|
||
};
|
||
|
||
const IGNORABLE_CODEPOINTS = [
|
||
8232, // LINE_SEPARATOR
|
||
8233, // PARAGRAPH_SEPARATOR
|
||
];
|
||
const buildSubsetForFont = (font) => IGNORABLE_CODEPOINTS.reduce((acc, codePoint) => {
|
||
if (font &&
|
||
font.hasGlyphForCodePoint &&
|
||
font.hasGlyphForCodePoint(codePoint)) {
|
||
return acc;
|
||
}
|
||
return [...acc, String.fromCharCode(codePoint)];
|
||
}, []);
|
||
const ignoreChars = (fragments) => fragments.map((fragment) => {
|
||
const charSubset = buildSubsetForFont(fragment.attributes.font[0]);
|
||
const subsetRegex = new RegExp(charSubset.join('|'));
|
||
return {
|
||
string: fragment.string.replace(subsetRegex, ''),
|
||
attributes: fragment.attributes,
|
||
};
|
||
});
|
||
|
||
const PREPROCESSORS = [ignoreChars, embedEmojis];
|
||
const isImage$1 = (node) => node.type === P.Image;
|
||
const isTextInstance$2 = (node) => node.type === P.TextInstance;
|
||
/**
|
||
* Get textkit fragments of given node object
|
||
*
|
||
* @param fontStore - Font store
|
||
* @param instance - Node
|
||
* @param parentLink - Parent link
|
||
* @param level - Fragment level
|
||
* @returns Text fragments
|
||
*/
|
||
const getFragments = (fontStore, instance, parentLink = null, level = 0) => {
|
||
if (!instance)
|
||
return [{ string: '' }];
|
||
let fragments = [];
|
||
const { color = 'black', direction = 'ltr', fontFamily = 'Helvetica', fontWeight, fontStyle, fontSize = 18, textAlign, lineHeight, textDecoration, textDecorationColor, textDecorationStyle, textTransform, letterSpacing, textIndent, opacity, verticalAlign, } = instance.style;
|
||
const fontFamilies = typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])];
|
||
// Fallback font
|
||
fontFamilies.push('Helvetica');
|
||
const font = fontFamilies.map((fontFamilyName) => {
|
||
const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle };
|
||
const obj = fontStore.getFont(opts);
|
||
return obj?.data;
|
||
});
|
||
// Don't pass main background color to textkit. Will be rendered by the render package instead
|
||
const backgroundColor = level === 0 ? null : instance.style.backgroundColor;
|
||
const attributes = {
|
||
font,
|
||
color,
|
||
opacity,
|
||
fontSize,
|
||
lineHeight,
|
||
direction,
|
||
verticalAlign,
|
||
backgroundColor,
|
||
indent: textIndent,
|
||
characterSpacing: letterSpacing,
|
||
strikeStyle: textDecorationStyle,
|
||
underlineStyle: textDecorationStyle,
|
||
underline: textDecoration === 'underline' ||
|
||
textDecoration === 'underline line-through' ||
|
||
textDecoration === 'line-through underline',
|
||
strike: textDecoration === 'line-through' ||
|
||
textDecoration === 'underline line-through' ||
|
||
textDecoration === 'line-through underline',
|
||
strikeColor: textDecorationColor || color,
|
||
underlineColor: textDecorationColor || color,
|
||
// @ts-expect-error allow this props access
|
||
link: parentLink || instance.props?.src || instance.props?.href,
|
||
align: textAlign || (direction === 'rtl' ? 'right' : 'left'),
|
||
};
|
||
for (let i = 0; i < instance.children.length; i += 1) {
|
||
const child = instance.children[i];
|
||
if (isImage$1(child)) {
|
||
fragments.push({
|
||
string: String.fromCharCode(0xfffc),
|
||
attributes: {
|
||
...attributes,
|
||
attachment: {
|
||
width: (child.style.width || fontSize),
|
||
height: (child.style.height || fontSize),
|
||
image: child.image.data,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
else if (isTextInstance$2(child)) {
|
||
fragments.push({
|
||
string: transformText(child.value, textTransform),
|
||
attributes,
|
||
});
|
||
}
|
||
else if (child) {
|
||
fragments.push(...getFragments(fontStore, child, attributes.link, level + 1));
|
||
}
|
||
}
|
||
for (let i = 0; i < PREPROCESSORS.length; i += 1) {
|
||
const preprocessor = PREPROCESSORS[i];
|
||
fragments = preprocessor(fragments);
|
||
}
|
||
return fragments;
|
||
};
|
||
/**
|
||
* Get textkit attributed string from text node
|
||
*
|
||
* @param fontStore - Font store
|
||
* @param instance Node
|
||
* @returns Attributed string
|
||
*/
|
||
const getAttributedString = (fontStore, instance) => {
|
||
const fragments = getFragments(fontStore, instance);
|
||
return fromFragments(fragments);
|
||
};
|
||
|
||
const engines = {
|
||
bidi,
|
||
linebreaker,
|
||
justification,
|
||
textDecoration,
|
||
scriptItemizer,
|
||
wordHyphenation,
|
||
fontSubstitution,
|
||
};
|
||
const engine = layoutEngine(engines);
|
||
const getMaxLines = (node) => node.style?.maxLines;
|
||
const getTextOverflow = (node) => node.style?.textOverflow;
|
||
/**
|
||
* Get layout container for specific text node
|
||
*
|
||
* @param {number} width
|
||
* @param {number} height
|
||
* @param {Object} node
|
||
* @returns {Object} layout container
|
||
*/
|
||
const getContainer = (width, height, node) => {
|
||
const maxLines = getMaxLines(node);
|
||
const textOverflow = getTextOverflow(node);
|
||
return {
|
||
x: 0,
|
||
y: 0,
|
||
width,
|
||
maxLines,
|
||
height: height || Infinity,
|
||
truncateMode: textOverflow,
|
||
};
|
||
};
|
||
/**
|
||
* Get text layout options for specific text node
|
||
*
|
||
* @param {Object} node instance
|
||
* @returns {Object} layout options
|
||
*/
|
||
const getLayoutOptions = (fontStore, node) => ({
|
||
hyphenationPenalty: node.props.hyphenationPenalty,
|
||
shrinkWhitespaceFactor: { before: -0.5, after: -0.5 },
|
||
hyphenationCallback: node.props.hyphenationCallback ||
|
||
fontStore?.getHyphenationCallback() ||
|
||
null,
|
||
});
|
||
/**
|
||
* Get text lines for given node
|
||
*
|
||
* @param node - Node
|
||
* @param width - Container width
|
||
* @param height - Container height
|
||
* @param fontStore - Font store
|
||
* @returns Layout lines
|
||
*/
|
||
const layoutText = (node, width, height, fontStore) => {
|
||
const attributedString = getAttributedString(fontStore, node);
|
||
const container = getContainer(width, height, node);
|
||
const options = getLayoutOptions(fontStore, node);
|
||
const lines = engine(attributedString, container, options);
|
||
return lines.reduce((acc, line) => [...acc, ...line], []);
|
||
};
|
||
|
||
const isSvg$2 = (node) => node.type === P.Svg;
|
||
const isText$4 = (node) => node.type === P.Text;
|
||
const shouldIterate = (node) => !isSvg$2(node) && !isText$4(node);
|
||
const shouldLayoutText = (node) => isText$4(node) && !node.lines;
|
||
/**
|
||
* Performs text layout on text node if wasn't calculated before.
|
||
* Text layout is usually performed on Yoga's layout process (via setMeasureFunc),
|
||
* but we need to layout those nodes with fixed width and height.
|
||
*
|
||
* @param node
|
||
* @returns Layout node
|
||
*/
|
||
const resolveTextLayout = (node, fontStore) => {
|
||
if (shouldLayoutText(node)) {
|
||
const width = node.box.width - (node.box.paddingRight + node.box.paddingLeft);
|
||
const height = node.box.height - (node.box.paddingTop + node.box.paddingBottom);
|
||
node.lines = layoutText(node, width, height, fontStore);
|
||
}
|
||
if (shouldIterate(node)) {
|
||
if (!node.children)
|
||
return node;
|
||
const mapChild = (child) => resolveTextLayout(child, fontStore);
|
||
const children = node.children.map(mapChild);
|
||
return Object.assign({}, node, { children });
|
||
}
|
||
return node;
|
||
};
|
||
|
||
const BASE_INHERITABLE_PROPERTIES = [
|
||
'color',
|
||
'fontFamily',
|
||
'fontSize',
|
||
'fontStyle',
|
||
'fontWeight',
|
||
'letterSpacing',
|
||
'opacity',
|
||
'textDecoration',
|
||
'textTransform',
|
||
'lineHeight',
|
||
'textAlign',
|
||
'visibility',
|
||
'wordSpacing',
|
||
];
|
||
const TEXT_INHERITABLE_PROPERTIES = [
|
||
...BASE_INHERITABLE_PROPERTIES,
|
||
'backgroundColor',
|
||
];
|
||
const isType$2 = (type) => (node) => node.type === type;
|
||
const isSvg$1 = isType$2(P.Svg);
|
||
const isText$3 = isType$2(P.Text);
|
||
// Merge style values
|
||
const mergeValues = (styleName, value, inheritedValue) => {
|
||
switch (styleName) {
|
||
case 'textDecoration': {
|
||
// merge not none and not false textDecoration values to one rule
|
||
return [inheritedValue, value].filter((v) => v && v !== 'none').join(' ');
|
||
}
|
||
default:
|
||
return value;
|
||
}
|
||
};
|
||
// Merge inherited and node styles
|
||
const merge = (inheritedStyles, style) => {
|
||
const mergedStyles = { ...inheritedStyles };
|
||
Object.entries(style).forEach(([styleName, value]) => {
|
||
mergedStyles[styleName] = mergeValues(styleName, value, inheritedStyles[styleName]);
|
||
});
|
||
return mergedStyles;
|
||
};
|
||
/**
|
||
* Merges styles with node
|
||
*
|
||
* @param inheritedStyles - Style object
|
||
* @returns Merge styles function
|
||
*/
|
||
const mergeStyles = (inheritedStyles) => (node) => {
|
||
const style = merge(inheritedStyles, node.style || {});
|
||
return Object.assign({}, node, { style });
|
||
};
|
||
/**
|
||
* Inherit style values from the root to the leafs
|
||
*
|
||
* @param node - Document root
|
||
* @returns Document root with inheritance
|
||
*
|
||
*/
|
||
const resolveInheritance = (node) => {
|
||
if (isSvg$1(node))
|
||
return node;
|
||
if (!('children' in node))
|
||
return node;
|
||
const inheritableProperties = isText$3(node)
|
||
? TEXT_INHERITABLE_PROPERTIES
|
||
: BASE_INHERITABLE_PROPERTIES;
|
||
const inheritStyles = pick(inheritableProperties, node.style || {});
|
||
const resolveChild = compose(resolveInheritance, mergeStyles(inheritStyles));
|
||
const children = node.children.map(resolveChild);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
|
||
const getComputedMargin = (node, edge) => {
|
||
const { yogaNode } = node;
|
||
return yogaNode ? yogaNode.getComputedMargin(edge) : null;
|
||
};
|
||
/**
|
||
* Get Yoga computed magins. Zero otherwise
|
||
*
|
||
* @param node
|
||
* @returns Margins
|
||
*/
|
||
const getMargin = (node) => {
|
||
const { style, box } = node;
|
||
const marginTop = getComputedMargin(node, Yoga.Edge.Top) ||
|
||
box?.marginTop ||
|
||
style?.marginTop ||
|
||
0;
|
||
const marginRight = getComputedMargin(node, Yoga.Edge.Right) ||
|
||
box?.marginRight ||
|
||
style?.marginRight ||
|
||
0;
|
||
const marginBottom = getComputedMargin(node, Yoga.Edge.Bottom) ||
|
||
box?.marginBottom ||
|
||
style?.marginBottom ||
|
||
0;
|
||
const marginLeft = getComputedMargin(node, Yoga.Edge.Left) ||
|
||
box?.marginLeft ||
|
||
style?.marginLeft ||
|
||
0;
|
||
return { marginTop, marginRight, marginBottom, marginLeft };
|
||
};
|
||
|
||
/**
|
||
* Get Yoga computed position. Zero otherwise
|
||
*
|
||
* @param node
|
||
* @returns Position
|
||
*/
|
||
const getPosition = (node) => {
|
||
const { yogaNode } = node;
|
||
return {
|
||
top: yogaNode?.getComputedTop() || 0,
|
||
right: yogaNode?.getComputedRight() || 0,
|
||
bottom: yogaNode?.getComputedBottom() || 0,
|
||
left: yogaNode?.getComputedLeft() || 0,
|
||
};
|
||
};
|
||
|
||
const DEFAULT_DIMENSION = {
|
||
width: 0,
|
||
height: 0,
|
||
};
|
||
/**
|
||
* Get Yoga computed dimensions. Zero otherwise
|
||
*
|
||
* @param node
|
||
* @returns Dimensions
|
||
*/
|
||
const getDimension = (node) => {
|
||
const { yogaNode } = node;
|
||
if (!yogaNode)
|
||
return DEFAULT_DIMENSION;
|
||
return {
|
||
width: yogaNode.getComputedWidth(),
|
||
height: yogaNode.getComputedHeight(),
|
||
};
|
||
};
|
||
|
||
const getComputedBorder = (yogaNode, edge) => (yogaNode ? yogaNode.getComputedBorder(edge) : 0);
|
||
/**
|
||
* Get Yoga computed border width. Zero otherwise
|
||
*
|
||
* @param node
|
||
* @returns Border widths
|
||
*/
|
||
const getBorderWidth = (node) => {
|
||
const { yogaNode } = node;
|
||
return {
|
||
borderTopWidth: getComputedBorder(yogaNode, Yoga.Edge.Top),
|
||
borderRightWidth: getComputedBorder(yogaNode, Yoga.Edge.Right),
|
||
borderBottomWidth: getComputedBorder(yogaNode, Yoga.Edge.Bottom),
|
||
borderLeftWidth: getComputedBorder(yogaNode, Yoga.Edge.Left),
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Set display attribute to node's Yoga instance
|
||
*
|
||
* @param value - Display
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setDisplay = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (yogaNode) {
|
||
yogaNode.setDisplay(value === 'none' ? Yoga.Display.None : Yoga.Display.Flex);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
const OVERFLOW = {
|
||
hidden: Yoga.Overflow.Hidden,
|
||
scroll: Yoga.Overflow.Scroll,
|
||
};
|
||
/**
|
||
* Set overflow attribute to node's Yoga instance
|
||
*
|
||
* @param value - Overflow value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setOverflow = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (!isNil(value) && yogaNode) {
|
||
const overflow = OVERFLOW[value] || Yoga.Overflow.Visible;
|
||
yogaNode.setOverflow(overflow);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
const FLEX_WRAP = {
|
||
wrap: Yoga.Wrap.Wrap,
|
||
'wrap-reverse': Yoga.Wrap.WrapReverse,
|
||
};
|
||
/**
|
||
* Set flex wrap attribute to node's Yoga instance
|
||
*
|
||
* @param value - Flex wrap value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setFlexWrap = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (yogaNode) {
|
||
const flexWrap = FLEX_WRAP[value] || Yoga.Wrap.NoWrap;
|
||
yogaNode.setFlexWrap(flexWrap);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
/**
|
||
* Set generic yoga attribute to node's Yoga instance, handing `auto`, edges and percentage cases
|
||
*
|
||
* @param attr - Property
|
||
* @param edge - Edge
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setYogaValue = (attr, edge) => (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (!isNil(value) && yogaNode) {
|
||
const hasEdge = !isNil(edge);
|
||
const fixedMethod = `set${upperFirst(attr)}`;
|
||
const autoMethod = `${fixedMethod}Auto`;
|
||
const percentMethod = `${fixedMethod}Percent`;
|
||
const percent = matchPercent(value);
|
||
if (percent && !yogaNode[percentMethod]) {
|
||
throw new Error(`You can't pass percentage values to ${attr} property`);
|
||
}
|
||
if (percent) {
|
||
if (hasEdge) {
|
||
yogaNode[percentMethod]?.(edge, percent.value);
|
||
}
|
||
else {
|
||
yogaNode[percentMethod]?.(percent.value);
|
||
}
|
||
}
|
||
else if (value === 'auto') {
|
||
if (hasEdge) {
|
||
yogaNode[autoMethod]?.(edge);
|
||
}
|
||
else {
|
||
yogaNode[autoMethod]?.();
|
||
}
|
||
}
|
||
else if (hasEdge) {
|
||
yogaNode[fixedMethod]?.(edge, value);
|
||
}
|
||
else {
|
||
yogaNode[fixedMethod]?.(value);
|
||
}
|
||
}
|
||
return node;
|
||
};
|
||
|
||
/**
|
||
* Set flex grow attribute to node's Yoga instance
|
||
*
|
||
* @param value - Flex grow value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setFlexGrow = (value) => (node) => {
|
||
return setYogaValue('flexGrow')(value || 0)(node);
|
||
};
|
||
|
||
/**
|
||
* Set flex basis attribute to node's Yoga instance
|
||
*
|
||
* @param flex - Basis value
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setFlexBasis = setYogaValue('flexBasis');
|
||
|
||
const ALIGN = {
|
||
'flex-start': Yoga.Align.FlexStart,
|
||
center: Yoga.Align.Center,
|
||
'flex-end': Yoga.Align.FlexEnd,
|
||
stretch: Yoga.Align.Stretch,
|
||
baseline: Yoga.Align.Baseline,
|
||
'space-between': Yoga.Align.SpaceBetween,
|
||
'space-around': Yoga.Align.SpaceAround,
|
||
'space-evenly': Yoga.Align.SpaceEvenly,
|
||
};
|
||
/**
|
||
* Set generic align attribute to node's Yoga instance
|
||
*
|
||
* @param attr - Specific align property
|
||
* @param value - Specific align value
|
||
* @param node - Node
|
||
* @returns Node
|
||
*/
|
||
const setAlign = (attr) => (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
const defaultValue = attr === 'items' ? Yoga.Align.Stretch : Yoga.Align.Auto;
|
||
if (yogaNode) {
|
||
const align = ALIGN[value] || defaultValue;
|
||
yogaNode[`setAlign${upperFirst(attr)}`](align);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
/**
|
||
* Set align self attribute to node's Yoga instance
|
||
*
|
||
* @param align - Value
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setAlignSelf = setAlign('self');
|
||
|
||
/**
|
||
* Set align items attribute to node's Yoga instance
|
||
*
|
||
* @param align - Value
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setAlignItems = setAlign('items');
|
||
|
||
/**
|
||
* Set flex shrink attribute to node's Yoga instance
|
||
*
|
||
* @param value - Flex shrink value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setFlexShrink = (value) => (node) => {
|
||
return setYogaValue('flexShrink')(value || 1)(node);
|
||
};
|
||
|
||
/**
|
||
* Set aspect ratio attribute to node's Yoga instance
|
||
*
|
||
* @param value - Ratio
|
||
* @returns Node instance
|
||
*/
|
||
const setAspectRatio = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (!isNil(value) && yogaNode) {
|
||
yogaNode.setAspectRatio(value);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
/**
|
||
* Set align content attribute to node's Yoga instance
|
||
*
|
||
* @param align - Value
|
||
* @param node - Instance
|
||
* @returns Node instance
|
||
*/
|
||
const setAlignContent = setAlign('content');
|
||
|
||
const POSITION = {
|
||
absolute: Yoga.PositionType.Absolute,
|
||
relative: Yoga.PositionType.Relative,
|
||
static: Yoga.PositionType.Static,
|
||
};
|
||
/**
|
||
* Set position type attribute to node's Yoga instance
|
||
*
|
||
* @param value - Position position type
|
||
* @returns Node instance
|
||
*/
|
||
const setPositionType = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (!isNil(value) && yogaNode) {
|
||
yogaNode.setPositionType(POSITION[value]);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
const FLEX_DIRECTIONS = {
|
||
row: Yoga.FlexDirection.Row,
|
||
'row-reverse': Yoga.FlexDirection.RowReverse,
|
||
'column-reverse': Yoga.FlexDirection.ColumnReverse,
|
||
};
|
||
/**
|
||
* Set flex direction attribute to node's Yoga instance
|
||
*
|
||
* @param value - Flex direction value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setFlexDirection = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (yogaNode) {
|
||
const flexDirection = FLEX_DIRECTIONS[value] || Yoga.FlexDirection.Column;
|
||
yogaNode.setFlexDirection(flexDirection);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
const JUSTIFY_CONTENT = {
|
||
center: Yoga.Justify.Center,
|
||
'flex-end': Yoga.Justify.FlexEnd,
|
||
'space-between': Yoga.Justify.SpaceBetween,
|
||
'space-around': Yoga.Justify.SpaceAround,
|
||
'space-evenly': Yoga.Justify.SpaceEvenly,
|
||
};
|
||
/**
|
||
* Set justify content attribute to node's Yoga instance
|
||
*
|
||
* @param value - Justify content value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setJustifyContent = (value) => (node) => {
|
||
const { yogaNode } = node;
|
||
if (!isNil(value) && yogaNode) {
|
||
const justifyContent = JUSTIFY_CONTENT[value] || Yoga.Justify.FlexStart;
|
||
yogaNode.setJustifyContent(justifyContent);
|
||
}
|
||
return node;
|
||
};
|
||
|
||
/**
|
||
* Set margin top attribute to node's Yoga instance
|
||
*
|
||
* @param margin - Margin top
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMarginTop = setYogaValue('margin', Yoga.Edge.Top);
|
||
/**
|
||
* Set margin right attribute to node's Yoga instance
|
||
*
|
||
* @param margin - Margin right
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMarginRight = setYogaValue('margin', Yoga.Edge.Right);
|
||
/**
|
||
* Set margin bottom attribute to node's Yoga instance
|
||
*
|
||
* @param margin - Margin bottom
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMarginBottom = setYogaValue('margin', Yoga.Edge.Bottom);
|
||
/**
|
||
* Set margin left attribute to node's Yoga instance
|
||
*
|
||
* @param margin - Margin left
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMarginLeft = setYogaValue('margin', Yoga.Edge.Left);
|
||
|
||
/**
|
||
* Set padding top attribute to node's Yoga instance
|
||
*
|
||
* @param padding - Padding top
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPaddingTop = setYogaValue('padding', Yoga.Edge.Top);
|
||
/**
|
||
* Set padding right attribute to node's Yoga instance
|
||
*
|
||
* @param padding - Padding right
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPaddingRight = setYogaValue('padding', Yoga.Edge.Right);
|
||
/**
|
||
* Set padding bottom attribute to node's Yoga instance
|
||
*
|
||
* @param padding - Padding bottom
|
||
* @param node Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPaddingBottom = setYogaValue('padding', Yoga.Edge.Bottom);
|
||
/**
|
||
* Set padding left attribute to node's Yoga instance
|
||
*
|
||
* @param padding - Padding left
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPaddingLeft = setYogaValue('padding', Yoga.Edge.Left);
|
||
|
||
/**
|
||
* Set border top attribute to node's Yoga instance
|
||
*
|
||
* @param border - Border top width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setBorderTop = setYogaValue('border', Yoga.Edge.Top);
|
||
/**
|
||
* Set border right attribute to node's Yoga instance
|
||
*
|
||
* @param border - Border right width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setBorderRight = setYogaValue('border', Yoga.Edge.Right);
|
||
/**
|
||
* Set border bottom attribute to node's Yoga instance
|
||
*
|
||
* @param border - Border bottom width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setBorderBottom = setYogaValue('border', Yoga.Edge.Bottom);
|
||
/**
|
||
* Set border left attribute to node's Yoga instance
|
||
*
|
||
* @param border - Border left width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setBorderLeft = setYogaValue('border', Yoga.Edge.Left);
|
||
|
||
/**
|
||
* Set position top attribute to node's Yoga instance
|
||
*
|
||
* @param position - Position top
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPositionTop = setYogaValue('position', Yoga.Edge.Top);
|
||
/**
|
||
* Set position right attribute to node's Yoga instance
|
||
*
|
||
* @param position - Position right
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPositionRight = setYogaValue('position', Yoga.Edge.Right);
|
||
/**
|
||
* Set position bottom attribute to node's Yoga instance
|
||
*
|
||
* @param position - Position bottom
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPositionBottom = setYogaValue('position', Yoga.Edge.Bottom);
|
||
/**
|
||
* Set position left attribute to node's Yoga instance
|
||
*
|
||
* @param position - Position left
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setPositionLeft = setYogaValue('position', Yoga.Edge.Left);
|
||
|
||
/**
|
||
* Set width to node's Yoga instance
|
||
*
|
||
* @param width - Width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setWidth = setYogaValue('width');
|
||
/**
|
||
* Set min width to node's Yoga instance
|
||
*
|
||
* @param min - Width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMinWidth = setYogaValue('minWidth');
|
||
/**
|
||
* Set max width to node's Yoga instance
|
||
*
|
||
* @param max - Width
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMaxWidth = setYogaValue('maxWidth');
|
||
/**
|
||
* Set height to node's Yoga instance
|
||
*
|
||
* @param height - Height
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setHeight = setYogaValue('height');
|
||
/**
|
||
* Set min height to node's Yoga instance
|
||
*
|
||
* @param min - Height
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMinHeight = setYogaValue('minHeight');
|
||
/**
|
||
* Set max height to node's Yoga instance
|
||
*
|
||
* @param max - Height
|
||
* @param node - Node instance
|
||
* @returns Node instance
|
||
*/
|
||
const setMaxHeight = setYogaValue('maxHeight');
|
||
|
||
/**
|
||
* Set rowGap value to node's Yoga instance
|
||
*
|
||
* @param value - Gap value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setRowGap = setYogaValue('gap', Yoga.Gutter.Row);
|
||
/**
|
||
* Set columnGap value to node's Yoga instance
|
||
*
|
||
* @param value - Gap value
|
||
* @returns Node instance wrapper
|
||
*/
|
||
const setColumnGap = setYogaValue('gap', Yoga.Gutter.Column);
|
||
|
||
const getAspectRatio = (viewbox) => {
|
||
if (!viewbox)
|
||
return null;
|
||
if (typeof viewbox === 'string')
|
||
return null;
|
||
return (viewbox.maxX - viewbox.minX) / (viewbox.maxY - viewbox.minY);
|
||
};
|
||
/**
|
||
* Yoga svg measure function
|
||
*
|
||
* @param page
|
||
* @param node
|
||
* @returns Measure svg
|
||
*/
|
||
const measureCanvas$1 = (page, node) => (width, widthMode, height, heightMode) => {
|
||
const aspectRatio = getAspectRatio(node.props.viewBox) || 1;
|
||
if (widthMode === Yoga.MeasureMode.Exactly ||
|
||
widthMode === Yoga.MeasureMode.AtMost) {
|
||
return { width, height: width / aspectRatio };
|
||
}
|
||
if (heightMode === Yoga.MeasureMode.Exactly) {
|
||
return { width: height * aspectRatio };
|
||
}
|
||
return {};
|
||
};
|
||
|
||
/**
|
||
* Get lines width (if any)
|
||
*
|
||
* @param node
|
||
* @returns Lines width
|
||
*/
|
||
const linesWidth = (node) => {
|
||
if (!node.lines)
|
||
return 0;
|
||
return Math.max(0, ...node.lines.map((line) => line.xAdvance));
|
||
};
|
||
|
||
/**
|
||
* Get lines height (if any)
|
||
*
|
||
* @param node
|
||
* @returns Lines height
|
||
*/
|
||
const linesHeight = (node) => {
|
||
if (!node.lines)
|
||
return -1;
|
||
return node.lines.reduce((acc, line) => acc + line.box.height, 0);
|
||
};
|
||
|
||
const ALIGNMENT_FACTORS = { center: 0.5, right: 1 };
|
||
/**
|
||
* Yoga text measure function
|
||
*
|
||
* @param page
|
||
* @param node
|
||
* @param fontStore
|
||
* @returns {MeasureText} measure text function
|
||
*/
|
||
const measureText = (page, node, fontStore) => (width, widthMode, height) => {
|
||
if (widthMode === Yoga.MeasureMode.Exactly) {
|
||
if (!node.lines)
|
||
node.lines = layoutText(node, width, height, fontStore);
|
||
return { height: linesHeight(node) };
|
||
}
|
||
if (widthMode === Yoga.MeasureMode.AtMost) {
|
||
const alignFactor = ALIGNMENT_FACTORS[node.style?.textAlign] || 0;
|
||
if (!node.lines) {
|
||
node.lines = layoutText(node, width, height, fontStore);
|
||
node.alignOffset = (width - linesWidth(node)) * alignFactor; // Compensate align in variable width containers
|
||
}
|
||
return {
|
||
height: linesHeight(node),
|
||
width: Math.min(width, linesWidth(node)),
|
||
};
|
||
}
|
||
return {};
|
||
};
|
||
|
||
/**
|
||
* Get image ratio
|
||
*
|
||
* @param node - Image node
|
||
* @returns Image ratio
|
||
*/
|
||
const getRatio = (node) => {
|
||
return node.image?.data ? node.image.width / node.image.height : 1;
|
||
};
|
||
|
||
/**
|
||
* Checks if page has auto height
|
||
*
|
||
* @param page
|
||
* @returns Is page height auto
|
||
*/
|
||
const isHeightAuto = (page) => isNil(page.box?.height);
|
||
|
||
const SAFETY_HEIGHT$1 = 10;
|
||
/**
|
||
* Yoga image measure function
|
||
*
|
||
* @param page - Page
|
||
* @param node - Node
|
||
* @returns Measure image
|
||
*/
|
||
const measureImage = (page, node) => (width, widthMode, height, heightMode) => {
|
||
const imageRatio = getRatio(node);
|
||
const imageMargin = getMargin(node);
|
||
const pagePadding = getPadding(page);
|
||
// TODO: Check image percentage margins
|
||
const pageArea = isHeightAuto(page)
|
||
? Infinity
|
||
: (page.box?.height || 0) -
|
||
pagePadding.paddingTop -
|
||
pagePadding.paddingBottom -
|
||
imageMargin.marginTop -
|
||
imageMargin.marginBottom -
|
||
SAFETY_HEIGHT$1;
|
||
// Skip measure if image data not present yet
|
||
if (!node.image)
|
||
return { width: 0, height: 0 };
|
||
if (widthMode === Yoga.MeasureMode.Exactly &&
|
||
heightMode === Yoga.MeasureMode.Undefined) {
|
||
const scaledHeight = width / imageRatio;
|
||
return { height: Math.min(pageArea, scaledHeight) };
|
||
}
|
||
if (heightMode === Yoga.MeasureMode.Exactly &&
|
||
(widthMode === Yoga.MeasureMode.AtMost ||
|
||
widthMode === Yoga.MeasureMode.Undefined)) {
|
||
return { width: Math.min(height * imageRatio, width) };
|
||
}
|
||
if (widthMode === Yoga.MeasureMode.Exactly &&
|
||
heightMode === Yoga.MeasureMode.AtMost) {
|
||
const scaledHeight = width / imageRatio;
|
||
return { height: Math.min(height, pageArea, scaledHeight) };
|
||
}
|
||
if (widthMode === Yoga.MeasureMode.AtMost &&
|
||
heightMode === Yoga.MeasureMode.AtMost) {
|
||
if (imageRatio > 1) {
|
||
return {
|
||
width,
|
||
height: Math.min(width / imageRatio, height),
|
||
};
|
||
}
|
||
return {
|
||
height,
|
||
width: Math.min(height * imageRatio, width),
|
||
};
|
||
}
|
||
return { height, width };
|
||
};
|
||
|
||
const SAFETY_HEIGHT = 10;
|
||
const getMax = (values) => Math.max(-Infinity, ...values);
|
||
/**
|
||
* Helper object to predict canvas size
|
||
* TODO: Implement remaining functions (as close as possible);
|
||
*/
|
||
const measureCtx = () => {
|
||
const ctx = {};
|
||
const points = [];
|
||
const nil = () => ctx;
|
||
const addPoint = (x, y) => points.push([x, y]);
|
||
const moveTo = (x, y) => {
|
||
addPoint(x, y);
|
||
return ctx;
|
||
};
|
||
const rect = (x, y, w, h) => {
|
||
addPoint(x, y);
|
||
addPoint(x + w, y);
|
||
addPoint(x, y + h);
|
||
addPoint(x + w, y + h);
|
||
return ctx;
|
||
};
|
||
const ellipse = (x, y, rx, ry) => {
|
||
ry = ry || rx;
|
||
addPoint(x - rx, y - ry);
|
||
addPoint(x + rx, y - ry);
|
||
addPoint(x + rx, y + ry);
|
||
addPoint(x - rx, y + ry);
|
||
return ctx;
|
||
};
|
||
const polygon = (...pts) => {
|
||
points.push(...pts);
|
||
return ctx;
|
||
};
|
||
// Change dimensions
|
||
ctx.rect = rect;
|
||
ctx.moveTo = moveTo;
|
||
ctx.lineTo = moveTo;
|
||
ctx.circle = ellipse;
|
||
ctx.polygon = polygon;
|
||
ctx.ellipse = ellipse;
|
||
ctx.roundedRect = rect;
|
||
// To be implemented
|
||
ctx.text = nil;
|
||
ctx.path = nil;
|
||
ctx.lineWidth = nil;
|
||
ctx.bezierCurveTo = nil;
|
||
ctx.quadraticCurveTo = nil;
|
||
ctx.scale = nil;
|
||
ctx.rotate = nil;
|
||
ctx.translate = nil;
|
||
// These don't change dimensions
|
||
ctx.dash = nil;
|
||
ctx.clip = nil;
|
||
ctx.save = nil;
|
||
ctx.fill = nil;
|
||
ctx.font = nil;
|
||
ctx.stroke = nil;
|
||
ctx.lineCap = nil;
|
||
ctx.opacity = nil;
|
||
ctx.restore = nil;
|
||
ctx.lineJoin = nil;
|
||
ctx.fontSize = nil;
|
||
ctx.fillColor = nil;
|
||
ctx.miterLimit = nil;
|
||
ctx.strokeColor = nil;
|
||
ctx.fillOpacity = nil;
|
||
ctx.strokeOpacity = nil;
|
||
ctx.linearGradient = nil;
|
||
ctx.radialGradient = nil;
|
||
ctx.getWidth = () => getMax(points.map((p) => p[0]));
|
||
ctx.getHeight = () => getMax(points.map((p) => p[1]));
|
||
return ctx;
|
||
};
|
||
/**
|
||
* @typedef {Function} MeasureCanvas
|
||
* @returns {{ width: number, height: number }} canvas width and height
|
||
*/
|
||
/**
|
||
* Yoga canvas measure function
|
||
*
|
||
* @param {Object} page
|
||
* @param {Object} node
|
||
* @returns {MeasureCanvas} measure canvas
|
||
*/
|
||
const measureCanvas = (page, node) => () => {
|
||
const imageMargin = getMargin(node);
|
||
const pagePadding = getPadding(page);
|
||
// TODO: Check image percentage margins
|
||
const pageArea = isHeightAuto(page)
|
||
? Infinity
|
||
: (page.box?.height || 0) -
|
||
pagePadding.paddingTop -
|
||
pagePadding.paddingBottom -
|
||
imageMargin.marginTop -
|
||
imageMargin.marginBottom -
|
||
SAFETY_HEIGHT;
|
||
const ctx = measureCtx();
|
||
node.props.paint(ctx);
|
||
const width = ctx.getWidth();
|
||
const height = Math.min(pageArea, ctx.getHeight());
|
||
return { width, height };
|
||
};
|
||
|
||
const isType$1 = (type) => (node) => node.type === type;
|
||
const isSvg = isType$1(P.Svg);
|
||
const isText$2 = isType$1(P.Text);
|
||
const isNote = isType$1(P.Note);
|
||
const isPage = isType$1(P.Page);
|
||
const isImage = isType$1(P.Image);
|
||
const isCanvas = isType$1(P.Canvas);
|
||
const isTextInstance$1 = isType$1(P.TextInstance);
|
||
const setNodeHeight = (node) => {
|
||
const value = isPage(node) ? node.box?.height : node.style?.height;
|
||
return setHeight(value);
|
||
};
|
||
/**
|
||
* Set styles valeus into yoga node before layout calculation
|
||
*
|
||
* @param node
|
||
*/
|
||
const setYogaValues = (node) => {
|
||
compose(setNodeHeight(node), setWidth(node.style.width), setMinWidth(node.style.minWidth), setMaxWidth(node.style.maxWidth), setMinHeight(node.style.minHeight), setMaxHeight(node.style.maxHeight), setMarginTop(node.style.marginTop), setMarginRight(node.style.marginRight), setMarginBottom(node.style.marginBottom), setMarginLeft(node.style.marginLeft), setPaddingTop(node.style.paddingTop), setPaddingRight(node.style.paddingRight), setPaddingBottom(node.style.paddingBottom), setPaddingLeft(node.style.paddingLeft), setPositionType(node.style.position), setPositionTop(node.style.top), setPositionRight(node.style.right), setPositionBottom(node.style.bottom), setPositionLeft(node.style.left), setBorderTop(node.style.borderTopWidth), setBorderRight(node.style.borderRightWidth), setBorderBottom(node.style.borderBottomWidth), setBorderLeft(node.style.borderLeftWidth), setDisplay(node.style.display), setFlexDirection(node.style.flexDirection), setAlignSelf(node.style.alignSelf), setAlignContent(node.style.alignContent), setAlignItems(node.style.alignItems), setJustifyContent(node.style.justifyContent), setFlexWrap(node.style.flexWrap), setOverflow(node.style.overflow), setAspectRatio(node.style.aspectRatio), setFlexBasis(node.style.flexBasis), setFlexGrow(node.style.flexGrow), setFlexShrink(node.style.flexShrink), setRowGap(node.style.rowGap), setColumnGap(node.style.columnGap))(node);
|
||
};
|
||
/**
|
||
* Inserts child into parent' yoga node
|
||
*
|
||
* @param parent parent
|
||
* @returns Insert yoga nodes
|
||
*/
|
||
const insertYogaNodes = (parent) => (child) => {
|
||
parent.insertChild(child.yogaNode, parent.getChildCount());
|
||
return child;
|
||
};
|
||
const setMeasureFunc = (node, page, fontStore) => {
|
||
const { yogaNode } = node;
|
||
if (isText$2(node)) {
|
||
yogaNode.setMeasureFunc(measureText(page, node, fontStore));
|
||
}
|
||
if (isImage(node)) {
|
||
yogaNode.setMeasureFunc(measureImage(page, node));
|
||
}
|
||
if (isCanvas(node)) {
|
||
yogaNode.setMeasureFunc(measureCanvas(page, node));
|
||
}
|
||
if (isSvg(node)) {
|
||
yogaNode.setMeasureFunc(measureCanvas$1(page, node));
|
||
}
|
||
return node;
|
||
};
|
||
const isLayoutElement = (node) => !isText$2(node) && !isNote(node) && !isSvg(node);
|
||
/**
|
||
* @typedef {Function} CreateYogaNodes
|
||
* @param {Object} node
|
||
* @returns {Object} node with appended yoga node
|
||
*/
|
||
/**
|
||
* Creates and add yoga node to document tree
|
||
* Handles measure function for text and image nodes
|
||
*
|
||
* @returns Create yoga nodes
|
||
*/
|
||
const createYogaNodes = (page, fontStore, yoga) => (node) => {
|
||
const yogaNode = yoga.node.create();
|
||
const result = Object.assign({}, node, { yogaNode });
|
||
setYogaValues(result);
|
||
if (isLayoutElement(node) && node.children) {
|
||
const resolveChild = compose(insertYogaNodes(yogaNode), createYogaNodes(page, fontStore, yoga));
|
||
result.children = node.children.map(resolveChild);
|
||
}
|
||
setMeasureFunc(result, page, fontStore);
|
||
return result;
|
||
};
|
||
/**
|
||
* Performs yoga calculation
|
||
*
|
||
* @param page - Page node
|
||
* @returns Page node
|
||
*/
|
||
const calculateLayout = (page) => {
|
||
page.yogaNode.calculateLayout();
|
||
return page;
|
||
};
|
||
/**
|
||
* Saves Yoga layout result into 'box' attribute of node
|
||
*
|
||
* @param node
|
||
* @returns Node with box data
|
||
*/
|
||
const persistDimensions = (node) => {
|
||
if (isTextInstance$1(node))
|
||
return node;
|
||
const box = Object.assign(getPadding(node), getMargin(node), getBorderWidth(node), getPosition(node), getDimension(node));
|
||
const newNode = Object.assign({}, node, { box });
|
||
if (!node.children)
|
||
return newNode;
|
||
const children = node.children.map(persistDimensions);
|
||
return Object.assign({}, newNode, { children });
|
||
};
|
||
/**
|
||
* Removes yoga node from document tree
|
||
*
|
||
* @param node
|
||
* @returns Node without yoga node
|
||
*/
|
||
const destroyYogaNodes = (node) => {
|
||
const newNode = Object.assign({}, node);
|
||
delete newNode.yogaNode;
|
||
if (!node.children)
|
||
return newNode;
|
||
const children = node.children.map(destroyYogaNodes);
|
||
return Object.assign({}, newNode, { children });
|
||
};
|
||
/**
|
||
* Free yoga node from document tree
|
||
*
|
||
* @param node
|
||
* @returns Node without yoga node
|
||
*/
|
||
const freeYogaNodes = (node) => {
|
||
if (node.yogaNode)
|
||
node.yogaNode.freeRecursive();
|
||
return node;
|
||
};
|
||
/**
|
||
* Calculates page object layout using Yoga.
|
||
* Takes node values from 'box' and 'style' attributes, and persist them back into 'box'
|
||
* Destroy yoga values at the end.
|
||
*
|
||
* @param page - Object
|
||
* @returns Page object with correct 'box' layout attributes
|
||
*/
|
||
const resolvePageDimensions = (page, fontStore, yoga) => {
|
||
if (isNil(page))
|
||
return null;
|
||
return compose(destroyYogaNodes, freeYogaNodes, persistDimensions, calculateLayout, createYogaNodes(page, fontStore, yoga))(page);
|
||
};
|
||
/**
|
||
* Calculates root object layout using Yoga.
|
||
*
|
||
* @param node - Root object
|
||
* @param fontStore - Font store
|
||
* @returns Root object with correct 'box' layout attributes
|
||
*/
|
||
const resolveDimensions = (node, fontStore) => {
|
||
if (!node.children)
|
||
return node;
|
||
const resolveChild = (child) => resolvePageDimensions(child, fontStore, node.yoga);
|
||
const children = node.children.map(resolveChild);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
|
||
const isText$1 = (node) => node.type === P.Text;
|
||
// Prevent splitting elements by low decimal numbers
|
||
const SAFETY_THRESHOLD = 0.001;
|
||
const assingChildren = (children, node) => Object.assign({}, node, { children });
|
||
const getTop = (node) => node.box?.top || 0;
|
||
const allFixed = (nodes) => nodes.every(isFixed);
|
||
const isDynamic = (node) => node.props && 'render' in node.props;
|
||
const relayoutPage = compose(resolveTextLayout, resolvePageDimensions, resolveInheritance, resolvePageStyles);
|
||
const warnUnavailableSpace = (node) => {
|
||
console.warn(`Node of type ${node.type} can't wrap between pages and it's bigger than available page height`);
|
||
};
|
||
const splitNodes = (height, contentArea, nodes) => {
|
||
const currentChildren = [];
|
||
const nextChildren = [];
|
||
for (let i = 0; i < nodes.length; i += 1) {
|
||
const child = nodes[i];
|
||
const futureNodes = nodes.slice(i + 1);
|
||
const futureFixedNodes = futureNodes.filter(isFixed);
|
||
const nodeTop = getTop(child);
|
||
const nodeHeight = child.box.height;
|
||
const isOutside = height <= nodeTop;
|
||
const shouldBreak$1 = shouldBreak(child, futureNodes, height);
|
||
const shouldSplit = height + SAFETY_THRESHOLD < nodeTop + nodeHeight;
|
||
const canWrap = getWrap(child);
|
||
const fitsInsidePage = nodeHeight <= contentArea;
|
||
if (isFixed(child)) {
|
||
nextChildren.push(child);
|
||
currentChildren.push(child);
|
||
continue;
|
||
}
|
||
if (isOutside) {
|
||
const box = Object.assign({}, child.box, { top: child.box.top - height });
|
||
const next = Object.assign({}, child, { box });
|
||
nextChildren.push(next);
|
||
continue;
|
||
}
|
||
if (!fitsInsidePage && !canWrap) {
|
||
currentChildren.push(child);
|
||
nextChildren.push(...futureNodes);
|
||
warnUnavailableSpace(child);
|
||
break;
|
||
}
|
||
if (shouldBreak$1) {
|
||
const box = Object.assign({}, child.box, { top: child.box.top - height });
|
||
const props = Object.assign({}, child.props, {
|
||
wrap: true,
|
||
break: false,
|
||
});
|
||
const next = Object.assign({}, child, { box, props });
|
||
currentChildren.push(...futureFixedNodes);
|
||
nextChildren.push(next, ...futureNodes);
|
||
break;
|
||
}
|
||
if (shouldSplit) {
|
||
const [currentChild, nextChild] = split(child, height, contentArea);
|
||
// All children are moved to the next page, it doesn't make sense to show the parent on the current page
|
||
if (child.children.length > 0 && currentChild.children.length === 0) {
|
||
// But if the current page is empty then we can just include the parent on the current page
|
||
if (currentChildren.length === 0) {
|
||
currentChildren.push(child, ...futureFixedNodes);
|
||
nextChildren.push(...futureNodes);
|
||
}
|
||
else {
|
||
const box = Object.assign({}, child.box, {
|
||
top: child.box.top - height,
|
||
});
|
||
const next = Object.assign({}, child, { box });
|
||
currentChildren.push(...futureFixedNodes);
|
||
nextChildren.push(next, ...futureNodes);
|
||
}
|
||
break;
|
||
}
|
||
if (currentChild)
|
||
currentChildren.push(currentChild);
|
||
if (nextChild)
|
||
nextChildren.push(nextChild);
|
||
continue;
|
||
}
|
||
currentChildren.push(child);
|
||
}
|
||
return [currentChildren, nextChildren];
|
||
};
|
||
const splitChildren = (height, contentArea, node) => {
|
||
const children = node.children || [];
|
||
const availableHeight = height - getTop(node);
|
||
return splitNodes(availableHeight, contentArea, children);
|
||
};
|
||
const splitView = (node, height, contentArea) => {
|
||
const [currentNode, nextNode] = splitNode(node, height);
|
||
const [currentChilds, nextChildren] = splitChildren(height, contentArea, node);
|
||
return [
|
||
assingChildren(currentChilds, currentNode),
|
||
assingChildren(nextChildren, nextNode),
|
||
];
|
||
};
|
||
const split = (node, height, contentArea) => isText$1(node) ? splitText(node, height) : splitView(node, height, contentArea);
|
||
const shouldResolveDynamicNodes = (node) => {
|
||
const children = node.children || [];
|
||
return isDynamic(node) || children.some(shouldResolveDynamicNodes);
|
||
};
|
||
const resolveDynamicNodes = (props, node) => {
|
||
const isNodeDynamic = isDynamic(node);
|
||
// Call render prop on dynamic nodes and append result to children
|
||
const resolveChildren = (children = []) => {
|
||
if (isNodeDynamic) {
|
||
const res = node.props.render(props);
|
||
return (createInstances(res)
|
||
.filter(Boolean)
|
||
// @ts-expect-error rework dynamic nodes. conflicting types
|
||
.map((n) => resolveDynamicNodes(props, n)));
|
||
}
|
||
return children.map((c) => resolveDynamicNodes(props, c));
|
||
};
|
||
// We reset dynamic text box so it can be computed again later on
|
||
const resetHeight = isNodeDynamic && isText$1(node);
|
||
const box = resetHeight ? { ...node.box, height: 0 } : node.box;
|
||
const children = resolveChildren(node.children);
|
||
// @ts-expect-error handle text here specifically
|
||
const lines = isNodeDynamic ? null : node.lines;
|
||
return Object.assign({}, node, { box, lines, children });
|
||
};
|
||
const resolveDynamicPage = (props, page, fontStore, yoga) => {
|
||
if (shouldResolveDynamicNodes(page)) {
|
||
const resolvedPage = resolveDynamicNodes(props, page);
|
||
return relayoutPage(resolvedPage, fontStore, yoga);
|
||
}
|
||
return page;
|
||
};
|
||
const splitPage = (page, pageNumber, fontStore, yoga) => {
|
||
const wrapArea = getWrapArea(page);
|
||
const contentArea = getContentArea(page);
|
||
const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore, yoga);
|
||
const height = page.style.height;
|
||
const [currentChilds, nextChilds] = splitNodes(wrapArea, contentArea, dynamicPage.children);
|
||
const relayout = (node) =>
|
||
// @ts-expect-error rework pagination
|
||
relayoutPage(node, fontStore, yoga);
|
||
const currentBox = { ...page.box, height };
|
||
const currentPage = relayout(Object.assign({}, page, { box: currentBox, children: currentChilds }));
|
||
if (nextChilds.length === 0 || allFixed(nextChilds))
|
||
return [currentPage, null];
|
||
const nextBox = omit('height', page.box);
|
||
const nextProps = omit('bookmark', page.props);
|
||
const nextPage = relayout(Object.assign({}, page, {
|
||
props: nextProps,
|
||
box: nextBox,
|
||
children: nextChilds,
|
||
}));
|
||
return [currentPage, nextPage];
|
||
};
|
||
const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => {
|
||
const totalPages = pages.length;
|
||
const props = {
|
||
totalPages,
|
||
pageNumber: pageNumber + 1,
|
||
subPageNumber: page.subPageNumber + 1,
|
||
subPageTotalPages: page.subPageTotalPages,
|
||
};
|
||
return resolveDynamicPage(props, page, fontStore, yoga);
|
||
};
|
||
const assocSubPageData = (subpages) => {
|
||
return subpages.map((page, i) => ({
|
||
...page,
|
||
subPageNumber: i,
|
||
subPageTotalPages: subpages.length,
|
||
}));
|
||
};
|
||
const dissocSubPageData = (page) => {
|
||
return omit(['subPageNumber', 'subPageTotalPages'], page);
|
||
};
|
||
const paginate = (page, pageNumber, fontStore, yoga) => {
|
||
if (!page)
|
||
return [];
|
||
if (page.props?.wrap === false)
|
||
return [page];
|
||
let splittedPage = splitPage(page, pageNumber, fontStore, yoga);
|
||
const pages = [splittedPage[0]];
|
||
let nextPage = splittedPage[1];
|
||
while (nextPage !== null) {
|
||
splittedPage = splitPage(nextPage, pageNumber + pages.length, fontStore, yoga);
|
||
pages.push(splittedPage[0]);
|
||
nextPage = splittedPage[1];
|
||
}
|
||
return pages;
|
||
};
|
||
/**
|
||
* Performs pagination. This is the step responsible of breaking the whole document
|
||
* into pages following pagiation rules, such as `fixed`, `break` and dynamic nodes.
|
||
*
|
||
* @param root - Document node
|
||
* @param fontStore - Font store
|
||
* @returns Layout node
|
||
*/
|
||
const resolvePagination = (root, fontStore) => {
|
||
let pages = [];
|
||
let pageNumber = 1;
|
||
for (let i = 0; i < root.children.length; i += 1) {
|
||
const page = root.children[i];
|
||
let subpages = paginate(page, pageNumber, fontStore, root.yoga);
|
||
subpages = assocSubPageData(subpages);
|
||
pageNumber += subpages.length;
|
||
pages = pages.concat(subpages);
|
||
}
|
||
pages = pages.map((...args) => dissocSubPageData(resolvePageIndices(fontStore, root.yoga, ...args)));
|
||
return assingChildren(pages, root);
|
||
};
|
||
|
||
/**
|
||
* Translates page percentage horizontal paddings in fixed ones
|
||
*
|
||
* @param container - Page container
|
||
* @returns Resolve page horizontal padding
|
||
*/
|
||
const resolvePageHorizontalPadding = (container) => (value) => {
|
||
const match = matchPercent(value);
|
||
const width = container.width;
|
||
return match ? match.percent * width : value;
|
||
};
|
||
/**
|
||
* Translates page percentage vertical paddings in fixed ones
|
||
*
|
||
* @param container - Page container
|
||
* @returns Resolve page vertical padding
|
||
*/
|
||
const resolvePageVerticalPadding = (container) => (value) => {
|
||
const match = matchPercent(value);
|
||
const height = container.height;
|
||
return match ? match.percent * height : value;
|
||
};
|
||
/**
|
||
* Translates page percentage paddings in fixed ones
|
||
*
|
||
* @param page
|
||
* @returns Page with fixed paddings
|
||
*/
|
||
const resolvePagePaddings = (page) => {
|
||
const container = page.style;
|
||
const style = evolve({
|
||
paddingTop: resolvePageVerticalPadding(container),
|
||
paddingLeft: resolvePageHorizontalPadding(container),
|
||
paddingRight: resolvePageHorizontalPadding(container),
|
||
paddingBottom: resolvePageVerticalPadding(container),
|
||
}, page.style);
|
||
return Object.assign({}, page, { style });
|
||
};
|
||
/**
|
||
* Translates all pages percentage paddings in fixed ones
|
||
* This has to be computed from pages calculated size and not by Yoga
|
||
* because at this point we didn't performed pagination yet.
|
||
*
|
||
* @param root - Document root
|
||
* @returns Document root with translated page paddings
|
||
*/
|
||
const resolvePagesPaddings = (root) => {
|
||
if (!root.children)
|
||
return root;
|
||
const children = root.children.map(resolvePagePaddings);
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
|
||
const resolveRadius = (box) => (value) => {
|
||
if (!value)
|
||
return undefined;
|
||
const match = matchPercent(value);
|
||
return match ? match.percent * Math.min(box.width, box.height) : value;
|
||
};
|
||
/**
|
||
* Transforms percent border radius into fixed values
|
||
*
|
||
* @param node
|
||
* @returns Node
|
||
*/
|
||
const resolvePercentRadius = (node) => {
|
||
const style = evolve({
|
||
borderTopLeftRadius: resolveRadius(node.box),
|
||
borderTopRightRadius: resolveRadius(node.box),
|
||
borderBottomRightRadius: resolveRadius(node.box),
|
||
borderBottomLeftRadius: resolveRadius(node.box),
|
||
}, node.style || {});
|
||
const newNode = Object.assign({}, node, { style });
|
||
if (!node.children)
|
||
return newNode;
|
||
const children = node.children.map(resolvePercentRadius);
|
||
return Object.assign({}, newNode, { children });
|
||
};
|
||
|
||
/**
|
||
* Transform percent height into fixed
|
||
*
|
||
* @param height
|
||
* @returns Height
|
||
*/
|
||
const transformHeight = (pageArea, height) => {
|
||
const match = matchPercent(height);
|
||
return match ? match.percent * pageArea : height;
|
||
};
|
||
/**
|
||
* Get page area (height minus paddings)
|
||
*
|
||
* @param page
|
||
* @returns Page area
|
||
*/
|
||
const getPageArea = (page) => {
|
||
const pageHeight = page.style.height;
|
||
const pagePaddingTop = (page.style?.paddingTop || 0);
|
||
const pagePaddingBottom = (page.style?.paddingBottom || 0);
|
||
return pageHeight - pagePaddingTop - pagePaddingBottom;
|
||
};
|
||
/**
|
||
* Transform node percent height to fixed
|
||
*
|
||
* @param page
|
||
* @param node
|
||
* @returns Transformed node
|
||
*/
|
||
const resolveNodePercentHeight = (page, node) => {
|
||
if (isNil(page.style?.height))
|
||
return node;
|
||
if (isNil(node.style?.height))
|
||
return node;
|
||
const pageArea = getPageArea(page);
|
||
const height = transformHeight(pageArea, node.style.height);
|
||
const style = Object.assign({}, node.style, { height });
|
||
return Object.assign({}, node, { style });
|
||
};
|
||
/**
|
||
* Transform page immediate children with percent height to fixed
|
||
*
|
||
* @param page
|
||
* @returns Transformed page
|
||
*/
|
||
const resolvePagePercentHeight = (page) => {
|
||
if (!page.children)
|
||
return page;
|
||
const resolveChild = (child) => resolveNodePercentHeight(page, child);
|
||
const children = page.children.map(resolveChild);
|
||
return Object.assign({}, page, { children });
|
||
};
|
||
/**
|
||
* Transform all page immediate children with percent height to fixed.
|
||
* This is needed for computing correct dimensions on pre-pagination layout.
|
||
*
|
||
* @param root - Document root
|
||
* @returns Transformed document root
|
||
*/
|
||
const resolvePercentHeight = (root) => {
|
||
if (!root.children)
|
||
return root;
|
||
const children = root.children.map(resolvePagePercentHeight);
|
||
return Object.assign({}, root, { children });
|
||
};
|
||
|
||
const isType = (type) => (node) => node.type === type;
|
||
const isLink = isType(P.Link);
|
||
const isText = isType(P.Text);
|
||
const isTextInstance = isType(P.TextInstance);
|
||
/**
|
||
* Checks if node has render prop
|
||
*
|
||
* @param node
|
||
* @returns Has render prop?
|
||
*/
|
||
const hasRenderProp = (node) => 'render' in node.props;
|
||
/**
|
||
* Checks if node is text type (Text or TextInstance)
|
||
*
|
||
* @param node
|
||
* @returns Are all children text instances?
|
||
*/
|
||
const isTextType = (node) => isText(node) || isTextInstance(node);
|
||
/**
|
||
* Checks if is tet link that needs to be wrapped in Text
|
||
*
|
||
* @param node
|
||
* @returns Are all children text instances?
|
||
*/
|
||
const isTextLink = (node) => {
|
||
const children = node.children || [];
|
||
// Text string inside a Link
|
||
if (children.every(isTextInstance))
|
||
return true;
|
||
// Text node inside a Link
|
||
if (children.every(isText))
|
||
return false;
|
||
return children.every(isTextType);
|
||
};
|
||
/**
|
||
* Wraps node children inside Text node
|
||
*
|
||
* @param node
|
||
* @returns Node with intermediate Text child
|
||
*/
|
||
const wrapText = (node) => {
|
||
const textElement = {
|
||
type: P.Text,
|
||
props: {},
|
||
style: {},
|
||
box: {},
|
||
children: node.children,
|
||
};
|
||
return Object.assign({}, node, { children: [textElement] });
|
||
};
|
||
const transformLink = (node) => {
|
||
if (!isLink(node))
|
||
return node;
|
||
// If has render prop substitute the instance by a Text, that will
|
||
// ultimately render the inline Link via the textkit PDF renderer.
|
||
if (hasRenderProp(node))
|
||
return Object.assign({}, node, { type: P.Text });
|
||
// If is a text link (either contains Text or TextInstance), wrap it
|
||
// inside a Text element so styles are applied correctly
|
||
if (isTextLink(node))
|
||
return wrapText(node);
|
||
return node;
|
||
};
|
||
/**
|
||
* Transforms Link layout to correctly render text and dynamic rendered links
|
||
*
|
||
* @param node
|
||
* @returns Node with link substitution
|
||
*/
|
||
const resolveLinkSubstitution = (node) => {
|
||
if (!node.children)
|
||
return node;
|
||
const resolveChild = compose(transformLink, resolveLinkSubstitution);
|
||
const children = node.children.map(resolveChild);
|
||
return Object.assign({}, node, { children });
|
||
};
|
||
|
||
const layout = asyncCompose(resolveZIndex, resolveOrigin, resolveAssets, resolvePagination, resolveTextLayout, resolvePercentRadius, resolveDimensions, resolveSvg, resolveAssets, resolveInheritance, resolvePercentHeight, resolvePagesPaddings, resolveStyles, resolveLinkSubstitution, resolveBookmarks, resolvePageSizes, resolveYoga);
|
||
|
||
export { layout as default };
|