UNPKG

@portabletext/block-tools

Version:

Can format HTML, Slate JSON or Sanity block array into any other format.

156 lines (139 loc) 3.99 kB
import { isPortableTextSpan, type PortableTextSpan, type PortableTextTextBlock, } from '@sanity/types' import {isEqual} from 'lodash' import type {TypedObject} from '../types' import {keyGenerator} from './randomKey' /** * Block normalization options * * @public */ export interface BlockNormalizationOptions { /** * Decorator names that are allowed within portable text blocks, eg `em`, `strong` */ allowedDecorators?: string[] /** * Name of the portable text block type, if not `block` */ blockTypeName?: string /** * Custom key generator function */ keyGenerator?: () => string } /** * Normalizes a block by ensuring it has a `_key` property. If the block is a * portable text block, additional normalization is applied: * * - Ensures it has `children` and `markDefs` properties * - Ensures it has at least one child (adds an empty span if empty) * - Joins sibling spans that has the same marks * - Removes decorators that are not allowed according to the schema * - Removes marks that have no annotation definition * * @param node - The block to normalize * @param options - Options for normalization process. See {@link BlockNormalizationOptions} * @returns Normalized block * @public */ export function normalizeBlock( node: TypedObject, options: BlockNormalizationOptions = {}, ): Omit< TypedObject | PortableTextTextBlock<TypedObject | PortableTextSpan>, '_key' > & { _key: string } { if (node._type !== (options.blockTypeName || 'block')) { return '_key' in node ? (node as TypedObject & {_key: string}) : { ...node, _key: options.keyGenerator ? options.keyGenerator() : keyGenerator(), } } const block: Omit< PortableTextTextBlock<TypedObject | PortableTextSpan>, 'style' > = { _key: options.keyGenerator ? options.keyGenerator() : keyGenerator(), children: [], markDefs: [], ...node, } const lastChild = block.children[block.children.length - 1] if (!lastChild) { // A block must at least have an empty span type child block.children = [ { _type: 'span', _key: options.keyGenerator ? options.keyGenerator() : keyGenerator(), text: '', marks: [], }, ] return block } const usedMarkDefs: string[] = [] const allowedDecorators = options.allowedDecorators && Array.isArray(options.allowedDecorators) ? options.allowedDecorators : false block.children = block.children .reduce( (acc, child) => { const previousChild = acc[acc.length - 1] if ( previousChild && isPortableTextSpan(child) && isPortableTextSpan(previousChild) && isEqual(previousChild.marks, child.marks) ) { if ( lastChild && lastChild === child && child.text === '' && block.children.length > 1 ) { return acc } previousChild.text += child.text return acc } acc.push(child) return acc }, [] as (TypedObject | PortableTextSpan)[], ) .map((child) => { if (!child) { throw new Error('missing child') } child._key = options.keyGenerator ? options.keyGenerator() : keyGenerator() if (isPortableTextSpan(child)) { if (!child.marks) { child.marks = [] } else if (allowedDecorators) { child.marks = child.marks.filter((mark) => { const isAllowed = allowedDecorators.includes(mark) const isUsed = block.markDefs?.some((def) => def._key === mark) return isAllowed || isUsed }) } usedMarkDefs.push(...child.marks) } return child }) // Remove leftover (unused) markDefs block.markDefs = (block.markDefs || []).filter((markDef) => usedMarkDefs.includes(markDef._key), ) return block }