import type { Logger } from './logger.js'; export interface TextSegment { text: string; features: FacetFeature[]; } export interface Facet { index: { byteStart: number; byteEnd: number }; features: FacetFeature[]; } export type FacetFeature = | { $type: 'app.offprint.richtext.facet#bold' } | { $type: 'app.offprint.richtext.facet#italic' } | { $type: 'app.offprint.richtext.facet#underline' } | { $type: 'app.offprint.richtext.facet#strikethrough' } | { $type: 'app.offprint.richtext.facet#code' } | { $type: 'app.offprint.richtext.facet#link'; uri: string } | { $type: 'app.offprint.richtext.facet#highlight'; color?: string }; export interface TextBlock { plaintext: string; facets?: Facet[]; } export function buildTextFromInline(node: Node, activeFeatures: FacetFeature[], log: Logger): TextSegment[] { const segments: TextSegment[] = []; for (const child of Array.from(node.childNodes) as Node[]) { if (child.nodeType === 3) { // text node const text = (child as Text).data ?? ''; if (text) { segments.push({ text, features: [...activeFeatures] }); } } else if (child.nodeType === 1) { const el = child as Element; const tag = el.tagName.toLowerCase(); const href = el.getAttribute('href'); const newFeatures = [...activeFeatures]; switch (tag) { case 'strong': case 'b': newFeatures.push({ $type: 'app.offprint.richtext.facet#bold' }); break; case 'em': case 'i': newFeatures.push({ $type: 'app.offprint.richtext.facet#italic' }); break; case 'u': newFeatures.push({ $type: 'app.offprint.richtext.facet#underline' }); break; case 's': case 'del': newFeatures.push({ $type: 'app.offprint.richtext.facet#strikethrough' }); break; case 'code': newFeatures.push({ $type: 'app.offprint.richtext.facet#code' }); break; case 'mark': newFeatures.push({ $type: 'app.offprint.richtext.facet#highlight' }); break; case 'a': if (href) { newFeatures.push({ $type: 'app.offprint.richtext.facet#link', uri: href }); } break; case 'br': segments.push({ text: '\n', features: [...activeFeatures] }); continue; default: // fall through to recursive processing without adding features break; } segments.push(...buildTextFromInline(el, newFeatures, log)); } } return segments; } export function assembleTextBlock(segments: TextSegment[]): TextBlock { const plaintext = segments.map((s) => s.text).join(''); const encoder = new TextEncoder(); let byteCursor = 0; const facetList: Facet[] = []; for (const segment of segments) { const segmentBytes = encoder.encode(segment.text); const byteStart = byteCursor; const byteEnd = byteCursor + segmentBytes.length; for (const feature of segment.features) { facetList.push({ index: { byteStart, byteEnd }, features: [feature], }); } byteCursor = byteEnd; } return { plaintext, facets: facetList.length > 0 ? facetList : undefined, }; } export function inlineToTextBlock(node: Node, log: Logger): TextBlock { const segments = buildTextFromInline(node, [], log); return assembleTextBlock(segments); }