# core/converters/section.converter.ts

> 172 lines of code and 39 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-converters-section-converter-ts
Source text: https://banes-lab.com/source/build-scripts/core/converters/section.converter.ts.txt

Listed in [core/converters](https://banes-lab.com/api/source/build-scripts/core/converters.md), after [core/converters/search.index.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/search.index.converter.ts.md) and before [core/converters/section.page.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/section.page.converter.ts.md).

## Definitions

- `tabsOf` (lexical_declaration, line 46, exported)
- `routePath` (lexical_declaration, line 38)
- `sectionLeaves` (lexical_declaration, line 147, exported)
- `sectionList` (lexical_declaration, line 34)
- `targetsOf` (lexical_declaration, line 98)
- `documentPages` (lexical_declaration, line 65, exported)
- `sectionPlans` (lexical_declaration, line 69, exported)
- `linksOf` (lexical_declaration, line 102)
- `sectionRelations` (lexical_declaration, line 131)
- `FRAGMENT` (lexical_declaration, line 25)
- `SECTION_KIND` (lexical_declaration, line 26)
- `HEADING_MARK` (lexical_declaration, line 27)
- `LINE_END` (lexical_declaration, line 28)
- `isSection` (lexical_declaration, line 30)
- `route` (lexical_declaration, line 39)
- `{ content }` (lexical_declaration, line 47, exported)
- `id` (lexical_declaration, line 51, exported)
- `label` (lexical_declaration, line 52, exported)
- `sections` (lexical_declaration, line 61, exported)
- `withoutHeading` (lexical_declaration, line 91, exported)
- `trimmed` (lexical_declaration, line 92, exported)
- `end` (lexical_declaration, line 93, exported)
- `first` (lexical_declaration, line 94, exported)
- `identity` (lexical_declaration, line 106, exported)
- `graphOf` (lexical_declaration, line 114)
- `graph` (lexical_declaration, line 115)
- `sectionEdges` (lexical_declaration, line 119, exported)
- `outgoing` (lexical_declaration, line 120, exported)
- `incoming` (lexical_declaration, line 121, exported)
- `held` (lexical_declaration, line 124, exported)
- `folder` (lexical_declaration, line 136)
- `tabs` (lexical_declaration, line 151, exported)
- `bodies` (lexical_declaration, line 152, exported)
- `{ identity }` (lexical_declaration, line 164, exported)
- `href` (lexical_declaration, line 165, exported)
- `data` (lexical_declaration, line 166, exported)
- `at` (lexical_declaration, line 181, exported)
- `body` (lexical_declaration, line 182, exported)
- `leaf` (lexical_declaration, line 183, exported)

## Contained in

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)

## Uses

- [configuration/strings/catalog.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/catalog.strings.ts.md)
- [core/converters/link.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/link.converter.ts.md)
- [core/converters/section.segment.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/section.segment.converter.ts.md)

## Used by

- [core/converters/index.page.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/index.page.converter.ts.md)
- [core/coordinators/catalog.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/catalog.coordinator.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)

## Source

```typescript
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);
    });
};
```
