import { FENCE_MARK, LINK_CLOSE, LINK_MIDDLE } from "#configuration/constants/chapter.constants"; import { PAYLOAD_ID_KEY, PAYLOAD_SECTIONS_KEY } from "#configuration/constants/leak.constants"; import { SECTION_HEADING, SECTION_MARK } from "#configuration/constants/writing.constants"; import type { ContentGraph } from "#types/coverage.types"; import type { PageSection } from "#types/writing.types"; import { fingerprintOf } from "@govlab/content-fingerprint"; import { isRecord } from "#core/selectors/payload.selector"; const LINE_END = "\n"; export const nodeOf = function nodeOf(page: string, section: string): string { return page + SECTION_MARK + section; }; export const teachingNodes = function teachingNodes(graphs: readonly ContentGraph[]): readonly string[] { return graphs.flatMap((graph) => Object.entries(graph.sections).flatMap(([section, held]) => held.teaches.length > 0 ? [nodeOf(graph.page, section)] : [], ), ); }; export const nodePartsOf = function nodePartsOf(node: string): { readonly page: string; readonly section: string } { const cut = node.indexOf(SECTION_MARK); return { page: node.slice(0, cut), section: node.slice(cut + SECTION_MARK.length) }; }; export const conceptTeachers = function conceptTeachers( graphs: readonly ContentGraph[], ): ReadonlyMap { const teachers = new Map(); for (const graph of graphs) { for (const [section, held] of Object.entries(graph.sections)) { for (const concept of held.teaches) { teachers.set(concept, [...(teachers.get(concept) ?? []), nodeOf(graph.page, section)]); } } } return teachers; }; export const stringsOf = function stringsOf( value: unknown, skip?: (record: Record) => boolean, ): string[] { if (typeof value === "string") { return [value]; } if (Array.isArray(value)) { return value.flatMap((entry: unknown) => stringsOf(entry, skip)); } if (!isRecord(value) || skip?.(value) === true) { return []; } return Object.values(value).flatMap((entry) => stringsOf(entry, skip)); }; const lineTargetsOf = function lineTargetsOf(line: string): string[] { const targets: string[] = []; let at = line.indexOf(LINK_MIDDLE); while (at !== -1) { const start = at + LINK_MIDDLE.length; const close = line.indexOf(LINK_CLOSE, start); if (close === -1) { return targets; } targets.push(line.slice(start, close)); at = line.indexOf(LINK_MIDDLE, close); } return targets; }; export const linkTargetsOf = function linkTargetsOf(text: string): string[] { let fenced = false; return text.split(LINE_END).flatMap((line) => { if (line.startsWith(FENCE_MARK)) { fenced = !fenced; return []; } return fenced ? [] : lineTargetsOf(line); }); }; const sectionIdOf = function sectionIdOf(value: unknown): string | null { const id = isRecord(value) ? value[PAYLOAD_ID_KEY] : undefined; return typeof id === "string" ? id : null; }; interface Split { readonly body: unknown; readonly found: readonly (readonly [string, unknown])[]; } const keyedSections = function keyedSections(list: readonly unknown[]): (readonly [string, unknown])[] { return list.flatMap((section) => { const id = sectionIdOf(section); return id === null ? [] : [[id, section] as const]; }); }; const splitSections = function splitSections(value: unknown): Split { if (Array.isArray(value)) { const parts = value.map(splitSections); return { body: parts.map((part) => part.body), found: parts.flatMap((part) => part.found) }; } if (!isRecord(value)) { return { body: value, found: [] }; } const entries = Object.entries(value).map(([key, entry]) => { if (key !== PAYLOAD_SECTIONS_KEY || !Array.isArray(entry)) { return { key, ...splitSections(entry) }; } const rest = splitSections(entry.filter((section: unknown) => sectionIdOf(section) === null)); return { body: rest.body, found: [...keyedSections(entry), ...rest.found], key }; }); return { body: Object.fromEntries(entries.map((entry) => [entry.key, entry.body])), found: entries.flatMap((entry) => entry.found), }; }; const sectionOf = function sectionOf(page: string, key: string, value: unknown): PageSection { const strings = stringsOf(value); const links = [...new Set(strings.flatMap(linkTargetsOf))].sort((a, b) => a.localeCompare(b)); return { fingerprint: fingerprintOf(strings), key, links, page }; }; export const sectionsOf = function sectionsOf(page: string, content: unknown): PageSection[] { const { body, found } = splitSections(content); return [sectionOf(page, page, body), ...found.map(([id, section]) => sectionOf(page, nodeOf(page, id), section))]; }; export const markdownSectionsOf = function markdownSectionsOf(page: string, file: string, text: string): PageSection[] { const parts: { readonly id: string; readonly lines: string[] }[] = [{ id: file, lines: [] }]; for (const line of text.split(LINE_END)) { if (line.startsWith(SECTION_HEADING)) { parts.push({ id: file + SECTION_MARK + line.slice(SECTION_HEADING.length), lines: [] }); } parts.at(-1)?.lines.push(line); } return parts.map((part) => sectionOf(page, nodeOf(page, part.id), part.lines)); }; export const moduleSectionOf = function moduleSectionOf( page: string, file: string, literals: readonly string[], ): PageSection { return sectionOf(page, nodeOf(page, file), literals); };