core/converters/learning.catalog.converter.ts
core/converters/learning.catalog.converter.ts is a file in Bane's Lab Build Scripts. 174 lines of code and 36 definitions.
import { CATALOG_FILE_BUDGET, INDEX_PART_BUDGET, INDEX_PART_KIND } from "#configuration/constants/catalog.constants";
import {
CHAPTER_PREFIX,
GRAPH_VOCABULARY,
PAGE_KIND,
PART_KIND,
ROUTE_PREFIX,
SECTION_KIND,
SITE_LAYER,
TAB_KIND,
} from "#configuration/constants/graph.constants";
import type { Leaf, Link, Linker, Navigation, NumberRow } from "#types/catalog.types";
import { indexPart, numbersIndex, pageIndex, routeIndex } from "#core/resolvers/catalog.resolver";
import { packedParts, serialize } from "#core/stores/catalog.store";
import { renderNumbers, renderRoute } from "#core/formatters/route.formatter";
import { Buffer } from "node:buffer";
import type { DiscoveredRoute } from "#types/site.types";
import type { Graph } from "#types/graph.types";
import type { GraphNode } from "@banes-lab/web/types/graph.types.js";
import type { LearningBlock } from "@banes-lab/web/types/learning.types.js";
import type { RouteStop } from "#types/learning.types";
import { indexPartTitle } from "#configuration/strings/catalog.strings";
import { renderIndex } from "#core/formatters/index.formatter";
const ROUTE_REF = "api:route";
const ROUTE_KIND = "route";
const NUMBERS_REF = "api:numbers";
const NUMBERS_KIND = "numbers";
const PART_REF_MARK = "/";
const ROUTE_KINDS: ReadonlySet<string> = new Set([PAGE_KIND, TAB_KIND]);
export const routeStops = function routeStops(blocks: readonly LearningBlock[]): readonly RouteStop[] {
return blocks
.flatMap((block) => block.stops.map((stop) => ({ block: block.label, code: block.code, stop })))
.map(({ block, code, stop }, position) => ({
block,
code,
id: stop.id,
label: stop.label,
path: stop.path,
position: position + 1,
requires: stop.requires,
}));
};
const stopLink = function stopLink(linker: Linker, stop: RouteStop | undefined): Link | null {
return stop === undefined ? null : linker.link(stop.label, CHAPTER_PREFIX + stop.path);
};
export const navigationOf = function navigationOf(
stops: readonly RouteStop[],
linker: Linker,
): (href: string) => Navigation | null {
const byPath = new Map(stops.map((stop) => [stop.path, stop]));
const byId = new Map(stops.map((stop) => [stop.id, stop]));
return (href) => {
const stop = byPath.get(href);
if (stop === undefined) {
return null;
}
return {
next: stopLink(linker, stops[stop.position]),
position: stop.position,
previous: stopLink(linker, stops[stop.position - 2]),
requires: stop.requires.flatMap((id) => {
const found = stopLink(linker, byId.get(id));
return found === null ? [] : [found];
}),
stop: stop.id,
total: stops.length,
};
};
};
const routeLinkOf = function routeLinkOf(node: GraphNode, route: DiscoveredRoute, linker: Linker): Link {
const address = pageIndex(route.page, route.tab);
return {
href: linker.site + route.path,
json: address.json,
label: node.title,
markdown: address.markdown,
ref: node.ref,
};
};
export const numberRows = function numberRows(
graph: Graph,
routes: readonly DiscoveredRoute[],
linker: Linker,
): readonly NumberRow[] {
const parents = new Map(
graph.edges
.filter((edge) => edge.relation === GRAPH_VOCABULARY.contains)
.map((edge) => [edge.to, edge.from] as const),
);
const routeOf = new Map(routes.map((route) => [ROUTE_PREFIX + route.path, route]));
const ordinals = new Map<string, number>();
const rowOf = function rowOf(node: GraphNode, number: string): NumberRow | null {
const route = routeOf.get(node.ref);
if (ROUTE_KINDS.has(node.kind) && route !== undefined) {
return { kind: node.kind, link: routeLinkOf(node, route, linker), number, part: null };
}
if (node.kind === SECTION_KIND) {
return { kind: node.kind, link: linker.link(node.title, node.ref), number, part: null };
}
const section = parents.get(node.ref);
if (node.kind !== PART_KIND || section === undefined) {
return null;
}
const part = (ordinals.get(section) ?? 0) + 1;
ordinals.set(section, part);
return { kind: node.kind, link: linker.link(node.title, section), number, part };
};
return graph.nodes.flatMap((node) => {
const row = node.layer === SITE_LAYER && node.number !== null ? rowOf(node, node.number) : null;
return row === null ? [] : [row];
});
};
const numberPart = function numberPart(rows: readonly NumberRow[], part: number, total: number, title: string): Leaf {
const ref = NUMBERS_REF + PART_REF_MARK + String(part);
const partTitle = indexPartTitle(title, part, total);
return {
data: { numbers: rows, part, ref, title: partTitle, total },
identity: {
address: indexPart(numbersIndex(), String(part)),
href: null,
kind: INDEX_PART_KIND,
ref,
summary: null,
title: partTitle,
},
markdown: renderNumbers(partTitle, rows),
};
};
export const numbersLeaves = function numbersLeaves(
rows: readonly NumberRow[],
site: string,
title: string,
budget = CATALOG_FILE_BUDGET,
partBudget = INDEX_PART_BUDGET,
): readonly Leaf[] {
const identity = {
address: numbersIndex(),
href: null,
kind: NUMBERS_KIND,
ref: NUMBERS_REF,
summary: null,
title,
};
const whole = { numbers: rows, ref: NUMBERS_REF, title, total: rows.length };
if (Buffer.byteLength(serialize(whole)) <= budget) {
return [{ data: whole, identity, markdown: renderNumbers(title, rows) }];
}
const parts = packedParts(rows, partBudget);
const leaves = parts.map((part, index) => numberPart(part, index + 1, parts.length, title));
const summaries = leaves.map((leaf, index) => ({
count: parts[index]?.length ?? 0,
first: parts[index]?.at(0)?.number ?? "",
json: site + leaf.identity.address.json,
last: parts[index]?.at(-1)?.number ?? "",
markdown: leaf.identity.address.markdown === null ? null : site + leaf.identity.address.markdown,
}));
const data = { parts: summaries, ref: NUMBERS_REF, title, total: rows.length };
return [...leaves, { data, identity, markdown: renderIndex(identity, data, [], site) }];
};
export const routeLeaf = function routeLeaf(stops: readonly RouteStop[], linker: Linker, title: string): Leaf {
const entries = stops.map((stop) => ({
block: stop.block,
link: linker.link(stop.label, CHAPTER_PREFIX + stop.path),
position: stop.position,
requires: stop.requires.map((id) => stops.find((candidate) => candidate.id === id)?.position ?? null),
stop: stop.id,
}));
const address = routeIndex();
return {
data: { ref: ROUTE_REF, stops: entries, title, total: stops.length },
identity: { address, href: null, kind: ROUTE_KIND, ref: ROUTE_REF, summary: null, title },
markdown: renderRoute(title, entries),
};
};