core/converters/learning.converter.ts
core/converters/learning.converter.ts is a file in Bane's Lab Build Scripts. 184 lines of code and 46 definitions.
import type {
Block,
BlockStop,
LearningRouter,
SectionRecord,
Stop,
TabRecord,
TabsFor,
Unit,
} from "#types/learning.types";
import type { ContentGraph, GraphSection } from "@banes-lab/web/types/methodology.types.js";
import {
FOLDED_CHARACTERS,
SITE_PAGES,
SPINE,
UNSAFE_LABEL_CHARACTERS,
} from "#configuration/constants/learning.constants";
import { conceptTeachers, nodeOf } from "@banes-lab/content/core/converters/section.converter.ts";
import { isRecord } from "#core/selectors/base.selector";
const FRAGMENT = "#";
const SUBSTITUTE = "'";
const SEPARATOR = " - ";
const LOWEST = 32;
const HIGHEST = 126;
const TWO_DIGITS = 10;
const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
export const chunkCode = function chunkCode(index: number): string {
const first = ALPHABET[Math.floor(index / ALPHABET.length) % ALPHABET.length] ?? "";
const second = ALPHABET[index % ALPHABET.length] ?? "";
return `${first}${second}`;
};
export const textOf = function textOf(value: unknown): string {
return typeof value === "string" ? value : "";
};
export const sectionsIn = function sectionsIn(tab: TabRecord): readonly SectionRecord[] {
const sections: unknown = tab.sections;
if (!Array.isArray(sections)) {
return [];
}
return sections.filter(isRecord).filter((section: SectionRecord) => textOf(section.title).length > 0);
};
export const safeLabel = function safeLabel(title: string): string {
return Array.from(new Intl.Segmenter().segment(title), (part) => part.segment)
.map((character) => FOLDED_CHARACTERS.get(character) ?? character)
.map((character) => (UNSAFE_LABEL_CHARACTERS.has(character) ? SUBSTITUTE : character))
.filter((character) => {
const code = character.codePointAt(0) ?? 0;
return code >= LOWEST && code <= HIGHEST;
})
.join("");
};
const ordinal = function ordinal(value: number): string {
return value < TWO_DIGITS ? `0${String(value)}` : String(value);
};
export const heldIn = function heldIn(
graph: ContentGraph,
sections: readonly SectionRecord[],
): readonly GraphSection[] {
const byId = new Map(Object.entries(graph.sections));
return sections.flatMap((section) => {
const held = byId.get(textOf(section.id));
return held === undefined ? [] : [held];
});
};
export const unitsOf = function unitsOf(
page: string,
graph: ContentGraph,
tabs: readonly TabRecord[],
): readonly Unit[] {
return tabs.flatMap((tab, index) => {
const sections = sectionsIn(tab);
const held = heldIn(graph, sections);
const teaches = held.flatMap((entry) => entry.teaches);
if (teaches.length === 0) {
return [];
}
const inside = new Set(teaches);
const external = [...new Set(held.flatMap((entry) => entry.requires))].filter(
(concept) => !inside.has(concept),
);
return [
{ external, first: index === 0, label: textOf(tab.label), page, sections, tab: textOf(tab.id), teaches },
];
});
};
const readyIn = function readyIn(pending: readonly Unit[], taught: ReadonlySet<string>): Unit | undefined {
return pending.find((unit) => unit.external.every((concept) => taught.has(concept)));
};
export const orderUnits = function orderUnits(spine: readonly Unit[], others: readonly Unit[]): readonly Unit[] {
const ordered: Unit[] = [];
const taught = new Set<string>();
let pending = [...others];
const emit = function emit(unit: Unit): void {
ordered.push(unit);
for (const concept of unit.teaches) {
taught.add(concept);
}
};
const drain = function drain(): void {
const ready = readyIn(pending, taught);
if (ready === undefined) {
return;
}
emit(ready);
pending = pending.filter((unit) => unit !== ready);
drain();
};
for (const unit of spine) {
emit(unit);
drain();
}
for (const unit of pending) {
emit(unit);
}
return ordered;
};
interface Draft {
readonly node: string;
readonly requires: readonly string[];
readonly stop: Omit<Stop, "requires">;
}
const draftsOf = function draftsOf(
router: LearningRouter,
units: readonly Unit[],
graphs: readonly ContentGraph[],
): Draft[] {
const byPage = new Map(graphs.map((graph) => [graph.page, graph]));
const drafts: Draft[] = [];
for (const [chunk, unit] of units.entries()) {
for (const section of unit.sections) {
const id = textOf(section.id);
const number = drafts.length + 1;
drafts.push({
node: nodeOf(unit.page, id),
requires: byPage.get(unit.page)?.sections[id]?.requires ?? [],
stop: {
block: unit.label,
code: chunkCode(chunk),
id: `${chunkCode(chunk)}${String(drafts.length + 1)}`,
label: `${ordinal(number)}${SEPARATOR}${safeLabel(textOf(section.title))}`,
owner: unit.page,
path: unit.first
? router.pagePath(unit.page) + FRAGMENT + id
: router.tabLink(unit.page, unit.tab, id),
},
});
}
}
return drafts;
};
export const teachingRoute = function teachingRoute(
router: LearningRouter,
graphs: readonly ContentGraph[],
tabsFor: TabsFor,
): readonly Stop[] {
const byPage = new Map(graphs.map((graph) => [graph.page, graph]));
const unitsFor = function unitsFor(page: string): readonly Unit[] {
const graph = byPage.get(page);
return graph === undefined ? [] : unitsOf(page, graph, tabsFor(page));
};
const others = SITE_PAGES.filter((page) => page !== SPINE).flatMap(unitsFor);
const drafts = draftsOf(router, orderUnits(unitsFor(SPINE), others), graphs);
const teachers = conceptTeachers(graphs);
const idOf = new Map(drafts.map((draft) => [draft.node, draft.stop.id]));
return drafts.map((draft) => {
const required = draft.requires.flatMap((concept) => {
const id = idOf.get(teachers.get(concept)?.at(-1) ?? "");
return id === undefined || id === draft.stop.id ? [] : [id];
});
return { ...draft.stop, requires: [...new Set(required)] };
});
};
export const teachingBlocks = function teachingBlocks(stops: readonly Stop[]): readonly Block[] {
const blocks: { code: string; label: string; owner: string; stops: BlockStop[] }[] = [];
let held: string | null = null;
for (const stop of stops) {
if (stop.code !== held) {
blocks.push({ code: stop.code, label: stop.block, owner: stop.owner, stops: [] });
held = stop.code;
}
blocks.at(-1)?.stops.push({ id: stop.id, label: stop.label, path: stop.path, requires: stop.requires });
}
return blocks;
};