# domain/converters/link.vocabulary.converter.ts

> 153 lines of code and 39 definitions.

Tree: Site tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/tree#file-domain-converters-link-vocabulary-converter-ts
Source text: https://banes-lab.com/source/tree/domain/converters/link.vocabulary.converter.ts.txt

Listed in [domain/converters](https://banes-lab.com/api/source/tree/domain/converters.md), after [domain/converters/link.converter.ts](https://banes-lab.com/source/tree/domain/converters/link.converter.ts.md) and before [domain/converters/loop.converter.ts](https://banes-lab.com/source/tree/domain/converters/loop.converter.ts.md).

## Definitions

- `linkText` (lexical_declaration, line 22, exported)
- `withHref` (lexical_declaration, line 48)
- `linkLesson` (lexical_declaration, line 92)
- `linkBlock` (lexical_declaration, line 106)
- `joinEntry` (lexical_declaration, line 63)
- `linkTabs` (lexical_declaration, line 151, exported)
- `linkEntry` (lexical_declaration, line 84)
- `linkSubsection` (lexical_declaration, line 126)
- `linkSection` (lexical_declaration, line 135, exported)
- `anchorOf` (lexical_declaration, line 17)
- `sameWords` (lexical_declaration, line 37)
- `entryFor` (lexical_declaration, line 41, exported)
- `joinGlossary` (lexical_declaration, line 76, exported)
- `linkBlocks` (lexical_declaration, line 119)
- `linkPages` (lexical_declaration, line 165, exported)
- `HYPHEN` (lexical_declaration, line 9)
- `SPACE` (lexical_declaration, line 10)
- `Linking` (interface_declaration, line 12)
- `matches` (lexical_declaration, line 23, exported)
- `out` (lexical_declaration, line 24, exported)
- `cursor` (lexical_declaration, line 25, exported)
- `kindLabel` (lexical_declaration, line 33)
- `words` (lexical_declaration, line 42, exported)
- `[first]` (lexical_declaration, line 43, exported)
- `candidates` (lexical_declaration, line 44, exported)
- `href` (lexical_declaration, line 49)
- `placementOf` (lexical_declaration, line 53)
- `found` (lexical_declaration, line 55)
- `record` (lexical_declaration, line 64)
- `placed` (lexical_declaration, line 69)
- `layer` (lexical_declaration, line 70)
- `domains` (lexical_declaration, line 71)
- `code` (lexical_declaration, line 72)
- `own` (lexical_declaration, line 85)
- `linking` (lexical_declaration, line 140, exported)
- `intro` (lexical_declaration, line 141, exported)
- `blocks` (lexical_declaration, line 142, exported)
- `index` (lexical_declaration, line 156, exported)
- `site` (lexical_declaration, line 170, exported)

## Uses

- [domain/converters/chapter.converter.ts](https://banes-lab.com/source/tree/domain/converters/chapter.converter.ts.md)

## Records this file grounds

- [Derived state](https://banes-lab.com/disciplined-methodology/verify/derived-state.md)
- [Architectural rules and principles](https://banes-lab.com/software-architecture/glossary/architectural-principles.md)

## Source

```typescript
import type { Block, GlossaryEntry, LessonBlock } from "#types/block.types";
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 { hrefOf } from "#domain/converters/ontology.converter";
import { wordsOf } from "#core/normalizers/word.normalizer";

const HYPHEN = "-";
const SPACE = " ";

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

const anchorOf = function anchorOf(entry: VocabularyEntry, label: string): string {
    const href = hrefOf(entry.ref);
    return href === null ? label : `<a href="${href}">${label}</a>`;
};

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)),
        ),
    }));
};

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) }));
};
```
