# tools/core/readers/document.reader.ts

> 150 lines of code and 40 definitions.

Tree: Coordination tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/coordination#file-coordination-tools-core-readers-document-reader-ts
Source text: https://banes-lab.com/assets/sources/source.6204c90285b74c8d508feaa448f62f16175fddac99b21b55b6673fa373d26241.generated.txt

## Definitions

- `unfencedLines` (lexical_declaration, line 32, exported)
- `fieldShape` (lexical_declaration, line 107)
- `bodyShape` (lexical_declaration, line 135)
- `isListMarker` (lexical_declaration, line 88)
- `indentOf` (lexical_declaration, line 55)
- `isBlank` (lexical_declaration, line 63)
- `isOrdinal` (lexical_declaration, line 80)
- `isIndent` (lexical_declaration, line 47)
- `isDigit` (lexical_declaration, line 51)
- `isBullet` (lexical_declaration, line 75)
- `fenceStep` (lexical_declaration, line 112)
- `frontmatterStep` (lexical_declaration, line 123)
- `classify` (lexical_declaration, line 155)
- `SourceLine` (interface_declaration, line 4, exported)
- `ReadState` (interface_declaration, line 9)
- `Shape` (interface_declaration, line 15)
- `Classified` (interface_declaration, line 20)
- `FENCE` (lexical_declaration, line 24)
- `FRONTMATTER` (lexical_declaration, line 26)
- `FIELD_BREAKS` (lexical_declaration, line 28)
- `INITIAL` (lexical_declaration, line 30)
- `out` (lexical_declaration, line 33, exported)
- `fenced` (lexical_declaration, line 34, exported)
- `fence` (lexical_declaration, line 37, exported)
- `headingDepth` (lexical_declaration, line 67)
- `char` (lexical_declaration, line 76)
- `fieldAt` (lexical_declaration, line 92)
- `colon` (lexical_declaration, line 93)
- `key` (lexical_declaration, line 94)
- `cursor` (lexical_declaration, line 99)
- `field` (lexical_declaration, line 108)
- `depth` (lexical_declaration, line 143)
- `indent` (lexical_declaration, line 148)
- `readDocument` (lexical_declaration, line 159, exported)
- `segments` (lexical_declaration, line 160, exported)
- `state` (lexical_declaration, line 161, exported)
- `offset` (lexical_declaration, line 162, exported)
- `step` (lexical_declaration, line 165, exported)
- `number` (lexical_declaration, line 166, exported)
- `inlines` (lexical_declaration, line 167, exported)

## Used by

- [tools/core/readers/record.reader.ts](https://banes-lab.com/source/coordination/tools/core/readers/record.reader.ts.md)
- [tools/core/readers/rule.reader.ts](https://banes-lab.com/source/coordination/tools/core/readers/rule.reader.ts.md)

## Source

````typescript
import type { Document, Segment, SegmentKind } from "../types/segment.types.ts";
import { inlinesOf } from "./token.reader.ts";

export interface SourceLine {
    readonly text: string;
    readonly number: number;
}

interface ReadState {
    readonly fenced: boolean;
    readonly frontmatter: boolean;
    readonly closed: boolean;
}

interface Shape {
    readonly kind: SegmentKind;
    readonly extra: Partial<Segment>;
}

interface Classified extends Shape {
    readonly next: ReadState;
}

const FENCE = "```";

const FRONTMATTER = "---";

const FIELD_BREAKS = [" ", "\t", "`", "|"];

const INITIAL: ReadState = { closed: false, fenced: false, frontmatter: false };

export const unfencedLines = function unfencedLines(source: string): SourceLine[] {
    const out: SourceLine[] = [];
    let fenced = false;

    for (const [index, text] of source.split("\n").entries()) {
        const fence = text.startsWith(FENCE);
        if (!fence && !fenced) {
            out.push({ number: index + 1, text });
        }
        fenced = fence ? !fenced : fenced;
    }

    return out;
};

const isIndent = function isIndent(char: string): boolean {
    return char === " " || char === "\t";
};

const isDigit = function isDigit(char: string): boolean {
    return char >= "0" && char <= "9";
};

const indentOf = function indentOf(line: string): number {
    let cursor = 0;
    while (cursor < line.length && isIndent(line.charAt(cursor))) {
        cursor += 1;
    }
    return cursor;
};

const isBlank = function isBlank(line: string): boolean {
    return indentOf(line) === line.length;
};

const headingDepth = function headingDepth(line: string): number {
    let depth = 0;
    while (line.charAt(depth) === "#") {
        depth += 1;
    }
    return depth;
};

const isBullet = function isBullet(line: string, at: number): boolean {
    const char = line.charAt(at);
    return (char === "-" || char === "*") && line.charAt(at + 1) === " ";
};

const isOrdinal = function isOrdinal(line: string, at: number): boolean {
    let cursor = at;
    while (isDigit(line.charAt(cursor))) {
        cursor += 1;
    }
    return cursor !== at && line.startsWith(". ", cursor);
};

const isListMarker = function isListMarker(line: string, at: number): boolean {
    return isBullet(line, at) || isOrdinal(line, at);
};

const fieldAt = function fieldAt(line: string, from: number): { key: string; value: string } | null {
    const colon = line.indexOf(":", from);
    const key = colon <= from ? "" : line.slice(from, colon);
    if (key.length === 0 || FIELD_BREAKS.some((mark) => key.includes(mark))) {
        return null;
    }

    let cursor = colon + 1;
    while (line.charAt(cursor) === " ") {
        cursor += 1;
    }

    return { key, value: line.slice(cursor).trimEnd() };
};

const fieldShape = function fieldShape(line: string, kind: "field" | "frontmatter-field"): Shape {
    const field = fieldAt(line, indentOf(line));
    return field === null ? { extra: {}, kind: "text" } : { extra: { key: field.key, value: field.value }, kind };
};

const fenceStep = function fenceStep(state: ReadState, line: string): Classified | null {
    if (line.startsWith(FENCE)) {
        return {
            extra: {},
            kind: state.fenced ? "fence-close" : "fence-open",
            next: { ...state, fenced: !state.fenced },
        };
    }
    return state.fenced ? { extra: {}, kind: "fence-body", next: state } : null;
};

const frontmatterStep = function frontmatterStep(state: ReadState, line: string, index: number): Classified | null {
    if (line.trimEnd() !== FRONTMATTER || state.closed) {
        return null;
    }
    if (!state.frontmatter && index === 0) {
        return { extra: {}, kind: "frontmatter-open", next: { ...state, frontmatter: true } };
    }
    return state.frontmatter
        ? { extra: {}, kind: "frontmatter-close", next: { ...state, closed: true, frontmatter: false } }
        : null;
};

const bodyShape = function bodyShape(state: ReadState, line: string): Shape {
    if (isBlank(line)) {
        return { extra: {}, kind: "blank" };
    }
    if (state.frontmatter) {
        return fieldShape(line, "frontmatter-field");
    }

    const depth = headingDepth(line);
    if (depth > 0 && line.charAt(depth) === " ") {
        return { extra: { depth, key: line.slice(depth).trim() }, kind: "heading" };
    }

    const indent = indentOf(line);
    if (line.charAt(indent) === "|") {
        return { extra: {}, kind: "table-row" };
    }
    return isListMarker(line, indent) ? { extra: {}, kind: "list-item" } : fieldShape(line, "field");
};

const classify = function classify(state: ReadState, line: string, index: number): Classified {
    return fenceStep(state, line) ?? frontmatterStep(state, line, index) ?? { ...bodyShape(state, line), next: state };
};

export const readDocument = function readDocument(path: string, source: string): Document {
    const segments: Segment[] = [];
    let state = INITIAL;
    let offset = 0;

    for (const [index, line] of source.split("\n").entries()) {
        const step = classify(state, line, index);
        const number = index + 1;
        const inlines = state.fenced ? [] : inlinesOf(line, offset, number);

        segments.push({
            end: offset + line.trimEnd().length,
            inlines,
            kind: step.kind,
            line: number,
            start: offset,
            text: line,
            ...step.extra,
        });

        state = step.next;
        offset += line.length + 1;
    }

    return { path, segments, source };
};
````
