import type { MarkdownNode } from "react-native-nitro-markdown/headless"; import type { SelectableMarkdownSkill } from "./SelectableMarkdownText.types";import { resolveMarkdownInlineCodePresentation, resolveMarkdownLinkPresentation, type MarkdownFileIcon,} from "./markdownLinks"; export interface NativeMarkdownTextRun { readonly text: string; readonly bold?: boolean; readonly italic?: boolean; readonly strikethrough?: boolean; readonly code?: boolean; readonly href?: string; readonly externalHost?: string; readonly fileIcon?: MarkdownFileIcon; readonly skillName?: string; readonly skillLabel?: string; readonly role?: | "body" | "heading" | "list-marker" | "list-break" | "quote-marker" | "code-block" | "code-language" | "divider" | "spacer"; readonly headingLevel?: number; readonly depth?: number; readonly spacing?: number; readonly firstLineHeadIndent?: number; readonly headIndent?: number; readonly paragraphSpacing?: number;} export type NativeMarkdownDocumentChunk = | { readonly kind: "selectable"; readonly key: string; readonly node: MarkdownNode; } | { readonly kind: "rich"; readonly key: string; readonly node: MarkdownNode; }; interface RunContext { readonly bold: boolean; readonly italic: boolean; readonly strikethrough: boolean; readonly code: boolean; readonly href?: string; readonly externalHost?: string; readonly fileIcon?: MarkdownFileIcon; readonly role?: NativeMarkdownTextRun["role"]; readonly headingLevel?: number; readonly depth?: number; readonly spacing?: number; readonly firstLineHeadIndent?: number; readonly headIndent?: number; readonly paragraphSpacing?: number;} const EMPTY_CONTEXT: RunContext = { bold: false, italic: false, strikethrough: false, code: false,}; const INLINE_HTML_TAG_PATTERN = /<\/?(?:kbd|mark|sub|sup|u)(?:\s[^>]*)?>/gi; function decodeCodePoint(codePoint: number, entity: string): string { if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff) { return entity; } return String.fromCodePoint(codePoint);} function decodeHtmlEntitiesOnce(value: string): string { return value.replace( /&(?:#(\d+)|#x([0-9a-f]+)|amp|apos|gt|lt|nbsp|quot);/gi, (entity, decimal: string | undefined, hexadecimal: string | undefined) => { if (decimal) { return decodeCodePoint(Number.parseInt(decimal, 10), entity); } if (hexadecimal) { return decodeCodePoint(Number.parseInt(hexadecimal, 16), entity); } switch (entity.toLowerCase()) { case "&": return "&"; case "'": return "'"; case ">": return ">"; case "<": return "<"; case " ": return "\u00a0"; case """: return '"'; default: return entity; } }, );} function decodeHtmlEntities(value: string): string { let decoded = value; for (let pass = 0; pass < 2; pass += 1) { const next = decodeHtmlEntitiesOnce(decoded); if (next === decoded) { break; } decoded = next; } return decoded;} function textNodeContent(value: string): string { return decodeHtmlEntities(value).replace(INLINE_HTML_TAG_PATTERN, "");} function inlineHtmlText(value: string): string { if (/^<br\s*\/?>$/i.test(value.trim())) { return "\n"; } return decodeHtmlEntities(value.replace(/<[^>]+>/g, ""));} function sameRunStyle(left: NativeMarkdownTextRun, right: NativeMarkdownTextRun): boolean { return ( left.bold === right.bold && left.italic === right.italic && left.strikethrough === right.strikethrough && left.code === right.code && left.href === right.href && left.externalHost === right.externalHost && left.fileIcon === right.fileIcon && left.skillName === right.skillName && left.skillLabel === right.skillLabel && left.role === right.role && left.headingLevel === right.headingLevel && left.depth === right.depth && left.spacing === right.spacing && left.firstLineHeadIndent === right.firstLineHeadIndent && left.headIndent === right.headIndent && left.paragraphSpacing === right.paragraphSpacing );} function appendRun( runs: NativeMarkdownTextRun[], text: string, context: RunContext,): NativeMarkdownTextRun[] { if (text.length === 0) { return runs; } const run: NativeMarkdownTextRun = { text, ...(context.bold ? { bold: true } : {}), ...(context.italic ? { italic: true } : {}), ...(context.strikethrough ? { strikethrough: true } : {}), ...(context.code ? { code: true } : {}), ...(context.href ? { href: context.href } : {}), ...(context.externalHost ? { externalHost: context.externalHost } : {}), ...(context.fileIcon ? { fileIcon: context.fileIcon } : {}), ...(context.role ? { role: context.role } : {}), ...(context.headingLevel ? { headingLevel: context.headingLevel } : {}), ...(context.depth ? { depth: context.depth } : {}), ...(context.spacing ? { spacing: context.spacing } : {}), ...(context.firstLineHeadIndent !== undefined ? { firstLineHeadIndent: context.firstLineHeadIndent } : {}), ...(context.headIndent !== undefined ? { headIndent: context.headIndent } : {}), ...(context.paragraphSpacing !== undefined ? { paragraphSpacing: context.paragraphSpacing } : {}), }; const previous = runs.at(-1); if (previous && sameRunStyle(previous, run)) { runs[runs.length - 1] = { ...previous, text: previous.text + run.text }; return runs; } runs.push(run); return runs;} const SKILL_TOKEN_REGEX = /(^|\s)\$(?![0-9][0-9_]*(?:[kKmMbBtT]|[eE][0-9]+)?(?:\s|$))(?=[a-zA-Z0-9:_-]*[a-zA-Z])([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g; function formatSkillLabel(skill: SelectableMarkdownSkill): string { const displayName = skill.displayName?.trim(); if (displayName) { return displayName; } return skill.name .split(/[\s:_-]+/) .filter(Boolean) .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(" ");} function decorateSkillRuns( runs: ReadonlyArray<NativeMarkdownTextRun>, skills: ReadonlyArray<SelectableMarkdownSkill>,): ReadonlyArray<NativeMarkdownTextRun> { if (skills.length === 0) { return runs; } const skillByName = new Map(skills.map((skill) => [skill.name, skill])); const decorated: NativeMarkdownTextRun[] = []; for (const run of runs) { if (run.code || run.href || run.fileIcon || run.role === "code-block") { decorated.push(run); continue; } let cursor = 0; let matched = false; for (const match of run.text.matchAll(SKILL_TOKEN_REGEX)) { const prefix = match[1] ?? ""; const name = match[2] ?? ""; const skill = skillByName.get(name); if (!skill) { continue; } const start = (match.index ?? 0) + prefix.length; const end = start + name.length + 1; if (start > cursor) { decorated.push({ ...run, text: run.text.slice(cursor, start) }); } decorated.push({ ...run, text: run.text.slice(start, end), skillName: name, skillLabel: formatSkillLabel(skill), }); cursor = end; matched = true; } if (!matched) { decorated.push(run); } else if (cursor < run.text.length) { decorated.push({ ...run, text: run.text.slice(cursor) }); } } return decorated;} function appendChildren( runs: NativeMarkdownTextRun[], node: MarkdownNode, context: RunContext,): NativeMarkdownTextRun[] { for (const child of node.children ?? []) { appendNode(runs, child, context); } return runs;} function nodeTextContent(node: MarkdownNode): string { if (node.content !== undefined) { return node.content; } return (node.children ?? []).map(nodeTextContent).join("");} function appendNode( runs: NativeMarkdownTextRun[], node: MarkdownNode, context: RunContext,): NativeMarkdownTextRun[] { switch (node.type) { case "text": case "math_inline": return appendRun(runs, textNodeContent(nodeTextContent(node)), context); case "html_inline": return appendRun(runs, inlineHtmlText(nodeTextContent(node)), context); case "code_inline": { const content = nodeTextContent(node); const presentation = context.href ? null : resolveMarkdownInlineCodePresentation(content); return presentation ? appendRun(runs, presentation.label, { ...context, href: presentation.href, fileIcon: presentation.icon, }) : appendRun(runs, content, { ...context, code: true }); } case "soft_break": return appendRun(runs, " ", context); case "line_break": return appendRun(runs, "\n", context); case "bold": return appendChildren(runs, node, { ...context, bold: true }); case "italic": return appendChildren(runs, node, { ...context, italic: true }); case "strikethrough": return appendChildren(runs, node, { ...context, strikethrough: true }); case "link": { const presentation = resolveMarkdownLinkPresentation(node.href ?? ""); if (presentation.kind === "file") { return appendRun(runs, presentation.label, { ...context, href: presentation.href, fileIcon: presentation.icon, }); } if (presentation.kind === "external") { return appendChildren(runs, node, { ...context, href: presentation.href, externalHost: presentation.host, }); } return appendChildren(runs, node, { ...context, ...(presentation.href ? { href: presentation.href } : {}), }); } case "image": return appendRun(runs, node.alt ?? node.title ?? "", context); default: return appendChildren(runs, node, context); }} export function nativeMarkdownTextRuns(node: MarkdownNode): ReadonlyArray<NativeMarkdownTextRun> { return appendChildren([], node, EMPTY_CONTEXT);} export function nativeMarkdownWithPreservedSoftBreaks(node: MarkdownNode): MarkdownNode { const children = node.children?.map(nativeMarkdownWithPreservedSoftBreaks); return { ...node, ...(node.type === "soft_break" ? { type: "line_break" as const } : {}), ...(children ? { children } : {}), };} function appendBlockTerminator( runs: NativeMarkdownTextRun[], context: RunContext,): NativeMarkdownTextRun[] { return appendRun(runs, "\n", context);} function appendSpacer(runs: NativeMarkdownTextRun[], spacing: number): NativeMarkdownTextRun[] { return appendRun(runs, "\n", { ...EMPTY_CONTEXT, role: "spacer", spacing });} function appendInlineChildren( runs: NativeMarkdownTextRun[], node: MarkdownNode, context: RunContext,): NativeMarkdownTextRun[] { for (const child of node.children ?? []) { appendNode(runs, child, context); } return runs;} function isInlineNode(node: MarkdownNode): boolean { return ( node.type === "text" || node.type === "bold" || node.type === "italic" || node.type === "strikethrough" || node.type === "link" || node.type === "image" || node.type === "code_inline" || node.type === "math_inline" || node.type === "html_inline" || node.type === "soft_break" || node.type === "line_break" );} export function nativeMarkdownListItemBlocks(node: MarkdownNode): ReadonlyArray<MarkdownNode> { const blocks: MarkdownNode[] = []; let inlineNodes: MarkdownNode[] = []; const flushInlineNodes = () => { if (inlineNodes.length === 0) { return; } blocks.push({ type: "paragraph", children: inlineNodes }); inlineNodes = []; }; for (const child of node.children ?? []) { if (isInlineNode(child)) { inlineNodes.push(child); continue; } flushInlineNodes(); blocks.push(child); } flushInlineNodes(); return blocks;} function appendListItem( runs: NativeMarkdownTextRun[], node: MarkdownNode, marker: string, depth: number, markerColumnWidth: number,): NativeMarkdownTextRun[] { const firstLineHeadIndent = Math.max(0, depth - 1) * 20; appendRun(runs, `${marker}\t`, { ...EMPTY_CONTEXT, role: "list-marker", depth, firstLineHeadIndent, headIndent: firstLineHeadIndent + markerColumnWidth, paragraphSpacing: 2, }); const children = node.children ?? []; let wroteInlineContent = false; for (const child of children) { if (child.type === "paragraph") { appendInlineChildren(runs, child, { ...EMPTY_CONTEXT, role: "body", depth, }); wroteInlineContent = true; continue; } if (child.type === "list") { if (wroteInlineContent) { appendBlockTerminator(runs, { ...EMPTY_CONTEXT, role: "list-break", depth, spacing: 1, }); } appendList(runs, child, depth + 1); wroteInlineContent = false; continue; } if (isInlineNode(child)) { appendNode(runs, child, { ...EMPTY_CONTEXT, role: "body", depth, }); wroteInlineContent = true; continue; } appendDocumentBlock(runs, child, depth); wroteInlineContent = true; } if (wroteInlineContent) { appendBlockTerminator(runs, { ...EMPTY_CONTEXT, role: "list-break", depth, spacing: depth === 1 ? 4 : 2, }); } return runs;} function appendList( runs: NativeMarkdownTextRun[], node: MarkdownNode, depth: number,): NativeMarkdownTextRun[] { const ordered = node.ordered ?? false; const start = node.start ?? 1; const children = node.children ?? []; const markers = children.map((child, index) => child.type === "task_list_item" ? child.checked ? "☑︎" : "☐︎" : ordered ? `${start + index}.` : depth % 3 === 2 ? "◦" : depth % 3 === 0 ? "▪︎" : "•",Lines 1–500 of 783. Load more
import type { MarkdownNode } from "react-native-nitro-markdown/headless"; import type { SelectableMarkdownSkill } from "./SelectableMarkdownText.types"; import { resolveMarkdownInlineCodePresentation, resolveMarkdownLinkPresentation, type MarkdownFileIcon, } from "./markdownLinks"; export interface NativeMarkdownTextRun { readonly text: string; readonly bold?: boolean; readonly italic?: boolean; readonly strikethrough?: boolean; readonly code?: boolean; readonly href?: string; readonly externalHost?: string; readonly fileIcon?: MarkdownFileIcon; readonly skillName?: string; readonly skillLabel?: string; readonly role?: | "body" | "heading" | "list-marker" | "list-break" | "quote-marker" | "code-block" | "code-language" | "divider" | "spacer"; readonly headingLevel?: number; readonly depth?: number; readonly spacing?: number; readonly firstLineHeadIndent?: number; readonly headIndent?: number; readonly paragraphSpacing?: number; } export type NativeMarkdownDocumentChunk = | { readonly kind: "selectable"; readonly key: string; readonly node: MarkdownNode; } | { readonly kind: "rich"; readonly key: string; readonly node: MarkdownNode; }; interface RunContext { readonly bold: boolean; readonly italic: boolean; readonly strikethrough: boolean; readonly code: boolean; readonly href?: string; readonly externalHost?: string; readonly fileIcon?: MarkdownFileIcon; readonly role?: NativeMarkdownTextRun["role"]; readonly headingLevel?: number; readonly depth?: number; readonly spacing?: number; readonly firstLineHeadIndent?: number; readonly headIndent?: number; readonly paragraphSpacing?: number; } const EMPTY_CONTEXT: RunContext = { bold: false, italic: false, strikethrough: false, code: false, }; const INLINE_HTML_TAG_PATTERN = /<\/?(?:kbd|mark|sub|sup|u)(?:\s[^>]*)?>/gi; function decodeCodePoint(codePoint: number, entity: string): string { if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff) { return entity; } return String.fromCodePoint(codePoint); } function decodeHtmlEntitiesOnce(value: string): string { return value.replace( /&(?:#(\d+)|#x([0-9a-f]+)|amp|apos|gt|lt|nbsp|quot);/gi, (entity, decimal: string | undefined, hexadecimal: string | undefined) => { if (decimal) { return decodeCodePoint(Number.parseInt(decimal, 10), entity); } if (hexadecimal) { return decodeCodePoint(Number.parseInt(hexadecimal, 16), entity); } switch (entity.toLowerCase()) { case "&": return "&"; case "'": return "'"; case ">": return ">"; case "<": return "<"; case " ": return "\u00a0"; case """: return '"'; default: return entity; } }, ); } function decodeHtmlEntities(value: string): string { let decoded = value; for (let pass = 0; pass < 2; pass += 1) { const next = decodeHtmlEntitiesOnce(decoded); if (next === decoded) { break; } decoded = next; } return decoded; } function textNodeContent(value: string): string { return decodeHtmlEntities(value).replace(INLINE_HTML_TAG_PATTERN, ""); } function inlineHtmlText(value: string): string { if (/^<br\s*\/?>$/i.test(value.trim())) { return "\n"; } return decodeHtmlEntities(value.replace(/<[^>]+>/g, "")); } function sameRunStyle(left: NativeMarkdownTextRun, right: NativeMarkdownTextRun): boolean { return ( left.bold === right.bold && left.italic === right.italic && left.strikethrough === right.strikethrough && left.code === right.code && left.href === right.href && left.externalHost === right.externalHost && left.fileIcon === right.fileIcon && left.skillName === right.skillName && left.skillLabel === right.skillLabel && left.role === right.role && left.headingLevel === right.headingLevel && left.depth === right.depth && left.spacing === right.spacing && left.firstLineHeadIndent === right.firstLineHeadIndent && left.headIndent === right.headIndent && left.paragraphSpacing === right.paragraphSpacing ); } function appendRun( runs: NativeMarkdownTextRun[], text: string, context: RunContext, ): NativeMarkdownTextRun[] { if (text.length === 0) { return runs; } const run: NativeMarkdownTextRun = { text, ...(context.bold ? { bold: true } : {}), ...(context.italic ? { italic: true } : {}), ...(context.strikethrough ? { strikethrough: true } : {}), ...(context.code ? { code: true } : {}), ...(context.href ? { href: context.href } : {}), ...(context.externalHost ? { externalHost: context.externalHost } : {}), ...(context.fileIcon ? { fileIcon: context.fileIcon } : {}), ...(context.role ? { role: context.role } : {}), ...(context.headingLevel ? { headingLevel: context.headingLevel } : {}), ...(context.depth ? { depth: context.depth } : {}), ...(context.spacing ? { spacing: context.spacing } : {}), ...(context.firstLineHeadIndent !== undefined ? { firstLineHeadIndent: context.firstLineHeadIndent } : {}), ...(context.headIndent !== undefined ? { headIndent: context.headIndent } : {}), ...(context.paragraphSpacing !== undefined ? { paragraphSpacing: context.paragraphSpacing } : {}), }; const previous = runs.at(-1); if (previous && sameRunStyle(previous, run)) { runs[runs.length - 1] = { ...previous, text: previous.text + run.text }; return runs; } runs.push(run); return runs; } const SKILL_TOKEN_REGEX = /(^|\s)\$(?![0-9][0-9_]*(?:[kKmMbBtT]|[eE][0-9]+)?(?:\s|$))(?=[a-zA-Z0-9:_-]*[a-zA-Z])([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g; function formatSkillLabel(skill: SelectableMarkdownSkill): string { const displayName = skill.displayName?.trim(); if (displayName) { return displayName; } return skill.name .split(/[\s:_-]+/) .filter(Boolean) .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(" "); } function decorateSkillRuns( runs: ReadonlyArray<NativeMarkdownTextRun>, skills: ReadonlyArray<SelectableMarkdownSkill>, ): ReadonlyArray<NativeMarkdownTextRun> { if (skills.length === 0) { return runs; } const skillByName = new Map(skills.map((skill) => [skill.name, skill])); const decorated: NativeMarkdownTextRun[] = []; for (const run of runs) { if (run.code || run.href || run.fileIcon || run.role === "code-block") { decorated.push(run); continue; } let cursor = 0; let matched = false; for (const match of run.text.matchAll(SKILL_TOKEN_REGEX)) { const prefix = match[1] ?? ""; const name = match[2] ?? ""; const skill = skillByName.get(name); if (!skill) { continue; } const start = (match.index ?? 0) + prefix.length; const end = start + name.length + 1; if (start > cursor) { decorated.push({ ...run, text: run.text.slice(cursor, start) }); } decorated.push({ ...run, text: run.text.slice(start, end), skillName: name, skillLabel: formatSkillLabel(skill), }); cursor = end; matched = true; } if (!matched) { decorated.push(run); } else if (cursor < run.text.length) { decorated.push({ ...run, text: run.text.slice(cursor) }); } } return decorated; } function appendChildren( runs: NativeMarkdownTextRun[], node: MarkdownNode, context: RunContext, ): NativeMarkdownTextRun[] { for (const child of node.children ?? []) { appendNode(runs, child, context); } return runs; } function nodeTextContent(node: MarkdownNode): string { if (node.content !== undefined) { return node.content; } return (node.children ?? []).map(nodeTextContent).join(""); } function appendNode( runs: NativeMarkdownTextRun[], node: MarkdownNode, context: RunContext, ): NativeMarkdownTextRun[] { switch (node.type) { case "text": case "math_inline": return appendRun(runs, textNodeContent(nodeTextContent(node)), context); case "html_inline": return appendRun(runs, inlineHtmlText(nodeTextContent(node)), context); case "code_inline": { const content = nodeTextContent(node); const presentation = context.href ? null : resolveMarkdownInlineCodePresentation(content); return presentation ? appendRun(runs, presentation.label, { ...context, href: presentation.href, fileIcon: presentation.icon, }) : appendRun(runs, content, { ...context, code: true }); } case "soft_break": return appendRun(runs, " ", context); case "line_break": return appendRun(runs, "\n", context); case "bold": return appendChildren(runs, node, { ...context, bold: true }); case "italic": return appendChildren(runs, node, { ...context, italic: true }); case "strikethrough": return appendChildren(runs, node, { ...context, strikethrough: true }); case "link": { const presentation = resolveMarkdownLinkPresentation(node.href ?? ""); if (presentation.kind === "file") { return appendRun(runs, presentation.label, { ...context, href: presentation.href, fileIcon: presentation.icon, }); } if (presentation.kind === "external") { return appendChildren(runs, node, { ...context, href: presentation.href, externalHost: presentation.host, }); } return appendChildren(runs, node, { ...context, ...(presentation.href ? { href: presentation.href } : {}), }); } case "image": return appendRun(runs, node.alt ?? node.title ?? "", context); default: return appendChildren(runs, node, context); } } export function nativeMarkdownTextRuns(node: MarkdownNode): ReadonlyArray<NativeMarkdownTextRun> { return appendChildren([], node, EMPTY_CONTEXT); } export function nativeMarkdownWithPreservedSoftBreaks(node: MarkdownNode): MarkdownNode { const children = node.children?.map(nativeMarkdownWithPreservedSoftBreaks); return { ...node, ...(node.type === "soft_break" ? { type: "line_break" as const } : {}), ...(children ? { children } : {}), }; } function appendBlockTerminator( runs: NativeMarkdownTextRun[], context: RunContext, ): NativeMarkdownTextRun[] { return appendRun(runs, "\n", context); } function appendSpacer(runs: NativeMarkdownTextRun[], spacing: number): NativeMarkdownTextRun[] { return appendRun(runs, "\n", { ...EMPTY_CONTEXT, role: "spacer", spacing }); } function appendInlineChildren( runs: NativeMarkdownTextRun[], node: MarkdownNode, context: RunContext, ): NativeMarkdownTextRun[] { for (const child of node.children ?? []) { appendNode(runs, child, context); } return runs; } function isInlineNode(node: MarkdownNode): boolean { return ( node.type === "text" || node.type === "bold" || node.type === "italic" || node.type === "strikethrough" || node.type === "link" || node.type === "image" || node.type === "code_inline" || node.type === "math_inline" || node.type === "html_inline" || node.type === "soft_break" || node.type === "line_break" ); } export function nativeMarkdownListItemBlocks(node: MarkdownNode): ReadonlyArray<MarkdownNode> { const blocks: MarkdownNode[] = []; let inlineNodes: MarkdownNode[] = []; const flushInlineNodes = () => { if (inlineNodes.length === 0) { return; } blocks.push({ type: "paragraph", children: inlineNodes }); inlineNodes = []; }; for (const child of node.children ?? []) { if (isInlineNode(child)) { inlineNodes.push(child); continue; } flushInlineNodes(); blocks.push(child); } flushInlineNodes(); return blocks; } function appendListItem( runs: NativeMarkdownTextRun[], node: MarkdownNode, marker: string, depth: number, markerColumnWidth: number, ): NativeMarkdownTextRun[] { const firstLineHeadIndent = Math.max(0, depth - 1) * 20; appendRun(runs, `${marker}\t`, { ...EMPTY_CONTEXT, role: "list-marker", depth, firstLineHeadIndent, headIndent: firstLineHeadIndent + markerColumnWidth, paragraphSpacing: 2, }); const children = node.children ?? []; let wroteInlineContent = false; for (const child of children) { if (child.type === "paragraph") { appendInlineChildren(runs, child, { ...EMPTY_CONTEXT, role: "body", depth, }); wroteInlineContent = true; continue; } if (child.type === "list") { if (wroteInlineContent) { appendBlockTerminator(runs, { ...EMPTY_CONTEXT, role: "list-break", depth, spacing: 1, }); } appendList(runs, child, depth + 1); wroteInlineContent = false; continue; } if (isInlineNode(child)) { appendNode(runs, child, { ...EMPTY_CONTEXT, role: "body", depth, }); wroteInlineContent = true; continue; } appendDocumentBlock(runs, child, depth); wroteInlineContent = true; } if (wroteInlineContent) { appendBlockTerminator(runs, { ...EMPTY_CONTEXT, role: "list-break", depth, spacing: depth === 1 ? 4 : 2, }); } return runs; } function appendList( runs: NativeMarkdownTextRun[], node: MarkdownNode, depth: number, ): NativeMarkdownTextRun[] { const ordered = node.ordered ?? false; const start = node.start ?? 1; const children = node.children ?? []; const markers = children.map((child, index) => child.type === "task_list_item" ? child.checked ? "☑︎" : "☐︎" : ordered ? `${start + index}.` : depth % 3 === 2 ? "◦" : depth % 3 === 0 ? "▪︎" : "•",