# core/parsers/document.parser.ts

> 193 lines of code and 35 definitions.

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

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

## Definitions

- `parse` (lexical_declaration, line 203, exported)
- `flushInvariant` (method_definition, line 66)
- `handleDeclaration` (method_definition, line 87)
- `handleHeader` (method_definition, line 95)
- `openHeader` (method_definition, line 108)
- `WORD_SEPARATOR` (lexical_declaration, line 17)
- `Mode` (type_alias_declaration, line 19)
- `ParseState` (class_declaration, line 21)
- `doc` (public_field_definition, line 22)
- `node` (public_field_definition, line 23)
- `gate` (public_field_definition, line 24)
- `invariant` (public_field_definition, line 25)
- `mode` (public_field_definition, line 26)
- `inMeta` (public_field_definition, line 27)
- `constructor` (method_definition, line 29)
- `finish` (method_definition, line 33)
- `consume` (method_definition, line 38)
- `handlers` (lexical_declaration, line 44)
- `flushNode` (method_definition, line 57)
- `handleMeta` (method_definition, line 73)
- `header` (lexical_declaration, line 99)
- `handleBareHead` (method_definition, line 120)
- `handleReportHead` (method_definition, line 130)
- `handleMode` (method_definition, line 140)
- `consumeInvariantLine` (method_definition, line 155)
- `handleGateLine` (method_definition, line 166)
- `marker` (lexical_declaration, line 167)
- `step` (lexical_declaration, line 179)
- `handleNodeLine` (method_definition, line 188)
- `{ node }` (lexical_declaration, line 189)
- `field` (lexical_declaration, line 193)
- `lines` (lexical_declaration, line 204, exported)
- `front` (lexical_declaration, line 205, exported)
- `state` (lexical_declaration, line 206, exported)
- `index` (lexical_declaration, line 207, exported)

## Contained in

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

## Uses

- [core/parsers/check.parser.ts](https://banes-lab.com/source/context/core/parsers/check.parser.ts.md)
- [core/parsers/invariant.parser.ts](https://banes-lab.com/source/context/core/parsers/invariant.parser.ts.md)
- [core/parsers/metadata.parser.ts](https://banes-lab.com/source/context/core/parsers/metadata.parser.ts.md)
- [core/parsers/node.parser.ts](https://banes-lab.com/source/context/core/parsers/node.parser.ts.md)

## Used by

- [core/factories/grammar.factory.ts](https://banes-lab.com/source/context/core/factories/grammar.factory.ts.md)

## Linked from

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

## Source

```typescript
import {
    BARE_INVARIANT_HEADS,
    BULLET,
    DECLARATION_PREFIX,
    HEADER_PREFIX,
    INVARIANT_PREFIX,
    META_MARKER,
    REPORT_HEAD,
} from "#configuration/constants/grammar.constants";
import type { PagDocument, PagGate, PagNode, ParsedHeader } from "#types/grammar.document.types";
import { fieldOf, parseDeclaration, readFrontmatter } from "#core/parsers/metadata.parser";
import { gateMarkerOf, stepGate } from "#core/parsers/check.parser";
import { nodeFieldOf, nodeOf, parseHeader } from "#core/parsers/node.parser";
import { parseInvariant } from "#core/parsers/invariant.parser";
import { splitLines } from "#core/converters/text.converter";

const WORD_SEPARATOR = " ";

type Mode = "bare" | "body" | "gate" | "invariants" | "repair" | "report";

class ParseState {
    public doc: PagDocument;
    private node: PagNode | null = null;
    private gate: PagGate | null = null;
    private invariant: { text: string; line: number } | null = null;
    private mode: Mode = "body";
    private inMeta = false;

    public constructor(frontmatter: string | null) {
        this.doc = { declaration: null, frontmatter, invariants: [], meta: {}, nodes: [], report: null, retired: [] };
    }

    public finish(): void {
        this.flushInvariant();
        this.flushNode();
    }

    public consume(trimmed: string, lineNo: number): void {
        if (trimmed === "") {
            this.inMeta = false;
            this.mode = this.mode === "gate" ? "body" : this.mode;
            return;
        }
        const handlers = [
            (): boolean => this.handleMeta(trimmed),
            (): boolean => this.handleDeclaration(trimmed, lineNo),
            (): boolean => this.handleHeader(trimmed, lineNo),
            (): boolean => this.handleBareHead(trimmed, lineNo),
            (): boolean => this.handleReportHead(trimmed, lineNo),
            (): boolean => this.handleMode(trimmed, lineNo),
            (): boolean => this.handleGateLine(trimmed, lineNo),
            (): boolean => this.handleNodeLine(trimmed),
        ];
        handlers.some((handler) => handler());
    }

    private flushNode(): void {
        if (this.node) {
            this.node.gate = this.gate;
            this.doc.nodes.push(this.node);
        }
        this.node = null;
        this.gate = null;
    }

    private flushInvariant(): void {
        if (this.invariant) {
            this.doc.invariants.push(parseInvariant(this.invariant.text, this.invariant.line));
        }
        this.invariant = null;
    }

    private handleMeta(trimmed: string): boolean {
        if (trimmed.startsWith(META_MARKER)) {
            this.inMeta = true;
            return true;
        }
        const field = this.inMeta ? fieldOf(trimmed) : null;
        if (field !== null) {
            this.doc.meta[field.key] = field.value;
            return true;
        }
        this.inMeta = false;
        return false;
    }

    private handleDeclaration(trimmed: string, lineNo: number): boolean {
        if (this.doc.declaration || !trimmed.startsWith(DECLARATION_PREFIX)) {
            return false;
        }
        this.doc.declaration = parseDeclaration(trimmed, lineNo);
        return true;
    }

    private handleHeader(trimmed: string, lineNo: number): boolean {
        if (!trimmed.startsWith(HEADER_PREFIX) || gateMarkerOf(trimmed) !== null) {
            return false;
        }
        const header = parseHeader(trimmed.slice(HEADER_PREFIX.length));
        if (header.kind !== "comment") {
            this.flushInvariant();
            this.flushNode();
            this.openHeader(header, trimmed, lineNo);
        }
        return true;
    }

    private openHeader(header: ParsedHeader, trimmed: string, lineNo: number): void {
        if (header.kind === "invariants" || header.kind === "repair") {
            this.mode = header.kind;
            return;
        }
        if (header.kind === "retired-unit") {
            this.doc.retired.push({ kind: "unit", line: lineNo, token: header.head });
        }
        this.mode = "body";
        this.node = nodeOf(header, trimmed.slice(HEADER_PREFIX.length), lineNo);
    }

    private handleBareHead(trimmed: string, lineNo: number): boolean {
        if (!BARE_INVARIANT_HEADS.has(trimmed)) {
            return false;
        }
        this.flushInvariant();
        this.doc.retired.push({ kind: "invariant", line: lineNo, token: trimmed });
        this.mode = "bare";
        return true;
    }

    private handleReportHead(trimmed: string, lineNo: number): boolean {
        if (trimmed !== REPORT_HEAD) {
            return false;
        }
        this.flushInvariant();
        this.doc.report = { fields: {}, line: lineNo };
        this.mode = "report";
        return true;
    }

    private handleMode(trimmed: string, lineNo: number): boolean {
        if (this.mode === "invariants") {
            this.consumeInvariantLine(trimmed, lineNo);
            return true;
        }
        if (this.mode === "report") {
            const field = fieldOf(trimmed);
            if (field !== null && this.doc.report) {
                this.doc.report.fields[field.key] = field.value;
            }
            return true;
        }
        return this.mode === "bare" ? trimmed.startsWith(BULLET) : this.mode === "repair";
    }

    private consumeInvariantLine(trimmed: string, lineNo: number): void {
        if (trimmed.startsWith(INVARIANT_PREFIX)) {
            this.flushInvariant();
            this.invariant = { line: lineNo, text: trimmed };
            return;
        }
        if (this.invariant) {
            this.invariant.text = `${this.invariant.text}${WORD_SEPARATOR}${trimmed}`;
        }
    }

    private handleGateLine(trimmed: string, lineNo: number): boolean {
        const marker = gateMarkerOf(trimmed);
        if (marker !== null && this.node) {
            this.gate = { checks: [], line: lineNo, refusal: null, result: null, standing: null };
            if (marker.retired) {
                this.doc.retired.push({ kind: "gate", line: lineNo, token: trimmed });
            }
            this.mode = "gate";
            return true;
        }
        if (this.mode !== "gate" || this.gate === null) {
            return false;
        }
        const step = stepGate(this.gate, trimmed, lineNo);
        this.gate = step.gate;
        if (step.retired !== null) {
            this.doc.retired.push(step.retired);
        }
        this.mode = step.closes ? "body" : this.mode;
        return step.consumed;
    }

    private handleNodeLine(trimmed: string): boolean {
        const { node } = this;
        if (node === null) {
            return false;
        }
        const field = nodeFieldOf(trimmed);
        if (field === null) {
            node.directives.push(trimmed);
        } else {
            node[field.field] = field.value;
        }
        return true;
    }
}

export const parse = function parse(text: string): PagDocument {
    const lines = splitLines(text);
    const front = readFrontmatter(lines);
    const state = new ParseState(front.frontmatter);
    for (let index = front.end; index < lines.length; index += 1) {
        state.consume((lines[index] ?? "").trim(), index + 1);
    }
    state.finish();
    return state.doc;
};
```
