@storyblok/richtext
Version:
Storyblok RichText Resolver
470 lines (469 loc) • 16 kB
text/typescript
import { Attributes, Extension, Mark, Node } from "@tiptap/core";
import * as _tiptap_extension_list0 from "@tiptap/extension-list";
import * as _tiptap_extension_details0 from "@tiptap/extension-details";
import * as _tiptap_extension_table0 from "@tiptap/extension-table";
import * as _tiptap_extension_blockquote0 from "@tiptap/extension-blockquote";
import * as _tiptap_extension_code_block0 from "@tiptap/extension-code-block";
import * as _tiptap_extension_emoji0 from "@tiptap/extension-emoji";
import * as _tiptap_extension_hard_break0 from "@tiptap/extension-hard-break";
import * as _tiptap_extension_heading0 from "@tiptap/extension-heading";
import * as _tiptap_extension_horizontal_rule0 from "@tiptap/extension-horizontal-rule";
import * as _tiptap_extension_paragraph0 from "@tiptap/extension-paragraph";
import * as _tiptap_extension_text_align0 from "@tiptap/extension-text-align";
import * as _tiptap_extension_bold0 from "@tiptap/extension-bold";
import * as _tiptap_extension_code0 from "@tiptap/extension-code";
import * as _tiptap_extension_highlight0 from "@tiptap/extension-highlight";
import * as _tiptap_extension_italic0 from "@tiptap/extension-italic";
import * as _tiptap_extension_link0 from "@tiptap/extension-link";
import * as _tiptap_extension_strike0 from "@tiptap/extension-strike";
import * as _tiptap_extension_subscript0 from "@tiptap/extension-subscript";
import * as _tiptap_extension_superscript0 from "@tiptap/extension-superscript";
import * as _tiptap_extension_underline0 from "@tiptap/extension-underline";
import { ISbComponentType } from "storyblok-js-client";
//#region src/types/index.d.ts
declare enum BlockTypes {
DOCUMENT = "doc",
HEADING = "heading",
PARAGRAPH = "paragraph",
QUOTE = "blockquote",
OL_LIST = "ordered_list",
UL_LIST = "bullet_list",
LIST_ITEM = "list_item",
CODE_BLOCK = "code_block",
HR = "horizontal_rule",
BR = "hard_break",
IMAGE = "image",
EMOJI = "emoji",
COMPONENT = "blok",
TABLE = "table",
TABLE_ROW = "tableRow",
TABLE_CELL = "tableCell",
TABLE_HEADER = "tableHeader"
}
declare enum MarkTypes {
BOLD = "bold",
STRONG = "strong",
STRIKE = "strike",
UNDERLINE = "underline",
ITALIC = "italic",
CODE = "code",
LINK = "link",
ANCHOR = "anchor",
STYLED = "styled",
SUPERSCRIPT = "superscript",
SUBSCRIPT = "subscript",
TEXT_STYLE = "textStyle",
HIGHLIGHT = "highlight"
}
declare enum TextTypes {
TEXT = "text"
}
declare enum LinkTargets {
SELF = "_self",
BLANK = "_blank"
}
declare enum LinkTypes {
URL = "url",
STORY = "story",
ASSET = "asset",
EMAIL = "email"
}
/**
* Represents text alignment attributes that can be applied to block-level elements.
*/
interface TextAlignmentAttrs {
textAlign?: 'left' | 'center' | 'right' | 'justify';
}
/**
* Represents common attributes that can be applied to block-level elements.
*/
interface BlockAttributes extends TextAlignmentAttrs {
class?: string;
id?: string;
[key: string]: any;
}
interface StoryblokRichTextDocumentNode {
type: string;
content?: StoryblokRichTextDocumentNode[];
attrs?: BlockAttributes;
text?: string;
marks?: StoryblokRichTextDocumentNode[];
}
type StoryblokRichTextNodeTypes = BlockTypes | MarkTypes | TextTypes;
interface StoryblokRichTextNode<T = string> {
type: StoryblokRichTextNodeTypes;
content: StoryblokRichTextNode<T>[];
children?: T;
attrs?: BlockAttributes;
text?: string;
}
interface LinkNode<T = string> extends StoryblokRichTextNode<T> {
type: MarkTypes.LINK | MarkTypes.ANCHOR;
linktype: LinkTypes;
attrs: BlockAttributes;
}
interface MarkNode<T = string> extends StoryblokRichTextNode<T> {
type: MarkTypes.BOLD | MarkTypes.ITALIC | MarkTypes.UNDERLINE | MarkTypes.STRIKE | MarkTypes.CODE | MarkTypes.LINK | MarkTypes.ANCHOR | MarkTypes.STYLED | MarkTypes.SUPERSCRIPT | MarkTypes.SUBSCRIPT | MarkTypes.TEXT_STYLE | MarkTypes.HIGHLIGHT;
attrs?: BlockAttributes;
}
interface TextNode<T = string> extends StoryblokRichTextNode<T> {
type: TextTypes.TEXT;
text: string;
marks?: MarkNode<T>[];
}
/**
* 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[];
}
/**
* Represents the options for rendering rich text.
*/
interface StoryblokRichTextOptions<T = string, S = (tag: string, attrs: BlockAttributes, children?: T) => T> {
/**
* Defines the function that will be used to render the final HTML string (vanilla) or Framework component (React, Vue).
*
* @example
*
* ```typescript
* const renderFn = (tag: string, attrs: Record<string, any>, text?: string) => {
* return `<${tag} ${Object.keys(attrs).map(key => `${key}="${attrs[key]}"`).join(' ')}>${text}</${tag}>`
* }
*
* const options: StoryblokRichTextOptions = {
* renderFn
* }
* ```
*/
renderFn?: S;
/**
* Defines the function that will be used to render HTML text.
*
* @example
*
* ```typescript
* import { h, createTextVNode } from 'vue'
*
* const options: StoryblokRichTextOptions = {
* renderFn: h,
* textFn: createTextVNode
* }
* ```
*/
textFn?: (text: string, attrs?: BlockAttributes) => T;
/**
* Defines opt-out image optimization options.
*
* @example
*
* ```typescript
* const options: StoryblokRichTextOptions = {
* optimizeImages: true
* }
* ```
*
* @example
*
* ```typescript
* const options: StoryblokRichTextOptions = {
* optimizeImages: {
* class: 'my-image',
* width: 800,
* height: 600,
* loading: 'lazy',
* }
* ```
*/
optimizeImages?: boolean | Partial<StoryblokRichTextImageOptimizationOptions>;
/**
* Defines whether to use the key attribute in the resolvers for framework use cases.
* @default false
* @example
*
* ```typescript
*
* const options: StoryblokRichTextOptions = {
* renderFn: h,
* keyedResolvers: true
* }
* ```
*/
keyedResolvers?: boolean;
/**
* Custom tiptap extensions to override or add node/mark rendering.
* Extensions are merged with the built-in defaults, overriding by key.
*/
tiptapExtensions?: Record<string, any>;
}
//#endregion
//#region src/extensions/nodes.d.ts
declare const ComponentBlok: Node<{
renderComponent: ((blok: Record<string, unknown>, id?: string) => unknown) | null;
}, any>;
//#endregion
//#region src/extensions/marks.d.ts
interface StyledOptions {
allowedStyles?: string[];
}
//#endregion
//#region src/extensions/index.d.ts
interface StyleOption {
name: string;
value: string;
}
interface StoryblokExtensionOptions {
optimizeImages?: boolean | Partial<StoryblokRichTextImageOptimizationOptions>;
allowCustomAttributes?: boolean;
styleOptions?: StyleOption[];
}
declare function getStoryblokExtensions(options?: StoryblokExtensionOptions): {
image: Node<{
optimizeImages: boolean | Partial<StoryblokRichTextImageOptimizationOptions>;
}, any>;
link: Mark<_tiptap_extension_link0.LinkOptions, any>;
styled: Mark<StyledOptions, any>;
reporter: Mark<any, any>;
document: Node<any, any>;
text: Node<any, any>;
paragraph: Node<_tiptap_extension_paragraph0.ParagraphOptions, any>;
blockquote: Node<_tiptap_extension_blockquote0.BlockquoteOptions, any>;
heading: Node<_tiptap_extension_heading0.HeadingOptions, any>;
bulletList: Node<_tiptap_extension_list0.BulletListOptions, any>;
orderedList: Node<_tiptap_extension_list0.OrderedListOptions, any>;
listItem: Node<_tiptap_extension_list0.ListItemOptions, any>;
codeBlock: Node<_tiptap_extension_code_block0.CodeBlockOptions, any>;
hardBreak: Node<_tiptap_extension_hard_break0.HardBreakOptions, any>;
horizontalRule: Node<_tiptap_extension_horizontal_rule0.HorizontalRuleOptions, any>;
emoji: Node<_tiptap_extension_emoji0.EmojiOptions, _tiptap_extension_emoji0.EmojiStorage>;
table: Node<_tiptap_extension_table0.TableOptions, any>;
tableRow: Node<_tiptap_extension_table0.TableRowOptions, any>;
tableCell: Node<_tiptap_extension_table0.TableCellOptions, any>;
tableHeader: Node<_tiptap_extension_table0.TableHeaderOptions, any>;
blok: Node<{
renderComponent: ((blok: Record<string, unknown>, id?: string) => unknown) | null;
}, any>;
details: Node<_tiptap_extension_details0.DetailsOptions, any>;
detailsContent: Node<_tiptap_extension_details0.DetailsContentOptions, any>;
detailsSummary: Node<_tiptap_extension_details0.DetailsSummaryOptions, any>;
bold: Mark<_tiptap_extension_bold0.BoldOptions, any>;
italic: Mark<_tiptap_extension_italic0.ItalicOptions, any>;
strike: Mark<_tiptap_extension_strike0.StrikeOptions, any>;
underline: Mark<_tiptap_extension_underline0.UnderlineOptions, any>;
code: Mark<_tiptap_extension_code0.CodeOptions, any>;
superscript: Mark<_tiptap_extension_superscript0.SuperscriptExtensionOptions, any>;
subscript: Mark<_tiptap_extension_subscript0.SubscriptExtensionOptions, any>;
highlight: Mark<_tiptap_extension_highlight0.HighlightOptions, any>;
textStyle: Mark<any, any>;
anchor: Mark<any, any>;
textAlign: Extension<_tiptap_extension_text_align0.TextAlignOptions, any>;
};
//#endregion
//#region src/richtext-segment.d.ts
/**
* Segment Types
*/
interface TextSegment {
kind: 'text';
text: string;
}
interface NodeSegment {
kind: 'node';
type: string;
tag: string | null;
attrs: Attributes;
content: SBRichTextSegment[];
}
interface MarkSegment {
kind: 'mark';
type: string;
tag: string | null;
attrs: Attributes;
content: SBRichTextSegment[];
}
interface ComponentSegment {
kind: 'component';
type: string;
props: Record<string, unknown>;
}
type SBRichTextSegment = TextSegment | NodeSegment | MarkSegment | ComponentSegment;
type StoryblokExtensions = ReturnType<typeof getStoryblokExtensions>;
type StoryblokSegmentType = keyof StoryblokExtensions;
/**
* Renderer Options
*/
interface StoryblokRichTextOptionsNew {
optimizeImages?: boolean;
/**
* Called when a node has no extension renderer
*/
onUnknownNode?: (node: StoryblokRichTextNode) => SBRichTextSegment[];
/**
* Called when a mark has no extension renderer
*/
onUnknownMark?: (mark: any) => SBRichTextSegment[];
}
declare function getRichTextSegments(richText: StoryblokRichTextNode, options?: StoryblokRichTextOptionsNew): SBRichTextSegment[];
declare function isVoidElement(tag: string): boolean;
/**
* Parses an inline CSS style string into an object.
*
* Example:
* parseStyleString("width: 1.25em; height: 1.25em; vertical-align: text-top")
* -> { width: "1.25em", height: "1.25em", "vertical-align": "text-top" }
*/
declare function parseStyleString(style: string): Record<string, string>;
//#endregion
//#region src/render-segments.d.ts
interface RendererAdapter<T = unknown> {
createElement: (tag: string, attrs?: Record<string, unknown>, children?: T[]) => T;
createText: (text: string) => T;
createComponent?: (type: StoryblokSegmentType, props: Record<string, unknown>) => T;
}
declare function renderSegments<T>(segments: SBRichTextSegment[], adapter: RendererAdapter<T>, customComponents: StoryblokSegmentType[]): T[];
//#endregion
//#region src/richtext.d.ts
/**
* Creates a rich text resolver with the given options.
*/
declare function richTextResolver<T>(options?: StoryblokRichTextOptions<T>): {
render: (node: StoryblokRichTextNode<T> | StoryblokRichTextDocumentNode) => T;
};
//#endregion
//#region src/utils/segment-richtext.d.ts
type SbBlokKeyDataTypes = string | number | object | boolean | undefined;
interface SbBlokData extends ISbComponentType<string> {
[index: string]: SbBlokKeyDataTypes;
}
interface RichTextHtmlSegment {
type: 'html';
content: string;
}
interface RichTextBlokSegment {
type: 'blok';
blok: SbBlokData;
}
type RichTextSegment = RichTextHtmlSegment | RichTextBlokSegment;
/**
* Converts a Storyblok Rich Text document into a linear list of segments.
*
* The returned segments preserve the original content order and consist of:
* - HTML segments for regular rich text content
* - Blok segments for embedded Storyblok components
*
* This allows consumers to render HTML normally while handling Storyblok
* components separately using framework-specific logic.
*
* @param doc - The Storyblok Rich Text document to process
* @param options - Optional rich text resolver options
* @returns An ordered array of rich text segments (HTML and bloks)
*
* @example
* ```ts
* const segments = segmentStoryblokRichText(richTextDoc);
*
* for (const segment of segments) {
* if (segment.type === 'html') {
* renderHtml(segment.content);
* }
*
* if (segment.type === 'blok') {
* renderBlokComponent(segment.blok);
* }
* }
* ```
*/
declare function segmentStoryblokRichText(doc: StoryblokRichTextNode<string>, options?: StoryblokRichTextOptions<string>): RichTextSegment[];
//#endregion
//#region src/index.d.ts
/**
* Wraps a framework component (React, Vue, etc.) for use as a tag
* in Tiptap's `renderHTML` DOMOutputSpec.
*
* Tiptap's `DOMOutputSpec` type only accepts strings at position 0,
* but the Storyblok richtext resolver also handles component references.
* Use this helper to satisfy TypeScript without a manual `as unknown as string` type assertion.
*
* @example
* ```typescript
* import { Mark } from '@tiptap/core';
* import { asTag } from '@storyblok/vue'; // or @storyblok/react
* import { RouterLink } from 'vue-router';
*
* const CustomLink = Mark.create({
* name: 'link',
* renderHTML({ HTMLAttributes }) {
* return [asTag(RouterLink), { to: HTMLAttributes.href }, 0];
* },
* });
* ```
*/
declare function asTag(component: unknown): string;
//#endregion
export { BlockAttributes, BlockTypes, ComponentBlok, LinkNode, LinkTargets, LinkTypes, MarkNode, MarkSegment, MarkTypes, NodeSegment, RendererAdapter, RichTextBlokSegment, RichTextHtmlSegment, RichTextSegment, SBRichTextSegment, SbBlokData, SbBlokKeyDataTypes, StoryblokExtensions, StoryblokRichTextDocumentNode, StoryblokRichTextImageOptimizationOptions, StoryblokRichTextNode, StoryblokRichTextNodeTypes, StoryblokRichTextOptions, StoryblokRichTextOptionsNew, StoryblokSegmentType, TextAlignmentAttrs, TextNode, TextSegment, TextTypes, asTag, getRichTextSegments, isVoidElement, parseStyleString, renderSegments, richTextResolver, segmentStoryblokRichText };
//# sourceMappingURL=index.d.mts.map