# core/analyzers/markdown.analyzer.ts

> 75 lines of code and 31 definitions.

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

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

## Definitions

- `conventionHits` (lexical_declaration, line 74, exported)
- `barePathHits` (lexical_declaration, line 42)
- `langLabelHit` (lexical_declaration, line 22)
- `feed` (method_definition, line 60)
- `fenceHit` (lexical_declaration, line 34)
- `CODE_LANGS` (lexical_declaration, line 6)
- `FENCE_INTENTS` (lexical_declaration, line 9)
- `BARE_FENCE` (lexical_declaration, line 10)
- `LABEL_SEPARATOR` (lexical_declaration, line 11)
- `fenceRunLength` (lexical_declaration, line 13)
- `char` (lexical_declaration, line 14)
- `run` (lexical_declaration, line 15)
- `space` (lexical_declaration, line 23)
- `lang` (lexical_declaration, line 24)
- `rest` (lexical_declaration, line 28)
- `colon` (lexical_declaration, line 29)
- `intent` (lexical_declaration, line 30)
- `info` (lexical_declaration, line 35)
- `span` (lexical_declaration, line 46)
- `token` (lexical_declaration, line 47)
- `ConventionScanner` (class_declaration, line 56)
- `hits` (public_field_definition, line 57)
- `fence` (public_field_definition, line 58)
- `trimmed` (lexical_declaration, line 61)
- `wasOpen` (lexical_declaration, line 62)
- `opened` (lexical_declaration, line 64)
- `lines` (lexical_declaration, line 75, exported)
- `start` (lexical_declaration, line 76, exported)
- `mask` (lexical_declaration, line 77, exported)
- `scanner` (lexical_declaration, line 78, exported)
- `at` (lexical_declaration, line 79, exported)

## Contained in

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

## Uses

- [core/converters/markdown.converter.ts](https://banes-lab.com/source/docs/core/converters/markdown.converter.ts.md)
- [core/parsers/markdown.parser.ts](https://banes-lab.com/source/docs/core/parsers/markdown.parser.ts.md)
- [core/selectors/markdown.selector.ts](https://banes-lab.com/source/docs/core/selectors/markdown.selector.ts.md)

## Used by

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

## Linked from

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

## Source

````typescript
import type { ConventionHit, Fence } from "#types/markdown.types";
import { NO_FENCE, bodyStart, codeLineMask, splitLines, stepFence } from "#core/parsers/markdown.parser";
import { isProsePath, nextToken, trimEdges } from "#core/selectors/markdown.selector";
import { blankMarkup } from "#core/converters/markdown.converter";

const CODE_LANGS: ReadonlySet<string> = new Set(
    "ts tsx js jsx mjs cjs go rs py java rb php c cpp cs kt swift scala".split(" "),
);
const FENCE_INTENTS: ReadonlySet<string> = new Set(["API", "EXAMPLE", "CODE", "CONFIG"]);
const BARE_FENCE = "```";
const LABEL_SEPARATOR = ":";

const fenceRunLength = function fenceRunLength(trimmed: string): number {
    const char = trimmed.charAt(0);
    let run = 0;
    while (run < trimmed.length && trimmed.charAt(run) === char) {
        run += 1;
    }
    return run;
};

const langLabelHit = function langLabelHit(info: string, lineNo: number): ConventionHit | null {
    const space = info.indexOf(" ");
    const lang = space === -1 ? info : info.slice(0, space);
    if (!CODE_LANGS.has(lang)) {
        return null;
    }
    const rest = space === -1 ? "" : info.slice(space + 1).trim();
    const colon = rest.indexOf(LABEL_SEPARATOR);
    const intent = colon === -1 ? "" : rest.slice(0, colon).trim();
    return FENCE_INTENTS.has(intent) ? null : { code: "unlabeled-code-fence", col: 1, line: lineNo, token: lang };
};

const fenceHit = function fenceHit(trimmed: string, lineNo: number): ConventionHit | null {
    const info = trimmed.slice(fenceRunLength(trimmed)).trim();
    if (info.length === 0) {
        return { code: "untagged-code-fence", col: 1, line: lineNo, token: BARE_FENCE };
    }
    return langLabelHit(info, lineNo);
};

const barePathHits = function barePathHits(prose: string, lineNo: number): ConventionHit[] {
    const hits: ConventionHit[] = [];
    let at = 0;
    while (at < prose.length) {
        const span = nextToken(prose, at);
        const token = trimEdges(prose.slice(span.start, span.next));
        if (isProsePath(token)) {
            hits.push({ code: "bare-path", col: span.start + 1, line: lineNo, token });
        }
        at = span.next;
    }
    return hits;
};

class ConventionScanner {
    public readonly hits: ConventionHit[] = [];
    private fence: Fence = NO_FENCE;

    public feed(line: string, lineNo: number, masked: boolean): void {
        const trimmed = line.trim();
        const wasOpen = this.fence.open;
        this.fence = stepFence(trimmed, this.fence);
        const opened = !wasOpen && this.fence.open ? fenceHit(trimmed, lineNo) : null;
        if (opened !== null) {
            this.hits.push(opened);
        }
        if (!masked) {
            this.hits.push(...barePathHits(blankMarkup(line), lineNo));
        }
    }
}

export const conventionHits = function conventionHits(source: string): ConventionHit[] {
    const lines = splitLines(source);
    const start = bodyStart(lines);
    const mask = codeLineMask(lines, start);
    const scanner = new ConventionScanner();
    for (let at = start; at < lines.length; at += 1) {
        scanner.feed(lines[at] ?? "", at + 1, mask[at] ?? false);
    }
    return scanner.hits;
};
````
