# core/converters/section.converter.ts

> 134 lines of code and 30 definitions.

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

Listed in [core/converters](https://banes-lab.com/api/source/content/core/converters.md), after [core/converters/principle.converter.ts](https://banes-lab.com/source/content/core/converters/principle.converter.ts.md).

## Definitions

- `nodeOf` (lexical_declaration, line 11, exported)
- `sectionOf` (lexical_declaration, line 122)
- `stringsOf` (lexical_declaration, line 42, exported)
- `nodePartsOf` (lexical_declaration, line 23, exported)
- `markdownSectionsOf` (lexical_declaration, line 133, exported)
- `conceptTeachers` (lexical_declaration, line 28, exported)
- `lineTargetsOf` (lexical_declaration, line 58)
- `sectionsOf` (lexical_declaration, line 128, exported)
- `moduleSectionOf` (lexical_declaration, line 144, exported)
- `teachingNodes` (lexical_declaration, line 15, exported)
- `linkTargetsOf` (lexical_declaration, line 73, exported)
- `splitSections` (lexical_declaration, line 101)
- `LINE_END` (lexical_declaration, line 9)
- `cut` (lexical_declaration, line 24, exported)
- `teachers` (lexical_declaration, line 31, exported)
- `targets` (lexical_declaration, line 59)
- `at` (lexical_declaration, line 60)
- `start` (lexical_declaration, line 62)
- `close` (lexical_declaration, line 63)
- `fenced` (lexical_declaration, line 74, exported)
- `sectionIdOf` (lexical_declaration, line 84)
- `Split` (interface_declaration, line 89)
- `keyedSections` (lexical_declaration, line 94)
- `id` (lexical_declaration, line 96)
- `entries` (lexical_declaration, line 109)
- `rest` (lexical_declaration, line 113)
- `strings` (lexical_declaration, line 123)
- `links` (lexical_declaration, line 124)
- `{ body, found }` (lexical_declaration, line 129, exported)
- `parts` (lexical_declaration, line 134, exported)

## Contained in

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

## Uses

- [core/selectors/payload.selector.ts](https://banes-lab.com/source/content/core/selectors/payload.selector.ts.md)

## Used by

- [core/loaders/leak.loader.ts](https://banes-lab.com/source/content/core/loaders/leak.loader.ts.md)
- [core/loaders/panel.loader.ts](https://banes-lab.com/source/content/core/loaders/panel.loader.ts.md)
- [core/loaders/section.loader.ts](https://banes-lab.com/source/content/core/loaders/section.loader.ts.md)
- [core/validators/coverage.validator.ts](https://banes-lab.com/source/content/core/validators/coverage.validator.ts.md)
- [core/validators/evidence.validator.ts](https://banes-lab.com/source/content/core/validators/evidence.validator.ts.md)
- [runtime/entrypoints/coverage.entrypoint.ts](https://banes-lab.com/source/content/runtime/entrypoints/coverage.entrypoint.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/content/folder-content-core-converters.md)
- [core/loaders](https://banes-lab.com/anatomy/content/folder-content-core-loaders.md)
- [core/selectors](https://banes-lab.com/anatomy/content/folder-content-core-selectors.md)
- [core/validators](https://banes-lab.com/anatomy/content/folder-content-core-validators.md)
- [runtime/entrypoints](https://banes-lab.com/anatomy/content/folder-content-runtime-entrypoints.md)

## Source

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