core/parsers/check.parser.ts

core/parsers/check.parser.ts is a file in GovLab Context. 167 lines of code and 40 definitions.

import {
    ARM_SEPARATOR,
    CHECK_MARKERS,
    EVIDENCE_OPEN,
    GATE_MARKER,
    MEASURED_MARKER,
    POPULATION_MARKER,
    POPULATION_SLASH,
    REFUSAL_PREFIX,
    RESULT_ARROWS,
    RESULT_BLOCKED,
    RESULT_OWNER_OPEN,
    RESULT_PASS,
    RESULT_PREFIX,
    RESULT_UNKNOWN,
    RETIRED_CHECK_MARKERS,
    RETIRED_GATE_MARKERS,
    STANDING_PREFIX,
} from "#configuration/constants/grammar.constants";
import type {
    GateStep,
    MarkerHit,
    PagCheck,
    PagFailureArm,
    PagGate,
    PagPopulation,
    PagResult,
} from "#types/grammar.document.types";
import { valueAfter } from "#core/converters/text.converter";

const PAREN_CLOSE = ")";

export const gateMarkerOf = function gateMarkerOf(trimmed: string): { retired: boolean } | null {
    const lower = trimmed.toLowerCase();
    if (lower.startsWith(GATE_MARKER)) {
        return { retired: false };
    }
    return RETIRED_GATE_MARKERS.some((marker) => lower.startsWith(marker)) ? { retired: true } : null;
};

export const checkMarkerOf = function checkMarkerOf(trimmed: string): MarkerHit | null {
    const live = CHECK_MARKERS.find((marker) => trimmed.startsWith(marker));
    if (live !== undefined) {
        return { marker: live.trim(), retired: false };
    }
    const retired = RETIRED_CHECK_MARKERS.find((marker) => trimmed.startsWith(marker));
    return retired === undefined ? null : { marker: retired.trim(), retired: true };
};

const populationOf = function populationOf(text: string): PagPopulation | null {
    const over = text.indexOf(POPULATION_MARKER);
    if (over === -1) {
        return null;
    }
    const afterOver = text.slice(over + POPULATION_MARKER.length);
    const measured = afterOver.indexOf(MEASURED_MARKER);
    const set = (measured === -1 ? afterOver : afterOver.slice(0, measured)).trim();
    const counts = measured === -1 ? "" : afterOver.slice(measured + MEASURED_MARKER.length);
    const slash = counts.indexOf(POPULATION_SLASH);
    return {
        measured: (slash === -1 ? counts : counts.slice(0, slash)).trim(),
        set,
        whole: slash === -1 ? "" : counts.slice(slash + 1).trim(),
    };
};

const stripTrailingParen = function stripTrailingParen(text: string): string {
    const trimmed = text.trim();
    return trimmed.endsWith(PAREN_CLOSE) ? trimmed.slice(0, -1).trim() : trimmed;
};

const firstMarkerAt = function firstMarkerAt(rest: string, candidates: number[]): number {
    const present = candidates.filter((index) => index !== -1);
    return present.length === 0 ? rest.length : Math.min(...present);
};

const evidenceOf = function evidenceOf(rest: string, evidenceAt: number, over: number): string | null {
    if (evidenceAt === -1) {
        return null;
    }
    const end = over === -1 || over < evidenceAt ? rest.length : over;
    return stripTrailingParen(rest.slice(evidenceAt + EVIDENCE_OPEN.length, end));
};

export const parseCheck = function parseCheck(trimmed: string, marker: string, line: number): PagCheck {
    const rest = trimmed.slice(marker.length).trim();
    const evidenceAt = rest.indexOf(EVIDENCE_OPEN);
    const over = rest.indexOf(POPULATION_MARKER);
    return {
        condition: rest.slice(0, firstMarkerAt(rest, [evidenceAt, over])).trim(),
        evidence: evidenceOf(rest, evidenceAt, over),
        line,
        marker,
        population: populationOf(rest),
    };
};

const ownerOf = function ownerOf(arm: string): string {
    const open = arm.indexOf(RESULT_OWNER_OPEN);
    if (open === -1) {
        return "";
    }
    const close = arm.indexOf(PAREN_CLOSE, open);
    return arm.slice(open + RESULT_OWNER_OPEN.length, close === -1 ? arm.length : close).trim();
};

const firstArrowAt = function firstArrowAt(arm: string): { at: number; length: number } | null {
    const hits = RESULT_ARROWS.map((arrow) => ({ at: arm.indexOf(arrow), length: arrow.length })).filter(
        (hit) => hit.at !== -1,
    );
    const [head, ...rest] = hits;
    return head === undefined ? null : rest.reduce((first, hit) => (hit.at < first.at ? hit : first), head);
};

const targetAfter = function targetAfter(trimmed: string, word: string): string | null {
    if (!trimmed.startsWith(word)) {
        return null;
    }
    const rest = trimmed.slice(word.length).trim();
    const arrow = RESULT_ARROWS.find((candidate) => rest.startsWith(candidate));
    return arrow === undefined ? null : rest.slice(arrow.length).trim();
};

const failureOf = function failureOf(arm: string): PagFailureArm {
    const arrow = firstArrowAt(arm);
    return { name: (arrow === null ? arm : arm.slice(0, arrow.at)).trim(), owner: ownerOf(arm) };
};

const applyArm = function applyArm(result: PagResult, arm: string): PagResult {
    const trimmed = arm.trim();
    if (trimmed === "") {
        return result;
    }
    const pass = targetAfter(trimmed, RESULT_PASS);
    if (pass !== null) {
        return { ...result, pass };
    }
    const unknown = targetAfter(trimmed, RESULT_UNKNOWN);
    if (unknown !== null) {
        return { ...result, unknown: unknown.startsWith(RESULT_BLOCKED) ? RESULT_BLOCKED : unknown };
    }
    return { ...result, failures: [...result.failures, failureOf(trimmed)] };
};

export const withResultArms = function withResultArms(result: PagResult, text: string): PagResult {
    return text.split(ARM_SEPARATOR).reduce(applyArm, result);
};

export const parseResult = function parseResult(trimmed: string, line: number): PagResult {
    return withResultArms({ failures: [], line, pass: "", unknown: null }, trimmed.slice(RESULT_PREFIX.length));
};

const clauseOf = function clauseOf(gate: PagGate, trimmed: string, lineNo: number): PagGate | null {
    if (trimmed.startsWith(REFUSAL_PREFIX)) {
        return { ...gate, refusal: valueAfter(trimmed, REFUSAL_PREFIX) };
    }
    if (trimmed.startsWith(STANDING_PREFIX)) {
        return { ...gate, standing: valueAfter(trimmed, STANDING_PREFIX) };
    }
    if (trimmed.startsWith(RESULT_PREFIX)) {
        return { ...gate, result: parseResult(trimmed, lineNo) };
    }
    if (trimmed.startsWith(ARM_SEPARATOR) && gate.result !== null) {
        return { ...gate, result: withResultArms(gate.result, trimmed) };
    }
    return null;
};

export const stepGate = function stepGate(gate: PagGate, trimmed: string, lineNo: number): GateStep {
    const hit = checkMarkerOf(trimmed);
    if (hit !== null) {
        const retired = hit.retired ? { kind: "check" as const, line: lineNo, token: hit.marker } : null;
        return {
            closes: false,
            consumed: true,
            gate: { ...gate, checks: [...gate.checks, parseCheck(trimmed, hit.marker, lineNo)] },
            retired,
        };
    }
    const next = clauseOf(gate, trimmed, lineNo);
    return next === null
        ? { closes: gate.result !== null, consumed: false, gate, retired: null }
        : { closes: false, consumed: true, gate: next, retired: null };
};