UNPKG

@portabletext/toolkit

Version:

Toolkit of handy utility functions for dealing with Portable Text

105 lines (91 loc) 3.12 kB
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) }