@storyblok/richtext
Version:
Storyblok RichText Resolver
818 lines (808 loc) • 24.5 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region src/static/dynamic-resolvers.ts
const resolveHeadingTag = (attrs) => {
return `h${typeof attrs?.level === "number" ? Math.min(6, Math.max(1, attrs.level)) : 1}`;
};
//#endregion
//#region src/static/render-map.generated.ts
/**
* Render config for Tiptap nodes
*/
const NODE_RENDER_MAP = {
paragraph: {
"tag": "p",
"content": true
},
doc: null,
text: null,
blockquote: {
"tag": "blockquote",
"content": true
},
heading: { resolve: resolveHeadingTag },
bullet_list: {
"tag": "ul",
"content": true
},
ordered_list: {
"tag": "ol",
"attrs": { "order": 1 },
"content": true
},
list_item: {
"tag": "li",
"content": true
},
code_block: {
"tag": "pre",
"children": [{
"tag": "code",
"content": true
}]
},
hard_break: { "tag": "br" },
horizontal_rule: { "tag": "hr" },
image: { "tag": "img" },
emoji: {
"tag": "img",
"attrs": {
"style": "width: 1.25em; height: 1.25em; vertical-align: text-top;",
"draggable": "false",
"loading": "lazy"
}
},
table: {
"tag": "table",
"content": true
},
tableRow: {
"tag": "tr",
"content": true
},
tableCell: {
"tag": "td",
"content": true
},
tableHeader: {
"tag": "th",
"content": true
},
blok: {
"tag": "span",
"attrs": { "style": "display: none;" }
},
details: {
"tag": "details",
"content": true
},
detailsContent: {
"tag": "div",
"attrs": { "data-type": "detailsContent" },
"content": true
},
detailsSummary: {
"tag": "summary",
"content": true
}
};
/**
* Render config for Tiptap marks
*/
const MARK_RENDER_MAP = {
link: {
"tag": "a",
"content": true
},
bold: {
"tag": "strong",
"content": true
},
italic: {
"tag": "em",
"content": true
},
strike: {
"tag": "s",
"content": true
},
underline: {
"tag": "u",
"content": true
},
code: {
"tag": "code",
"content": true
},
superscript: {
"tag": "sup",
"content": true
},
subscript: {
"tag": "sub",
"content": true
},
highlight: {
"tag": "mark",
"content": true
},
textStyle: {
"tag": "span",
"content": true
},
anchor: {
"tag": "span",
"content": true
},
styled: {
"tag": "span",
"content": true
},
reporter: null
};
//#endregion
//#region src/static/style.ts
/**
* Converts a style object to a CSS string.
* @param style - The style object to convert.
* @returns A CSS string representation of the style object.
* @example
* const styleObj = { color: 'red', fontSize: '16px' };
* const cssString = styleToString(styleObj);
* console.log(cssString); // Output: "color: red; font-size: 16px"
*/
function styleToString(style) {
return Object.entries(style).filter(([, value]) => isValidStyleValue(value)).map(([key, value]) => `${camelToKebab(key)}: ${value};`).join(" ");
}
/**
* Converts a CSS string to a style object.
* @param style - The CSS string to convert.
* @returns A style object representation of the CSS string.
* @example
* const cssString = "color: red; font-size: 16px";
* const styleObj = stringToStyle(cssString);
* console.log(styleObj); // Output: { color: 'red', fontSize: '16px' }
*/
function stringToStyle(style) {
return style.split(";").map((rule) => rule.trim()).filter(Boolean).reduce((acc, rule) => {
const colonIdx = rule.indexOf(":");
if (colonIdx === -1) return acc;
const key = rule.slice(0, colonIdx).trim();
const value = rule.slice(colonIdx + 1).trim();
if (!key || !value) return acc;
acc[kebabToCamel(key)] = value;
return acc;
}, {});
}
function kebabToCamel(str) {
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
function camelToKebab(str) {
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
}
function isValidStyleValue(value) {
return value !== null && value !== void 0 && value !== "";
}
//#endregion
//#region src/static/attribute.ts
/**
* Maps Tiptap attribute names to CSS property names for specific element types.
*/
const STYLE_MAP = {
highlight: { color: "backgroundColor" },
textStyle: { color: "color" },
paragraph: { textAlign: "textAlign" },
heading: { textAlign: "textAlign" },
tableCell: {
backgroundColor: "backgroundColor",
colwidth: "width"
},
tableHeader: { colwidth: "width" }
};
/**
* Maps Tiptap attribute names to HTML attribute names.
*/
const DEFAULT_ATTR_MAP = {
fallbackImage: "src",
body: "data-body",
colspan: "colSpan",
rowspan: "rowSpan",
name: "data-name",
emoji: "data-emoji"
};
/**
* Attributes that should be excluded from the output.
*/
const EXCLUDED_ATTRS = new Set([
"level",
"linktype",
"uuid",
"anchor",
"meta_data",
"copyright",
"source"
]);
/**
* Resolves the href for Storyblok link types (story, email).
* @returns The resolved href, or undefined if no special handling is needed.
*/
function resolveStoryblokLinkHref(attrs) {
const { linktype, href, anchor } = attrs;
if (linktype === "story") return `${typeof href === "string" ? href : ""}${typeof anchor === "string" && anchor ? `#${anchor}` : ""}`;
if (linktype === "email" && typeof href === "string") return `mailto:${href.replace(/^mailto:/, "")}`;
}
/**
* Extracts static attributes and styles from the render map for a given element type.
*/
function getStaticAttrsFromRenderMap(type) {
const staticStyle = {};
let staticAttrs = {};
if (!(type in NODE_RENDER_MAP)) return {
staticAttrs,
staticStyle
};
const renderMap = NODE_RENDER_MAP[type];
if (!renderMap || !("attrs" in renderMap)) return {
staticAttrs,
staticStyle
};
const renderAttrs = renderMap.attrs || {};
const rawStyle = "style" in renderAttrs && typeof renderAttrs.style === "string" ? renderAttrs.style : "";
const { style: _style, ...rest } = renderAttrs;
staticAttrs = rest;
if (rawStyle) Object.assign(staticStyle, stringToStyle(rawStyle));
return {
staticAttrs,
staticStyle
};
}
/**
* Converts an attribute value to a CSS value based on the style map.
* Handles arrays (e.g., colwidth) and primitive values.
*/
function convertToStyleValue(value) {
if (Array.isArray(value)) return value[0] != null ? `${value[0]}px` : void 0;
if (typeof value === "number" || typeof value === "string") return value;
return String(value);
}
/**
* Processes a single attribute and adds it to either the style or rest object.
*/
function processAttribute(key, value, type, styleMap, attrMap, style, rest) {
if (!isValidStyleValue(value) || EXCLUDED_ATTRS.has(key)) return;
if (key in styleMap) {
const cssProp = styleMap[key];
const cssValue = convertToStyleValue(value);
if (cssValue !== void 0 && isValidStyleValue(cssValue)) style[cssProp] = cssValue;
return;
}
const attrName = attrMap[key] ?? key;
if (attrName === "custom" && type === "link" && typeof value === "object" && value !== null) {
for (const [customKey, customValue] of Object.entries(value)) rest[customKey] = String(customValue);
return;
}
if (typeof value === "object" && value !== null) {
rest[attrName] = JSON.stringify(value);
return;
}
rest[attrName] = value;
}
/**
* Process Tiptap attributes into HTML attributes and inline styles.
* Applies internal style mappings and allows extending or overriding
* default attribute mappings via `extendAttrMap`.
*
* @param type - {@link SbRichTextElement}
* @param attrs - Attributes from the node/mark
* @param extendAttrMap - {@link AttrMap} Additional attribute mappings (overrides defaults)
* @returns Processed attributes with optional `style` object
*/
function processAttrs(type, attrs = {}, extendAttrMap = {}) {
const { staticAttrs, staticStyle } = getStaticAttrsFromRenderMap(type);
const style = { ...staticStyle };
const rest = {};
const styleMap = STYLE_MAP[type] || {};
const attrMap = {
...DEFAULT_ATTR_MAP,
...extendAttrMap
};
const mergedAttrs = {
...attrs,
...staticAttrs
};
for (const [key, value] of Object.entries(mergedAttrs)) processAttribute(key, value, type, styleMap, attrMap, style, rest);
if (type === "link") {
const linkHref = resolveStoryblokLinkHref(attrs);
if (linkHref !== void 0) rest.href = linkHref;
}
return {
...rest,
...Object.keys(style).length > 0 && { style }
};
}
/**
* Escapes special HTML characters in attribute values.
*/
const escapeAttr = (value) => String(value).replace(/[&"'<>]/g, (char) => {
switch (char) {
case "&": return "&";
case "\"": return """;
case "'": return "'";
case "<": return "<";
case ">": return ">";
default: return char;
}
});
//#endregion
//#region src/utils/index.ts
/**
* Deep equality comparison for plain objects, arrays, and primitives.
*/
function deepEqual(a, b) {
if (a === b) return true;
if (a === null || a === void 0 || b === null || b === void 0) return a === b;
if (typeof a !== typeof b) return false;
if (typeof a !== "object") return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (Array.isArray(a)) {
if (a.length !== b.length) return false;
return a.every((v, i) => deepEqual(v, b[i]));
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
}
const SELF_CLOSING_TAGS = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr"
];
/**
* Escapes HTML entities in a string.
*
* @param {string} unsafeText
* @return {*} {string}
*
* @example
*
* ```typescript
* const unsafeText = '<script>alert("Hello")<\/script>'
*
* const safeText = escapeHtml(unsafeText)
*
* console.log(safeText) // '<script>alert("Hello")</script>'
* ```
*/
function escapeHtml(unsafeText) {
return unsafeText.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
//#endregion
//#region src/static/node-helpers.ts
/**
* Gets the link mark from a text node, or null if not present.
* @param node - The node to check
* @returns The link mark if found, null otherwise
*/
function getTextNodeLinkMark(node) {
if (node.type !== "text" || !node.marks) return null;
for (const mark of node.marks) if (mark.type === "link") return mark;
return null;
}
/**
* Checks if two link marks have identical attributes.
* Used for merging adjacent text nodes with the same link.
* @param markA - First link mark
* @param markB - Second link mark
* @returns True if the marks have identical attributes
*/
function areLinkMarksEqual(markA, markB) {
if (!markA || !markB) return false;
return deepEqual(markA.attrs ?? {}, markB.attrs ?? {});
}
/**
* Gets non-link marks from a text node.
* Used when rendering text inside a merged link group.
* @param node - The text node
* @returns Array of marks excluding the link mark
*/
function getInnerMarks(node) {
if (node.type !== "text" || !node.marks) return [];
return node.marks.filter((m) => m.type !== "link");
}
/**
* Identifies groups of adjacent text nodes that share the same link mark.
* Returns an array of groups where each group is either:
* - A single non-text node or text node without link
* - Multiple consecutive text nodes with identical link marks
*
* @param children - Array of child nodes to group
* @returns Array of node groups for rendering
*/
function groupLinkNodes(children) {
const groups = [];
let i = 0;
const len = children.length;
while (i < len) {
const node = children[i];
const linkMark = getTextNodeLinkMark(node);
if (linkMark) {
const groupNodes = [node];
let end = i + 1;
while (end < len && areLinkMarksEqual(linkMark, getTextNodeLinkMark(children[end]))) {
groupNodes.push(children[end]);
end++;
}
groups.push({
nodes: groupNodes,
linkMark
});
i = end;
} else {
groups.push({
nodes: [node],
linkMark: null
});
i++;
}
}
return groups;
}
/**
* Checks if a table row contains only tableHeader cells.
* Used to determine which rows belong in thead vs tbody.
* @param row - The table row node to check
* @returns True if all cells are tableHeader type
*/
function isTableHeaderRow(row) {
const cells = row.content;
if (!cells?.length) return false;
for (const cell of cells) if (cell.type !== "tableHeader") return false;
return true;
}
/**
* Splits table rows into header rows and body rows.
* Header rows are contiguous tableHeader rows at the start.
* @param rows - Array of table row nodes
* @returns Object with headerRows and bodyRows arrays
*/
function splitTableRows(rows) {
if (!rows?.length) return {
headerRows: [],
bodyRows: []
};
let headerEnd = 0;
while (headerEnd < rows.length && isTableHeaderRow(rows[headerEnd])) headerEnd++;
return {
headerRows: rows.slice(0, headerEnd),
bodyRows: rows.slice(headerEnd)
};
}
//#endregion
//#region src/images-optimization.ts
function optimizeImage(src, options) {
if (!options) return {
src,
attrs: {}
};
let w = 0;
let h = 0;
const attrs = {};
const filterParams = [];
function validateAndPushFilterParam(value, min, max, filter, filterParams) {
if (typeof value !== "number" || value <= min || value >= max) console.warn(`[StoryblokRichText] - ${filter.charAt(0).toUpperCase() + filter.slice(1)} value must be a number between ${min} and ${max} (inclusive)`);
else filterParams.push(`${filter}(${value})`);
}
if (typeof options === "object") {
if (options.width !== void 0) if (typeof options.width === "number" && options.width >= 0) {
attrs.width = options.width;
w = options.width;
} else console.warn("[StoryblokRichText] - Width value must be a number greater than or equal to 0");
if (options.height !== void 0) if (typeof options.height === "number" && options.height >= 0) {
attrs.height = options.height;
h = options.height;
} else console.warn("[StoryblokRichText] - Height value must be a number greater than or equal to 0");
if (options.height === 0 && options.width === 0) {
delete attrs.width;
delete attrs.height;
console.warn("[StoryblokRichText] - Width and height values cannot both be 0");
}
if (options.loading && ["lazy", "eager"].includes(options.loading)) attrs.loading = options.loading;
if (options.class) attrs.class = options.class;
if (options.filters) {
const { filters } = options || {};
const { blur, brightness, fill, format, grayscale, quality, rotate } = filters || {};
if (blur) validateAndPushFilterParam(blur, 0, 100, "blur", filterParams);
if (quality) validateAndPushFilterParam(quality, 0, 100, "quality", filterParams);
if (brightness) validateAndPushFilterParam(brightness, 0, 100, "brightness", filterParams);
if (fill) filterParams.push(`fill(${fill})`);
if (grayscale) filterParams.push(`grayscale()`);
if (rotate && [
0,
90,
180,
270
].includes(options.filters.rotate || 0)) filterParams.push(`rotate(${rotate})`);
if (format && [
"webp",
"png",
"jpeg"
].includes(format)) filterParams.push(`format(${format})`);
}
if (options.srcset) attrs.srcset = options.srcset.map((entry) => {
if (typeof entry === "number") return `${src}/m/${entry}x0/${filterParams.length > 0 ? `filters:${filterParams.join(":")}` : ""} ${entry}w`;
if (Array.isArray(entry) && entry.length === 2) {
const [entryWidth, entryHeight] = entry;
return `${src}/m/${entryWidth}x${entryHeight}/${filterParams.length > 0 ? `filters:${filterParams.join(":")}` : ""} ${entryWidth}w`;
} else {
console.warn("[StoryblokRichText] - srcset entry must be a number or a tuple of two numbers");
return;
}
}).join(", ");
if (options.sizes) attrs.sizes = options.sizes.join(", ");
}
let resultSrc = `${src}/m/`;
if (w > 0 || h > 0) resultSrc = `${resultSrc}${w}x${h}/`;
if (filterParams.length > 0) resultSrc = `${resultSrc}filters:${filterParams.join(":")}`;
return {
src: resultSrc,
attrs
};
}
//#endregion
//#region src/static/util.ts
/**
* Resolves a component from the provided components map based on the type.
* @param type - The type of the component to resolve.
* @param components - The components map to search in.
* @returns The resolved component or undefined if not found.
* @example
* const components = {
* 'heading': MyCustomHeading,
* };
* const resolvedComponent = resolveComponent('heading', components);
* console.log(resolvedComponent); // Output: MyCustomHeading
*/
function resolveComponent(type, components) {
return components?.[type];
}
/**
* Resolves the HTML tag for a given Richtext node or mark.
* @param node - The Richtext node or mark to resolve the tag for.
* @returns The resolved HTML tag as a string, or null if no tag could be resolved.
* @example
* const node = { type: 'paragraph', attrs: {} };
* const tag = resolveTag(node);
* console.log(tag); // Output: "p"
*/
function resolveTag(node) {
const type = node.type;
const entry = NODE_RENDER_MAP[type] ?? MARK_RENDER_MAP[type];
if (!entry) return null;
if ("resolve" in entry && typeof entry.resolve === "function") return entry.resolve(node.attrs);
if ("tag" in entry && typeof entry.tag === "string") return entry.tag;
return null;
}
/**
* Checks if a given HTML tag is self-closing.
* @param tag - The HTML tag to check.
* @returns True if the tag is self-closing, false otherwise.
* @example
* console.log(isSelfClosing('img')); // Output: true
* console.log(isSelfClosing('div')); // Output: false
*
*/
function isSelfClosing(tag) {
return SELF_CLOSING_TAGS.includes(tag);
}
/**
* Returns static child definitions for a given RichText node.
*
* @param node - The RichText node
* @returns Static child render specs, or null if none exist
*
* @example
* const children = getStaticChildren({ type: 'table', attrs: {} });
* // [{ tag: 'tbody', content: true }]
*/
function getStaticChildren(node) {
const renderMap = NODE_RENDER_MAP[node.type];
return renderMap && "children" in renderMap ? renderMap.children : null;
}
//#endregion
//#region src/static/render-richtext.ts
/**
* Renders a Storyblok RichText JSON document to an HTML string.
*
* @param document - RichText JSON document, array of nodes, or nullish value
* @param options - Renderer configuration with custom node/mark renderers
* @returns Rendered HTML string
*
* @example
* ```ts
* const html = renderRichText({
* type: 'doc',
* content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }]
* });
* // => '<p>Hello</p>'
* ```
*/
function renderRichText(document, options) {
if (!document) return "";
if (Array.isArray(document)) return renderChildren(document, options);
const nodes = document.type === "doc" ? document.content : [document];
return nodes?.length ? renderChildren(nodes, options) : "";
}
/** Renders a single node to HTML. */
function renderNode(node, options) {
if (node.type === "text") return renderTextNode(node, node.marks, options);
const customRenderer = options?.renderers?.[node.type];
if (customRenderer) return customRenderer(node);
if (node.type === "blok") {
console.warn("Rendering of \"blok\" nodes is not supported in richTextRenderer.");
return "";
}
const tag = resolveTag(node);
if (!tag) return node.content ? renderChildren(node.content, options) : "";
if (node.type === "image" && options?.optimizeImages) return renderOptimizedImage(node, options);
const htmlAttrs = buildHtmlAttrs(node.type, node.attrs);
if (isSelfClosing(tag)) return `<${tag}${htmlAttrs}>`;
if (node.type === "table") return `<${tag}${htmlAttrs}>${renderTableRows(node.content, options)}</${tag}>`;
const content = node.content ? renderChildren(node.content, options) : "";
const staticChildren = getStaticChildren(node);
if (staticChildren) return `<${tag}>${renderStaticStructure(node.type, staticChildren, node.attrs, content)}</${tag}>`;
return `<${tag}${htmlAttrs}>${content}</${tag}>`;
}
/** Renders an image node with optimization applied. */
function renderOptimizedImage(node, options) {
const attrs = node.attrs;
const src = attrs?.src;
let finalAttrs = attrs;
if (src) {
const { src: optimizedSrc, attrs: extraAttrs } = optimizeImage(src, options.optimizeImages);
finalAttrs = {
...attrs,
src: optimizedSrc,
...extraAttrs
};
}
const htmlAttrs = buildHtmlAttrs("image", finalAttrs);
return htmlAttrs ? `<img${htmlAttrs}>` : "<img>";
}
/**
* Renders child nodes, merging adjacent text nodes that share the same link mark.
* This produces cleaner HTML: `<a href="...">text <b>bold</b> more</a>`
* instead of: `<a>text</a><a><b>bold</b></a><a>more</a>`
*/
function renderChildren(children, options) {
let result = "";
let i = 0;
const len = children.length;
while (i < len) {
const node = children[i];
const linkMark = getTextNodeLinkMark(node);
if (linkMark) {
let end = i + 1;
while (end < len && areLinkMarksEqual(linkMark, getTextNodeLinkMark(children[end]))) end++;
result += renderLinkGroup(children, i, end, linkMark, options);
i = end;
} else {
result += renderNode(node, options);
i++;
}
}
return result;
}
/** Renders a text node with its marks. */
function renderTextNode(node, marks, options) {
let html = escapeHtml(node.text);
if (!marks?.length) return html;
for (const mark of marks) html = wrapWithMark(html, mark, options);
return html;
}
/** Wraps content with a single mark tag. */
function wrapWithMark(content, mark, options) {
const customRenderer = options?.renderers?.[mark.type];
if (customRenderer) return customRenderer({
...mark,
children: content
});
const tag = resolveTag(mark);
if (!tag) return content;
return `<${tag}${buildHtmlAttrs(mark.type, mark.attrs)}>${content}</${tag}>`;
}
/** Link Mark Merging */
/** Renders consecutive text nodes (from start to end) under a single link tag. */
function renderLinkGroup(children, start, end, linkMark, options) {
let inner = "";
for (let i = start; i < end; i++) {
const node = children[i];
const innerMarks = node.marks?.filter((m) => m.type !== "link");
inner += renderTextNode(node, innerMarks, options);
}
const tag = resolveTag(linkMark);
if (!tag) return inner;
return `<${tag}${buildHtmlAttrs(linkMark.type, linkMark.attrs)}>${inner}</${tag}>`;
}
/** Table Rendering */
/** Renders table rows with thead/tbody grouping based on cell types. */
function renderTableRows(rows, options) {
if (!rows?.length) return "";
let headerEnd = 0;
while (headerEnd < rows.length && isTableHeaderRow(rows[headerEnd])) headerEnd++;
let result = "";
if (headerEnd > 0) {
result += "<thead>";
for (let i = 0; i < headerEnd; i++) result += renderNode(rows[i], options);
result += "</thead>";
}
if (headerEnd < rows.length) {
result += "<tbody>";
for (let i = headerEnd; i < rows.length; i++) result += renderNode(rows[i], options);
result += "</tbody>";
}
return result;
}
/** Renders nested static structure defined in render map. */
function renderStaticStructure(type, specs, parentAttrs, content) {
let result = "";
for (const spec of specs) {
const { tag, children, attrs: specAttrs } = spec;
const htmlAttrs = buildHtmlAttrs(type, {
...specAttrs,
...parentAttrs
});
if (isSelfClosing(tag)) result += `<${tag}${htmlAttrs}>`;
else {
const inner = children ? renderStaticStructure(type, children, parentAttrs, content) : content;
result += `<${tag}${htmlAttrs}>${inner}</${tag}>`;
}
}
return result;
}
/** Builds HTML attribute string from node/mark type and attrs. */
function buildHtmlAttrs(type, attrs) {
const processed = processAttrs(type, attrs, {
colspan: "colspan",
rowspan: "rowspan"
});
const styleObj = processed.style;
const finalAttrs = { ...processed };
if (styleObj) finalAttrs.style = styleToString(styleObj);
return attrsToHtmlString(finalAttrs);
}
/** Converts attribute record to HTML string: ` key="value" key2="value2"` */
function attrsToHtmlString(attrs) {
let result = "";
for (const key in attrs) {
const value = attrs[key];
if (value != null) result += ` ${key}="${escapeAttr(value)}"`;
}
return result;
}
//#endregion
exports.areLinkMarksEqual = areLinkMarksEqual;
exports.getInnerMarks = getInnerMarks;
exports.getStaticChildren = getStaticChildren;
exports.getTextNodeLinkMark = getTextNodeLinkMark;
exports.groupLinkNodes = groupLinkNodes;
exports.isSelfClosing = isSelfClosing;
exports.isTableHeaderRow = isTableHeaderRow;
exports.processAttrs = processAttrs;
exports.renderRichText = renderRichText;
exports.resolveComponent = resolveComponent;
exports.resolveTag = resolveTag;
exports.splitTableRows = splitTableRows;
exports.stringToStyle = stringToStyle;
exports.styleToString = styleToString;
//# sourceMappingURL=static.cjs.map