Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

@portabletext/toolkit

Package Overview
Dependencies
Maintainers
10
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@portabletext/toolkit - npm Package Compare versions

Comparing version
4.0.0
to
5.0.0
+1
dist/index.d.ts.map
{"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":"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"}
+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
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
{
"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"
}
}
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'
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
}
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