core/converters/section.converter.ts
core/converters/section.converter.ts is a file in Bane's Lab Build Scripts. 172 lines of code and 39 definitions.
import {
CONTAINED_IN_RELATION,
CONTAINS_RELATION,
EVIDENCE_FOR_RELATION,
EVIDENCE_RELATION_ID,
LINKED_FROM_RELATION,
LINKS_TO_RELATION,
} from "@banes-lab/web/constants/graph.constants";
import type { DiscoveredPage, Discovery } from "#types/site.types";
import type { Identity, Leaf, Link, Linker, Relation } from "#types/catalog.types";
import { PAYLOAD_ID_KEY, PAYLOAD_LABEL_KEY } from "@banes-lab/content/configuration/constants/leak.constants.ts";
import type { SectionEdges, SectionPlan, SectionShape, SectionSources, TabShape } from "#types/section.types";
import { linkTargetsOf, stringsOf } from "@banes-lab/content/core/converters/section.converter.ts";
import { tabsIn, textAt } from "@banes-lab/content/core/selectors/payload.selector.ts";
import { CHAPTER_PREFIX } from "#configuration/constants/graph.constants";
import type { ContentGraph } from "@banes-lab/web/types/methodology.types.js";
import { isRecord } from "#core/selectors/base.selector";
import { noRoute } from "#configuration/strings/catalog.strings";
import { partedSection } from "#core/converters/section.segment.converter";
import { placedOf } from "#core/converters/location.converter";
import { relationGroups } from "#core/converters/link.converter";
import { renderSectionLeaf } from "#core/formatters/section.formatter";
import { sectionLeaf } from "#core/resolvers/catalog.resolver";
const FRAGMENT = "#";
const SECTION_KIND = "section";
const HEADING_MARK = "#";
const LINE_END = "\n";
const isSection = function isSection(value: unknown): value is SectionShape {
return isRecord(value) && typeof value["id"] === "string" && typeof value["title"] === "string";
};
const sectionList = function sectionList(value: unknown): readonly SectionShape[] {
return isRecord(value) && Array.isArray(value["sections"]) ? value["sections"].filter(isSection) : [];
};
const routePath = function routePath(discovery: Discovery, page: string, tab: string | null): string {
const route = discovery.routes.find((candidate) => candidate.page === page && candidate.tab === tab);
if (route === undefined) {
throw new Error(noRoute(page, String(tab)));
}
return route.path;
};
export const tabsOf = function tabsOf(discovery: Discovery, page: DiscoveredPage): readonly TabShape[] {
const { content } = page;
const tabs = tabsIn(content);
if (tabs.length > 0) {
return tabs.map((tab, index) => {
const id = textAt(tab, PAYLOAD_ID_KEY) ?? "";
const label = textAt(tab, PAYLOAD_LABEL_KEY) ?? id;
return {
id,
label,
path: routePath(discovery, page.id, index === 0 ? null : id),
sections: sectionList(tab),
};
});
}
const sections = sectionList(content);
return sections.length === 0 ? [] : [{ id: null, label: page.label, path: page.path, sections }];
};
export const documentPages = function documentPages(discovery: Discovery): readonly DiscoveredPage[] {
return discovery.pages.filter((page) => tabsOf(discovery, page).some((tab) => tab.id === null));
};
export const sectionPlans = function sectionPlans(
discovery: Discovery,
summarize: (markdown: string) => string | null,
): readonly SectionPlan[] {
return discovery.pages.flatMap((page) =>
tabsOf(discovery, page).flatMap((tab) =>
tab.sections.map((section) => {
const href = tab.path + FRAGMENT + section.id;
const identity: Identity = {
address: sectionLeaf(page.id, tab.id, section.id),
href,
kind: SECTION_KIND,
ref: CHAPTER_PREFIX + href,
summary: section.intro === undefined ? null : summarize(section.intro),
title: section.title,
};
return { identity, page, section, tab };
}),
),
);
};
export const withoutHeading = function withoutHeading(markdown: string): string {
const trimmed = markdown.trim();
const end = trimmed.indexOf(LINE_END);
const first = end === -1 ? trimmed : trimmed.slice(0, end);
return first.startsWith(HEADING_MARK) ? trimmed.slice(first.length).trim() : trimmed;
};
const targetsOf = function targetsOf(value: unknown): readonly string[] {
return stringsOf(value).flatMap(linkTargetsOf);
};
const linksOf = function linksOf(plan: SectionPlan, linker: Linker): readonly Link[] {
const held = new Map<string, Link>();
for (const target of targetsOf(plan.section)) {
const href = target.startsWith(FRAGMENT) ? null : target;
const identity = href === null ? null : linker.byHref(href);
if (identity !== null && identity.ref !== plan.identity.ref && !held.has(identity.ref)) {
held.set(identity.ref, linker.link(identity.title, identity.ref));
}
}
return [...held.values()];
};
const graphOf = function graphOf(graphs: readonly ContentGraph[], plan: SectionPlan): object | null {
const graph = graphs.find((candidate) => candidate.page === plan.page.id);
return graph?.sections[plan.section.id] ?? null;
};
export const sectionEdges = function sectionEdges(plans: readonly SectionPlan[], linker: Linker): SectionEdges {
const outgoing = new Map(plans.map((plan) => [plan.identity.ref, linksOf(plan, linker)]));
const incoming = new Map<string, Link[]>();
for (const plan of plans) {
for (const link of outgoing.get(plan.identity.ref) ?? []) {
const held = incoming.get(link.ref ?? "") ?? [];
incoming.set(link.ref ?? "", [...held, linker.link(plan.identity.title, plan.identity.ref)]);
}
}
return { incoming, outgoing };
};
const sectionRelations = function sectionRelations(
sources: SectionSources,
ref: string,
href: string,
): readonly Relation[] {
const folder = sources.folder(ref);
return relationGroups([
{ links: sources.links(ref), relation: LINKS_TO_RELATION },
{ links: folder?.contains ?? [], relation: CONTAINS_RELATION },
{ links: folder?.containedIn ? [folder.containedIn] : [], relation: CONTAINED_IN_RELATION },
{ links: sources.linkedBy(ref), relation: LINKED_FROM_RELATION },
{ links: sources.evidence(href), relation: EVIDENCE_RELATION_ID },
{ links: sources.grounds(ref), relation: EVIDENCE_FOR_RELATION },
]);
};
export const sectionLeaves = function sectionLeaves(
plans: readonly SectionPlan[],
sources: SectionSources,
): readonly Leaf[] {
const tabs = new Map(plans.map((plan) => [plan.tab.path, plan.tab]));
const bodies = new Map(
[...tabs.values()].map((tab) => [
tab.path,
sources
.bodies(
tab.path,
tab.sections.map((section) => section.id),
)
.map(withoutHeading),
]),
);
return plans.flatMap((plan) => {
const { identity } = plan;
const href = identity.href ?? "";
const data = {
content: plan.section,
graph: graphOf(sources.graphs, plan),
href: sources.linker.site + href,
number: sources.numbers.get(identity.ref) ?? null,
page: plan.page.id,
ref: identity.ref,
relations: sectionRelations(sources, identity.ref, href),
route: sources.navigation(href),
section: plan.section.id,
summary: identity.summary,
tab: plan.tab.id,
title: identity.title,
...placedOf(sources.placement(identity.ref)),
};
const at = plan.tab.sections.indexOf(plan.section);
const body = sources.linker.relink(bodies.get(plan.tab.path)?.[at] ?? "", plan.tab.path);
const leaf = { data, identity, markdown: renderSectionLeaf(data, plan.page.label, plan.tab.label, body) };
return partedSection(leaf, plan.section, sources.linker.site);
});
};