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