import { allowedNodeTypes, blockNodeType, blockquoteNodeType, codeNodeType, headingNodeType, inlineBlockNodeType, inlineItemNodeType, inlineNodeTypes, itemLinkNodeType, linkNodeType, listItemNodeType, listNodeType, paragraphNodeType, rootNodeType, spanNodeType, thematicBreakNodeType, } from './definitions'; import { Block, BlockId, Blockquote, CdaStructuredTextRecord, CdaStructuredTextValue, Code, Document, Heading, InlineBlock, InlineItem, InlineNode, ItemLink, Link, List, ListItem, Node, NodeType, Paragraph, Root, Span, StructuredText, ThematicBreak, WithChildrenNode, } from './types'; export function hasChildren< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is WithChildrenNode { return 'children' in node; } export function isInlineNode< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is InlineNode { return (inlineNodeTypes as NodeType[]).includes(node.type); } export function isHeading< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is Heading { return node.type === headingNodeType; } export function isSpan( node: Node, ): node is Span { return node.type === spanNodeType; } export function isRoot( node: Node, ): node is Root { return node.type === rootNodeType; } export function isParagraph< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is Paragraph { return node.type === paragraphNodeType; } export function isList( node: Node, ): node is List { return node.type === listNodeType; } export function isListItem< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is ListItem { return node.type === listItemNodeType; } export function isBlockquote< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is Blockquote { return node.type === blockquoteNodeType; } export function isBlock( node: Node, ): node is Block { return node.type === blockNodeType; } /** * Narrows a union of block item shapes to the member whose model * (`item_type`) matches `Id`. * * Matches existing (persisted) blocks — items that carry an `id` and * whose `relationships.item_type.data.id` matches. That covers both the * `nested: true` response form and the "updated block" variant of * request payloads. Filtered out: * - bare string IDs (no `id` property to read) * - newly-created blocks in request payloads (they have no `id` yet — * the caller already knows the type, so finding them by model is * not the typical use case) * * `relationships` is constrained as optional so the updated-block * request shape (where `relationships?` may be omitted when patching an * existing block) is still picked up. */ export type NarrowBlockItemByItemType = Extract< T, { id: string; relationships?: { item_type: { data: { type: 'item_type'; id: Id } } }; } >; function itemHasItemTypeId(item: unknown, itemTypeId: string): boolean { if (typeof item !== 'object' || item === null) return false; const relationships = (item as { relationships?: unknown }).relationships; if (typeof relationships !== 'object' || relationships === null) return false; const itemType = (relationships as { item_type?: unknown }).item_type; if (typeof itemType !== 'object' || itemType === null) return false; const data = (itemType as { data?: unknown }).data; if (typeof data !== 'object' || data === null) return false; return (data as { id?: unknown }).id === itemTypeId; } /** * Type guard that narrows a `block` node to the variant whose `item` * belongs to a specific model. * * Two call styles are available: * * - Curried: `isBlockWithItemOfType(itemTypeId)` returns a predicate, * handy with `findFirstNode` / `findAllNodes` / `Array#filter`. * - Direct: `isBlockWithItemOfType(itemTypeId, node)` checks a node * inline (e.g. inside an `if`). * * For the literal `Id` to be preserved (and narrowing to work), the * `itemTypeId` argument must be typed as a literal — use `as const` on * pre-set ID constants. * * @example * ```ts * // Curried * const needle = findFirstNode( * body.document, * isBlockWithItemOfType(WARNING_BLOCK_TYPE_ID), * ); * * // Direct * if (isBlockWithItemOfType(WARNING_BLOCK_TYPE_ID, node)) { * node.item; // narrowed to the Warning item shape * } * ``` */ export function isBlockWithItemOfType( itemTypeId: Id, ): ( node: Node, ) => node is Block>; export function isBlockWithItemOfType< Id extends string, BlockItemType = BlockId, InlineBlockItemType = BlockId >( itemTypeId: Id, node: Node, ): node is Block>; export function isBlockWithItemOfType( itemTypeId: string, node?: Node, ): unknown { if (node === undefined) { return (n: Node): boolean => n.type === blockNodeType && itemHasItemTypeId((n as Block).item, itemTypeId); } return ( node.type === blockNodeType && itemHasItemTypeId((node as Block).item, itemTypeId) ); } export function isInlineBlock< BlockItemType = BlockId, InlineBlockItemType = BlockId >( node: Node, ): node is InlineBlock { return node.type === inlineBlockNodeType; } /** * Type guard that narrows an `inlineBlock` node to the variant whose * `item` belongs to a specific model. * * Mirrors {@link isBlockWithItemOfType} for inline blocks; supports both * the curried form (`isInlineBlockWithItemOfType(itemTypeId)`) and the * direct form (`isInlineBlockWithItemOfType(itemTypeId, node)`). * * For the literal `Id` to be preserved (and narrowing to work), the * `itemTypeId` argument must be typed as a literal — use `as const` on * pre-set ID constants. * * @example * ```ts * // Curried * const needle = findFirstNode( * body.document, * isInlineBlockWithItemOfType(CALLOUT_BLOCK_TYPE_ID), * ); * * // Direct * if (isInlineBlockWithItemOfType(CALLOUT_BLOCK_TYPE_ID, node)) { * node.item; // narrowed to the Callout item shape * } * ``` */ export function isInlineBlockWithItemOfType( itemTypeId: Id, ): ( node: Node, ) => node is InlineBlock>; export function isInlineBlockWithItemOfType< Id extends string, BlockItemType = BlockId, InlineBlockItemType = BlockId >( itemTypeId: Id, node: Node, ): node is InlineBlock>; export function isInlineBlockWithItemOfType( itemTypeId: string, node?: Node, ): unknown { if (node === undefined) { return (n: Node): boolean => n.type === inlineBlockNodeType && itemHasItemTypeId((n as InlineBlock).item, itemTypeId); } return ( node.type === inlineBlockNodeType && itemHasItemTypeId((node as InlineBlock).item, itemTypeId) ); } export function isCode( node: Node, ): node is Code { return node.type === codeNodeType; } export function isLink( node: Node, ): node is Link { return node.type === linkNodeType; } export function isItemLink< BlockItemType = BlockId, InlineBlockItemType = BlockId >(node: Node): node is ItemLink { return node.type === itemLinkNodeType; } export function isInlineItem< BlockItemType = BlockId, InlineBlockItemType = BlockId >(node: Node): node is InlineItem { return node.type === inlineItemNodeType; } export function isThematicBreak< BlockItemType = BlockId, InlineBlockItemType = BlockId >(node: Node): node is ThematicBreak { return node.type === thematicBreakNodeType; } function isObject(obj: unknown): obj is Record { return Boolean(typeof obj === 'object' && obj); } export function isNodeType(value: string): value is NodeType { return allowedNodeTypes.includes(value as NodeType); } export function isNode( obj: unknown, ): obj is Node { return Boolean( isObject(obj) && 'type' in obj && typeof obj.type === 'string' && isNodeType(obj.type), ); } export function isCdaStructuredTextValue< BlockRecord extends CdaStructuredTextRecord, LinkRecord extends CdaStructuredTextRecord, InlineBlockRecord extends CdaStructuredTextRecord >( obj: unknown, ): obj is CdaStructuredTextValue { return Boolean(isObject(obj) && 'value' in obj && isDocument(obj.value)); } /** * @deprecated Use isCdaStructuredTextValue instead */ export function isStructuredText< BlockRecord extends CdaStructuredTextRecord, LinkRecord extends CdaStructuredTextRecord, InlineBlockRecord extends CdaStructuredTextRecord >( obj: unknown, ): obj is StructuredText { return isCdaStructuredTextValue(obj); } export function isDocument< BlockItemType = BlockId, InlineBlockItemType = BlockId >(obj: unknown): obj is Document { return Boolean( isObject(obj) && 'schema' in obj && 'document' in obj && obj.schema === 'dast', ); } export function isEmptyDocument(obj: unknown): boolean { if (!obj) { return true; } const document = isCdaStructuredTextValue(obj) && isDocument(obj.value) ? obj.value : isDocument(obj) ? obj : null; if (!document) { throw new Error( 'Passed object is neither null, a Structured Text value or a DAST document', ); } return ( document.document.children.length === 1 && document.document.children[0].type === 'paragraph' && document.document.children[0].children.length === 1 && document.document.children[0].children[0].type === 'span' && document.document.children[0].children[0].value === '' ); }