core/parsers/markdown.parser.ts

core/parsers/markdown.parser.ts is a file in GovLab Docs. 138 lines of code and 38 definitions.

import type { Fence, FenceRun, MaskState } from "#types/markdown.types";
import { FRONTMATTER_FENCE } from "#configuration/constants/document.constants";
import { isDigit } from "@govlab/constants";

export const NO_FENCE: Fence = { char: "", len: 0, open: false };

const MIN_FENCE_LEN = 3;
const TAB_WIDTH = 4;
const INDENT_CODE_WIDTH = 4;
const LIST_CONTENT_OFFSET = 2;
const FENCE_CHARS: ReadonlySet<string> = new Set(["`", "~"]);
const INLINE_SPACE: ReadonlySet<string> = new Set([" ", "\t"]);
const BULLET_MARKERS: readonly string[] = ["- ", "* ", "+ "];
const ORDINAL_MARKERS: ReadonlySet<string> = new Set([".", ")"]);
const CARRIAGE_RETURN = "\r";

const fenceRun = function fenceRun(trimmed: string): FenceRun | null {
    const char = trimmed.charAt(0);
    if (!FENCE_CHARS.has(char)) {
        return null;
    }
    let len = 0;
    while (len < trimmed.length && trimmed.charAt(len) === char) {
        len += 1;
    }
    return len >= MIN_FENCE_LEN ? { char, len } : null;
};

const isBareClose = function isBareClose(trimmed: string, fence: Fence): boolean {
    const run = fenceRun(trimmed);
    if (run?.char !== fence.char || run.len < fence.len) {
        return false;
    }
    for (let at = run.len; at < trimmed.length; at += 1) {
        if (!INLINE_SPACE.has(trimmed.charAt(at))) {
            return false;
        }
    }
    return true;
};

export const stepFence = function stepFence(trimmed: string, fence: Fence): Fence {
    if (!fence.open) {
        const run = fenceRun(trimmed);
        return run === null ? fence : { char: run.char, len: run.len, open: true };
    }
    return isBareClose(trimmed, fence) ? NO_FENCE : fence;
};

export const splitLines = function splitLines(source: string): string[] {
    return source.split("\n").map((line) => (line.endsWith(CARRIAGE_RETURN) ? line.slice(0, -1) : line));
};

export const bodyStart = function bodyStart(lines: readonly string[]): number {
    if ((lines[0] ?? "").trim() !== FRONTMATTER_FENCE) {
        return 0;
    }
    for (let at = 1; at < lines.length; at += 1) {
        if ((lines[at] ?? "").trim() === FRONTMATTER_FENCE) {
            return at + 1;
        }
    }
    return 0;
};

const leadingWidth = function leadingWidth(line: string): number {
    let width = 0;
    for (const char of line) {
        if (!INLINE_SPACE.has(char)) {
            return width;
        }
        width += char === " " ? 1 : TAB_WIDTH;
    }
    return width;
};

const isListMarker = function isListMarker(trimmed: string): boolean {
    if (BULLET_MARKERS.some((marker) => trimmed.startsWith(marker))) {
        return true;
    }
    let at = 0;
    while (at < trimmed.length && isDigit(trimmed.charAt(at))) {
        at += 1;
    }
    return at > 0 && ORDINAL_MARKERS.has(trimmed.charAt(at)) && trimmed.charAt(at + 1) === " ";
};

const detectIndentedCode = function detectIndentedCode(indent: number, trimmed: string, state: MaskState): boolean {
    if (isListMarker(trimmed)) {
        state.listContentIndent = indent + LIST_CONTENT_OFFSET;
        return false;
    }
    if (indent >= INDENT_CODE_WIDTH && state.prevBlank && state.listContentIndent < 0) {
        state.inIndentedCode = true;
        return true;
    }
    return false;
};

const classifyContentLine = function classifyContentLine(line: string, trimmed: string, state: MaskState): boolean {
    const indent = leadingWidth(line);
    if (state.inIndentedCode && indent >= INDENT_CODE_WIDTH) {
        state.prevBlank = false;
        return true;
    }
    state.inIndentedCode = false;
    if (state.listContentIndent >= 0 && indent < state.listContentIndent) {
        state.listContentIndent = -1;
    }
    const code = detectIndentedCode(indent, trimmed, state);
    state.prevBlank = false;
    return code;
};

const fenced = function fenced(state: MaskState): boolean {
    state.inIndentedCode = false;
    state.prevBlank = false;
    return true;
};

const blankLine = function blankLine(state: MaskState): boolean {
    const inCode = state.inIndentedCode;
    state.prevBlank = true;
    return inCode;
};

const stepMask = function stepMask(line: string, state: MaskState): boolean {
    const trimmed = line.trim();
    const wasOpen = state.fence.open;
    state.fence = stepFence(trimmed, state.fence);
    if (wasOpen || state.fence.open) {
        return fenced(state);
    }
    if (trimmed === "") {
        return blankLine(state);
    }
    return classifyContentLine(line, trimmed, state);
};

export const codeLineMask = function codeLineMask(lines: readonly string[], start: number): boolean[] {
    const mask = Array.from({ length: lines.length }, () => false);
    const state: MaskState = { fence: NO_FENCE, inIndentedCode: false, listContentIndent: -1, prevBlank: true };
    for (let at = start; at < lines.length; at += 1) {
        mask[at] = stepMask(lines[at] ?? "", state);
    }
    return mask;
};

export const proseLines = function proseLines(source: string): { line: string; lineNo: number }[] {
    const lines = splitLines(source);
    const start = bodyStart(lines);
    const mask = codeLineMask(lines, start);
    return lines.flatMap((line, at) => (at < start || mask[at] === true ? [] : [{ line, lineNo: at + 1 }]));
};