Leon4gr45's picture
Include updated package-lock.json (part 12)
391a73c verified
Raw
History Blame Contribute Delete
105 kB
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-xs';
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$1 = (node) => node.type === P.Defs;
const getDefs = (node) => {
const children = node.children || [];
const defs = children.find(isDefs$1);
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$1 = /url\(['"]?#([^'"]+)['"]?\)/;
const replaceDef = (defs, value) => {
if (!value)
return undefined;
if (!URL_REGEX$1.test(value))
return value;
const match = value.match(URL_REGEX$1);
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 markerStart = `markerStart` in props
? replaceDef(defs, props?.markerStart)
: undefined;
const markerMid = `markerMid` in props
? replaceDef(defs, props?.markerMid)
: undefined;
const markerEnd = `markerEnd` in props
? replaceDef(defs, props?.markerEnd)
: undefined;
const newProps = Object.assign({}, node.props, {
fill,
clipPath,
markerStart,
markerMid,
markerEnd,
});
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 isMarker = (node) => node.type === P.Marker;
const STYLE_PROPS = [
'width',
'height',
'color',
'stroke',
'strokeWidth',
'opacity',
'fillOpacity',
'strokeOpacity',
'fill',
'fillRule',
'clipPath',
'offset',
'transform',
'strokeLinejoin',
'strokeLinecap',
'strokeDasharray',
'gradientUnits',
'gradientTransform',
'stopColor',
'stopOpacity',
];
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;
};
// Skip transformColor for url() references (gradients, patterns)
const URL_REGEX = /^url\(/;
const transformColorSafe = (value) => {
if (typeof value === 'string' && URL_REGEX.test(value)) {
return value;
}
return transformColor(value);
};
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,
fontSize: parseFloat$1,
strokeWidth: parseFloat$1,
strokeMiterlimit: parseFloat$1,
strokeDashoffset: parseFloat$1,
offset: parsePercent,
fill: transformColorSafe,
fillOpacity: parsePercent,
opacity: parsePercent,
stroke: transformColorSafe,
strokeOpacity: parsePercent,
stopOpacity: parsePercent,
stopColor: transformColor,
transform: parseTransform(container),
gradientTransform: parseTransform(container),
}, props);
return Object.assign({}, node, { props });
};
// SVG spec: if rx is specified but ry is not, ry defaults to rx (and vice versa)
const resolveRectRadius = (node) => {
if (node.type !== P.Rect || !node.props)
return node;
const { rx, ry } = node.props;
// If both are specified or neither is specified, no change needed
if ((rx && ry) || (!rx && !ry))
return node;
const newProps = Object.assign({}, node.props, {
rx: rx ?? ry,
ry: ry ?? rx,
});
return Object.assign({}, node, { props: newProps });
};
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), resolveRectRadius, pickStyleProps, addMissingTspan, removeNoneValues, mergeStyles$1);
// Gradient transforms typically don't use percentages, so use minimal container
const ZERO_CONTAINER = { width: 0, height: 0 };
const parseGradientTransform = parseTransform(ZERO_CONTAINER);
// Process DEFS children (LINEAR_GRADIENT, RADIAL_GRADIENT, STOP) without container-based percent transform
const parseDefsProps = (node) => {
const props = evolve({
// Gradient coordinates (percent becomes 0-1 range)
x1: parsePercent,
y1: parsePercent,
x2: parsePercent,
y2: parsePercent,
cx: parsePercent,
cy: parsePercent,
fx: parsePercent,
fy: parsePercent,
r: parsePercent,
gradientTransform: parseGradientTransform,
// Stop properties
offset: parsePercent,
stopColor: transformColor,
stopOpacity: parsePercent,
// Marker properties
refX: parseFloat$1,
refY: parseFloat$1,
markerWidth: parseFloat$1,
markerHeight: parseFloat$1,
viewBox: parseViewbox,
}, node.props || {});
return Object.assign({}, node, { props });
};
const getMarkerContainer = (node) => {
const props = node.props || {};
const viewBox = 'viewBox' in props
? props.viewBox
: null;
if (viewBox) {
return { width: viewBox.maxX, height: viewBox.maxY };
}
const markerWidth = 'markerWidth' in props ? props.markerWidth : 3;
const markerHeight = 'markerHeight' in props ? props.markerHeight : 3;
return { width: markerWidth, height: markerHeight };
};
const resolveMarkerChildren = (node) => {
if (!node.children)
return node;
const container = getMarkerContainer(node);
const resolveChild = compose(resolveChildren(container), resolveSvgNode(container));
const children = node.children.map(resolveChild);
return Object.assign({}, node, { children });
};
const resolveDefsChildren = (node) => {
if (!node.children)
return node;
const children = node.children.map((child) => {
const parsed = parseDefsProps(child);
if (isMarker(parsed))
return resolveMarkerChildren(parsed);
return resolveDefsChildren(parsed);
});
return Object.assign({}, node, { children });
};
const isDefs = (node) => node.type === P.Defs;
const resolveChildren = (container) => (node) => {
if (!node.children)
return node;
const resolveChild = compose(resolveChildren(container), resolveSvgNode(container));
// Process DEFS separately without container-based percent transform
const children = node.children.map((child) => isDefs(child) ? resolveDefsChildren(child) : resolveChild(child));
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);
};
const isSvgImage = (node) => node.type === P.Image && node.image?.format === 'svg';
function convertParsedNode(node) {
return {
type: node.type,
props: node.props,
style: {},
children: node.children?.map(convertParsedNode),
...('value' in node && { value: node.value }),
};
}
function convertToSvgNode(imageNode) {
const image = imageNode.image;
const width = imageNode.style?.width ?? image.width;
const height = imageNode.style?.height ?? image.height;
const viewBox = parseViewbox(image.data.props.viewBox);
return {
type: P.Svg,
props: {
width,
height,
viewBox,
preserveAspectRatio: { align: 'xMidYMid', meetOrSlice: 'meet' },
},
style: { ...imageNode.style, width, height },
box: imageNode.box,
origin: imageNode.origin,
yogaNode: imageNode.yogaNode,
children: image.data.children.map(convertParsedNode),
};
}
/**
* Pre-process SVG nodes so they can be rendered in the next steps.
* Also converts Image nodes containing SVG data into SvgNodes.
*
* @param node - Root node
* @param fontStore - Font store
* @returns Root node
*/
const resolveSvg = (node, fontStore) => {
const resolved = isSvgImage(node) ? convertToSvgNode(node) : node;
if (!('children' in resolved))
return resolved;
const resolveChild = (child) => resolveSvg(child, fontStore);
const root = isSvg$3(resolved) ? resolveSvgRoot(resolved, fontStore) : resolved;
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);
/* eslint-disable no-console */
// 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;
})
.catch((e) => {
console.warn(e, 'Failed to load emoji image');
emojis[emoji].loading = false;
}));
}
});
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;
};
/**
* Parses an HTML-compatible srcSet string into structured entries.
* Only width descriptors (e.g. "300w") are supported.
*/
const parseSrcSet = (srcSet) => {
return srcSet
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
.reduce((acc, entry) => {
const parts = entry.split(/\s+/);
const uri = parts[0];
const descriptor = parts[1];
if (descriptor && descriptor.endsWith('w')) {
const width = parseFloat(descriptor);
if (!Number.isNaN(width))
acc.push({ uri, width });
}
return acc;
}, []);
};
/**
* Selects the most appropriate image source from a parsed srcSet
* based on the target display width.
*
* Picks the smallest source that is >= targetWidth,
* or the largest available if none are large enough.
*/
const selectSource = (entries, targetWidth) => {
if (entries.length === 0)
return '';
const sorted = [...entries].sort((a, b) => a.width - b.width);
const match = sorted.find((e) => e.width >= targetWidth);
return match ? match.uri : sorted[sorted.length - 1].uri;
};
// TODO: Unify with page/getSize.ts parseValue/transformUnit into a shared unit parser
/**
* Parses a CSS length value into points.
* Supports: px, pt (default), in, mm, cm, vw, %.
* vw and % are resolved relative to containerWidth.
*/
const parseLength = (value, containerWidth) => {
const match = /^(-?\d*\.?\d+)\s*(px|pt|in|mm|cm|vw|%)?$/i.exec(value.trim());
if (!match)
return undefined;
const num = parseFloat(match[1]);
if (Number.isNaN(num))
return undefined;
const unit = match[2]?.toLowerCase();
switch (unit) {
case 'in':
return num * 72;
case 'mm':
return num * (72 / 25.4);
case 'cm':
return num * (72 / 2.54);
case 'px':
return num;
case 'vw':
return (num / 100) * containerWidth;
case '%':
return (num / 100) * containerWidth;
case 'pt':
default:
return num;
}
};
/**
* Evaluates a media condition like "(min-width: 600px)" or "(max-width: 400px)"
* against the container width.
*/
const matchesCondition = (condition, containerWidth) => {
const match = /\(\s*(min-width|max-width)\s*:\s*(-?\d*\.?\d+)\s*(px|pt|in|mm|cm|vw|%)?\s*\)/i.exec(condition);
if (!match)
return false;
const feature = match[1].toLowerCase();
const threshold = parseLength(`${match[2]}${match[3] || ''}`, containerWidth);
if (threshold == null)
return false;
if (feature === 'min-width')
return containerWidth >= threshold;
if (feature === 'max-width')
return containerWidth <= threshold;
return false;
};
/**
* Parses sizes attribute to get the target display width.
*
* Accepts a number (used directly as points) or a string.
* For strings, evaluates media conditions against containerWidth
* and returns the first matching entry's length value.
*
* Format: "(media-condition) length, (media-condition) length, default-length"
*/
const parseSizes = (sizes, containerWidth) => {
if (sizes == null)
return undefined;
if (typeof sizes === 'number')
return sizes;
const entries = sizes.split(',').map((s) => s.trim());
for (const entry of entries) {
const conditionMatch = /^\(([^)]+)\)\s+(.+)$/.exec(entry);
if (conditionMatch) {
const condition = `(${conditionMatch[1]})`;
const lengthStr = conditionMatch[2];
if (matchesCondition(condition, containerWidth)) {
return parseLength(lengthStr, containerWidth);
}
}
else {
return parseLength(entry, containerWidth);
}
}
return undefined;
};
/**
* Get image source, resolving srcSet if present.
*
* @param node - Image node
* @param pageWidth - Page width for srcSet resolution
* @returns Image src
*/
const getSource = (node, pageWidth) => {
const { srcSet, sizes } = node.props;
if (srcSet) {
const entries = parseSrcSet(srcSet);
if (entries.length > 0) {
const targetWidth = parseSizes(sizes, pageWidth) || pageWidth;
const uri = selectSource(entries, targetWidth);
if (uri)
return uri;
}
}
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 appends data to node.
* Ideally this fn should be immutable.
*
* @param node - Image or ImageBackground node
* @param pageWidth - Page width for srcSet resolution
*/
const fetchImage = async (node, pageWidth) => {
const { cache } = node.props;
const src = getSource(node, pageWidth);
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;
const isImageBackground = (node) => node.type === P.ImageBackground;
/**
* Get all asset promises that need to be resolved
*
* @param fontStore - Font store
* @param node - Root node
* @returns Asset promises
*/
const fetchAssets = (fontStore, node, pageWidth) => {
const promises = [];
const listToExplore = [node];
const emojiSource = fontStore ? fontStore.getEmojiSource() : null;
while (listToExplore.length > 0) {
const n = listToExplore.shift();
if (isImage$2(n) || isImageBackground(n)) {
promises.push(fetchImage(n, pageWidth));
}
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 assets for a page
*
* @param fontStore - Font store
* @param page - Page node
* @returns Asset promises
*/
const fetchPageAssets = (fontStore, page) => {
const pageWidth = page.style?.width;
return page.children
?.map((child) => fetchAssets(fontStore, child, pageWidth))
.flat();
};
/**
* 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 = node.children
.map((page) => fetchPageAssets(fontStore, page))
.flat();
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 });
};
/**
* Normalize bookmark value, expanding a plain string title into a full
* bookmark object with default fit and expanded values
*
* @param bookmark - Bookmark value
* @returns Normalized bookmark object
*/
const getBookmarkValue = (bookmark) => {
return typeof bookmark === 'string'
? { title: bookmark, fit: false, expanded: false }
: bookmark;
};
/**
* Traverse document tree and resolve bookmark hierarchy, assigning each
* bookmark a ref index and a reference to its nearest bookmark ancestor
*
* @param node - Document node
* @returns Document node with resolved bookmarks
*/
const resolveBookmarks = (node) => {
let refs = 0;
const listToExplore = (node.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 && child.props.bookmark) {
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 || value === 'auto')
return undefined;
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) => {
const result = { width: transformUnit(width, dpi) || 0 };
const h = transformUnit(height, dpi);
if (h !== undefined)
result.height = h;
return result;
};
/**
* 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) => {
const result = { width: v.height || 0 };
if (v.width !== undefined)
result.height = v.width;
return result;
};
/**
* 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,
},
props: {
...node.props,
bookmark: null,
},
});
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) => {
if (elements.length === 0)
return null;
return 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 nonFixedFuture = futureElements.filter((node) => !('fixed' in node.props));
const endOfFurthestFutureElement = getFurthestEnd(nonFixedFuture);
// When there are no future non-fixed siblings, use only minPresenceAhead
if (endOfFurthestFutureElement === null)
return afterMinPresenceAhead;
return Math.min(afterMinPresenceAhead, endOfFurthestFutureElement);
};
const shouldBreak = (child, futureElements, height, previousElements) => {
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 = previousElements.filter((node) => !isFixed(node)).length > 0;
return (getBreak(child) ||
(shouldSplit && !canWrap) ||
(!shouldSplit && endOfPresence > height && breakingImprovesPresence));
};
const IGNORABLE_CODEPOINTS = [
8232, // LINE_SEPARATOR
8233, // PARAGRAPH_SEPARATOR
8288, // WORD_JOINER
];
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 isRasterImage = (node) => !!node.image && node.image.format !== 'svg';
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) && isRasterImage(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 && !isImage$1(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), width };
}
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, currentChildren);
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 };