@portabletext/to-html
Version:
Render Portable Text to HTML
239 lines (238 loc) • 8.78 kB
JavaScript
import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextToolkitList, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, nestLists, spanToPlainText } from "@portabletext/toolkit";
const allowedProtocols = [
"http",
"https",
"mailto",
"tel"
], charMap = {
"&": "amp",
"<": "lt",
">": "gt",
"\"": "quot",
"'": "#x27"
};
function escapeHTML(str) {
return replaceMultipleSpaces(str.replace(/[&<>"']/g, (s) => `&${charMap[s]};`));
}
function replaceMultipleSpaces(str) {
return str.replace(/ {2,}/g, (match) => `${" ".repeat(match.length - 1)} `);
}
function uriLooksSafe(uri) {
let url = (uri || "").trim(), first = url.charAt(0);
if (first === "#" || first === "/") return !0;
let colonIndex = url.indexOf(":");
if (colonIndex === -1) return !0;
let proto = url.slice(0, colonIndex).toLowerCase();
if (allowedProtocols.indexOf(proto) !== -1) return !0;
let queryIndex = url.indexOf("?");
if (queryIndex !== -1 && colonIndex > queryIndex) return !0;
let hashIndex = url.indexOf("#");
return hashIndex !== -1 && colonIndex > hashIndex;
}
const defaultLists = {
number: ({ children }) => `<ol>${children}</ol>`,
bullet: ({ children }) => `<ul>${children}</ul>`
}, DefaultListItem = ({ children }) => `<li>${children}</li>`, link = ({ children, value }) => {
let href = value?.href || "";
return uriLooksSafe(href) ? `<a href="${escapeHTML(href)}">${children}</a>` : children;
}, defaultMarks = {
em: ({ children }) => `<em>${children}</em>`,
strong: ({ children }) => `<strong>${children}</strong>`,
code: ({ children }) => `<code>${children}</code>`,
underline: ({ children }) => `<span style="text-decoration:underline">${children}</span>`,
"strike-through": ({ children }) => `<del>${children}</del>`,
link
}, getTemplate = (type, prop) => `Unknown ${type}, specify a component for it in the \`components.${prop}\` option`, unknownTypeWarning = (typeName) => getTemplate(`block type "${typeName}"`, "types"), unknownMarkWarning = (markType) => getTemplate(`mark type "${markType}"`, "marks"), unknownBlockStyleWarning = (blockStyle) => getTemplate(`block style "${blockStyle}"`, "block"), unknownListStyleWarning = (listStyle) => getTemplate(`list style "${listStyle}"`, "list"), unknownListItemStyleWarning = (listStyle) => getTemplate(`list item style "${listStyle}"`, "listItem");
function printWarning(message) {
console.warn(message);
}
const defaultComponents = {
types: {},
block: {
normal: ({ children }) => `<p>${children}</p>`,
blockquote: ({ children }) => `<blockquote>${children}</blockquote>`,
h1: ({ children }) => `<h1>${children}</h1>`,
h2: ({ children }) => `<h2>${children}</h2>`,
h3: ({ children }) => `<h3>${children}</h3>`,
h4: ({ children }) => `<h4>${children}</h4>`,
h5: ({ children }) => `<h5>${children}</h5>`,
h6: ({ children }) => `<h6>${children}</h6>`
},
marks: defaultMarks,
list: defaultLists,
listItem: DefaultListItem,
hardBreak: () => "<br/>",
escapeHTML,
unknownType: ({ value, isInline }) => {
let warning = unknownTypeWarning(value._type);
return isInline ? `<span style="display:none">${warning}</span>` : `<div style="display:none">${warning}</div>`;
},
unknownMark: ({ markType, children }) => `<span class="unknown__pt__mark__${markType}">${children}</span>`,
unknownList: ({ children }) => `<ul>${children}</ul>`,
unknownListItem: ({ children }) => `<li>${children}</li>`,
unknownBlockStyle: ({ children }) => `<p>${children}</p>`
};
function mergeComponents(parent, overrides) {
let { block, list, listItem, marks, types, ...rest } = overrides;
return {
...parent,
block: mergeDeeply(parent, overrides, "block"),
list: mergeDeeply(parent, overrides, "list"),
listItem: mergeDeeply(parent, overrides, "listItem"),
marks: mergeDeeply(parent, overrides, "marks"),
types: mergeDeeply(parent, overrides, "types"),
...rest
};
}
function mergeDeeply(parent, overrides, key) {
let override = overrides[key], parentVal = parent[key];
return typeof override == "function" ? override : override ? typeof parentVal == "function" ? override : {
...parentVal,
...override
} : parentVal;
}
function toHTML(value, options = {}) {
let { components: componentOverrides, onMissingComponent: missingComponentHandler = printWarning } = options, handleMissingComponent = missingComponentHandler || noop, nested = nestLists(Array.isArray(value) ? value : [value], "html"), renderNode = getNodeRenderer(componentOverrides ? mergeComponents(defaultComponents, componentOverrides) : defaultComponents, handleMissingComponent);
return nested.map((node, index) => renderNode({
node,
index,
isInline: !1,
renderNode
})).join("");
}
const getNodeRenderer = (components, handleMissingComponent) => {
function renderNode(options) {
let { node, index, isInline } = options;
return isPortableTextToolkitList(node) ? renderList(node, index) : isPortableTextListItemBlock(node) ? renderListItem(node, index) : isPortableTextToolkitSpan(node) ? renderSpan(node) : isPortableTextBlock(node) ? renderBlock(node, index, isInline) : isPortableTextToolkitTextNode(node) ? renderText(node) : renderCustomBlock(node, index, isInline);
}
function renderListItem(node, index) {
let tree = serializeBlock({
node,
index,
isInline: !1,
renderNode
}), renderer = components.listItem, itemHandler = (typeof renderer == "function" ? renderer : renderer[node.listItem]) || components.unknownListItem;
if (itemHandler === components.unknownListItem) {
let style = node.listItem || "bullet";
handleMissingComponent(unknownListItemStyleWarning(style), {
type: style,
nodeType: "listItemStyle"
});
}
let children = tree.children;
if (node.style && node.style !== "normal") {
let { listItem, ...blockNode } = node;
children = renderNode({
node: blockNode,
index,
isInline: !1,
renderNode
});
}
return itemHandler({
value: node,
index,
isInline: !1,
renderNode,
children
});
}
function renderList(node, index) {
let children = node.children.map((child, childIndex) => renderNode({
node: child._key ? child : {
...child,
_key: `li-${index}-${childIndex}`
},
index: childIndex,
isInline: !1,
renderNode
})), component = components.list, list = (typeof component == "function" ? component : component[node.listItem]) || components.unknownList;
if (list === components.unknownList) {
let style = node.listItem || "bullet";
handleMissingComponent(unknownListStyleWarning(style), {
nodeType: "listStyle",
type: style
});
}
return list({
value: node,
index,
isInline: !1,
renderNode,
children: children.join("")
});
}
function renderSpan(node) {
let { markDef, markType, markKey } = node, span = components.marks[markType] || components.unknownMark, children = node.children.map((child, childIndex) => renderNode({
node: child,
index: childIndex,
isInline: !0,
renderNode
}));
return span === components.unknownMark && handleMissingComponent(unknownMarkWarning(markType), {
nodeType: "mark",
type: markType
}), span({
text: spanToPlainText(node),
value: markDef,
markType,
markKey,
renderNode,
children: children.join("")
});
}
function renderBlock(node, index, isInline) {
let { _key, ...props } = serializeBlock({
node,
index,
isInline,
renderNode
}), style = props.node.style || "normal", block = (typeof components.block == "function" ? components.block : components.block[style]) || components.unknownBlockStyle;
return block === components.unknownBlockStyle && handleMissingComponent(unknownBlockStyleWarning(style), {
nodeType: "blockStyle",
type: style
}), block({
...props,
value: props.node,
renderNode
});
}
function renderText(node) {
if (node.text === "\n") {
let hardBreak = components.hardBreak;
return hardBreak ? hardBreak() : "\n";
}
return components.escapeHTML(node.text);
}
function renderCustomBlock(value, index, isInline) {
let node = components.types[value._type];
return node || handleMissingComponent(unknownTypeWarning(value._type), {
nodeType: "block",
type: value._type
}), (node || components.unknownType)({
value,
isInline,
index,
renderNode
});
}
return renderNode;
};
function serializeBlock(options) {
let { node, index, isInline, renderNode } = options, children = buildMarksTree(node).map((child, i) => renderNode({
node: child,
isInline: !0,
index: i,
renderNode
}));
return {
_key: node._key || `block-${index}`,
children: children.join(""),
index,
isInline,
node
};
}
function noop() {}
export { defaultComponents, escapeHTML, mergeComponents, toHTML, uriLooksSafe };
//# sourceMappingURL=index.js.map