UNPKG

datocms-structured-text-generic-html-renderer

Version:

A set of Typescript types and helpers to work with DatoCMS Structured Text fields

255 lines (231 loc) 6.3 kB
import { Adapter, CdaStructuredTextValue, Document, render as genericRender, isBlockquote, isCode, isHeading, isLink, isList, isListItem, isParagraph, isRoot, isSpan, isThematicBreak, Mark, MetaEntry, Node, NodeWithMeta, Record, RenderContext, RenderError, RenderResult, RenderRule, renderRule, Span, TrasformFn, } from 'datocms-structured-text-utils'; export { RenderError, renderRule as renderNodeRule }; export function markToTagName(mark: Mark): string { switch (mark) { case 'emphasis': return 'em'; case 'underline': return 'u'; case 'strikethrough': return 's'; case 'highlight': return 'mark'; default: return mark; } } export function renderSpanValue< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn >({ node, key, adapter: { renderNode, renderText, renderFragment }, }: RenderContext<H, T, F, Span>): RenderResult<H, T, F> { const lines = node.value.split(/\n/); if (lines.length === 0) { return renderText(node.value, key); } return renderFragment( lines.slice(1).reduce( (acc, line, index) => { return acc.concat([ renderNode('br', { key: `${key}-br-${index}` }), renderText(line, `${key}-line-${index}`), ]); }, [renderText(lines[0], `${key}-line-first`)], ), key, ); } type RenderMarkContext< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn > = { mark: string; adapter: Adapter<H, T, F>; key: string; children: Exclude<RenderResult<H, T, F>, null | undefined>[] | undefined; }; export type RenderMarkRule< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn > = { appliable: (mark: string) => boolean; apply: (ctx: RenderMarkContext<H, T, F>) => RenderResult<H, T, F>; }; export function renderMarkRule< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn >( guard: string | ((mark: string) => boolean), transform: (ctx: RenderMarkContext<H, T, F>) => RenderResult<H, T, F>, ): RenderMarkRule<H, T, F> { return { appliable: typeof guard === 'string' ? (mark) => mark === guard : guard, apply: transform, }; } export function spanNodeRenderRule< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn >({ customMarkRules, }: { customMarkRules: RenderMarkRule<H, T, F>[]; }): RenderRule<H, T, F> { return renderRule(isSpan, (context) => { const { adapter, key, node } = context; return (node.marks || []).reduce((children, mark) => { if (!children) { return undefined; } const matchingCustomRule = customMarkRules.find((rule) => rule.appliable(mark), ); if (matchingCustomRule) { return matchingCustomRule.apply({ adapter, key, mark, children }); } return adapter.renderNode(markToTagName(mark), { key }, children); }, renderSpanValue(context)); }); } export type TransformMetaContext = { node: NodeWithMeta; meta: Array<MetaEntry>; }; export type TransformedMeta = | { [prop: string]: unknown; } | null | undefined; export type TransformMetaFn = ( context: TransformMetaContext, ) => TransformedMeta; export const defaultMetaTransformer: TransformMetaFn = ({ meta }) => { const attributes: TransformedMeta = {}; for (const entry of meta) { if (['target', 'title', 'rel'].includes(entry.id)) { attributes[entry.id] = entry.value; } } return attributes; }; export type RenderOptions< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn > = { adapter: Adapter<H, T, F>; customNodeRules?: RenderRule<H, T, F>[]; metaTransformer?: TransformMetaFn; customMarkRules?: RenderMarkRule<H, T, F>[]; }; export function render< H extends TrasformFn, T extends TrasformFn, F extends TrasformFn, BlockRecord extends Record, LinkRecord extends Record, InlineBlockRecord extends Record >( structuredTextOrNode: | CdaStructuredTextValue<BlockRecord, LinkRecord, InlineBlockRecord> | Document | Node | null | undefined, options: RenderOptions<H, T, F>, ): RenderResult<H, T, F> { const metaTransformer = options.metaTransformer || defaultMetaTransformer; return genericRender(options.adapter, structuredTextOrNode, [ ...(options.customNodeRules || []), renderRule(isRoot, ({ adapter: { renderFragment }, key, children }) => { return renderFragment(children, key); }), renderRule(isParagraph, ({ adapter: { renderNode }, key, children }) => { return renderNode('p', { key }, children); }), renderRule(isList, ({ adapter: { renderNode }, node, key, children }) => { return renderNode( node.style === 'bulleted' ? 'ul' : 'ol', { key }, children, ); }), renderRule(isListItem, ({ adapter: { renderNode }, key, children }) => { return renderNode('li', { key }, children); }), renderRule( isBlockquote, ({ adapter: { renderNode }, key, node, children }) => { const childrenWithAttribution = node.attribution ? [ ...(children || []), renderNode(`footer`, { key: 'footer' }, node.attribution), ] : children; return renderNode('blockquote', { key }, childrenWithAttribution); }, ), renderRule(isCode, ({ adapter: { renderNode, renderText }, key, node }) => { return renderNode( 'pre', { key, 'data-language': node.language }, renderNode('code', null, renderText(node.code)), ); }), renderRule(isLink, ({ adapter: { renderNode }, key, children, node }) => { const meta = node.meta ? metaTransformer({ node, meta: node.meta }) : {}; return renderNode( 'a', { ...(meta || {}), key, href: node.url }, children, ); }), renderRule(isThematicBreak, ({ adapter: { renderNode }, key }) => { return renderNode('hr', { key }); }), renderRule( isHeading, ({ node, adapter: { renderNode }, children, key }) => { return renderNode(`h${node.level}`, { key }, children); }, ), spanNodeRenderRule({ customMarkRules: options.customMarkRules || [] }), ]); }