datocms-structured-text-to-html-string
Version:
Convert DatoCMS Structured Text field to HTML string
307 lines (273 loc) • 10.4 kB
text/typescript
import {
defaultMetaTransformer,
render as genericHtmlRender,
RenderMarkRule,
renderMarkRule,
renderNodeRule,
TransformedMeta,
TransformMetaFn,
} from 'datocms-structured-text-generic-html-renderer';
import {
Adapter,
CdaStructuredTextRecord,
CdaStructuredTextValue,
Document as StructuredTextDocument,
isBlock,
isInlineBlock,
isInlineItem,
isItemLink,
isStructuredText,
Node,
Record as StructuredTextGraphQlResponseRecord,
RenderError,
RenderResult,
RenderRule,
StructuredText as StructuredTextGraphQlResponse,
TypesafeCdaStructuredTextValue,
TypesafeStructuredText as TypesafeStructuredTextGraphQlResponse,
} from 'datocms-structured-text-utils';
import vhtml from 'vhtml';
export { renderNodeRule, renderMarkRule, RenderError };
export type {
StructuredTextDocument,
CdaStructuredTextValue,
TypesafeCdaStructuredTextValue,
CdaStructuredTextRecord,
};
type AdapterReturn = string | null;
const vhtmlAdapter = (
tagName: string | null,
attrs?: Record<string, string> | null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...children: any[]
): AdapterReturn => {
if (attrs) {
delete attrs.key;
}
return vhtml(tagName as string, attrs, ...children);
};
export const defaultAdapter = {
renderNode: vhtmlAdapter,
renderFragment: (children: AdapterReturn[]): AdapterReturn =>
vhtmlAdapter(null, null, children),
renderText: (text: string): AdapterReturn => text,
};
type H = typeof defaultAdapter.renderNode;
type T = typeof defaultAdapter.renderText;
type F = typeof defaultAdapter.renderFragment;
type RenderInlineRecordContext<
R extends StructuredTextGraphQlResponseRecord
> = {
record: R;
adapter: Adapter<H, T, F>;
};
type RenderRecordLinkContext<R extends StructuredTextGraphQlResponseRecord> = {
record: R;
adapter: Adapter<H, T, F>;
children: RenderResult<H, T, F>;
transformedMeta: TransformedMeta;
};
type RenderBlockContext<R extends StructuredTextGraphQlResponseRecord> = {
record: R;
adapter: Adapter<H, T, F>;
};
export type RenderSettings<
BlockRecord extends StructuredTextGraphQlResponseRecord = StructuredTextGraphQlResponseRecord,
LinkRecord extends StructuredTextGraphQlResponseRecord = StructuredTextGraphQlResponseRecord,
InlineBlockRecord extends StructuredTextGraphQlResponseRecord = StructuredTextGraphQlResponseRecord
> = {
/** A set of additional rules to convert the document to HTML **/
customNodeRules?: RenderRule<H, T, F>[];
/** A set of additional rules to convert the document to HTML **/
customMarkRules?: RenderMarkRule<H, T, F>[];
/** Function that converts 'link' and 'itemLink' `meta` into HTML attributes */
metaTransformer?: TransformMetaFn;
/** Fuction that converts an 'inlineItem' node into an HTML string **/
renderInlineRecord?: (
context: RenderInlineRecordContext<LinkRecord>,
) => string | null;
/** Fuction that converts an 'itemLink' node into an HTML string **/
renderLinkToRecord?: (
context: RenderRecordLinkContext<LinkRecord>,
) => string | null;
/** Fuction that converts a 'block' node into an HTML string **/
renderBlock?: (context: RenderBlockContext<BlockRecord>) => string | null;
/** Fuction that converts an 'inlineBlock' node into an HTML string **/
renderInlineBlock?: (
context: RenderBlockContext<InlineBlockRecord>,
) => string | null;
/** Fuction that converts a simple string text into an HTML string **/
renderText?: T;
/** React.createElement-like function to use to convert a node into an HTML string **/
renderNode?: H;
/** Function to use to generate a React.Fragment **/
renderFragment?: F;
/** @deprecated use `customNodeRules` instead **/
customRules?: RenderRule<H, T, F>[];
};
export function render<
BlockRecord extends StructuredTextGraphQlResponseRecord = StructuredTextGraphQlResponseRecord,
LinkRecord extends StructuredTextGraphQlResponseRecord = StructuredTextGraphQlResponseRecord,
InlineBlockRecord extends StructuredTextGraphQlResponseRecord = StructuredTextGraphQlResponseRecord
>(
/** The actual field value you get from DatoCMS **/
structuredTextOrNode:
| StructuredTextGraphQlResponse<BlockRecord, LinkRecord, InlineBlockRecord>
| StructuredTextDocument
| Node
| null
| undefined,
/** Additional render settings **/
settings?: RenderSettings<BlockRecord, LinkRecord, InlineBlockRecord>,
): ReturnType<F> | null {
const renderInlineRecord = settings?.renderInlineRecord;
const renderLinkToRecord = settings?.renderLinkToRecord;
const renderBlock = settings?.renderBlock;
const renderInlineBlock = settings?.renderInlineBlock;
const customRules = settings?.customNodeRules || settings?.customRules || [];
const result = genericHtmlRender(structuredTextOrNode, {
adapter: {
renderText: settings?.renderText || defaultAdapter.renderText,
renderNode: settings?.renderNode || defaultAdapter.renderNode,
renderFragment: settings?.renderFragment || defaultAdapter.renderFragment,
},
customMarkRules: settings?.customMarkRules,
metaTransformer: settings?.metaTransformer,
customNodeRules: [
...customRules,
renderNodeRule(isInlineItem, ({ node, adapter }) => {
if (!renderInlineRecord) {
throw new RenderError(
`The Structured Text document contains an 'inlineItem' node, but no 'renderInlineRecord' option is specified!`,
node,
);
}
if (
!isStructuredText(structuredTextOrNode) ||
!structuredTextOrNode.links
) {
throw new RenderError(
`The document contains an 'inlineItem' node, but the passed value is not a Structured Text GraphQL response, or .links is not present!`,
node,
);
}
const item = structuredTextOrNode.links.find(
(item) => item.id === node.item,
);
if (!item) {
throw new RenderError(
`The Structured Text document contains an 'inlineItem' node, but cannot find a record with ID ${node.item} inside .links!`,
node,
);
}
return renderInlineRecord({ record: item, adapter });
}),
renderNodeRule(isItemLink, ({ node, children, adapter }) => {
if (!renderLinkToRecord) {
throw new RenderError(
`The Structured Text document contains an 'itemLink' node, but no 'renderLinkToRecord' option is specified!`,
node,
);
}
if (
!isStructuredText(structuredTextOrNode) ||
!structuredTextOrNode.links
) {
throw new RenderError(
`The document contains an 'itemLink' node, but the passed value is not a Structured Text GraphQL response, or .links is not present!`,
node,
);
}
const item = structuredTextOrNode.links.find(
(item) => item.id === node.item,
);
if (!item) {
throw new RenderError(
`The Structured Text document contains an 'itemLink' node, but cannot find a record with ID ${node.item} inside .links!`,
node,
);
}
return renderLinkToRecord({
record: item,
adapter,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children: (children as any) as ReturnType<F>,
transformedMeta: node.meta
? (settings?.metaTransformer || defaultMetaTransformer)({
node,
meta: node.meta,
})
: null,
});
}),
renderNodeRule(isBlock, ({ node, adapter }) => {
if (!renderBlock) {
throw new RenderError(
`The Structured Text document contains a 'block' node, but no 'renderBlock' option is specified!`,
node,
);
}
if (
!isStructuredText(structuredTextOrNode) ||
!structuredTextOrNode.blocks
) {
throw new RenderError(
`The document contains a 'block' node, but the passed value is not a Structured Text GraphQL response, or .blocks is not present!`,
node,
);
}
const item = structuredTextOrNode.blocks.find(
(item) => item.id === node.item,
);
if (!item) {
throw new RenderError(
`The Structured Text document contains a 'block' node, but cannot find a record with ID ${node.item} inside .blocks!`,
node,
);
}
return renderBlock({ record: item, adapter });
}),
renderNodeRule(isInlineBlock, ({ node, adapter }) => {
if (!renderInlineBlock) {
throw new RenderError(
`The Structured Text document contains an 'inlineBlock' node, but no 'renderInlineBlock' option is specified!`,
node,
);
}
if (
!isStructuredText(structuredTextOrNode) ||
!structuredTextOrNode.inlineBlocks
) {
throw new RenderError(
`The document contains an 'inlineBlock' node, but the passed value is not a Structured Text GraphQL response, or .inlineBlocks is not present!`,
node,
);
}
const item = structuredTextOrNode.inlineBlocks.find(
(item) => item.id === node.item,
);
if (!item) {
throw new RenderError(
`The Structured Text document contains an 'inlineBlock' node, but cannot find a record with ID ${node.item} inside .inlineBlocks!`,
node,
);
}
return renderInlineBlock({ record: item, adapter });
}),
],
});
return result || null;
}
// ============================================================================
// DEPRECATED EXPORTS - kept for backward compatibility
// ============================================================================
/**
* @deprecated Use renderNodeRule instead
*/
export { renderNodeRule as renderRule };
/** @deprecated Use CdaStructuredTextValue */
export type { StructuredTextGraphQlResponse };
/** @deprecated Use TypesafeCdaStructuredTextValue */
export type { TypesafeStructuredTextGraphQlResponse };
/** @deprecated Use CdaStructuredTextRecord */
export type { StructuredTextGraphQlResponseRecord };