import { ALGORITHMS_TAB, ALGO_ANCHOR, ALGO_DOMAIN_ANCHOR, ARCH_ANCHOR, ARCH_CATEGORY_ANCHOR, FORCE_ANCHOR, KIND_ANCHOR, LAYER_ANCHOR, LEXICON_TAB, LEX_ANCHOR, LEX_CATEGORY_ANCHOR, ONTOLOGY_TAB_ORDER, PRINCIPLES_TAB, REASONING_TAB, REASON_ANCHOR, RELATION_ANCHOR, SCHEMA_TAB, STAGE_ANCHOR, TENSION_ANCHOR, } from "#core/ids/ontology.ids"; import { ALGO_DOMAIN_FACE, ALGO_FACE, ARCH_CATEGORY_FACE, ARCH_FACE, FACE_SEPARATOR, FORCE_FACE, KIND_FACE, LAYER_FACE, LEX_CATEGORY_FACE, LEX_FACE, REASON_FACE, RELATION_FACE, STAGE_FACE, TENSION_FACE, } from "@govlab/constants"; import type { Block, CodeBlock, GlossaryEntry } from "#types/block.types"; import { FIELD_SEPARATOR, LABEL_SEPARATOR, LIST_SEPARATOR, NONE_LABEL } from "#configuration/strings/ontology.strings"; import { TEXT_LANGUAGE, TYPESCRIPT_LANGUAGE } from "#configuration/constants/code.constants"; import { ANCHOR_PREFIX } from "#configuration/constants/document.constants"; import { CHAPTER_FACE } from "#configuration/constants/vocabulary.constants"; import type { EdgeRef } from "#types/link.types"; import type { ExemplarView } from "#types/ontology.types"; import { ONTOLOGY_PAGE } from "#core/ids/page.ids"; import { tabPath } from "#assets/link.assets"; const TYPESCRIPT_LANG = "ts"; const QUOTE = '"'; const APOSTROPHE = "'"; const NODE_OPEN = '["'; const NODE_CLOSE = '"]'; const LINE_END = "\n"; const INDENT = " "; const SAFE = "abcdefghijklmnopqrstuvwxyz0123456789"; const NODE_PREFIX = "n_"; const UNDERSCORE = "_"; const HYPHEN = "-"; const TAB_BY_FACE: ReadonlyMap = new Map([ [ARCH_FACE, { anchor: ARCH_ANCHOR, tab: PRINCIPLES_TAB }], [ARCH_CATEGORY_FACE, { anchor: ARCH_CATEGORY_ANCHOR, tab: PRINCIPLES_TAB }], [LEX_FACE, { anchor: LEX_ANCHOR, tab: LEXICON_TAB }], [LEX_CATEGORY_FACE, { anchor: LEX_CATEGORY_ANCHOR, tab: LEXICON_TAB }], [ALGO_FACE, { anchor: ALGO_ANCHOR, tab: ALGORITHMS_TAB }], [ALGO_DOMAIN_FACE, { anchor: ALGO_DOMAIN_ANCHOR, tab: ALGORITHMS_TAB }], [REASON_FACE, { anchor: REASON_ANCHOR, tab: REASONING_TAB }], [STAGE_FACE, { anchor: STAGE_ANCHOR, tab: REASONING_TAB }], [TENSION_FACE, { anchor: TENSION_ANCHOR, tab: SCHEMA_TAB }], [LAYER_FACE, { anchor: LAYER_ANCHOR, tab: SCHEMA_TAB }], [FORCE_FACE, { anchor: FORCE_ANCHOR, tab: SCHEMA_TAB }], [KIND_FACE, { anchor: KIND_ANCHOR, tab: SCHEMA_TAB }], [RELATION_FACE, { anchor: RELATION_ANCHOR, tab: SCHEMA_TAB }], ]); const ORDER = ONTOLOGY_TAB_ORDER.map((id) => ({ id })); export const anchorOf = function anchorOf(ref: string): string | null { const split = ref.indexOf(FACE_SEPARATOR); const face = TAB_BY_FACE.get(ref.slice(0, split)); return face === undefined ? null : face.anchor + ref.slice(split + 1); }; export const refOfAnchor = function refOfAnchor(fragment: string): string | null { let found: string | null = null; let width = 0; for (const [face, { anchor }] of TAB_BY_FACE) { if (fragment.startsWith(anchor) && anchor.length > width) { found = face + FACE_SEPARATOR + fragment.slice(anchor.length); width = anchor.length; } } return found; }; export const hrefOf = function hrefOf(ref: string): string | null { const split = ref.indexOf(FACE_SEPARATOR); if (ref.slice(0, split) === CHAPTER_FACE) { return ref.slice(split + 1); } const face = TAB_BY_FACE.get(ref.slice(0, split)); return face === undefined ? null : tabPath(ONTOLOGY_PAGE, ORDER, face.tab) + ANCHOR_PREFIX + face.anchor + ref.slice(split + 1); }; export const linkOf = function linkOf(edge: EdgeRef): string { const href = edge.ref === null ? null : hrefOf(edge.ref); return href === null ? edge.label : `${edge.label}`; }; export const faceLink = function faceLink(face: string, id: string, label: string): string { return linkOf({ label, ref: face + FACE_SEPARATOR + id }); }; export const linkList = function linkList(edges: readonly EdgeRef[]): string { return edges.length === 0 ? NONE_LABEL : edges.map(linkOf).join(LIST_SEPARATOR); }; export const plainList = function plainList(items: readonly string[]): string { return items.length === 0 ? NONE_LABEL : items.join(LIST_SEPARATOR); }; export const labelled = function labelled(label: string, value: string | null): string | null { return value === null || value.length === 0 ? null : label + LABEL_SEPARATOR + value; }; export const linked = function linked(label: string, edge: EdgeRef | null): string | null { return edge === null ? null : labelled(label, linkOf(edge)); }; export const fields = function fields(parts: readonly (string | null)[]): string { return parts.filter((part): part is string => part !== null && part.length > 0).join(FIELD_SEPARATOR); }; export const row = function row(term: string, description: string): GlossaryEntry { return { description, term }; }; export const linkRow = function linkRow(term: string, edges: readonly EdgeRef[]): readonly GlossaryEntry[] { return edges.length === 0 ? [] : [row(term, linkList(edges))]; }; export const glossary = function glossary(entries: readonly GlossaryEntry[]): Block { return { entries, kind: "glossary" }; }; export const text = function text(value: string): Block { return { kind: "text", text: value }; }; export const chips = function chips(parts: readonly (string | null)[]): Block { return { items: parts.filter((part): part is string => part !== null && part.length > 0), kind: "list" }; }; export const codeOf = function codeOf(title: string, code: string, language: string): CodeBlock { return { code, kind: "code", language, title }; }; export const exemplarBlocks = function exemplarBlocks( exemplar: ExemplarView | null, beforeTitle: string, afterTitle: string, ): readonly Block[] { if (exemplar === null) { return []; } const language = exemplar.lang === TYPESCRIPT_LANG ? TYPESCRIPT_LANGUAGE : TEXT_LANGUAGE; return [codeOf(beforeTitle, exemplar.before, language), codeOf(afterTitle, exemplar.after, language)]; }; export const nodeId = function nodeId(id: string): string { let out = NODE_PREFIX; for (const char of id.toLowerCase()) { out += SAFE.includes(char) ? char : UNDERSCORE; } return out; }; export const slugOf = function slugOf(label: string): string { let out = ""; let gap = false; for (const char of label.toLowerCase()) { if (SAFE.includes(char)) { out += gap && out.length > 0 ? HYPHEN + char : char; gap = false; } else { gap = true; } } return out; }; export const nodeLabel = function nodeLabel(label: string): string { return label.replaceAll(QUOTE, APOSTROPHE); }; export const node = function node(id: string, label: string): string { return INDENT + nodeId(id) + NODE_OPEN + nodeLabel(label) + NODE_CLOSE; }; export const diagram = function diagram(header: string, lines: readonly string[]): string { return [header, ...lines].join(LINE_END); }; export const edgeLine = function edgeLine(from: string, arrow: string, to: string): string { return INDENT + nodeId(from) + arrow + nodeId(to); }; export const localId = function localId(edge: EdgeRef, face: string, members: ReadonlySet): string | null { if (edge.ref === null) { return null; } const prefix = face + FACE_SEPARATOR; if (!edge.ref.startsWith(prefix)) { return null; } const id = edge.ref.slice(prefix.length); return members.has(id) ? id : null; };