core/converters/section.converter.ts
core/converters/section.converter.ts is a file in Bane's Lab Content. 134 lines of code and 30 definitions.
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<string, readonly string[]> {
const teachers = new Map<string, string[]>();
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<string, unknown>) => 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);
};