# domain/converters/link.converter.ts

> 157 lines of code and 42 definitions.

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

## Definitions

- `linkText` (lexical_declaration, line 37, exported)
- `withHref` (lexical_declaration, line 63)
- `linkLesson` (lexical_declaration, line 107)
- `linkBlock` (lexical_declaration, line 121)
- `joinEntry` (lexical_declaration, line 78)
- `linkEntry` (lexical_declaration, line 99)
- `linkSubsection` (lexical_declaration, line 141)
- `linkSection` (lexical_declaration, line 150, exported)
- `anchorOf` (lexical_declaration, line 32)
- `sameWords` (lexical_declaration, line 52)
- `linkTabs` (lexical_declaration, line 166, exported)
- `stageLink` (lexical_declaration, line 15, exported)
- `layerLink` (lexical_declaration, line 19, exported)
- `nodeLink` (lexical_declaration, line 23, exported)
- `entryFor` (lexical_declaration, line 56, exported)
- `joinGlossary` (lexical_declaration, line 91, exported)
- `linkBlocks` (lexical_declaration, line 134)
- `HYPHEN` (lexical_declaration, line 10)
- `SPACE` (lexical_declaration, line 11)
- `LAYER_PREFIX` (lexical_declaration, line 12)
- `NODE_PREFIX` (lexical_declaration, line 13)
- `Linking` (interface_declaration, line 27)
- `matches` (lexical_declaration, line 38, exported)
- `out` (lexical_declaration, line 39, exported)
- `cursor` (lexical_declaration, line 40, exported)
- `kindLabel` (lexical_declaration, line 48)
- `words` (lexical_declaration, line 57, exported)
- `[first]` (lexical_declaration, line 58, exported)
- `candidates` (lexical_declaration, line 59, exported)
- `href` (lexical_declaration, line 64)
- `placementOf` (lexical_declaration, line 68)
- `found` (lexical_declaration, line 70)
- `record` (lexical_declaration, line 79)
- `placed` (lexical_declaration, line 84)
- `layer` (lexical_declaration, line 85)
- `domains` (lexical_declaration, line 86)
- `code` (lexical_declaration, line 87)
- `own` (lexical_declaration, line 100)
- `linking` (lexical_declaration, line 155, exported)
- `intro` (lexical_declaration, line 156, exported)
- `blocks` (lexical_declaration, line 157, exported)
- `index` (lexical_declaration, line 171, exported)

## Uses

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

## Source

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