import { ALWAYS_WORD, ARROW, BACKTICK, BOLD_MARK, COLON, GATE_TAIL, HEADING_MARK, LEAD_IN_SEPARATOR, LINE_END, LIST_MARKER, LOCKED_MARKERS, NEVER_WORD, PAREN_CLOSE, PAREN_OPEN, RULE_PREFIX, } from "#configuration/constants/inventory.constants"; import type { InventoryRecord } from "#types/inventory.types"; const NOT_FOUND = -1; const headingText = function headingText(line: string): string | null { if (!line.startsWith(HEADING_MARK)) { return null; } let at = 0; while (at < line.length && line.charAt(at) === HEADING_MARK) { at += 1; } return line.slice(at).trim(); }; const slugPrefixAccepted = function slugPrefixAccepted(prefix: string): boolean { if (prefix.length === 0 || prefix === LIST_MARKER) { return true; } return prefix.startsWith(BOLD_MARK) && prefix.endsWith(BOLD_MARK + LEAD_IN_SEPARATOR); }; interface SlugSpan { readonly slug: string; readonly tail: string; } const slugOf = function slugOf(line: string): SlugSpan | null { const open = line.indexOf(BACKTICK); if (open === NOT_FOUND || !slugPrefixAccepted(line.slice(0, open))) { return null; } const close = line.indexOf(BACKTICK, open + 1); if (close === NOT_FOUND || close === open + 1) { return null; } return { slug: line.slice(open + 1, close), tail: line.slice(close + 1) }; }; interface LockedSpan { readonly locked: boolean; readonly rest: string; } const lockedOf = function lockedOf(tail: string): LockedSpan { const trimmed = tail.trimStart(); for (const marker of LOCKED_MARKERS) { if (trimmed.startsWith(marker)) { return { locked: true, rest: trimmed.slice(marker.length).trimStart() }; } } return { locked: false, rest: trimmed }; }; interface GateSpan { readonly directive: string; readonly gate: string | null; } const gateOf = function gateOf(directive: string): GateSpan { const at = directive.indexOf(GATE_TAIL); if (at === NOT_FOUND) { return { directive: directive.trim(), gate: null }; } return { directive: directive.slice(0, at).trim(), gate: directive.slice(at + GATE_TAIL.length).trim() }; }; const ruleOf = function ruleOf(line: string, source: string, tier: string): InventoryRecord | null { const span = slugOf(line); if (span === null) { return null; } const { locked, rest } = lockedOf(span.tail); if (!rest.startsWith(COLON)) { return null; } const { directive, gate } = gateOf(rest.slice(COLON.length)); return { always: null, directive, gate, kind: "rule", locked, never: null, slug: span.slug, source, tier }; }; const arrowOutsideParens = function arrowOutsideParens(text: string): number { let depth = 0; for (let at = 0; at < text.length; at += 1) { const char = text.charAt(at); if (char === ARROW && depth === 0) { return at; } depth += char === PAREN_OPEN ? 1 : 0; depth -= char === PAREN_CLOSE ? 1 : 0; } return NOT_FOUND; }; const stripLead = function stripLead(text: string, lead: string): string | null { const trimmed = text.trim(); return trimmed.startsWith(lead) ? trimmed.slice(lead.length).trim() : null; }; const avoidanceOf = function avoidanceOf(line: string, source: string, tier: string): InventoryRecord | null { if (!line.startsWith(RULE_PREFIX)) { return null; } const colon = line.indexOf(COLON); if (colon === NOT_FOUND) { return null; } const slug = line.slice(RULE_PREFIX.length, colon).trim(); const directive = line.slice(colon + COLON.length).trim(); const arrow = arrowOutsideParens(directive); if (arrow === NOT_FOUND) { return null; } const never = stripLead(directive.slice(0, arrow), NEVER_WORD); const always = stripLead(directive.slice(arrow + ARROW.length), ALWAYS_WORD); if (never === null || always === null) { return null; } return { always, directive, gate: null, kind: "avoidance", locked: false, never, slug, source, tier }; }; export const scanRules = function scanRules(text: string, source: string): InventoryRecord[] { const records: InventoryRecord[] = []; let tier = ""; for (const raw of text.split(LINE_END)) { const line = raw.trim(); const heading = headingText(line); if (heading !== null) { tier = heading; continue; } const record = ruleOf(line, source, tier) ?? avoidanceOf(line, source, tier); if (record !== null) { records.push(record); } } return records; }; export const lineCount = function lineCount(text: string): number { return text.split(LINE_END).length; };