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 };
};