@storyblok/richtext
Version:
Storyblok RichText Resolver
570 lines • 17 kB
text/typescript
//#region src/types/index.d.ts
/**
* Represents the configuration options for optimizing images in rich text content.
*/
interface StoryblokRichTextImageOptimizationOptions {
/**
* CSS class to be applied to the image.
*/
class: string;
/**
* Width of the image in pixels.
*/
width: number;
/**
* Height of the image in pixels.
*/
height: number;
/**
* Loading strategy for the image. 'lazy' loads the image when it enters the viewport. 'eager' loads the image immediately.
*/
loading: 'lazy' | 'eager';
/**
* Optional filters that can be applied to the image to adjust its appearance.
*
* @example
*
* ```typescript
* const filters: Partial<StoryblokRichTextImageOptimizationOptions['filters']> = {
* blur: 5,
* brightness: 150,
* grayscale: true
* }
* ```
*/
filters: Partial<{
blur: number;
brightness: number;
fill: 'transparent';
format: 'webp' | 'png' | 'jpg';
grayscale: boolean;
quality: number;
rotate: 0 | 90 | 180 | 270;
}>;
/**
* Defines a set of source set values that tell the browser different image sizes to load based on screen conditions.
* The entries can be just the width in pixels or a tuple of width and pixel density.
*
* @example
*
* ```typescript
* const srcset: (number | [number, number])[] = [
* 320,
* [640, 2]
* ]
* ```
*/
srcset: (number | [number, number])[];
/**
* A list of sizes that correspond to different viewport widths, instructing the browser on which srcset source to use.
*
* @example
*
* ```typescript
* const sizes: string[] = [
* '(max-width: 320px) 280px',
* '(max-width: 480px) 440px',
* '800px'
* ]
* ```
*/
sizes: string[];
}
//#endregion
//#region src/extensions/richtext-attrs.d.ts
/** Node Attribute Types */
interface ParagraphAttrs {
textAlign: 'left' | 'center' | 'right' | 'justify' | null;
[key: string]: unknown;
}
interface HeadingAttrs {
textAlign: 'left' | 'center' | 'right' | 'justify' | null;
level?: 1 | 2 | 3 | 4 | 5 | 6;
[key: string]: unknown;
}
interface CodeBlockAttrs {
class: string | null;
[key: string]: unknown;
}
interface OrderedListAttrs {
order?: number;
[key: string]: unknown;
}
interface TableCellAttrs {
colspan?: number;
rowspan?: number;
colwidth?: number[] | null;
backgroundColor?: string | null;
[key: string]: unknown;
}
interface TableHeaderAttrs {
colspan?: number;
rowspan?: number;
colwidth?: number[] | null;
[key: string]: unknown;
}
interface ImageAttrs {
id: number | null;
alt: string | null;
src: string;
title: string | null;
source: string | null;
copyright: string | null;
meta_data: {
alt: string | null;
title: string | null;
source: string | null;
copyright: string | null;
} | null;
[key: string]: unknown;
}
interface EmojiAttrs {
name: string;
emoji: string;
fallbackImage: string;
[key: string]: unknown;
}
/** Mark Attribute Types */
interface LinkAttrs {
href: string | null;
uuid: string | null;
anchor: string | null;
target: '_self' | '_blank' | '_parent' | '_top' | null;
linktype: 'story' | 'url' | 'email' | 'asset' | null;
custom?: Record<string, unknown>;
[key: string]: unknown;
}
interface HighlightAttrs {
color: string;
[key: string]: unknown;
}
interface TextStyleAttrs {
color?: string | null;
id?: string | null;
class?: string | null;
[key: string]: unknown;
}
interface AnchorAttrs {
id: string;
[key: string]: unknown;
}
interface StyledAttrs {
class: string | null;
[key: string]: unknown;
}
/** Maps node names to their attribute types. */
interface NodeAttrTypeMap {
paragraph: ParagraphAttrs;
heading: HeadingAttrs;
code_block: CodeBlockAttrs;
ordered_list: OrderedListAttrs;
tableCell: TableCellAttrs;
tableHeader: TableHeaderAttrs;
image: ImageAttrs;
emoji: EmojiAttrs;
}
/** Maps mark names to their attribute types. */
interface MarkAttrTypeMap {
link: LinkAttrs;
highlight: HighlightAttrs;
textStyle: TextStyleAttrs;
anchor: AnchorAttrs;
styled: StyledAttrs;
}
//#endregion
//#region src/static/types.generated.d.ts
/** Attribute types for all Tiptap node extensions */
interface TiptapNodeAttributes {
paragraph: NodeAttrTypeMap['paragraph'];
doc: Record<string, never>;
text: Record<string, never>;
blockquote: Record<string, never>;
heading: NodeAttrTypeMap['heading'];
bullet_list: Record<string, never>;
ordered_list: NodeAttrTypeMap['ordered_list'];
list_item: Record<string, never>;
code_block: NodeAttrTypeMap['code_block'];
hard_break: Record<string, never>;
horizontal_rule: Record<string, never>;
image: NodeAttrTypeMap['image'];
emoji: NodeAttrTypeMap['emoji'];
table: Record<string, never>;
tableRow: Record<string, never>;
tableCell: NodeAttrTypeMap['tableCell'];
tableHeader: NodeAttrTypeMap['tableHeader'];
blok: {
id?: string | null;
body?: SbBlokData[] | null;
};
details: Record<string, never>;
detailsContent: Record<string, never>;
detailsSummary: Record<string, never>;
}
/** Attribute types for all Tiptap mark extensions */
interface TiptapMarkAttributes {
link: MarkAttrTypeMap['link'];
bold: Record<string, never>;
italic: Record<string, never>;
strike: Record<string, never>;
underline: Record<string, never>;
code: Record<string, never>;
superscript: Record<string, never>;
subscript: Record<string, never>;
highlight: MarkAttrTypeMap['highlight'];
textStyle: MarkAttrTypeMap['textStyle'];
anchor: MarkAttrTypeMap['anchor'];
styled: MarkAttrTypeMap['styled'];
reporter: Record<string, never>;
}
type TiptapNodeName = keyof TiptapNodeAttributes;
type TiptapMarkName = keyof TiptapMarkAttributes;
type PMNode = {
type: 'paragraph';
attrs?: TiptapNodeAttributes['paragraph'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'doc';
attrs?: TiptapNodeAttributes['doc'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'text';
attrs?: TiptapNodeAttributes['text'];
content?: PMNode[];
marks?: PMMark[];
text: string;
} | {
type: 'blockquote';
attrs?: TiptapNodeAttributes['blockquote'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'heading';
attrs?: TiptapNodeAttributes['heading'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'bullet_list';
attrs?: TiptapNodeAttributes['bullet_list'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'ordered_list';
attrs?: TiptapNodeAttributes['ordered_list'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'list_item';
attrs?: TiptapNodeAttributes['list_item'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'code_block';
attrs?: TiptapNodeAttributes['code_block'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'hard_break';
attrs?: TiptapNodeAttributes['hard_break'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'horizontal_rule';
attrs?: TiptapNodeAttributes['horizontal_rule'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'image';
attrs?: TiptapNodeAttributes['image'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'emoji';
attrs?: TiptapNodeAttributes['emoji'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'table';
attrs?: TiptapNodeAttributes['table'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'tableRow';
attrs?: TiptapNodeAttributes['tableRow'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'tableCell';
attrs?: TiptapNodeAttributes['tableCell'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'tableHeader';
attrs?: TiptapNodeAttributes['tableHeader'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'blok';
attrs?: TiptapNodeAttributes['blok'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'details';
attrs?: TiptapNodeAttributes['details'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'detailsContent';
attrs?: TiptapNodeAttributes['detailsContent'];
content?: PMNode[];
marks?: PMMark[];
} | {
type: 'detailsSummary';
attrs?: TiptapNodeAttributes['detailsSummary'];
content?: PMNode[];
marks?: PMMark[];
};
type PMMark = {
type: 'link';
attrs?: TiptapMarkAttributes['link'];
} | {
type: 'bold';
attrs?: TiptapMarkAttributes['bold'];
} | {
type: 'italic';
attrs?: TiptapMarkAttributes['italic'];
} | {
type: 'strike';
attrs?: TiptapMarkAttributes['strike'];
} | {
type: 'underline';
attrs?: TiptapMarkAttributes['underline'];
} | {
type: 'code';
attrs?: TiptapMarkAttributes['code'];
} | {
type: 'superscript';
attrs?: TiptapMarkAttributes['superscript'];
} | {
type: 'subscript';
attrs?: TiptapMarkAttributes['subscript'];
} | {
type: 'highlight';
attrs?: TiptapMarkAttributes['highlight'];
} | {
type: 'textStyle';
attrs?: TiptapMarkAttributes['textStyle'];
} | {
type: 'anchor';
attrs?: TiptapMarkAttributes['anchor'];
} | {
type: 'styled';
attrs?: TiptapMarkAttributes['styled'];
} | {
type: 'reporter';
attrs?: TiptapMarkAttributes['reporter'];
};
//#endregion
//#region src/static/types.d.ts
type SbRichTextElement = Exclude<TiptapNodeName | TiptapMarkName, 'text'>;
/** Valid attribute values for DOM elements */
type AttrValue = string | number | boolean;
type HtmlTag = keyof HTMLElementTagNameMap;
interface ISbComponentType<T extends string> {
_uid?: string;
component?: T;
_editable?: string;
}
type SbBlokKeyDataTypes = string | number | object | boolean | undefined;
interface SbBlokData extends ISbComponentType<string> {
[index: string]: SbBlokKeyDataTypes;
}
interface RenderSpec {
tag: string;
attrs?: Record<string, AttrValue> & {
style?: string;
};
content?: boolean;
children?: RenderSpec[];
resolve?: (attrs: unknown) => string;
}
/** Canonical type for a Storyblok RichText JSON root */
type SbRichTextDoc = PMNode;
/** Base props for node/mark components */
type SbRichTextProps<T extends SbRichTextElement> = T extends PMNode['type'] ? Extract<PMNode, {
type: T;
}> : T extends PMMark['type'] ? Extract<PMMark, {
type: T;
}> & {
children: string;
} : never;
/** Generic component map for any renderer target */
type SbRichTextComponents<TComponent = string> = { [K in SbRichTextElement]?: (props: SbRichTextProps<K>) => TComponent };
interface SbRichTextOptions {
renderers?: SbRichTextComponents<string>;
optimizeImages?: boolean | Partial<StoryblokRichTextImageOptimizationOptions>;
}
//#endregion
//#region src/static/attribute.d.ts
type AttrMap = Record<string, string>;
/**
* 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
*/
declare function processAttrs(type: SbRichTextElement, attrs?: Record<string, unknown>, extendAttrMap?: AttrMap): Record<string, unknown>;
//#endregion
//#region src/static/node-helpers.d.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
*/
declare function getTextNodeLinkMark(node: SbRichTextDoc): LinkMark | null;
type LinkMark = PMMark & {
type: 'link';
};
/**
* 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
*/
declare function areLinkMarksEqual(markA: LinkMark | null, markB: LinkMark | null): boolean;
/**
* 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
*/
declare function getInnerMarks(node: SbRichTextDoc): PMMark[];
/**
* 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
*/
declare function groupLinkNodes(children: SbRichTextDoc[]): Array<{
nodes: SbRichTextDoc[];
linkMark: LinkMark | null;
}>;
/**
* 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
*/
declare function isTableHeaderRow(row: SbRichTextDoc): boolean;
/**
* 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
*/
declare function splitTableRows(rows: SbRichTextDoc[] | undefined): {
headerRows: SbRichTextDoc[];
bodyRows: SbRichTextDoc[];
};
//#endregion
//#region src/static/render-richtext.d.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>'
* ```
*/
declare function renderRichText(document: SbRichTextDoc | SbRichTextDoc[] | null | undefined, options?: SbRichTextOptions): string;
//#endregion
//#region src/static/style.d.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"
*/
declare function styleToString(style: Record<string, AttrValue>): string;
/**
* 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' }
*/
declare function stringToStyle(style: string): Record<string, string>;
//#endregion
//#region src/static/util.d.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
*/
declare function resolveComponent<K extends SbRichTextElement, TComponent>(type: K, components?: SbRichTextComponents<TComponent>): SbRichTextComponents<TComponent>[K] | undefined;
/**
* 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"
*/
declare function resolveTag(node: PMNode | PMMark): HtmlTag | 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
*
*/
declare function isSelfClosing(tag: HtmlTag | string): boolean;
/**
* 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 }]
*/
declare function getStaticChildren(node: PMNode): readonly [{
readonly tag: "code";
readonly content: true;
}] | null;
//#endregion
export { type PMMark, type PMNode, type RenderSpec, type SbRichTextComponents, type SbRichTextDoc, type SbRichTextElement, type SbRichTextProps, areLinkMarksEqual, getInnerMarks, getStaticChildren, getTextNodeLinkMark, groupLinkNodes, isSelfClosing, isTableHeaderRow, processAttrs, renderRichText, resolveComponent, resolveTag, splitTableRows, stringToStyle, styleToString };
//# sourceMappingURL=static.d.cts.map