# core/parsers/node.parser.ts

> 119 lines of code and 27 definitions.

Tree: GovLab Context
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/context#file-context-core-parsers-node-parser-ts
Source text: https://banes-lab.com/source/context/core/parsers/node.parser.ts.txt

Listed in [core/parsers](https://banes-lab.com/api/source/context/core/parsers.md), after [core/parsers/metadata.parser.ts](https://banes-lab.com/source/context/core/parsers/metadata.parser.ts.md).

## Definitions

- `parseNodeHeader` (lexical_declaration, line 60)
- `parseTag` (lexical_declaration, line 34, exported)
- `titleOf` (lexical_declaration, line 53)
- `parseRetiredHeader` (lexical_declaration, line 67)
- `nodeOf` (lexical_declaration, line 112, exported)
- `parseHeader` (lexical_declaration, line 79, exported)
- `nodeFieldOf` (lexical_declaration, line 107, exported)
- `COMMENT` (lexical_declaration, line 23)
- `WORD_SEPARATOR` (lexical_declaration, line 24)
- `COLON` (lexical_declaration, line 25)
- `slotsOf` (lexical_declaration, line 27)
- `close` (lexical_declaration, line 36, exported)
- `slots` (lexical_declaration, line 40, exported)
- `yields` (lexical_declaration, line 41, exported)
- `dash` (lexical_declaration, line 54)
- `body` (lexical_declaration, line 55)
- `open` (lexical_declaration, line 56, exported)
- `second` (lexical_declaration, line 61)
- `number` (lexical_declaration, line 62)
- `afterHead` (lexical_declaration, line 63)
- `colon` (lexical_declaration, line 68)
- `rawNumber` (lexical_declaration, line 69)
- `trimmed` (lexical_declaration, line 80, exported)
- `tokens` (lexical_declaration, line 87, exported)
- `head` (lexical_declaration, line 88, exported)
- `NODE_FIELDS` (lexical_declaration, line 98)
- `entry` (lexical_declaration, line 108, exported)

## Contained in

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

## Uses

- [core/converters/text.converter.ts](https://banes-lab.com/source/context/core/converters/text.converter.ts.md)

## Used by

- [core/parsers/document.parser.ts](https://banes-lab.com/source/context/core/parsers/document.parser.ts.md)

## Linked from

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

## Source

```typescript
import {
    CONTRACT_FRESHNESS,
    CONTRACT_INPUT,
    CONTRACT_TRANSFORM,
    EM_DASH,
    HEAD_TOKEN_MIN,
    INVARIANT_BLOCK_HEAD,
    NODE_HEAD,
    NODE_TAG_SLOTS,
    REPAIR_EDGE_HEAD,
    RETIRED_UNIT_HEADS,
    TAG_CLOSE,
    TAG_CUE,
    TAG_GENESIS,
    TAG_OPEN,
    TAG_PURPOSE,
    TAG_SEPARATOR,
    TAG_YIELDS,
} from "#configuration/constants/grammar.constants";
import type { PagNode, PagNodeField, PagNodeFieldValue, PagNodeTag, ParsedHeader } from "#types/grammar.document.types";
import { valueAfter } from "#core/converters/text.converter";

const COMMENT: ParsedHeader = { head: "", kind: "comment", number: null, tag: null, title: "" };
const WORD_SEPARATOR = " ";
const COLON = ":";

const slotsOf = function slotsOf(inner: string): string[] {
    return inner
        .split(TAG_SEPARATOR)
        .map((slot) => slot.trim())
        .filter((slot) => slot !== "");
};

export const parseTag = function parseTag(text: string): PagNodeTag | null {
    const open = text.indexOf(TAG_OPEN);
    const close = text.lastIndexOf(TAG_CLOSE);
    if (open === -1 || close === -1 || close < open) {
        return null;
    }
    const slots = slotsOf(text.slice(open + 1, close));
    const yields = slots[NODE_TAG_SLOTS - 1] ?? "";
    if (slots.length !== NODE_TAG_SLOTS || !yields.startsWith(TAG_YIELDS)) {
        return null;
    }
    return {
        axis: slots[1] ?? "",
        layer: slots[0] ?? "",
        mathType: slots[2] ?? "",
        yields: valueAfter(yields, TAG_YIELDS),
    };
};

const titleOf = function titleOf(afterHead: string): string {
    const dash = afterHead.indexOf(EM_DASH);
    const body = dash === -1 ? afterHead : afterHead.slice(dash + EM_DASH.length);
    const open = body.indexOf(TAG_OPEN);
    return (open === -1 ? body : body.slice(0, open)).trim();
};

const parseNodeHeader = function parseNodeHeader(afterHash: string, tokens: string[]): ParsedHeader {
    const second = tokens[1] ?? "";
    const number = second === "" || second === EM_DASH ? null : second;
    const afterHead = afterHash.slice(NODE_HEAD.length + 1 + (number ?? "").length);
    return { head: NODE_HEAD, kind: "node", number, tag: parseTag(afterHash), title: titleOf(afterHead) };
};

const parseRetiredHeader = function parseRetiredHeader(afterHash: string, tokens: string[]): ParsedHeader {
    const colon = afterHash.indexOf(COLON);
    const rawNumber = tokens[1] ?? "";
    return {
        head: tokens[0] ?? "",
        kind: "retired-unit",
        number: rawNumber === "" ? null : (rawNumber.split(COLON)[0] ?? null),
        tag: null,
        title: colon === -1 ? afterHash.trim() : afterHash.slice(colon + 1).trim(),
    };
};

export const parseHeader = function parseHeader(afterHash: string): ParsedHeader {
    const trimmed = afterHash.trim();
    if (trimmed.startsWith(INVARIANT_BLOCK_HEAD)) {
        return { ...COMMENT, head: INVARIANT_BLOCK_HEAD, kind: "invariants" };
    }
    if (trimmed.startsWith(REPAIR_EDGE_HEAD)) {
        return { ...COMMENT, head: REPAIR_EDGE_HEAD, kind: "repair" };
    }
    const tokens = trimmed.split(WORD_SEPARATOR).filter(Boolean);
    const head = tokens[0] ?? "";
    if (tokens.length < HEAD_TOKEN_MIN) {
        return COMMENT;
    }
    if (head === NODE_HEAD) {
        return parseNodeHeader(trimmed, tokens);
    }
    return RETIRED_UNIT_HEADS.has(head) ? parseRetiredHeader(trimmed, tokens) : COMMENT;
};

const NODE_FIELDS: readonly [string, PagNodeField][] = [
    [TAG_PURPOSE, "purpose"],
    [TAG_CUE, "cue"],
    [TAG_GENESIS, "genesis"],
    [CONTRACT_INPUT, "input"],
    [CONTRACT_TRANSFORM, "transform"],
    [CONTRACT_FRESHNESS, "freshness"],
];

export const nodeFieldOf = function nodeFieldOf(trimmed: string): PagNodeFieldValue | null {
    const entry = NODE_FIELDS.find(([prefix]) => trimmed.startsWith(prefix));
    return entry === undefined ? null : { field: entry[1], value: valueAfter(trimmed, entry[0]) };
};

export const nodeOf = function nodeOf(header: ParsedHeader, raw: string, line: number): PagNode {
    return {
        cue: null,
        directives: [],
        freshness: null,
        gate: null,
        genesis: null,
        head: header.head,
        header: raw,
        input: null,
        line,
        number: header.number,
        purpose: null,
        tag: header.tag,
        title: header.title,
        transform: null,
    };
};
```
