# core/converters/record.page.converter.ts

> 133 lines of code and 38 definitions.

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

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

## Definitions

- `pageLinkOf` (lexical_declaration, line 37, exported)
- `isRecordLeaf` (lexical_declaration, line 25)
- `placementOf` (lexical_declaration, line 111)
- `distinctPages` (lexical_declaration, line 85, exported)
- `anchorOf` (lexical_declaration, line 51)
- `kindLabel` (lexical_declaration, line 57)
- `distinctSubject` (lexical_declaration, line 73)
- `factsOf` (lexical_declaration, line 98)
- `recordSubjectOf` (lexical_declaration, line 123, exported)
- `recordPathOf` (lexical_declaration, line 29, exported)
- `SLASH` (lexical_declaration, line 19)
- `FRAGMENT` (lexical_declaration, line 20)
- `HYPHEN` (lexical_declaration, line 21)
- `SPACE` (lexical_declaration, line 22)
- `RECORD_PREFIX` (lexical_declaration, line 23)
- `{ markdown }` (lexical_declaration, line 30, exported)
- `local` (lexical_declaration, line 38, exported)
- `tabPathOf` (lexical_declaration, line 45)
- `href` (lexical_declaration, line 52)
- `cut` (lexical_declaration, line 53)
- `headingOf` (lexical_declaration, line 61)
- `countsOf` (lexical_declaration, line 65)
- `isShared` (lexical_declaration, line 69)
- `kind` (lexical_declaration, line 74)
- `{label}` (lexical_declaration, line 75)
- `title` (lexical_declaration, line 76)
- `description` (lexical_declaration, line 79)
- `shared` (lexical_declaration, line 86, exported)
- `textFact` (lexical_declaration, line 93)
- `value` (lexical_declaration, line 94)
- `closure` (lexical_declaration, line 99)
- `facts` (lexical_declaration, line 100)
- `{ next, previous }` (lexical_declaration, line 115)
- `{ identity, record, site }` (lexical_declaration, line 124, exported)
- `tabPath` (lexical_declaration, line 125, exported)
- `tabTitle` (lexical_declaration, line 126, exported)
- `name` (lexical_declaration, line 127, exported)
- `path` (lexical_declaration, line 128, exported)

## Contained in

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

## Uses

- [configuration/strings/page.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/page.strings.ts.md)

## Used by

- [core/converters/record.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/record.converter.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)

## Source

```typescript
import {
    CANONICAL_LABEL,
    CLOSURE_LABEL,
    CLOSURE_LINK_LABEL,
    FORMED_BY_LABEL,
    RECORD_LABEL,
    SCOPE_LABEL,
    SEVERITY_LABEL,
} from "@banes-lab/web/strings/ontology.strings";
import type { Identity, Link } from "#types/catalog.types";
import type { RecordData, RecordLeaf, RecordPageInput, RecordSubjectCounts } from "#types/record.types";
import type { RecordFact, RecordLink, RecordPlacement, RecordSubject } from "@banes-lab/web/types/page.types.js";
import { SITE_NAME, TITLE_SEPARATOR } from "@banes-lab/web/strings/page.strings";
import { missingAlternate, recordDescription } from "#configuration/strings/page.strings";
import { MARKDOWN_EXTENSION } from "#configuration/constants/site.constants";
import { RECORDS_SEGMENT } from "#configuration/constants/catalog.constants";
import { recordText } from "#core/selectors/record.selector";

const SLASH = "/";
const FRAGMENT = "#";
const HYPHEN = "-";
const SPACE = " ";
const RECORD_PREFIX = SLASH + RECORDS_SEGMENT + SLASH;

const isRecordLeaf = function isRecordLeaf(path: string): boolean {
    return path.startsWith(RECORD_PREFIX) && path.endsWith(MARKDOWN_EXTENSION);
};

export const recordPathOf = function recordPathOf(identity: Identity): string {
    const { markdown } = identity.address;
    if (markdown === null || !isRecordLeaf(markdown)) {
        throw new Error(missingAlternate(identity.ref));
    }
    return markdown.slice(0, -MARKDOWN_EXTENSION.length);
};

export const pageLinkOf = function pageLinkOf(site: string, link: Link): RecordLink {
    const local = link.markdown?.startsWith(site) === true ? link.markdown.slice(site.length) : null;
    if (local !== null && isRecordLeaf(local)) {
        return { label: link.label, path: site + local.slice(0, -MARKDOWN_EXTENSION.length) };
    }
    return { label: link.label, path: link.href };
};

const tabPathOf = function tabPathOf(identity: Identity): string {
    const href = identity.href ?? SLASH;
    const cut = href.indexOf(FRAGMENT);
    return cut === -1 ? href : href.slice(0, cut);
};

const anchorOf = function anchorOf(identity: Identity): string | null {
    const href = identity.href ?? "";
    const cut = href.indexOf(FRAGMENT);
    return cut === -1 ? null : href.slice(cut + FRAGMENT.length);
};

const kindLabel = function kindLabel(kind: string): string {
    return kind.split(HYPHEN).join(SPACE);
};

const headingOf = function headingOf(subject: Pick<RecordSubject, "code" | "name">): string {
    return subject.code === null ? subject.name : `${subject.name} (${subject.code})`;
};

const countsOf = function countsOf(values: readonly string[]): ReadonlyMap<string, number> {
    return values.reduce((counts, value) => counts.set(value, (counts.get(value) ?? 0) + 1), new Map<string, number>());
};

const isShared = function isShared(counts: ReadonlyMap<string, number>, value: string): boolean {
    return (counts.get(value) ?? 0) > 1;
};

const distinctSubject = function distinctSubject(subject: RecordSubject, shared: RecordSubjectCounts): RecordSubject {
    const kind = kindLabel(subject.kind);
    const {label} = subject.collection;
    const title = isShared(shared.titles, subject.title)
        ? [headingOf(subject), kind, label, SITE_NAME].join(TITLE_SEPARATOR)
        : subject.title;
    const description = isShared(shared.descriptions, subject.description)
        ? recordDescription(headingOf(subject), kind, label, subject.summary)
        : subject.description;
    return { ...subject, description, title };
};

export const distinctPages = function distinctPages(leaves: readonly RecordLeaf[]): readonly RecordLeaf[] {
    const shared = {
        descriptions: countsOf(leaves.map((leaf) => leaf.page.description)),
        titles: countsOf(leaves.map((leaf) => leaf.page.title)),
    };
    return leaves.map((leaf) => ({ ...leaf, page: distinctSubject(leaf.page, shared) }));
};

const textFact = function textFact(data: RecordData, label: string, key: string): RecordFact | null {
    const value = recordText(data, key);
    return value === null ? null : { label, value: { label: value, path: null } };
};

const factsOf = function factsOf(data: RecordData, tabTitle: string): readonly RecordFact[] {
    const closure = recordText(data, "closure");
    const facts = [
        { label: RECORD_LABEL, value: { label: data.ref, path: null } },
        textFact(data, SEVERITY_LABEL, "severity"),
        textFact(data, SCOPE_LABEL, "scope"),
        textFact(data, FORMED_BY_LABEL, "formedBy"),
        data.href === null ? null : { label: CANONICAL_LABEL, value: { label: tabTitle, path: data.href } },
        closure === null ? null : { label: CLOSURE_LABEL, value: { label: CLOSURE_LINK_LABEL, path: closure } },
    ];
    return facts.filter((fact): fact is RecordFact => fact !== null);
};

const placementOf = function placementOf(data: RecordData, site: string): RecordPlacement | null {
    if (data.up === null) {
        return null;
    }
    const { next, previous } = data.siblings ?? { next: null, previous: null };
    return {
        next: next === null ? null : pageLinkOf(site, next),
        previous: previous === null ? null : pageLinkOf(site, previous),
        up: pageLinkOf(site, data.up),
    };
};

export const recordSubjectOf = function recordSubjectOf(input: RecordPageInput): RecordSubject {
    const { identity, record, site } = input;
    const tabPath = tabPathOf(identity);
    const tabTitle = input.tabLabel(tabPath);
    const name = headingOf(record);
    const path = recordPathOf(identity);
    return {
        aliases: input.aliases,
        alternates: { json: identity.address.json, markdown: path + MARKDOWN_EXTENSION },
        code: record.code,
        collection: { label: tabTitle, path: tabPath },
        description: record.summary ?? recordDescription(name, kindLabel(record.kind), tabTitle, null),
        facts: factsOf(input.data, tabTitle),
        kind: record.kind,
        layer: input.layer === null ? null : pageLinkOf(site, input.layer),
        name: record.name,
        path,
        placement: placementOf(input.data, site),
        relations: input.relations.map((relation) => ({
            links: relation.links.map((link) => pageLinkOf(site, link)),
            relation: relation.relation,
        })),
        section: anchorOf(identity),
        summary: record.summary,
        title: name + TITLE_SEPARATOR + tabTitle + TITLE_SEPARATOR + SITE_NAME,
    };
};
```
