@portabletext/toolkit
Advanced tools
| {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/asserters.ts","../src/buildMarksTree.ts","../src/nestLists.ts","../src/sortMarksByOccurences.ts","../src/spanToPlainText.ts","../src/toPlainText.ts"],"sourcesContent":[],"mappings":";;AAUA;AAKA;AAKY,cAVC,mBAAA,GAUD,MAAA;AAOZ;AAMA;AAoCA;AAmCiB,cAzFJ,qBAAA,GAyFI,QAAA;;;;AAAoC,KApFzC,mBAAA,GAoFyC,MAAA,GAAA,QAAA;;AAUrD;AAgBA;;;AAiBY,KAxHA,uBAAA,GAA0B,2BAwH1B,GAxHwD,6BAwHxD;;;;;UAlHK,2BAAA;ECjBjB;;;EAEW,KAAA,EAAA,OAAA;EAAA;AAgBX;;EAC4B,IAAA,EAAA,MAAA;EACjB;;AA2BX;EACS,IAAA,EAAA,MAAA;EAAoB;;;EAiB7B,KAAgB,EAAA,MAAA;EACP;;;EACG,QAAA,EAAA,MAAA;EAWZ;;;EAEW,QAAA,EDlCC,2BCkCD,EAAA;;AAWX;;;;AAEW,UDxCM,6BAAA,CCwCN;;ACnEX;;EAAsE,KAAA,EAAA,OAAA;EAC3C;;;EACvB,IAAA,EAAA,MAAA;EAAmC;;;;EC5BvC;;;EAGI,KAAA,EAAA,MAAA;EAAA;AAwBJ;;EAAkD,QAAA,EAAA,MAAA;EAAoB;;;EAG9D,QAAA,EAAA,CHoDK,yBGpDL,GHoDiC,6BGpDjC,CAAA,EAAA;;AACR;;;AAAsE,UHyDrD,2BAAA,SAAoC,yBGzDiB,CH0DpE,0BG1DoE,EH2DpE,gBG3DoE,GH2DjD,uBG3DiD,CAAA,CAAA,CAAA;;;;;AAItE;AAAoC,UH+DnB,eAAA,CG/DmB;EAAc;;;EAG9C,KAAA,EAAA,OAAA;EAAI;;;;ACXR;;;;;AAGqC,UJoFpB,6BIpFoB,CAAA,UJqFzB,0BIrFyB,GJqFI,0BIrFJ,CAAA,CAAA;;AChCrC;;ECIA,KAAgB,EAAA,OAAA;EACP;;;EAA6C,IAAA,CAAA,EAAA,MAAA;;;;;YNgI1C;;;;;;;;;;;;;;;aAkBC,kBAAkB,gCAAgC;;AA3J/D;AAKA;AAKA;AAOA;AAMA;AAoCA;AAmCiB,iBCxFD,kBAAA,CDwFC,IAAA,ECvFT,oBDuFS,GCvFc,gBDuFd,CAAA,EAAA,IAAA,ICtFN,gBDsFM;;;;;;AAUjB;AAgBiB,iBChGD,mBAAA,CDgGC,IAAA,EC/FT,iBD+FS,GC/FW,WD+FX,CAAA,EAAA,IAAA,IC9FN,iBD8FM;;;;;;;AAmC8C,iBCtG/C,2BAAA,CDsG+C,KAAA,ECrGtD,iBDqGsD,GCrGlC,WDqGkC,CAAA,EAAA,KAAA,ICpGnD,yBDoGmD;;ACrJ/D;;;;;AAkBA;AACQ,iBA8CQ,yBAAA,CA9CR,KAAA,EA+CC,WA/CD,GA+Ce,uBA/Cf,CAAA,EAAA,KAAA,IAgDI,uBAhDJ;;;;AA4BR;;;;AAEY,iBA6BI,yBAAA,CA7BJ,IAAA,EA8BJ,WA9BI,GA8BU,6BA9BV,CAAA,EAAA,IAAA,IA+BD,6BA/BC;AAgBZ;;;;;AAaA;;AACsB,iBAYN,6BAAA,CAZM,IAAA,EAad,WAbc,GAaA,eAbA,CAAA,EAAA,IAAA,IAcX,eAdW;ADrFtB;AAKA;AAKA;AAOA;AAMA;AAoCA;AAmCA;;;;;;AAUA;AAgBA;;;;;;;;;AClHA;;;;;AAkBA;;;;AAEW,iBCMK,cDNL,CAAA,UCM8B,0BDN9B,GCM2D,0BDN3D,CAAA,CAAA,KAAA,ECOF,iBDPE,CCOgB,CDPhB,CAAA,CAAA,EAAA,CCQP,6BDRO,CCQuB,CDRvB,CAAA,GCQ4B,eDR5B,GCQ8C,oBDR9C,CAAA,EAAA;AD1BE,KGMD,0BHNC,CAAA,CAAA,CAAA,GGOT,CHPS,GGQT,2BHRS,GGST,6BHTS;AAKb;AAKA;AAOA;AAMA;AAoCA;AAmCA;;;;;;AAUA;AAgBA;;;;;;;;;AClHA;AACQ,iBE0BQ,SF1BR,CAAA,UE0B4B,WF1B5B,GE0B0C,iBF1B1C,GE0B8D,WF1B9D,CAAA,CAAA,MAAA,EE2BE,CF3BF,EAAA,EAAA,IAAA,EAAA,QAAA,CAAA,EAAA,CE6BJ,CF7BI,GE6BA,6BF7BA,CAAA,EAAA;AAAuB,iBE8Bf,SF9Be,CAAA,UE8BK,WF9BL,GE8BmB,iBF9BnB,GE8BuC,WF9BvC,CAAA,CAAA,MAAA,EE+BrB,CF/BqB,EAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EAAA,CEiC3B,CFjC2B,GEiCvB,2BFjCuB,CAAA,EAAA;AACpB,iBEiCK,SFjCL,CAAA,UEiCyB,WFjCzB,GEiCuC,iBFjCvC,GEiC2D,WFjC3D,CAAA,CAAA,MAAA,EEkCD,CFlCC,EAAA,EAAA,IAAA,EAAA,QAAA,GAAA,MAAA,CAAA,EAAA,CEoCP,CFpCO,GEoCH,2BFpCG,GEoC2B,6BFpC3B,CAAA,EAAA;;ADRX;AAKA;AAKA;AAOA;AAMA;AAoCA;AAmCA;;;;;;AAUA;AAgBA;;;;;;;;;AClHA;;;;;AAkBA;;;;;AA6BA;;;;AAEY,iBGtBI,qBAAA,CHsBJ,IAAA,EGrBJ,gBHqBI,GGrBe,WHqBf,EAAA,KAAA,EAAA,MAAA,EAAA,aAAA,EAAA,CGnBM,gBHmBN,GGnByB,WHmBzB,CAAA,EAAA,CAAA,EAAA,MAAA,EAAA;;ADvDZ;AAKA;AAKA;AAOA;AAMA;AAoCA;AAmCA;;;AAEqB,iBK5FL,eAAA,CL4FK,IAAA,EK5FiB,6BL4FjB,CAAA,EAAA,MAAA;;AAhGrB;AAKA;AAKA;AAOA;AAMA;AAoCA;AAmCA;;;;AAAqD,iBMtFrC,WAAA,CNsFqC,KAAA,EMrF5C,iBNqF4C,GMrFxB,oBNqFwB,EAAA,GMrFC,iBNqFD,EAAA,CAAA,EAAA,MAAA"} |
+260
-340
@@ -1,378 +0,298 @@ | ||
| import type { ArbitraryTypedObject } from "@portabletext/types"; | ||
| import type { PortableTextBlock } from "@portabletext/types"; | ||
| import type { PortableTextListItemBlock } from "@portabletext/types"; | ||
| import type { PortableTextMarkDefinition } from "@portabletext/types"; | ||
| import type { PortableTextSpan } from "@portabletext/types"; | ||
| import type { TypedObject } from "@portabletext/types"; | ||
| import { ArbitraryTypedObject, PortableTextBlock, PortableTextListItemBlock, PortableTextMarkDefinition, PortableTextSpan, TypedObject } from "@portabletext/types"; | ||
| /** | ||
| * Takes a Portable Text block and returns a nested tree of nodes optimized for rendering | ||
| * in HTML-like environments where you want marks/annotations to be nested inside of eachother. | ||
| * For instance, a naive span-by-span rendering might yield: | ||
| * | ||
| * ```html | ||
| * <strong>This block contains </strong> | ||
| * <strong><a href="https://some.url/">a link</a></strong> | ||
| * <strong> and some bolded and </strong> | ||
| * <em><strong>italicized text</strong></em> | ||
| * ``` | ||
| * | ||
| * ...whereas an optimal order would be: | ||
| * | ||
| * ```html | ||
| * <strong> | ||
| * This block contains <a href="https://some.url/">a link</a> | ||
| * and some bolded and <em>italicized text</em> | ||
| * </strong> | ||
| * ``` | ||
| * | ||
| * Note that since "native" Portable Text spans cannot be nested, | ||
| * this function returns an array of "toolkit specific" types: | ||
| * {@link ToolkitTextNode | `@text`} and {@link ToolkitNestedPortableTextSpan | `@span` }. | ||
| * | ||
| * The toolkit-specific type can hold both types, as well as any arbitrary inline objects, | ||
| * creating an actual tree. | ||
| * | ||
| * @param block - The Portable Text block to create a tree of nodes from | ||
| * @returns Array of (potentially) nested spans, text nodes and/or arbitrary inline objects | ||
| */ | ||
| export declare function buildMarksTree< | ||
| M extends PortableTextMarkDefinition = PortableTextMarkDefinition, | ||
| >( | ||
| block: PortableTextBlock<M>, | ||
| ): ( | ||
| | ToolkitNestedPortableTextSpan<M> | ||
| | ToolkitTextNode | ||
| | ArbitraryTypedObject | ||
| )[]; | ||
| * List nesting mode for HTML, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| declare const LIST_NEST_MODE_HTML = "html"; | ||
| /** | ||
| * Strict check to determine if node is a correctly formatted Portable Text block. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if valid Portable Text block, otherwise false | ||
| */ | ||
| export declare function isPortableTextBlock( | ||
| node: PortableTextBlock | TypedObject, | ||
| ): node is PortableTextBlock; | ||
| * List nesting mode for direct, nested lists, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| declare const LIST_NEST_MODE_DIRECT = "direct"; | ||
| /** | ||
| * Strict check to determine if node is a correctly formatted portable list item block. | ||
| * | ||
| * @param block - Block to check | ||
| * @returns True if valid Portable Text list item block, otherwise false | ||
| */ | ||
| export declare function isPortableTextListItemBlock( | ||
| block: PortableTextBlock | TypedObject, | ||
| ): block is PortableTextListItemBlock; | ||
| * List nesting mode, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| type ToolkitListNestMode = "html" | "direct"; | ||
| /** | ||
| * Strict check to determine if node is a correctly formatted Portable Text span. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if valid Portable Text span, otherwise false | ||
| */ | ||
| export declare function isPortableTextSpan( | ||
| node: ArbitraryTypedObject | PortableTextSpan, | ||
| ): node is PortableTextSpan; | ||
| * Toolkit-specific type representing a nested list | ||
| * | ||
| * See the `nestLists()` function for more info | ||
| */ | ||
| type ToolkitPortableTextList = ToolkitPortableTextHtmlList | ToolkitPortableTextDirectList; | ||
| /** | ||
| * Loose check to determine if block is a toolkit list node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param block - Block to check | ||
| * @returns True if toolkit list, otherwise false | ||
| */ | ||
| export declare function isPortableTextToolkitList( | ||
| block: TypedObject | ToolkitPortableTextList, | ||
| ): block is ToolkitPortableTextList; | ||
| /** | ||
| * Loose check to determine if span is a toolkit span node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param span - Span to check | ||
| * @returns True if toolkit span, otherwise false | ||
| */ | ||
| export declare function isPortableTextToolkitSpan( | ||
| span: TypedObject | ToolkitNestedPortableTextSpan, | ||
| ): span is ToolkitNestedPortableTextSpan; | ||
| /** | ||
| * Loose check to determine if node is a toolkit text node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if toolkit text node, otherwise false | ||
| */ | ||
| export declare function isPortableTextToolkitTextNode( | ||
| node: TypedObject | ToolkitTextNode, | ||
| ): node is ToolkitTextNode; | ||
| /** | ||
| * List nesting mode for direct, nested lists, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| export declare const LIST_NEST_MODE_DIRECT = "direct"; | ||
| /** | ||
| * List nesting mode for HTML, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| export declare const LIST_NEST_MODE_HTML = "html"; | ||
| /** | ||
| * Takes an array of blocks and returns an array of nodes optimized for rendering in HTML-like | ||
| * environment, where lists are nested inside of eachother instead of appearing "flat" as in | ||
| * native Portable Text data structures. | ||
| * | ||
| * Note that the list node is not a native Portable Text node type, and thus is represented | ||
| * using the {@link ToolkitPortableTextList | `@list`} type name (`{_type: '@list'}`). | ||
| * | ||
| * The nesting can be configured in two modes: | ||
| * | ||
| * - `direct`: deeper list nodes will appear as a direct child of the parent list | ||
| * - `html`, deeper list nodes will appear as a child of the last _list item_ in the parent list | ||
| * | ||
| * When using `direct`, all list nodes will be of type {@link ToolkitPortableTextDirectList}, | ||
| * while with `html` they will be of type {@link ToolkitPortableTextHtmlList} | ||
| * | ||
| * These modes are available as {@link LIST_NEST_MODE_HTML} and {@link LIST_NEST_MODE_DIRECT}. | ||
| * | ||
| * @param blocks - Array of Portable Text blocks and other arbitrary types | ||
| * @param mode - Mode to use for nesting, `direct` or `html` | ||
| * @returns Array of potentially nested nodes optimized for rendering | ||
| */ | ||
| export declare function nestLists< | ||
| T extends TypedObject = PortableTextBlock | TypedObject, | ||
| >(blocks: T[], mode: "direct"): (T | ToolkitPortableTextDirectList)[]; | ||
| export declare function nestLists< | ||
| T extends TypedObject = PortableTextBlock | TypedObject, | ||
| >(blocks: T[], mode: "html"): (T | ToolkitPortableTextHtmlList)[]; | ||
| export declare function nestLists< | ||
| T extends TypedObject = PortableTextBlock | TypedObject, | ||
| >( | ||
| blocks: T[], | ||
| mode: "direct" | "html", | ||
| ): (T | ToolkitPortableTextHtmlList | ToolkitPortableTextDirectList)[]; | ||
| /** | ||
| * Figures out the optimal order of marks, in order to minimize the amount of | ||
| * nesting/repeated elements in environments such as HTML. For instance, a naive | ||
| * implementation might render something like: | ||
| * | ||
| * ```html | ||
| * <strong>This block contains </strong> | ||
| * <strong><a href="https://some.url/">a link</a></strong> | ||
| * <strong> and some bolded text</strong> | ||
| * ``` | ||
| * | ||
| * ...whereas an optimal order would be: | ||
| * | ||
| * ```html | ||
| * <strong> | ||
| * This block contains <a href="https://some.url/">a link</a> and some bolded text | ||
| * </strong> | ||
| * ``` | ||
| * | ||
| * This is particularly necessary for cases like links, where you don't want multiple | ||
| * individual links for different segments of the link text, even if parts of it are | ||
| * bolded/italicized. | ||
| * | ||
| * This function is meant to be used like: `block.children.map(sortMarksByOccurences)`, | ||
| * and is used internally in {@link buildMarksTree | `buildMarksTree()`}. | ||
| * | ||
| * The marks are sorted in the following order: | ||
| * | ||
| * 1. Marks that are shared amongst the most adjacent siblings | ||
| * 2. Non-default marks (links, custom metadata) | ||
| * 3. Decorators (bold, emphasis, code etc), in a predefined, preferred order | ||
| * | ||
| * @param span - The current span to sort | ||
| * @param index - The index of the current span within the block | ||
| * @param blockChildren - All children of the block being sorted | ||
| * @returns Array of decorators and annotations, sorted by "most adjacent siblings" | ||
| */ | ||
| export declare function sortMarksByOccurences( | ||
| span: PortableTextSpan | TypedObject, | ||
| index: number, | ||
| blockChildren: (PortableTextSpan | TypedObject)[], | ||
| ): string[]; | ||
| /** | ||
| * Returns the plain-text representation of a | ||
| * {@link ToolkitNestedPortableTextSpan | toolkit-specific Portable Text span}. | ||
| * | ||
| * Useful if you have a subset of nested nodes and want the text from just those, | ||
| * instead of for the entire Portable Text block. | ||
| * | ||
| * @param span - Span node to get text from (Portable Text toolkit specific type) | ||
| * @returns The plain-text version of the span | ||
| */ | ||
| export declare function spanToPlainText( | ||
| span: ToolkitNestedPortableTextSpan, | ||
| ): string; | ||
| /** | ||
| * List nesting mode, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| export declare type ToolkitListNestMode = "html" | "direct"; | ||
| /** | ||
| * Toolkit-specific type representing a portable text span that can hold other spans. | ||
| * In this type, each span only has a single mark, instead of an array of them. | ||
| */ | ||
| export declare interface ToolkitNestedPortableTextSpan< | ||
| M extends PortableTextMarkDefinition = PortableTextMarkDefinition, | ||
| > { | ||
| * Toolkit-specific type representing a nested list in HTML mode, where deeper lists are nested | ||
| * inside of the _list items_, eg `<ul><li>Some text<ul><li>Deeper</li></ul></li></ul>` | ||
| */ | ||
| interface ToolkitPortableTextHtmlList { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@span"; | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@list"; | ||
| /** | ||
| * Unique key for this span | ||
| */ | ||
| _key?: string; | ||
| * Unique key for this list (within its parent) | ||
| */ | ||
| _key: string; | ||
| /** | ||
| * Holds the value (definition) of the mark in the case of annotations. | ||
| * `undefined` if the mark is a decorator (strong, em or similar). | ||
| */ | ||
| markDef?: M; | ||
| * List mode, signaling that list nodes will appear as children of the _list items_ | ||
| */ | ||
| mode: "html"; | ||
| /** | ||
| * The key of the mark definition (in the case of annotations). | ||
| * `undefined` if the mark is a decorator (strong, em or similar). | ||
| */ | ||
| markKey?: string; | ||
| * Level/depth of this list node (starts at `1`) | ||
| */ | ||
| level: number; | ||
| /** | ||
| * Type of the mark. For annotations, this is the `_type` property of the value. | ||
| * For decorators, it will hold the name of the decorator (strong, em or similar). | ||
| */ | ||
| markType: string; | ||
| * Style of this list item (`bullet`, `number` are common values, but can be customized) | ||
| */ | ||
| listItem: string; | ||
| /** | ||
| * Child nodes of this span. Can be toolkit-specific text nodes, nested spans | ||
| * or any inline object type. | ||
| */ | ||
| children: ( | ||
| | ToolkitTextNode | ||
| | ToolkitNestedPortableTextSpan | ||
| | ArbitraryTypedObject | ||
| )[]; | ||
| * Child nodes of this list - toolkit-specific list items which can themselves hold deeper lists | ||
| */ | ||
| children: ToolkitPortableTextListItem[]; | ||
| } | ||
| export declare type ToolkitNestListsOutputNode<T> = | ||
| | T | ||
| | ToolkitPortableTextHtmlList | ||
| | ToolkitPortableTextDirectList; | ||
| /** | ||
| * Toolkit-specific type representing a nested list in "direct" mode, where deeper lists are nested | ||
| * inside of the lists children, alongside other blocks. | ||
| */ | ||
| export declare interface ToolkitPortableTextDirectList { | ||
| * Toolkit-specific type representing a nested list in "direct" mode, where deeper lists are nested | ||
| * inside of the lists children, alongside other blocks. | ||
| */ | ||
| interface ToolkitPortableTextDirectList { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@list"; | ||
| /** | ||
| * Unique key for this list (within its parent) | ||
| */ | ||
| * Unique key for this list (within its parent) | ||
| */ | ||
| _key: string; | ||
| /** | ||
| * List mode, signaling that list nodes can appear as direct children | ||
| */ | ||
| * List mode, signaling that list nodes can appear as direct children | ||
| */ | ||
| mode: "direct"; | ||
| /** | ||
| * Level/depth of this list node (starts at `1`) | ||
| */ | ||
| * Level/depth of this list node (starts at `1`) | ||
| */ | ||
| level: number; | ||
| /** | ||
| * Style of this list item (`bullet`, `number` are common values, but can be customized) | ||
| */ | ||
| * Style of this list item (`bullet`, `number` are common values, but can be customized) | ||
| */ | ||
| listItem: string; | ||
| /** | ||
| * Child nodes of this list - either portable text list items, or another, deeper list | ||
| */ | ||
| * Child nodes of this list - either portable text list items, or another, deeper list | ||
| */ | ||
| children: (PortableTextListItemBlock | ToolkitPortableTextDirectList)[]; | ||
| } | ||
| /** | ||
| * Toolkit-specific type representing a nested list in HTML mode, where deeper lists are nested | ||
| * inside of the _list items_, eg `<ul><li>Some text<ul><li>Deeper</li></ul></li></ul>` | ||
| */ | ||
| export declare interface ToolkitPortableTextHtmlList { | ||
| * Toolkit-specific type representing a list item block, but where the children can be another list | ||
| */ | ||
| interface ToolkitPortableTextListItem extends PortableTextListItemBlock<PortableTextMarkDefinition, PortableTextSpan | ToolkitPortableTextList> {} | ||
| /** | ||
| * Toolkit-specific type representing a text node, used when nesting spans. | ||
| * | ||
| * See the {@link buildMarksTree | `buildMarksTree()` function} | ||
| */ | ||
| interface ToolkitTextNode { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@list"; | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@text"; | ||
| /** | ||
| * Unique key for this list (within its parent) | ||
| */ | ||
| _key: string; | ||
| * The actual string value of the text node | ||
| */ | ||
| text: string; | ||
| } | ||
| /** | ||
| * Toolkit-specific type representing a portable text span that can hold other spans. | ||
| * In this type, each span only has a single mark, instead of an array of them. | ||
| */ | ||
| interface ToolkitNestedPortableTextSpan<M extends PortableTextMarkDefinition = PortableTextMarkDefinition> { | ||
| /** | ||
| * List mode, signaling that list nodes will appear as children of the _list items_ | ||
| */ | ||
| mode: "html"; | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@span"; | ||
| /** | ||
| * Level/depth of this list node (starts at `1`) | ||
| */ | ||
| level: number; | ||
| * Unique key for this span | ||
| */ | ||
| _key?: string; | ||
| /** | ||
| * Style of this list item (`bullet`, `number` are common values, but can be customized) | ||
| */ | ||
| listItem: string; | ||
| * Holds the value (definition) of the mark in the case of annotations. | ||
| * `undefined` if the mark is a decorator (strong, em or similar). | ||
| */ | ||
| markDef?: M; | ||
| /** | ||
| * Child nodes of this list - toolkit-specific list items which can themselves hold deeper lists | ||
| */ | ||
| children: ToolkitPortableTextListItem[]; | ||
| * The key of the mark definition (in the case of annotations). | ||
| * `undefined` if the mark is a decorator (strong, em or similar). | ||
| */ | ||
| markKey?: string; | ||
| /** | ||
| * Type of the mark. For annotations, this is the `_type` property of the value. | ||
| * For decorators, it will hold the name of the decorator (strong, em or similar). | ||
| */ | ||
| markType: string; | ||
| /** | ||
| * Child nodes of this span. Can be toolkit-specific text nodes, nested spans | ||
| * or any inline object type. | ||
| */ | ||
| children: (ToolkitTextNode | ToolkitNestedPortableTextSpan | ArbitraryTypedObject)[]; | ||
| } | ||
| /** | ||
| * Toolkit-specific type representing a nested list | ||
| * | ||
| * See the `nestLists()` function for more info | ||
| */ | ||
| export declare type ToolkitPortableTextList = | ||
| | ToolkitPortableTextHtmlList | ||
| | ToolkitPortableTextDirectList; | ||
| * Strict check to determine if node is a correctly formatted Portable Text span. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if valid Portable Text span, otherwise false | ||
| */ | ||
| declare function isPortableTextSpan(node: ArbitraryTypedObject | PortableTextSpan): node is PortableTextSpan; | ||
| /** | ||
| * Toolkit-specific type representing a list item block, but where the children can be another list | ||
| */ | ||
| export declare interface ToolkitPortableTextListItem | ||
| extends PortableTextListItemBlock< | ||
| PortableTextMarkDefinition, | ||
| PortableTextSpan | ToolkitPortableTextList | ||
| > {} | ||
| * Strict check to determine if node is a correctly formatted Portable Text block. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if valid Portable Text block, otherwise false | ||
| */ | ||
| declare function isPortableTextBlock(node: PortableTextBlock | TypedObject): node is PortableTextBlock; | ||
| /** | ||
| * Toolkit-specific type representing a text node, used when nesting spans. | ||
| * | ||
| * See the {@link buildMarksTree | `buildMarksTree()` function} | ||
| */ | ||
| export declare interface ToolkitTextNode { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: "@text"; | ||
| /** | ||
| * The actual string value of the text node | ||
| */ | ||
| text: string; | ||
| } | ||
| * Strict check to determine if node is a correctly formatted portable list item block. | ||
| * | ||
| * @param block - Block to check | ||
| * @returns True if valid Portable Text list item block, otherwise false | ||
| */ | ||
| declare function isPortableTextListItemBlock(block: PortableTextBlock | TypedObject): block is PortableTextListItemBlock; | ||
| /** | ||
| * Takes a Portable Text block (or an array of them) and returns the text value | ||
| * of all the Portable Text span nodes. Adds whitespace when encountering inline, | ||
| * non-span nodes to ensure text flow is optimal. | ||
| * | ||
| * Note that this only accounts for regular Portable Text blocks - any text inside | ||
| * custom content types are not included in the output. | ||
| * | ||
| * @param block - Single block or an array of blocks to extract text from | ||
| * @returns The plain-text content of the blocks | ||
| */ | ||
| export declare function toPlainText( | ||
| block: PortableTextBlock | ArbitraryTypedObject[] | PortableTextBlock[], | ||
| ): string; | ||
| export {}; | ||
| * Loose check to determine if block is a toolkit list node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param block - Block to check | ||
| * @returns True if toolkit list, otherwise false | ||
| */ | ||
| declare function isPortableTextToolkitList(block: TypedObject | ToolkitPortableTextList): block is ToolkitPortableTextList; | ||
| /** | ||
| * Loose check to determine if span is a toolkit span node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param span - Span to check | ||
| * @returns True if toolkit span, otherwise false | ||
| */ | ||
| declare function isPortableTextToolkitSpan(span: TypedObject | ToolkitNestedPortableTextSpan): span is ToolkitNestedPortableTextSpan; | ||
| /** | ||
| * Loose check to determine if node is a toolkit text node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if toolkit text node, otherwise false | ||
| */ | ||
| declare function isPortableTextToolkitTextNode(node: TypedObject | ToolkitTextNode): node is ToolkitTextNode; | ||
| /** | ||
| * Takes a Portable Text block and returns a nested tree of nodes optimized for rendering | ||
| * in HTML-like environments where you want marks/annotations to be nested inside of eachother. | ||
| * For instance, a naive span-by-span rendering might yield: | ||
| * | ||
| * ```html | ||
| * <strong>This block contains </strong> | ||
| * <strong><a href="https://some.url/">a link</a></strong> | ||
| * <strong> and some bolded and </strong> | ||
| * <em><strong>italicized text</strong></em> | ||
| * ``` | ||
| * | ||
| * ...whereas an optimal order would be: | ||
| * | ||
| * ```html | ||
| * <strong> | ||
| * This block contains <a href="https://some.url/">a link</a> | ||
| * and some bolded and <em>italicized text</em> | ||
| * </strong> | ||
| * ``` | ||
| * | ||
| * Note that since "native" Portable Text spans cannot be nested, | ||
| * this function returns an array of "toolkit specific" types: | ||
| * {@link ToolkitTextNode | `@text`} and {@link ToolkitNestedPortableTextSpan | `@span` }. | ||
| * | ||
| * The toolkit-specific type can hold both types, as well as any arbitrary inline objects, | ||
| * creating an actual tree. | ||
| * | ||
| * @param block - The Portable Text block to create a tree of nodes from | ||
| * @returns Array of (potentially) nested spans, text nodes and/or arbitrary inline objects | ||
| */ | ||
| declare function buildMarksTree<M extends PortableTextMarkDefinition = PortableTextMarkDefinition>(block: PortableTextBlock<M>): (ToolkitNestedPortableTextSpan<M> | ToolkitTextNode | ArbitraryTypedObject)[]; | ||
| type ToolkitNestListsOutputNode<T> = T | ToolkitPortableTextHtmlList | ToolkitPortableTextDirectList; | ||
| /** | ||
| * Takes an array of blocks and returns an array of nodes optimized for rendering in HTML-like | ||
| * environment, where lists are nested inside of eachother instead of appearing "flat" as in | ||
| * native Portable Text data structures. | ||
| * | ||
| * Note that the list node is not a native Portable Text node type, and thus is represented | ||
| * using the {@link ToolkitPortableTextList | `@list`} type name (`{_type: '@list'}`). | ||
| * | ||
| * The nesting can be configured in two modes: | ||
| * | ||
| * - `direct`: deeper list nodes will appear as a direct child of the parent list | ||
| * - `html`, deeper list nodes will appear as a child of the last _list item_ in the parent list | ||
| * | ||
| * When using `direct`, all list nodes will be of type {@link ToolkitPortableTextDirectList}, | ||
| * while with `html` they will be of type {@link ToolkitPortableTextHtmlList} | ||
| * | ||
| * These modes are available as {@link LIST_NEST_MODE_HTML} and {@link LIST_NEST_MODE_DIRECT}. | ||
| * | ||
| * @param blocks - Array of Portable Text blocks and other arbitrary types | ||
| * @param mode - Mode to use for nesting, `direct` or `html` | ||
| * @returns Array of potentially nested nodes optimized for rendering | ||
| */ | ||
| declare function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>(blocks: T[], mode: "direct"): (T | ToolkitPortableTextDirectList)[]; | ||
| declare function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>(blocks: T[], mode: "html"): (T | ToolkitPortableTextHtmlList)[]; | ||
| declare function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>(blocks: T[], mode: "direct" | "html"): (T | ToolkitPortableTextHtmlList | ToolkitPortableTextDirectList)[]; | ||
| /** | ||
| * Figures out the optimal order of marks, in order to minimize the amount of | ||
| * nesting/repeated elements in environments such as HTML. For instance, a naive | ||
| * implementation might render something like: | ||
| * | ||
| * ```html | ||
| * <strong>This block contains </strong> | ||
| * <strong><a href="https://some.url/">a link</a></strong> | ||
| * <strong> and some bolded text</strong> | ||
| * ``` | ||
| * | ||
| * ...whereas an optimal order would be: | ||
| * | ||
| * ```html | ||
| * <strong> | ||
| * This block contains <a href="https://some.url/">a link</a> and some bolded text | ||
| * </strong> | ||
| * ``` | ||
| * | ||
| * This is particularly necessary for cases like links, where you don't want multiple | ||
| * individual links for different segments of the link text, even if parts of it are | ||
| * bolded/italicized. | ||
| * | ||
| * This function is meant to be used like: `block.children.map(sortMarksByOccurences)`, | ||
| * and is used internally in {@link buildMarksTree | `buildMarksTree()`}. | ||
| * | ||
| * The marks are sorted in the following order: | ||
| * | ||
| * 1. Marks that are shared amongst the most adjacent siblings | ||
| * 2. Non-default marks (links, custom metadata) | ||
| * 3. Decorators (bold, emphasis, code etc), in a predefined, preferred order | ||
| * | ||
| * @param span - The current span to sort | ||
| * @param index - The index of the current span within the block | ||
| * @param blockChildren - All children of the block being sorted | ||
| * @returns Array of decorators and annotations, sorted by "most adjacent siblings" | ||
| */ | ||
| declare function sortMarksByOccurences(span: PortableTextSpan | TypedObject, index: number, blockChildren: (PortableTextSpan | TypedObject)[]): string[]; | ||
| /** | ||
| * Returns the plain-text representation of a | ||
| * {@link ToolkitNestedPortableTextSpan | toolkit-specific Portable Text span}. | ||
| * | ||
| * Useful if you have a subset of nested nodes and want the text from just those, | ||
| * instead of for the entire Portable Text block. | ||
| * | ||
| * @param span - Span node to get text from (Portable Text toolkit specific type) | ||
| * @returns The plain-text version of the span | ||
| */ | ||
| declare function spanToPlainText(span: ToolkitNestedPortableTextSpan): string; | ||
| /** | ||
| * Takes a Portable Text block (or an array of them) and returns the text value | ||
| * of all the Portable Text span nodes. Adds whitespace when encountering inline, | ||
| * non-span nodes to ensure text flow is optimal. | ||
| * | ||
| * Note that this only accounts for regular Portable Text blocks - any text inside | ||
| * custom content types are not included in the output. | ||
| * | ||
| * @param block - Single block or an array of blocks to extract text from | ||
| * @returns The plain-text content of the blocks | ||
| */ | ||
| declare function toPlainText(block: PortableTextBlock | ArbitraryTypedObject[] | PortableTextBlock[]): string; | ||
| export { LIST_NEST_MODE_DIRECT, LIST_NEST_MODE_HTML, ToolkitListNestMode, ToolkitNestListsOutputNode, ToolkitNestedPortableTextSpan, ToolkitPortableTextDirectList, ToolkitPortableTextHtmlList, ToolkitPortableTextList, ToolkitPortableTextListItem, ToolkitTextNode, buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextSpan, isPortableTextToolkitList, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, nestLists, sortMarksByOccurences, spanToPlainText, toPlainText }; | ||
| //# sourceMappingURL=index.d.ts.map |
+148
-189
| function isPortableTextSpan(node) { | ||
| return node._type === "span" && "text" in node && typeof node.text == "string" && (typeof node.marks > "u" || Array.isArray(node.marks) && node.marks.every((mark) => typeof mark == "string")); | ||
| return node._type === "span" && "text" in node && typeof node.text == "string" && (node.marks === void 0 || Array.isArray(node.marks) && node.marks.every((mark) => typeof mark == "string")); | ||
| } | ||
| function isPortableTextBlock(node) { | ||
| return ( | ||
| // A block doesn't _have_ to be named 'block' - to differentiate between | ||
| // allowed child types and marks, one might name them differently | ||
| typeof node._type == "string" && // Toolkit-types like nested spans are @-prefixed | ||
| node._type[0] !== "@" && // `markDefs` isn't _required_ per say, but if it's there, it needs to be an array | ||
| (!("markDefs" in node) || !node.markDefs || Array.isArray(node.markDefs) && // Every mark definition needs to have an `_key` to be mappable in child spans | ||
| node.markDefs.every((def) => typeof def._key == "string")) && // `children` is required and needs to be an array | ||
| "children" in node && Array.isArray(node.children) && // All children are objects with `_type` (usually spans, but can contain other stuff) | ||
| node.children.every((child) => typeof child == "object" && "_type" in child) | ||
| ); | ||
| return typeof node._type == "string" && node._type[0] !== "@" && (!("markDefs" in node) || !node.markDefs || Array.isArray(node.markDefs) && node.markDefs.every((def) => typeof def._key == "string")) && "children" in node && Array.isArray(node.children) && node.children.every((child) => typeof child == "object" && "_type" in child); | ||
| } | ||
| function isPortableTextListItemBlock(block) { | ||
| return isPortableTextBlock(block) && "listItem" in block && typeof block.listItem == "string" && (typeof block.level > "u" || typeof block.level == "number"); | ||
| return isPortableTextBlock(block) && "listItem" in block && typeof block.listItem == "string" && (block.level === void 0 || typeof block.level == "number"); | ||
| } | ||
| function isPortableTextToolkitList(block) { | ||
| return block._type === "@list"; | ||
| return block._type === "@list"; | ||
| } | ||
| function isPortableTextToolkitSpan(span) { | ||
| return span._type === "@span"; | ||
| return span._type === "@span"; | ||
| } | ||
| function isPortableTextToolkitTextNode(node) { | ||
| return node._type === "@text"; | ||
| return node._type === "@text"; | ||
| } | ||
| const knownDecorators = ["strong", "em", "code", "underline", "strike-through"]; | ||
| const knownDecorators = [ | ||
| "strong", | ||
| "em", | ||
| "code", | ||
| "underline", | ||
| "strike-through" | ||
| ]; | ||
| function sortMarksByOccurences(span, index, blockChildren) { | ||
| if (!isPortableTextSpan(span) || !span.marks) | ||
| return []; | ||
| if (!span.marks.length) | ||
| return []; | ||
| const marks = span.marks.slice(), occurences = {}; | ||
| return marks.forEach((mark) => { | ||
| occurences[mark] = 1; | ||
| for (let siblingIndex = index + 1; siblingIndex < blockChildren.length; siblingIndex++) { | ||
| const sibling = blockChildren[siblingIndex]; | ||
| if (sibling && isPortableTextSpan(sibling) && Array.isArray(sibling.marks) && sibling.marks.indexOf(mark) !== -1) | ||
| occurences[mark]++; | ||
| else | ||
| break; | ||
| } | ||
| }), marks.sort((markA, markB) => sortMarks(occurences, markA, markB)); | ||
| if (!isPortableTextSpan(span) || !span.marks || !span.marks.length) return []; | ||
| let marks = span.marks.slice(), occurences = {}; | ||
| return marks.forEach((mark) => { | ||
| occurences[mark] = 1; | ||
| for (let siblingIndex = index + 1; siblingIndex < blockChildren.length; siblingIndex++) { | ||
| let sibling = blockChildren[siblingIndex]; | ||
| if (sibling && isPortableTextSpan(sibling) && Array.isArray(sibling.marks) && sibling.marks.indexOf(mark) !== -1) occurences[mark]++; | ||
| else break; | ||
| } | ||
| }), marks.sort((markA, markB) => sortMarks(occurences, markA, markB)); | ||
| } | ||
| function sortMarks(occurences, markA, markB) { | ||
| const aOccurences = occurences[markA], bOccurences = occurences[markB]; | ||
| if (aOccurences !== bOccurences) | ||
| return bOccurences - aOccurences; | ||
| const aKnownPos = knownDecorators.indexOf(markA), bKnownPos = knownDecorators.indexOf(markB); | ||
| return aKnownPos !== bKnownPos ? aKnownPos - bKnownPos : markA.localeCompare(markB); | ||
| let aOccurences = occurences[markA], bOccurences = occurences[markB]; | ||
| if (aOccurences !== bOccurences) return bOccurences - aOccurences; | ||
| let aKnownPos = knownDecorators.indexOf(markA), bKnownPos = knownDecorators.indexOf(markB); | ||
| return aKnownPos === bKnownPos ? markA.localeCompare(markB) : aKnownPos - bKnownPos; | ||
| } | ||
| function buildMarksTree(block) { | ||
| const { children } = block, markDefs = block.markDefs ?? []; | ||
| if (!children || !children.length) | ||
| return []; | ||
| const sortedMarks = children.map(sortMarksByOccurences), rootNode = { | ||
| _type: "@span", | ||
| children: [], | ||
| markType: "<unknown>" | ||
| }; | ||
| let nodeStack = [rootNode]; | ||
| for (let i = 0; i < children.length; i++) { | ||
| const span = children[i]; | ||
| if (!span) | ||
| continue; | ||
| const marksNeeded = sortedMarks[i] || []; | ||
| let pos = 1; | ||
| if (nodeStack.length > 1) | ||
| for (pos; pos < nodeStack.length; pos++) { | ||
| const mark = nodeStack[pos]?.markKey || "", index = marksNeeded.indexOf(mark); | ||
| if (index === -1) | ||
| break; | ||
| marksNeeded.splice(index, 1); | ||
| } | ||
| nodeStack = nodeStack.slice(0, pos); | ||
| let currentNode = nodeStack[nodeStack.length - 1]; | ||
| if (currentNode) { | ||
| for (const markKey of marksNeeded) { | ||
| const markDef = markDefs?.find((def) => def._key === markKey), markType = markDef ? markDef._type : markKey, node = { | ||
| _type: "@span", | ||
| _key: span._key, | ||
| children: [], | ||
| markDef, | ||
| markType, | ||
| markKey | ||
| }; | ||
| currentNode.children.push(node), nodeStack.push(node), currentNode = node; | ||
| } | ||
| if (isPortableTextSpan(span)) { | ||
| const lines = span.text.split(` | ||
| `); | ||
| for (let line = lines.length; line-- > 1; ) | ||
| lines.splice(line, 0, ` | ||
| `); | ||
| currentNode.children = currentNode.children.concat( | ||
| lines.map((text) => ({ _type: "@text", text })) | ||
| ); | ||
| } else | ||
| currentNode.children = currentNode.children.concat(span); | ||
| } | ||
| } | ||
| return rootNode.children; | ||
| let { children } = block, markDefs = block.markDefs ?? []; | ||
| if (!children || !children.length) return []; | ||
| let sortedMarks = children.map(sortMarksByOccurences), rootNode = { | ||
| _type: "@span", | ||
| children: [], | ||
| markType: "<unknown>" | ||
| }, nodeStack = [rootNode]; | ||
| for (let i = 0; i < children.length; i++) { | ||
| let span = children[i]; | ||
| if (!span) continue; | ||
| let marksNeeded = sortedMarks[i] || [], pos = 1; | ||
| if (nodeStack.length > 1) for (; pos < nodeStack.length; pos++) { | ||
| let mark = nodeStack[pos]?.markKey || "", index = marksNeeded.indexOf(mark); | ||
| if (index === -1) break; | ||
| marksNeeded.splice(index, 1); | ||
| } | ||
| nodeStack = nodeStack.slice(0, pos); | ||
| let currentNode = nodeStack[nodeStack.length - 1]; | ||
| if (currentNode) { | ||
| for (let markKey of marksNeeded) { | ||
| let markDef = markDefs?.find((def) => def._key === markKey), node = { | ||
| _type: "@span", | ||
| _key: span._key, | ||
| children: [], | ||
| markDef, | ||
| markType: markDef ? markDef._type : markKey, | ||
| markKey | ||
| }; | ||
| currentNode.children.push(node), nodeStack.push(node), currentNode = node; | ||
| } | ||
| if (isPortableTextSpan(span)) { | ||
| let lines = span.text.split("\n"); | ||
| for (let line = lines.length; line-- > 1;) lines.splice(line, 0, "\n"); | ||
| currentNode.children = currentNode.children.concat(lines.map((text) => ({ | ||
| _type: "@text", | ||
| text | ||
| }))); | ||
| } else currentNode.children = currentNode.children.concat(span); | ||
| } | ||
| } | ||
| return rootNode.children; | ||
| } | ||
| function nestLists(blocks, mode) { | ||
| const tree = []; | ||
| let currentList; | ||
| for (let i = 0; i < blocks.length; i++) { | ||
| const block = blocks[i]; | ||
| if (block) { | ||
| if (!isPortableTextListItemBlock(block)) { | ||
| tree.push(block), currentList = void 0; | ||
| continue; | ||
| } | ||
| if (!currentList) { | ||
| currentList = listFromBlock(block, i, mode), tree.push(currentList); | ||
| continue; | ||
| } | ||
| if (blockMatchesList(block, currentList)) { | ||
| currentList.children.push(block); | ||
| continue; | ||
| } | ||
| if ((block.level || 1) > currentList.level) { | ||
| const newList = listFromBlock(block, i, mode); | ||
| if (mode === "html") { | ||
| const lastListItem = currentList.children[currentList.children.length - 1], newLastChild = { | ||
| ...lastListItem, | ||
| children: [...lastListItem.children, newList] | ||
| }; | ||
| currentList.children[currentList.children.length - 1] = newLastChild; | ||
| } else | ||
| currentList.children.push( | ||
| newList | ||
| ); | ||
| currentList = newList; | ||
| continue; | ||
| } | ||
| if ((block.level || 1) < currentList.level) { | ||
| const matchingBranch = tree[tree.length - 1], match = matchingBranch && findListMatching(matchingBranch, block); | ||
| if (match) { | ||
| currentList = match, currentList.children.push(block); | ||
| continue; | ||
| } | ||
| currentList = listFromBlock(block, i, mode), tree.push(currentList); | ||
| continue; | ||
| } | ||
| if (block.listItem !== currentList.listItem) { | ||
| const matchingBranch = tree[tree.length - 1], match = matchingBranch && findListMatching(matchingBranch, { level: block.level || 1 }); | ||
| if (match && match.listItem === block.listItem) { | ||
| currentList = match, currentList.children.push(block); | ||
| continue; | ||
| } else { | ||
| currentList = listFromBlock(block, i, mode), tree.push(currentList); | ||
| continue; | ||
| } | ||
| } | ||
| console.warn("Unknown state encountered for block", block), tree.push(block); | ||
| } | ||
| } | ||
| return tree; | ||
| let tree = [], currentList; | ||
| for (let i = 0; i < blocks.length; i++) { | ||
| let block = blocks[i]; | ||
| if (block) { | ||
| if (!isPortableTextListItemBlock(block)) { | ||
| tree.push(block), currentList = void 0; | ||
| continue; | ||
| } | ||
| if (!currentList) { | ||
| currentList = listFromBlock(block, i, mode), tree.push(currentList); | ||
| continue; | ||
| } | ||
| if (blockMatchesList(block, currentList)) { | ||
| currentList.children.push(block); | ||
| continue; | ||
| } | ||
| if ((block.level || 1) > currentList.level) { | ||
| let newList = listFromBlock(block, i, mode); | ||
| if (mode === "html") { | ||
| let lastListItem = currentList.children[currentList.children.length - 1], newLastChild = { | ||
| ...lastListItem, | ||
| children: [...lastListItem.children, newList] | ||
| }; | ||
| currentList.children[currentList.children.length - 1] = newLastChild; | ||
| } else currentList.children.push(newList); | ||
| currentList = newList; | ||
| continue; | ||
| } | ||
| if ((block.level || 1) < currentList.level) { | ||
| let matchingBranch = tree[tree.length - 1], match = matchingBranch && findListMatching(matchingBranch, block); | ||
| if (match) { | ||
| currentList = match, currentList.children.push(block); | ||
| continue; | ||
| } | ||
| currentList = listFromBlock(block, i, mode), tree.push(currentList); | ||
| continue; | ||
| } | ||
| if (block.listItem !== currentList.listItem) { | ||
| let matchingBranch = tree[tree.length - 1], match = matchingBranch && findListMatching(matchingBranch, { level: block.level || 1 }); | ||
| if (match && match.listItem === block.listItem) { | ||
| currentList = match, currentList.children.push(block); | ||
| continue; | ||
| } else { | ||
| currentList = listFromBlock(block, i, mode), tree.push(currentList); | ||
| continue; | ||
| } | ||
| } | ||
| console.warn("Unknown state encountered for block", block), tree.push(block); | ||
| } | ||
| } | ||
| return tree; | ||
| } | ||
| function blockMatchesList(block, list) { | ||
| return (block.level || 1) === list.level && block.listItem === list.listItem; | ||
| return (block.level || 1) === list.level && block.listItem === list.listItem; | ||
| } | ||
| function listFromBlock(block, index, mode) { | ||
| return { | ||
| _type: "@list", | ||
| _key: `${block._key || `${index}`}-parent`, | ||
| mode, | ||
| level: block.level || 1, | ||
| listItem: block.listItem, | ||
| children: [block] | ||
| }; | ||
| return { | ||
| _type: "@list", | ||
| _key: `${block._key || `${index}`}-parent`, | ||
| mode, | ||
| level: block.level || 1, | ||
| listItem: block.listItem, | ||
| children: [block] | ||
| }; | ||
| } | ||
| function findListMatching(rootNode, matching) { | ||
| const level = matching.level || 1, style = matching.listItem || "normal", filterOnType = typeof matching.listItem == "string"; | ||
| if (isPortableTextToolkitList(rootNode) && (rootNode.level || 1) === level && filterOnType && (rootNode.listItem || "normal") === style) | ||
| return rootNode; | ||
| if (!("children" in rootNode)) | ||
| return; | ||
| const node = rootNode.children[rootNode.children.length - 1]; | ||
| return node && !isPortableTextSpan(node) ? findListMatching(node, matching) : void 0; | ||
| let level = matching.level || 1, style = matching.listItem || "normal", filterOnType = typeof matching.listItem == "string"; | ||
| if (isPortableTextToolkitList(rootNode) && (rootNode.level || 1) === level && filterOnType && (rootNode.listItem || "normal") === style) return rootNode; | ||
| if (!("children" in rootNode)) return; | ||
| let node = rootNode.children[rootNode.children.length - 1]; | ||
| return node && !isPortableTextSpan(node) ? findListMatching(node, matching) : void 0; | ||
| } | ||
| function spanToPlainText(span) { | ||
| let text = ""; | ||
| return span.children.forEach((current) => { | ||
| isPortableTextToolkitTextNode(current) ? text += current.text : isPortableTextToolkitSpan(current) && (text += spanToPlainText(current)); | ||
| }), text; | ||
| let text = ""; | ||
| return span.children.forEach((current) => { | ||
| isPortableTextToolkitTextNode(current) ? text += current.text : isPortableTextToolkitSpan(current) && (text += spanToPlainText(current)); | ||
| }), text; | ||
| } | ||
| const leadingSpace = /^\s/, trailingSpace = /\s$/; | ||
| function toPlainText(block) { | ||
| const blocks = Array.isArray(block) ? block : [block]; | ||
| let text = ""; | ||
| return blocks.forEach((current, index) => { | ||
| if (!isPortableTextBlock(current)) | ||
| return; | ||
| let pad = !1; | ||
| current.children.forEach((span) => { | ||
| isPortableTextSpan(span) ? (text += pad && text && !trailingSpace.test(text) && !leadingSpace.test(span.text) ? " " : "", text += span.text, pad = !1) : pad = !0; | ||
| }), index !== blocks.length - 1 && (text += ` | ||
| `); | ||
| }), text; | ||
| let blocks = Array.isArray(block) ? block : [block], text = ""; | ||
| return blocks.forEach((current, index) => { | ||
| if (!isPortableTextBlock(current)) return; | ||
| let pad = !1; | ||
| current.children.forEach((span) => { | ||
| isPortableTextSpan(span) ? (text += pad && text && !trailingSpace.test(text) && !leadingSpace.test(span.text) ? " " : "", text += span.text, pad = !1) : pad = !0; | ||
| }), index !== blocks.length - 1 && (text += "\n\n"); | ||
| }), text; | ||
| } | ||
| const LIST_NEST_MODE_HTML = "html", LIST_NEST_MODE_DIRECT = "direct"; | ||
| export { | ||
| LIST_NEST_MODE_DIRECT, | ||
| LIST_NEST_MODE_HTML, | ||
| buildMarksTree, | ||
| isPortableTextBlock, | ||
| isPortableTextListItemBlock, | ||
| isPortableTextSpan, | ||
| isPortableTextToolkitList, | ||
| isPortableTextToolkitSpan, | ||
| isPortableTextToolkitTextNode, | ||
| nestLists, | ||
| sortMarksByOccurences, | ||
| spanToPlainText, | ||
| toPlainText | ||
| }; | ||
| //# sourceMappingURL=index.js.map | ||
| export { LIST_NEST_MODE_DIRECT, LIST_NEST_MODE_HTML, buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextSpan, isPortableTextToolkitList, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, nestLists, sortMarksByOccurences, spanToPlainText, toPlainText }; | ||
| //# sourceMappingURL=index.js.map |
+20
-33
| { | ||
| "name": "@portabletext/toolkit", | ||
| "version": "4.0.0", | ||
| "version": "5.0.0", | ||
| "description": "Toolkit of handy utility functions for dealing with Portable Text", | ||
@@ -26,34 +26,28 @@ "keywords": [ | ||
| "exports": { | ||
| ".": { | ||
| "source": "./src/index.ts", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| ".": "./dist/index.js", | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "files": [ | ||
| "dist", | ||
| "src", | ||
| "README.md" | ||
| "dist" | ||
| ], | ||
| "browserslist": "extends @sanity/browserslist-config", | ||
| "dependencies": { | ||
| "@portabletext/types": "^3.0.0" | ||
| "@portabletext/types": "^4.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@changesets/changelog-github": "^0.5.1", | ||
| "@changesets/cli": "^2.29.7", | ||
| "@sanity/browserslist-config": "^1.0.5", | ||
| "@sanity/pkg-utils": "^8.1.29", | ||
| "@types/node": "^24.10.0", | ||
| "@vitest/coverage-v8": "^4.0.6", | ||
| "npm-run-all2": "^8.0.4", | ||
| "oxfmt": "^0.9.0", | ||
| "oxlint": "^1.25.0", | ||
| "oxlint-tsgolint": "^0.5.0", | ||
| "rimraf": "^4.4.1", | ||
| "typedoc": "^0.28.14", | ||
| "@changesets/changelog-github": "^0.5.2", | ||
| "@changesets/cli": "^2.29.8", | ||
| "@sanity/tsconfig": "^2.0.0", | ||
| "@sanity/tsdown-config": "^0.4.0", | ||
| "@types/node": "^24.10.2", | ||
| "@vitest/coverage-v8": "^4.0.15", | ||
| "oxfmt": "^0.17.0", | ||
| "oxlint": "^1.32.0", | ||
| "oxlint-tsgolint": "^0.8.4", | ||
| "tsdown": "^0.17.2", | ||
| "typedoc": "^0.28.15", | ||
| "typescript": "5.9.3", | ||
| "vitest": "^4.0.6" | ||
| "vitest": "^4.0.15" | ||
| }, | ||
@@ -63,18 +57,11 @@ "engines": { | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "scripts": { | ||
| "build": "run-s clean pkg:build pkg:check", | ||
| "clean": "rimraf dist coverage", | ||
| "build": "tsdown", | ||
| "coverage": "vitest run --coverage", | ||
| "docs:build": "typedoc", | ||
| "format": "oxfmt .", | ||
| "lint": "oxlint --deny-warnings --report-unused-disable-directives --type-aware", | ||
| "pkg:build": "pkg-utils build --strict", | ||
| "pkg:check": "pkg-utils --strict", | ||
| "lint": "oxlint --type-aware --type-check --deny-warnings --report-unused-disable-directives", | ||
| "release": "changeset publish", | ||
| "test": "vitest", | ||
| "type-check": "tsc --noEmit" | ||
| "test": "vitest" | ||
| } | ||
| } |
-112
| import type { | ||
| ArbitraryTypedObject, | ||
| PortableTextBlock, | ||
| PortableTextListItemBlock, | ||
| PortableTextSpan, | ||
| TypedObject, | ||
| } from '@portabletext/types' | ||
| import type {ToolkitNestedPortableTextSpan, ToolkitPortableTextList, ToolkitTextNode} from './types' | ||
| /** | ||
| * Strict check to determine if node is a correctly formatted Portable Text span. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if valid Portable Text span, otherwise false | ||
| */ | ||
| export function isPortableTextSpan( | ||
| node: ArbitraryTypedObject | PortableTextSpan, | ||
| ): node is PortableTextSpan { | ||
| return ( | ||
| node._type === 'span' && | ||
| 'text' in node && | ||
| typeof node.text === 'string' && | ||
| (typeof node.marks === 'undefined' || | ||
| (Array.isArray(node.marks) && node.marks.every((mark) => typeof mark === 'string'))) | ||
| ) | ||
| } | ||
| /** | ||
| * Strict check to determine if node is a correctly formatted Portable Text block. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if valid Portable Text block, otherwise false | ||
| */ | ||
| export function isPortableTextBlock( | ||
| node: PortableTextBlock | TypedObject, | ||
| ): node is PortableTextBlock { | ||
| return ( | ||
| // A block doesn't _have_ to be named 'block' - to differentiate between | ||
| // allowed child types and marks, one might name them differently | ||
| typeof node._type === 'string' && | ||
| // Toolkit-types like nested spans are @-prefixed | ||
| node._type[0] !== '@' && | ||
| // `markDefs` isn't _required_ per say, but if it's there, it needs to be an array | ||
| (!('markDefs' in node) || | ||
| !node.markDefs || | ||
| (Array.isArray(node.markDefs) && | ||
| // Every mark definition needs to have an `_key` to be mappable in child spans | ||
| node.markDefs.every((def) => typeof def._key === 'string'))) && | ||
| // `children` is required and needs to be an array | ||
| 'children' in node && | ||
| Array.isArray(node.children) && | ||
| // All children are objects with `_type` (usually spans, but can contain other stuff) | ||
| node.children.every((child) => typeof child === 'object' && '_type' in child) | ||
| ) | ||
| } | ||
| /** | ||
| * Strict check to determine if node is a correctly formatted portable list item block. | ||
| * | ||
| * @param block - Block to check | ||
| * @returns True if valid Portable Text list item block, otherwise false | ||
| */ | ||
| export function isPortableTextListItemBlock( | ||
| block: PortableTextBlock | TypedObject, | ||
| ): block is PortableTextListItemBlock { | ||
| return ( | ||
| isPortableTextBlock(block) && | ||
| 'listItem' in block && | ||
| typeof block.listItem === 'string' && | ||
| (typeof block.level === 'undefined' || typeof block.level === 'number') | ||
| ) | ||
| } | ||
| /** | ||
| * Loose check to determine if block is a toolkit list node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param block - Block to check | ||
| * @returns True if toolkit list, otherwise false | ||
| */ | ||
| export function isPortableTextToolkitList( | ||
| block: TypedObject | ToolkitPortableTextList, | ||
| ): block is ToolkitPortableTextList { | ||
| return block._type === '@list' | ||
| } | ||
| /** | ||
| * Loose check to determine if span is a toolkit span node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param span - Span to check | ||
| * @returns True if toolkit span, otherwise false | ||
| */ | ||
| export function isPortableTextToolkitSpan( | ||
| span: TypedObject | ToolkitNestedPortableTextSpan, | ||
| ): span is ToolkitNestedPortableTextSpan { | ||
| return span._type === '@span' | ||
| } | ||
| /** | ||
| * Loose check to determine if node is a toolkit text node. | ||
| * Only checks `_type`, assumes correct structure. | ||
| * | ||
| * @param node - Node to check | ||
| * @returns True if toolkit text node, otherwise false | ||
| */ | ||
| export function isPortableTextToolkitTextNode( | ||
| node: TypedObject | ToolkitTextNode, | ||
| ): node is ToolkitTextNode { | ||
| return node._type === '@text' | ||
| } |
| import type { | ||
| ArbitraryTypedObject, | ||
| PortableTextBlock, | ||
| PortableTextMarkDefinition, | ||
| } from '@portabletext/types' | ||
| import {isPortableTextSpan} from './asserters' | ||
| import {sortMarksByOccurences} from './sortMarksByOccurences' | ||
| import type {ToolkitNestedPortableTextSpan, ToolkitTextNode} from './types' | ||
| /** | ||
| * Takes a Portable Text block and returns a nested tree of nodes optimized for rendering | ||
| * in HTML-like environments where you want marks/annotations to be nested inside of eachother. | ||
| * For instance, a naive span-by-span rendering might yield: | ||
| * | ||
| * ```html | ||
| * <strong>This block contains </strong> | ||
| * <strong><a href="https://some.url/">a link</a></strong> | ||
| * <strong> and some bolded and </strong> | ||
| * <em><strong>italicized text</strong></em> | ||
| * ``` | ||
| * | ||
| * ...whereas an optimal order would be: | ||
| * | ||
| * ```html | ||
| * <strong> | ||
| * This block contains <a href="https://some.url/">a link</a> | ||
| * and some bolded and <em>italicized text</em> | ||
| * </strong> | ||
| * ``` | ||
| * | ||
| * Note that since "native" Portable Text spans cannot be nested, | ||
| * this function returns an array of "toolkit specific" types: | ||
| * {@link ToolkitTextNode | `@text`} and {@link ToolkitNestedPortableTextSpan | `@span` }. | ||
| * | ||
| * The toolkit-specific type can hold both types, as well as any arbitrary inline objects, | ||
| * creating an actual tree. | ||
| * | ||
| * @param block - The Portable Text block to create a tree of nodes from | ||
| * @returns Array of (potentially) nested spans, text nodes and/or arbitrary inline objects | ||
| */ | ||
| export function buildMarksTree<M extends PortableTextMarkDefinition = PortableTextMarkDefinition>( | ||
| block: PortableTextBlock<M>, | ||
| ): (ToolkitNestedPortableTextSpan<M> | ToolkitTextNode | ArbitraryTypedObject)[] { | ||
| const {children} = block | ||
| const markDefs = block.markDefs ?? [] | ||
| if (!children || !children.length) { | ||
| return [] | ||
| } | ||
| const sortedMarks = children.map(sortMarksByOccurences) | ||
| const rootNode: ToolkitNestedPortableTextSpan<M> = { | ||
| _type: '@span', | ||
| children: [], | ||
| markType: '<unknown>', | ||
| } | ||
| let nodeStack: ToolkitNestedPortableTextSpan<M>[] = [rootNode] | ||
| for (let i = 0; i < children.length; i++) { | ||
| const span = children[i] | ||
| if (!span) { | ||
| continue | ||
| } | ||
| const marksNeeded = sortedMarks[i] || [] | ||
| let pos = 1 | ||
| // Start at position one. Root is always plain and should never be removed | ||
| if (nodeStack.length > 1) { | ||
| for (pos; pos < nodeStack.length; pos++) { | ||
| const mark = nodeStack[pos]?.markKey || '' | ||
| const index = marksNeeded.indexOf(mark) | ||
| if (index === -1) { | ||
| break | ||
| } | ||
| marksNeeded.splice(index, 1) | ||
| } | ||
| } | ||
| // Keep from beginning to first miss | ||
| nodeStack = nodeStack.slice(0, pos) | ||
| // Add needed nodes | ||
| let currentNode = nodeStack[nodeStack.length - 1] | ||
| if (!currentNode) { | ||
| continue | ||
| } | ||
| for (const markKey of marksNeeded) { | ||
| const markDef = markDefs?.find((def) => def._key === markKey) | ||
| const markType = markDef ? markDef._type : markKey | ||
| const node: ToolkitNestedPortableTextSpan<M> = { | ||
| _type: '@span', | ||
| _key: span._key, | ||
| children: [], | ||
| markDef, | ||
| markType, | ||
| markKey, | ||
| } | ||
| currentNode.children.push(node) | ||
| nodeStack.push(node) | ||
| currentNode = node | ||
| } | ||
| // Split at newlines to make individual line chunks, but keep newline | ||
| // characters as individual elements in the array. We use these characters | ||
| // in the span serializer to trigger hard-break rendering | ||
| if (isPortableTextSpan(span)) { | ||
| const lines = span.text.split('\n') | ||
| for (let line = lines.length; line-- > 1; ) { | ||
| lines.splice(line, 0, '\n') | ||
| } | ||
| currentNode.children = currentNode.children.concat( | ||
| lines.map((text) => ({_type: '@text', text})), | ||
| ) | ||
| } else { | ||
| // This is some other inline object, not a text span | ||
| currentNode.children = currentNode.children.concat(span) | ||
| } | ||
| } | ||
| return rootNode.children | ||
| } |
| export * from './asserters' | ||
| export * from './buildMarksTree' | ||
| export * from './nestLists' | ||
| export * from './sortMarksByOccurences' | ||
| export * from './spanToPlainText' | ||
| export * from './toPlainText' | ||
| export * from './types' |
-202
| import type {PortableTextBlock, PortableTextListItemBlock, TypedObject} from '@portabletext/types' | ||
| import { | ||
| isPortableTextListItemBlock, | ||
| isPortableTextSpan, | ||
| isPortableTextToolkitList, | ||
| } from './asserters' | ||
| import type { | ||
| ToolkitListNestMode, | ||
| ToolkitPortableTextDirectList, | ||
| ToolkitPortableTextHtmlList, | ||
| ToolkitPortableTextList, | ||
| ToolkitPortableTextListItem, | ||
| } from './types' | ||
| export type ToolkitNestListsOutputNode<T> = | ||
| | T | ||
| | ToolkitPortableTextHtmlList | ||
| | ToolkitPortableTextDirectList | ||
| /** | ||
| * Takes an array of blocks and returns an array of nodes optimized for rendering in HTML-like | ||
| * environment, where lists are nested inside of eachother instead of appearing "flat" as in | ||
| * native Portable Text data structures. | ||
| * | ||
| * Note that the list node is not a native Portable Text node type, and thus is represented | ||
| * using the {@link ToolkitPortableTextList | `@list`} type name (`{_type: '@list'}`). | ||
| * | ||
| * The nesting can be configured in two modes: | ||
| * | ||
| * - `direct`: deeper list nodes will appear as a direct child of the parent list | ||
| * - `html`, deeper list nodes will appear as a child of the last _list item_ in the parent list | ||
| * | ||
| * When using `direct`, all list nodes will be of type {@link ToolkitPortableTextDirectList}, | ||
| * while with `html` they will be of type {@link ToolkitPortableTextHtmlList} | ||
| * | ||
| * These modes are available as {@link LIST_NEST_MODE_HTML} and {@link LIST_NEST_MODE_DIRECT}. | ||
| * | ||
| * @param blocks - Array of Portable Text blocks and other arbitrary types | ||
| * @param mode - Mode to use for nesting, `direct` or `html` | ||
| * @returns Array of potentially nested nodes optimized for rendering | ||
| */ | ||
| export function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>( | ||
| blocks: T[], | ||
| mode: 'direct', | ||
| ): (T | ToolkitPortableTextDirectList)[] | ||
| export function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>( | ||
| blocks: T[], | ||
| mode: 'html', | ||
| ): (T | ToolkitPortableTextHtmlList)[] | ||
| export function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>( | ||
| blocks: T[], | ||
| mode: 'direct' | 'html', | ||
| ): (T | ToolkitPortableTextHtmlList | ToolkitPortableTextDirectList)[] | ||
| export function nestLists<T extends TypedObject = PortableTextBlock | TypedObject>( | ||
| blocks: T[], | ||
| mode: ToolkitListNestMode, | ||
| ): ToolkitNestListsOutputNode<T>[] { | ||
| const tree: ToolkitNestListsOutputNode<T>[] = [] | ||
| let currentList: ToolkitPortableTextList | undefined | ||
| for (let i = 0; i < blocks.length; i++) { | ||
| const block = blocks[i] | ||
| if (!block) { | ||
| continue | ||
| } | ||
| if (!isPortableTextListItemBlock(block)) { | ||
| tree.push(block) | ||
| currentList = undefined | ||
| continue | ||
| } | ||
| // Start of a new list? | ||
| if (!currentList) { | ||
| currentList = listFromBlock(block, i, mode) | ||
| tree.push(currentList) | ||
| continue | ||
| } | ||
| // New list item within same list? | ||
| if (blockMatchesList(block, currentList)) { | ||
| currentList.children.push(block) | ||
| continue | ||
| } | ||
| // Different list props, are we going deeper? | ||
| if ((block.level || 1) > currentList.level) { | ||
| const newList = listFromBlock(block, i, mode) | ||
| if (mode === 'html') { | ||
| // Because HTML is kinda weird, nested lists needs to be nested within list items. | ||
| // So while you would think that we could populate the parent list with a new sub-list, | ||
| // we actually have to target the last list element (child) of the parent. | ||
| // However, at this point we need to be very careful - simply pushing to the list of children | ||
| // will mutate the input, and we don't want to blindly clone the entire tree. | ||
| // Clone the last child while adding our new list as the last child of it | ||
| const lastListItem = currentList.children[ | ||
| currentList.children.length - 1 | ||
| ] as ToolkitPortableTextListItem | ||
| const newLastChild: ToolkitPortableTextListItem = { | ||
| ...lastListItem, | ||
| children: [...lastListItem.children, newList], | ||
| } | ||
| // Swap the last child | ||
| currentList.children[currentList.children.length - 1] = newLastChild | ||
| } else { | ||
| ;(currentList as ToolkitPortableTextDirectList).children.push( | ||
| newList as ToolkitPortableTextDirectList, | ||
| ) | ||
| } | ||
| // Set the newly created, deeper list as the current | ||
| currentList = newList | ||
| continue | ||
| } | ||
| // Different list props, are we going back up the tree? | ||
| if ((block.level || 1) < currentList.level) { | ||
| // Current list has ended, and we need to hook up with a parent of the same level and type | ||
| const matchingBranch = tree[tree.length - 1] | ||
| const match = matchingBranch && findListMatching(matchingBranch, block) | ||
| if (match) { | ||
| currentList = match | ||
| currentList.children.push(block) | ||
| continue | ||
| } | ||
| // Similar parent can't be found, assume new list | ||
| currentList = listFromBlock(block, i, mode) | ||
| tree.push(currentList) | ||
| continue | ||
| } | ||
| // Different list props, different list style? | ||
| if (block.listItem !== currentList.listItem) { | ||
| const matchingBranch = tree[tree.length - 1] | ||
| const match = matchingBranch && findListMatching(matchingBranch, {level: block.level || 1}) | ||
| if (match && match.listItem === block.listItem) { | ||
| currentList = match | ||
| currentList.children.push(block) | ||
| continue | ||
| } else { | ||
| currentList = listFromBlock(block, i, mode) | ||
| tree.push(currentList) | ||
| continue | ||
| } | ||
| } | ||
| // oxlint-disable-next-line no-console | ||
| console.warn('Unknown state encountered for block', block) | ||
| tree.push(block) | ||
| } | ||
| return tree | ||
| } | ||
| function blockMatchesList(block: PortableTextBlock, list: ToolkitPortableTextList) { | ||
| return (block.level || 1) === list.level && block.listItem === list.listItem | ||
| } | ||
| function listFromBlock( | ||
| block: PortableTextListItemBlock, | ||
| index: number, | ||
| mode: ToolkitListNestMode, | ||
| ): ToolkitPortableTextList { | ||
| return { | ||
| _type: '@list', | ||
| _key: `${block._key || `${index}`}-parent`, | ||
| mode, | ||
| level: block.level || 1, | ||
| listItem: block.listItem, | ||
| children: [block], | ||
| } | ||
| } | ||
| function findListMatching<T extends TypedObject | PortableTextBlock>( | ||
| rootNode: T, | ||
| matching: Partial<PortableTextListItemBlock>, | ||
| ): ToolkitPortableTextList | undefined { | ||
| const level = matching.level || 1 | ||
| const style = matching.listItem || 'normal' | ||
| const filterOnType = typeof matching.listItem === 'string' | ||
| if ( | ||
| isPortableTextToolkitList(rootNode) && | ||
| (rootNode.level || 1) === level && | ||
| filterOnType && | ||
| (rootNode.listItem || 'normal') === style | ||
| ) { | ||
| return rootNode | ||
| } | ||
| if (!('children' in rootNode)) { | ||
| return undefined | ||
| } | ||
| const node = rootNode.children[rootNode.children.length - 1] | ||
| return node && !isPortableTextSpan(node) ? findListMatching(node, matching) : undefined | ||
| } |
| import type {PortableTextSpan, TypedObject} from '@portabletext/types' | ||
| import {isPortableTextSpan} from './asserters' | ||
| const knownDecorators = ['strong', 'em', 'code', 'underline', 'strike-through'] | ||
| /** | ||
| * Figures out the optimal order of marks, in order to minimize the amount of | ||
| * nesting/repeated elements in environments such as HTML. For instance, a naive | ||
| * implementation might render something like: | ||
| * | ||
| * ```html | ||
| * <strong>This block contains </strong> | ||
| * <strong><a href="https://some.url/">a link</a></strong> | ||
| * <strong> and some bolded text</strong> | ||
| * ``` | ||
| * | ||
| * ...whereas an optimal order would be: | ||
| * | ||
| * ```html | ||
| * <strong> | ||
| * This block contains <a href="https://some.url/">a link</a> and some bolded text | ||
| * </strong> | ||
| * ``` | ||
| * | ||
| * This is particularly necessary for cases like links, where you don't want multiple | ||
| * individual links for different segments of the link text, even if parts of it are | ||
| * bolded/italicized. | ||
| * | ||
| * This function is meant to be used like: `block.children.map(sortMarksByOccurences)`, | ||
| * and is used internally in {@link buildMarksTree | `buildMarksTree()`}. | ||
| * | ||
| * The marks are sorted in the following order: | ||
| * | ||
| * 1. Marks that are shared amongst the most adjacent siblings | ||
| * 2. Non-default marks (links, custom metadata) | ||
| * 3. Decorators (bold, emphasis, code etc), in a predefined, preferred order | ||
| * | ||
| * @param span - The current span to sort | ||
| * @param index - The index of the current span within the block | ||
| * @param blockChildren - All children of the block being sorted | ||
| * @returns Array of decorators and annotations, sorted by "most adjacent siblings" | ||
| */ | ||
| export function sortMarksByOccurences( | ||
| span: PortableTextSpan | TypedObject, | ||
| index: number, | ||
| blockChildren: (PortableTextSpan | TypedObject)[], | ||
| ): string[] { | ||
| if (!isPortableTextSpan(span) || !span.marks) { | ||
| return [] | ||
| } | ||
| if (!span.marks.length) { | ||
| return [] | ||
| } | ||
| // Slicing because we'll be sorting with `sort()`, which mutates | ||
| const marks = span.marks.slice() | ||
| const occurences: Record<string, number> = {} | ||
| marks.forEach((mark) => { | ||
| occurences[mark] = 1 | ||
| for (let siblingIndex = index + 1; siblingIndex < blockChildren.length; siblingIndex++) { | ||
| const sibling = blockChildren[siblingIndex] | ||
| if ( | ||
| sibling && | ||
| isPortableTextSpan(sibling) && | ||
| Array.isArray(sibling.marks) && | ||
| sibling.marks.indexOf(mark) !== -1 | ||
| ) { | ||
| occurences[mark]++ | ||
| } else { | ||
| break | ||
| } | ||
| } | ||
| }) | ||
| return marks.sort((markA, markB) => sortMarks(occurences, markA, markB)) | ||
| } | ||
| function sortMarks<U extends string, T extends Record<U, number>>( | ||
| occurences: T, | ||
| markA: U, | ||
| markB: U, | ||
| ): number { | ||
| const aOccurences = occurences[markA] | ||
| const bOccurences = occurences[markB] | ||
| if (aOccurences !== bOccurences) { | ||
| return bOccurences - aOccurences | ||
| } | ||
| const aKnownPos = knownDecorators.indexOf(markA) | ||
| const bKnownPos = knownDecorators.indexOf(markB) | ||
| // Sort known decorators last | ||
| if (aKnownPos !== bKnownPos) { | ||
| return aKnownPos - bKnownPos | ||
| } | ||
| // Sort other marks simply by key | ||
| return markA.localeCompare(markB) | ||
| } |
| import {isPortableTextToolkitSpan, isPortableTextToolkitTextNode} from './asserters' | ||
| import type {ToolkitNestedPortableTextSpan} from './types' | ||
| /** | ||
| * Returns the plain-text representation of a | ||
| * {@link ToolkitNestedPortableTextSpan | toolkit-specific Portable Text span}. | ||
| * | ||
| * Useful if you have a subset of nested nodes and want the text from just those, | ||
| * instead of for the entire Portable Text block. | ||
| * | ||
| * @param span - Span node to get text from (Portable Text toolkit specific type) | ||
| * @returns The plain-text version of the span | ||
| */ | ||
| export function spanToPlainText(span: ToolkitNestedPortableTextSpan): string { | ||
| let text = '' | ||
| span.children.forEach((current) => { | ||
| if (isPortableTextToolkitTextNode(current)) { | ||
| text += current.text | ||
| } else if (isPortableTextToolkitSpan(current)) { | ||
| text += spanToPlainText(current) | ||
| } | ||
| }) | ||
| return text | ||
| } |
| import type {ArbitraryTypedObject, PortableTextBlock} from '@portabletext/types' | ||
| import {isPortableTextBlock, isPortableTextSpan} from './asserters' | ||
| const leadingSpace = /^\s/ | ||
| const trailingSpace = /\s$/ | ||
| /** | ||
| * Takes a Portable Text block (or an array of them) and returns the text value | ||
| * of all the Portable Text span nodes. Adds whitespace when encountering inline, | ||
| * non-span nodes to ensure text flow is optimal. | ||
| * | ||
| * Note that this only accounts for regular Portable Text blocks - any text inside | ||
| * custom content types are not included in the output. | ||
| * | ||
| * @param block - Single block or an array of blocks to extract text from | ||
| * @returns The plain-text content of the blocks | ||
| */ | ||
| export function toPlainText( | ||
| block: PortableTextBlock | ArbitraryTypedObject[] | PortableTextBlock[], | ||
| ): string { | ||
| const blocks = Array.isArray(block) ? block : [block] | ||
| let text = '' | ||
| blocks.forEach((current, index) => { | ||
| if (!isPortableTextBlock(current)) { | ||
| return | ||
| } | ||
| let pad = false | ||
| current.children.forEach((span) => { | ||
| if (isPortableTextSpan(span)) { | ||
| // If the previous element was a non-span, and we have no natural whitespace | ||
| // between the previous and the next span, insert it to give the spans some | ||
| // room to breathe. However, don't do so if this is the first span. | ||
| text += pad && text && !trailingSpace.test(text) && !leadingSpace.test(span.text) ? ' ' : '' | ||
| text += span.text | ||
| pad = false | ||
| } else { | ||
| pad = true | ||
| } | ||
| }) | ||
| if (index !== blocks.length - 1) { | ||
| text += '\n\n' | ||
| } | ||
| }) | ||
| return text | ||
| } |
-168
| import type { | ||
| ArbitraryTypedObject, | ||
| PortableTextListItemBlock, | ||
| PortableTextMarkDefinition, | ||
| PortableTextSpan, | ||
| } from '@portabletext/types' | ||
| /** | ||
| * List nesting mode for HTML, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| export const LIST_NEST_MODE_HTML = 'html' | ||
| /** | ||
| * List nesting mode for direct, nested lists, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| export const LIST_NEST_MODE_DIRECT = 'direct' | ||
| /** | ||
| * List nesting mode, see the {@link nestLists | `nestLists()` function} | ||
| */ | ||
| export type ToolkitListNestMode = 'html' | 'direct' | ||
| /** | ||
| * Toolkit-specific type representing a nested list | ||
| * | ||
| * See the `nestLists()` function for more info | ||
| */ | ||
| export type ToolkitPortableTextList = ToolkitPortableTextHtmlList | ToolkitPortableTextDirectList | ||
| /** | ||
| * Toolkit-specific type representing a nested list in HTML mode, where deeper lists are nested | ||
| * inside of the _list items_, eg `<ul><li>Some text<ul><li>Deeper</li></ul></li></ul>` | ||
| */ | ||
| export interface ToolkitPortableTextHtmlList { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: '@list' | ||
| /** | ||
| * Unique key for this list (within its parent) | ||
| */ | ||
| _key: string | ||
| /** | ||
| * List mode, signaling that list nodes will appear as children of the _list items_ | ||
| */ | ||
| mode: 'html' | ||
| /** | ||
| * Level/depth of this list node (starts at `1`) | ||
| */ | ||
| level: number | ||
| /** | ||
| * Style of this list item (`bullet`, `number` are common values, but can be customized) | ||
| */ | ||
| listItem: string | ||
| /** | ||
| * Child nodes of this list - toolkit-specific list items which can themselves hold deeper lists | ||
| */ | ||
| children: ToolkitPortableTextListItem[] | ||
| } | ||
| /** | ||
| * Toolkit-specific type representing a nested list in "direct" mode, where deeper lists are nested | ||
| * inside of the lists children, alongside other blocks. | ||
| */ | ||
| export interface ToolkitPortableTextDirectList { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: '@list' | ||
| /** | ||
| * Unique key for this list (within its parent) | ||
| */ | ||
| _key: string | ||
| /** | ||
| * List mode, signaling that list nodes can appear as direct children | ||
| */ | ||
| mode: 'direct' | ||
| /** | ||
| * Level/depth of this list node (starts at `1`) | ||
| */ | ||
| level: number | ||
| /** | ||
| * Style of this list item (`bullet`, `number` are common values, but can be customized) | ||
| */ | ||
| listItem: string | ||
| /** | ||
| * Child nodes of this list - either portable text list items, or another, deeper list | ||
| */ | ||
| children: (PortableTextListItemBlock | ToolkitPortableTextDirectList)[] | ||
| } | ||
| /** | ||
| * Toolkit-specific type representing a list item block, but where the children can be another list | ||
| */ | ||
| export interface ToolkitPortableTextListItem | ||
| extends PortableTextListItemBlock< | ||
| PortableTextMarkDefinition, | ||
| PortableTextSpan | ToolkitPortableTextList | ||
| > {} | ||
| /** | ||
| * Toolkit-specific type representing a text node, used when nesting spans. | ||
| * | ||
| * See the {@link buildMarksTree | `buildMarksTree()` function} | ||
| */ | ||
| export interface ToolkitTextNode { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: '@text' | ||
| /** | ||
| * The actual string value of the text node | ||
| */ | ||
| text: string | ||
| } | ||
| /** | ||
| * Toolkit-specific type representing a portable text span that can hold other spans. | ||
| * In this type, each span only has a single mark, instead of an array of them. | ||
| */ | ||
| export interface ToolkitNestedPortableTextSpan< | ||
| M extends PortableTextMarkDefinition = PortableTextMarkDefinition, | ||
| > { | ||
| /** | ||
| * Type name, prefixed with `@` to signal that this is a toolkit-specific node. | ||
| */ | ||
| _type: '@span' | ||
| /** | ||
| * Unique key for this span | ||
| */ | ||
| _key?: string | ||
| /** | ||
| * Holds the value (definition) of the mark in the case of annotations. | ||
| * `undefined` if the mark is a decorator (strong, em or similar). | ||
| */ | ||
| markDef?: M | ||
| /** | ||
| * The key of the mark definition (in the case of annotations). | ||
| * `undefined` if the mark is a decorator (strong, em or similar). | ||
| */ | ||
| markKey?: string | ||
| /** | ||
| * Type of the mark. For annotations, this is the `_type` property of the value. | ||
| * For decorators, it will hold the name of the decorator (strong, em or similar). | ||
| */ | ||
| markType: string | ||
| /** | ||
| * Child nodes of this span. Can be toolkit-specific text nodes, nested spans | ||
| * or any inline object type. | ||
| */ | ||
| children: (ToolkitTextNode | ToolkitNestedPortableTextSpan | ArbitraryTypedObject)[] | ||
| } |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
13
-7.14%1
-66.67%57511
-29.05%7
-50%478
-62.57%1
Infinity%+ Added
- Removed
Updated