import type { Block, GlossaryEntry, LessonBlock } from "#types/block.types"; import type { PhraseIndex, VocabularyEntry } from "#types/vocabulary.types"; import { REASON_FACE, STAGE_FACE } from "@govlab/constants"; import type { Section, Subsection, Tab } from "#types/document.types"; import { buildPhraseIndex, matchPhrases } from "#core/matchers/vocabulary.matcher"; import { faceLink, hrefOf } from "#domain/converters/ontology.converter"; import { chapterRef } from "#domain/converters/chapter.converter"; import { wordsOf } from "#core/normalizers/word.normalizer"; const HYPHEN = "-"; const SPACE = " "; const LAYER_PREFIX = "layer-"; const NODE_PREFIX = "node-"; export const stageLink = function stageLink(id: string): string { return faceLink(STAGE_FACE, id, id); }; export const layerLink = function layerLink(id: string, label: string): string { return faceLink(REASON_FACE, LAYER_PREFIX + id, label); }; export const nodeLink = function nodeLink(id: string, label: string): string { return faceLink(REASON_FACE, NODE_PREFIX + id, label); }; interface Linking { readonly index: PhraseIndex; readonly seen: Set; } const anchorOf = function anchorOf(entry: VocabularyEntry, label: string): string { const href = hrefOf(entry.ref); return href === null ? label : `${label}`; }; export const linkText = function linkText(text: string, linking: Linking): string { const matches = matchPhrases(text, linking.index, linking.seen); let out = ""; let cursor = 0; for (const match of matches) { out += text.slice(cursor, match.start) + anchorOf(match.entry, text.slice(match.start, match.end)); cursor = match.end; } return out + text.slice(cursor); }; const kindLabel = function kindLabel(kind: string): string { return kind.split(HYPHEN).join(SPACE); }; const sameWords = function sameWords(left: readonly string[], right: readonly string[]): boolean { return left.length === right.length && left.every((word, at) => word === right[at]); }; export const entryFor = function entryFor(term: string, index: PhraseIndex): VocabularyEntry | null { const words = wordsOf(term); const [first] = words; const candidates = first === undefined ? [] : (index.byFirstWord.get(first) ?? []); return candidates.find(([phrase]) => sameWords(phrase, words))?.[1] ?? null; }; const withHref = function withHref(entry: GlossaryEntry, ref: string): GlossaryEntry { const href = hrefOf(ref); return href === null ? entry : { ...entry, href }; }; const placementOf = function placementOf(entry: GlossaryEntry, index: PhraseIndex): VocabularyEntry | null { for (const domain of entry.domains ?? []) { const found = entryFor(domain, index); if (found !== null) { return found; } } return null; }; const joinEntry = function joinEntry(entry: GlossaryEntry, index: PhraseIndex): GlossaryEntry { const record = entryFor(entry.term, index); if (record === null) { const layer = placementOf(entry, index); return layer === null ? entry : withHref(entry, layer.ref); } const placed = entry.domains ?? []; const layer = placed.length > 0 ? placed : (record.layer === null ? [] : [record.layer]); const domains = [kindLabel(record.kind), ...layer]; const code = entry.code ?? record.code ?? undefined; return withHref({ ...entry, ...(code === undefined ? {} : { code }), domains }, record.ref); }; export const joinGlossary = function joinGlossary( entries: readonly GlossaryEntry[], vocabulary: readonly VocabularyEntry[], ): readonly GlossaryEntry[] { const index = buildPhraseIndex(vocabulary); return entries.map((entry) => joinEntry(entry, index)); }; const linkEntry = function linkEntry(entry: GlossaryEntry, linking: Linking): GlossaryEntry { const own = entryFor(entry.term, linking.index); if (own !== null) { linking.seen.add(own.ref); } return { ...entry, description: linkText(entry.description, linking) }; }; const linkLesson = function linkLesson(block: LessonBlock, linking: Linking): LessonBlock { return { ...block, application: linkText(block.application, linking), ...(block.boundary === undefined ? {} : { boundary: linkText(block.boundary, linking) }), cause: linkText(block.cause, linking), decision: linkText(block.decision, linking), failureMode: linkText(block.failureMode, linking), principle: linkText(block.principle, linking), problem: linkText(block.problem, linking), validation: linkText(block.validation, linking), }; }; const linkBlock = function linkBlock(block: Block, linking: Linking): Block { if (block.kind === "text") { return { ...block, text: linkText(block.text, linking) }; } if (block.kind === "lesson") { return linkLesson(block, linking); } if (block.kind === "glossary") { return { ...block, entries: block.entries.map((entry) => linkEntry(entry, linking)) }; } return block; }; const linkBlocks = function linkBlocks( blocks: readonly Block[] | undefined, linking: Linking, ): readonly Block[] | undefined { return blocks?.map((block) => linkBlock(block, linking)); }; const linkSubsection = function linkSubsection(subsection: Subsection, linking: Linking): Subsection { const blocks = linkBlocks(subsection.blocks, linking); return { ...subsection, ...(blocks === undefined ? {} : { blocks }), ...(subsection.content === undefined ? {} : { content: linkText(subsection.content, linking) }), }; }; export const linkSection = function linkSection( section: Section, index: PhraseIndex, own: string | null = null, ): Section { const linking: Linking = { index, seen: new Set(own === null ? [] : [own]) }; const intro = section.intro === undefined ? undefined : linkText(section.intro, linking); const blocks = linkBlocks(section.blocks, linking); return { ...section, ...(blocks === undefined ? {} : { blocks }), ...(intro === undefined ? {} : { intro }), subsections: section.subsections.map((subsection) => linkSubsection(subsection, linking)), }; }; export const linkTabs = function linkTabs( tabs: readonly Tab[], vocabulary: readonly VocabularyEntry[], page: string | null = null, ): readonly Tab[] { const index = buildPhraseIndex(vocabulary); return tabs.map((tab) => ({ ...tab, sections: tab.sections.map((section) => linkSection(section, index, page === null ? null : chapterRef(page, tabs, tab.id, section.id)), ), })); };