# core/analyzers/diagram.analyzer.ts

> 84 lines of code and 27 definitions.

Tree: GovLab Docs
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/docs#file-docs-core-analyzers-diagram-analyzer-ts
Source text: https://banes-lab.com/source/docs/core/analyzers/diagram.analyzer.ts.txt

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

## Definitions

- `mermaidHardening` (lexical_declaration, line 91, exported)
- `trailingHasContent` (lexical_declaration, line 16)
- `scanPlain` (method_definition, line 68)
- `isStatementSeparator` (method_definition, line 50)
- `scanChar` (method_definition, line 54)
- `RESERVED_IN_LABEL` (lexical_declaration, line 5)
- `PARENS` (lexical_declaration, line 6)
- `TRAILING_BLANKS` (lexical_declaration, line 7)
- `DIRECTION_WORD` (lexical_declaration, line 8)
- `BREAK_TAG` (lexical_declaration, line 9)
- `MAX_ASCII` (lexical_declaration, line 10)
- `BR_END_OFFSET` (lexical_declaration, line 11)
- `QUOTE` (lexical_declaration, line 12)
- `OPEN_ANGLE` (lexical_declaration, line 13)
- `SEMICOLON` (lexical_declaration, line 14)
- `MermaidLineScanner` (class_declaration, line 25)
- `hits` (public_field_definition, line 26)
- `inQuote` (public_field_definition, line 27)
- `line` (public_field_definition, line 28)
- `lineNo` (public_field_definition, line 29)
- `constructor` (method_definition, line 31)
- `scan` (method_definition, line 36)
- `at` (lexical_declaration, line 37)
- `isHtmlBreak` (method_definition, line 46)
- `char` (lexical_declaration, line 55)
- `code` (lexical_declaration, line 56)
- `push` (method_definition, line 86)

## Contained in

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

## Uses

- [configuration/strings/diagram.strings.ts](https://banes-lab.com/source/docs/configuration/strings/diagram.strings.ts.md)
- [core/parsers/diagram.parser.ts](https://banes-lab.com/source/docs/core/parsers/diagram.parser.ts.md)

## Used by

- [core/analyzers/document.analyzer.ts](https://banes-lab.com/source/docs/core/analyzers/document.analyzer.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/docs/folder-docs-configuration-strings.md)
- [core/analyzers](https://banes-lab.com/anatomy/docs/folder-docs-core-analyzers.md)
- [core/parsers](https://banes-lab.com/anatomy/docs/folder-docs-core-parsers.md)

## Source

```typescript
import { HARDENING_DETAILS, nonAsciiDetail, parenDetail, reservedDetail } from "#configuration/strings/diagram.strings";
import type { MermaidHardeningCode, MermaidHardeningHit } from "#types/diagram.types";
import { mermaidBlocks } from "#core/parsers/diagram.parser";

const RESERVED_IN_LABEL: ReadonlySet<string> = new Set(["[", "]", "{", "}", "<", ">", "&"]);
const PARENS: ReadonlySet<string> = new Set(["(", ")"]);
const TRAILING_BLANKS: ReadonlySet<string> = new Set([" ", "\t", "\r"]);
const DIRECTION_WORD = "direction ";
const BREAK_TAG = "br";
const MAX_ASCII = 126;
const BR_END_OFFSET = 3;
const QUOTE = '"';
const OPEN_ANGLE = "<";
const SEMICOLON = ";";

const trailingHasContent = function trailingHasContent(line: string, from: number): boolean {
    for (let at = from; at < line.length; at += 1) {
        if (!TRAILING_BLANKS.has(line.charAt(at))) {
            return true;
        }
    }
    return false;
};

class MermaidLineScanner {
    public readonly hits: MermaidHardeningHit[] = [];
    private inQuote = false;
    private readonly line: string;
    private readonly lineNo: number;

    public constructor(line: string, lineNo: number) {
        this.line = line;
        this.lineNo = lineNo;
    }

    public scan(): MermaidHardeningHit[] {
        for (let at = 0; at < this.line.length; at += 1) {
            this.scanChar(at);
        }
        if (this.line.trim().toLowerCase().startsWith(DIRECTION_WORD)) {
            this.push("mermaid-subgraph-direction", 0, HARDENING_DETAILS.direction);
        }
        return this.hits;
    }

    private isHtmlBreak(char: string, at: number): boolean {
        return char === OPEN_ANGLE && this.line.slice(at + 1, at + BR_END_OFFSET).toLowerCase() === BREAK_TAG;
    }

    private isStatementSeparator(char: string, at: number): boolean {
        return char === SEMICOLON && !this.inQuote && trailingHasContent(this.line, at + 1);
    }

    private scanChar(at: number): void {
        const char = this.line.charAt(at);
        const code = this.line.codePointAt(at) ?? 0;
        if (code > MAX_ASCII) {
            this.push("mermaid-non-ascii", at, nonAsciiDetail(code));
            return;
        }
        if (char === QUOTE) {
            this.inQuote = !this.inQuote;
            return;
        }
        this.scanPlain(char, at);
    }

    private scanPlain(char: string, at: number): void {
        if (this.isHtmlBreak(char, at)) {
            this.push("mermaid-html-break", at, HARDENING_DETAILS.htmlBreak);
            return;
        }
        if (PARENS.has(char)) {
            this.push("mermaid-paren", at, parenDetail(char));
            return;
        }
        if (this.isStatementSeparator(char, at)) {
            this.push("mermaid-semicolon", at, HARDENING_DETAILS.semicolon);
            return;
        }
        if (this.inQuote && RESERVED_IN_LABEL.has(char)) {
            this.push("mermaid-label-reserved", at, reservedDetail(char));
        }
    }

    private push(code: MermaidHardeningCode, at: number, detail: string): void {
        this.hits.push({ code, col: at + 1, detail, line: this.lineNo });
    }
}

export const mermaidHardening = function mermaidHardening(source: string): MermaidHardeningHit[] {
    return mermaidBlocks(source).flatMap((block) =>
        block.code.split("\n").flatMap((line, offset) => new MermaidLineScanner(line, block.startLine + offset).scan()),
    );
};
```
