# shared/analyzers/diagram.analyzer.ts

> 149 lines of code and 40 definitions.

Tree: Governance tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/governance#file-governance-shared-analyzers-diagram-analyzer-ts
Source text: https://banes-lab.com/source/governance/shared/analyzers/diagram.analyzer.ts.txt

Listed in [shared/analyzers](https://banes-lab.com/api/source/governance/shared/analyzers.md), after [shared/analyzers/composition.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/composition.analyzer.ts.md) and before [shared/analyzers/document.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/document.analyzer.ts.md).

## Definitions

- `skipQuoted` (lexical_declaration, line 27)
- `nodeTokensOf` (lexical_declaration, line 107)
- `reservedNodeIdsOf` (lexical_declaration, line 148, exported)
- `skipBracket` (lexical_declaration, line 32)
- `groupIdEnd` (lexical_declaration, line 102)
- `skipLabel` (lexical_declaration, line 55)
- `skipEdge` (lexical_declaration, line 76)
- `readToken` (lexical_declaration, line 94)
- `usedAsNode` (lexical_declaration, line 141)
- `isDiagramSource` (lexical_declaration, line 161, exported)
- `LINE_END` (lexical_declaration, line 11)
- `SPACE` (lexical_declaration, line 12)
- `QUOTE` (lexical_declaration, line 13)
- `LABEL_BAR` (lexical_declaration, line 14)
- `OPEN_CLOSE` (lexical_declaration, line 15)
- `Token` (interface_declaration, line 21)
- `depth` (lexical_declaration, line 33)
- `skipInlineLabel` (lexical_declaration, line 60)
- `next` (lexical_declaration, line 64)
- `ARROW_HEAD` (lexical_declaration, line 73)
- `LABEL_OPENER_LENGTH` (lexical_declaration, line 74)
- `run` (lexical_declaration, line 81)
- `opensLabel` (lexical_declaration, line 86)
- `labelEnd` (lexical_declaration, line 90)
- `rest` (lexical_declaration, line 103)
- `tokens` (lexical_declaration, line 108)
- `at` (lexical_declaration, line 109)
- `char` (lexical_declaration, line 111)
- `close` (lexical_declaration, line 112)
- `token` (lexical_declaration, line 122)
- `nextChar` (lexical_declaration, line 132)
- `previousChar` (lexical_declaration, line 136)
- `head` (lexical_declaration, line 137)
- `after` (lexical_declaration, line 142)
- `before` (lexical_declaration, line 143)
- `target` (lexical_declaration, line 144)
- `found` (lexical_declaration, line 149, exported)
- `line` (lexical_declaration, line 151, exported)
- `[first = ""]` (lexical_declaration, line 162, exported)
- `[keyword = ""]` (lexical_declaration, line 163, exported)

## Used by

- [rules/eslint/strings-diagram-identifiers.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/strings-diagram-identifiers.eslint.rule.ts.md)

## Source

```typescript
import {
    DIAGRAM_EDGE_CHARACTERS,
    DIAGRAM_GROUP_KEYWORD,
    DIAGRAM_HEADERS,
    DIAGRAM_IDENTIFIER_CHARACTERS,
    DIAGRAM_KEYWORDS,
    DIAGRAM_SHAPE_OPENERS,
} from "../manifests/diagram.manifest.ts";
import type { ReservedNodeId } from "../../types/diagram.types.ts";

const LINE_END = "\n";
const SPACE = " ";
const QUOTE = '"';
const LABEL_BAR = "|";
const OPEN_CLOSE: ReadonlyMap<string, string> = new Map([
    ["[", "]"],
    ["(", ")"],
    ["{", "}"],
]);

interface Token {
    readonly end: number;
    readonly start: number;
    readonly word: string;
}

const skipQuoted = function skipQuoted(line: string, from: number): number {
    const close = line.indexOf(QUOTE, from + 1);
    return close === -1 ? line.length : close + 1;
};

const skipBracket = function skipBracket(line: string, from: number, close: string): number {
    let depth = 0;
    let at = from;
    while (at < line.length) {
        const char = line.charAt(at);
        if (char === QUOTE) {
            at = skipQuoted(line, at);
            continue;
        }
        if (OPEN_CLOSE.has(char)) {
            depth += 1;
        }
        if (char === close) {
            depth -= 1;
            if (depth === 0) {
                return at + 1;
            }
        }
        at += 1;
    }
    return line.length;
};

const skipLabel = function skipLabel(line: string, from: number): number {
    const close = line.indexOf(LABEL_BAR, from + 1);
    return close === -1 ? line.length : close + 1;
};

const skipInlineLabel = function skipInlineLabel(line: string, from: number): number {
    let at = from;
    while (at < line.length) {
        const char = line.charAt(at);
        const next = line.charAt(at + 1);
        if (DIAGRAM_EDGE_CHARACTERS.has(char) && DIAGRAM_EDGE_CHARACTERS.has(next)) {
            return at;
        }
        at += 1;
    }
    return line.length;
};

const ARROW_HEAD = ">";
const LABEL_OPENER_LENGTH = 2;

const skipEdge = function skipEdge(line: string, from: number): number {
    let at = from;
    while (at < line.length && DIAGRAM_EDGE_CHARACTERS.has(line.charAt(at))) {
        at += 1;
    }
    const run = line.slice(from, at);
    if (run.endsWith(ARROW_HEAD) || run.length !== LABEL_OPENER_LENGTH) {
        return at;
    }
    const rest = line.slice(at).trimStart();
    const opensLabel = rest.length > 0 && DIAGRAM_IDENTIFIER_CHARACTERS.includes(rest.charAt(0));
    if (!opensLabel) {
        return at;
    }
    const labelEnd = skipInlineLabel(line, line.length - rest.length);
    return labelEnd >= line.length ? labelEnd : skipEdge(line, labelEnd);
};

const readToken = function readToken(line: string, from: number): Token {
    let at = from;
    while (at < line.length && DIAGRAM_IDENTIFIER_CHARACTERS.includes(line.charAt(at))) {
        at += 1;
    }
    return { end: at, start: from, word: line.slice(from, at) };
};

const groupIdEnd = function groupIdEnd(line: string, from: number): number {
    const rest = line.slice(from).trimStart();
    return readToken(line, line.length - rest.length).end;
};

const nodeTokensOf = function nodeTokensOf(line: string): Token[] {
    const tokens: Token[] = [];
    let at = 0;
    while (at < line.length) {
        const char = line.charAt(at);
        const close = OPEN_CLOSE.get(char);
        if (close !== undefined) {
            at = skipBracket(line, at, close);
        } else if (char === QUOTE) {
            at = skipQuoted(line, at);
        } else if (char === LABEL_BAR) {
            at = skipLabel(line, at);
        } else if (DIAGRAM_EDGE_CHARACTERS.has(char)) {
            at = skipEdge(line, at);
        } else if (DIAGRAM_IDENTIFIER_CHARACTERS.includes(char)) {
            const token = readToken(line, at);
            tokens.push(token);
            at = token.word === DIAGRAM_GROUP_KEYWORD ? groupIdEnd(line, token.end) : token.end;
        } else {
            at += 1;
        }
    }
    return tokens;
};

const nextChar = function nextChar(line: string, from: number): string {
    return line.slice(from).trimStart().charAt(0);
};

const previousChar = function previousChar(line: string, before: number): string {
    const head = line.slice(0, before).trimEnd();
    return head.at(-1) ?? "";
};

const usedAsNode = function usedAsNode(line: string, token: Token): boolean {
    const after = nextChar(line, token.end);
    const before = previousChar(line, token.start);
    const target = DIAGRAM_EDGE_CHARACTERS.has(before) || before === LABEL_BAR;
    return DIAGRAM_SHAPE_OPENERS.has(after) || DIAGRAM_EDGE_CHARACTERS.has(after) || target;
};

export const reservedNodeIdsOf = function reservedNodeIdsOf(source: string): ReservedNodeId[] {
    const found: ReservedNodeId[] = [];
    source.split(LINE_END).forEach((raw, index) => {
        const line = raw.trim();
        for (const token of nodeTokensOf(line)) {
            if (DIAGRAM_KEYWORDS.has(token.word) && usedAsNode(line, token)) {
                found.push({ line: index + 1, word: token.word });
            }
        }
    });
    return found;
};

export const isDiagramSource = function isDiagramSource(source: string): boolean {
    const [first = ""] = source.trimStart().split(LINE_END);
    const [keyword = ""] = first.split(SPACE);
    return DIAGRAM_HEADERS.has(keyword);
};
```
