domain/converters/link.vocabulary.converter.ts

domain/converters/link.vocabulary.converter.ts is a file in Bane's Lab Site. 153 lines of code and 38 definitions.

import type { Block, GlossaryEntry, LessonBlock } from "#types/block.types";
import { HYPHEN, SPACE } from "#configuration/constants/document.constants";
import type { PageContent, PhraseIndex, VocabularyEntry } from "#types/vocabulary.types";
import type { Section, Subsection, Tab } from "#types/document.types";
import { buildPhraseIndex, matchPhrases } from "#core/matchers/vocabulary.matcher";
import { chapterRef, chapterVocabulary } from "#domain/converters/chapter.converter";
import { anchorMarkup } from "#core/formatters/link.formatter";
import { hrefOf } from "#domain/converters/ontology.converter";
import { wordsOf } from "#core/normalizers/word.normalizer";

interface Linking {
    readonly index: PhraseIndex;
    readonly seen: Set<string>;
}

const anchorOf = function anchorOf(entry: VocabularyEntry, label: string): string {
    return anchorMarkup(hrefOf(entry.ref), 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 derived = entry.code ?? record.code ?? undefined;
    const code = derived === entry.term ? undefined : derived;
    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)),
        ),
    }));
};

export const linkPages = function linkPages<Page extends PageContent>(
    pages: readonly PageContent[],
    linked: readonly Page[],
    vocabulary: readonly VocabularyEntry[],
): readonly Page[] {
    const site = [...vocabulary, ...chapterVocabulary(pages)];
    return linked.map((entry) => ({ ...entry, tabs: linkTabs(entry.tabs, site, entry.page) }));
};