mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-29 19:01:39 +00:00
parseRowCells now splits on unescaped pipes only and decodes escaped pipes while preserving ordinary backslashes and empty cells, so facts whose text contains literal | survive the fence->DB reconcile instead of being silently deleted. Fixes #2726.
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
/**
|
|
* Shared primitives for HTML-comment-delimited markdown fences.
|
|
*
|
|
* Two consumers today:
|
|
* - `src/core/takes-fence.ts` (v0.28+) — the `## Takes` fence.
|
|
* - `src/core/facts-fence.ts` (v0.32.2+) — the `## Facts` fence.
|
|
*
|
|
* Both fences share row-level shape (pipe-separated cells, strikethrough on
|
|
* the claim for inactive rows, optional separator row, escape-on-write for
|
|
* embedded pipes). Lifting these helpers out makes that contract a single
|
|
* source of truth — so a fix to one fence's parsing semantics applies to
|
|
* the other automatically.
|
|
*
|
|
* Behavior is byte-identical to the inlined versions that shipped in
|
|
* takes-fence at v0.28 — this is a refactor, not a behavior change. The
|
|
* takes-fence test suite is the regression gate.
|
|
*
|
|
* Future fences (hunches as a third category, anything else that needs
|
|
* fenced markdown tables) should consume these helpers and add only the
|
|
* domain-specific parser layer on top.
|
|
*/
|
|
|
|
/**
|
|
* Match a markdown table row's cell-stripped content.
|
|
*
|
|
* Returns `null` when the line is not a table row (doesn't start with `|`
|
|
* or has no second pipe). On a match, returns the cells with surrounding
|
|
* whitespace trimmed, with the outer pipes already stripped.
|
|
*
|
|
* Escaped pipes (`\|`) stay inside their cell and are decoded back to `|`.
|
|
* Other backslashes are preserved verbatim so existing fence text such as
|
|
* Windows paths remains byte-stable across a render/parse cycle.
|
|
*/
|
|
export function parseRowCells(line: string): string[] | null {
|
|
const trimmed = line.trim();
|
|
if (!trimmed.startsWith('|') || !trimmed.includes('|', 1)) return null;
|
|
const inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
|
|
const cells: string[] = [];
|
|
let cell = '';
|
|
for (let i = 0; i < inner.length; i++) {
|
|
const char = inner[i];
|
|
if (char === '\\' && inner[i + 1] === '|') {
|
|
cell += '|';
|
|
i += 1;
|
|
} else if (char === '|') {
|
|
cells.push(cell.trim());
|
|
cell = '';
|
|
} else {
|
|
cell += char;
|
|
}
|
|
}
|
|
cells.push(cell.trim());
|
|
return cells;
|
|
}
|
|
|
|
/**
|
|
* Markdown table separator detector. A row like `|---|---|---|` (or with
|
|
* colons for alignment) returns true. Used to skip past the header
|
|
* separator when iterating fence rows.
|
|
*/
|
|
export function isSeparatorRow(cells: string[]): boolean {
|
|
return cells.every(c => /^[-:\s]+$/.test(c)) && cells.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Detect strikethrough wrapping on a cell.
|
|
*
|
|
* `~~text~~` → `{ text: 'text', struck: true }`
|
|
* `text` → `{ text: 'text', struck: false }`
|
|
*
|
|
* Used by both fences to mark a row as inactive (takes: superseded /
|
|
* retracted; facts: superseded / forgotten — the parser distinguishes
|
|
* via the `context` cell at the domain layer, not here).
|
|
*/
|
|
export function stripStrikethrough(s: string): { text: string; struck: boolean } {
|
|
const m = s.match(/^~~(.+?)~~$/);
|
|
if (m) return { text: m[1].trim(), struck: true };
|
|
return { text: s, struck: false };
|
|
}
|
|
|
|
/**
|
|
* Trim a cell's surrounding whitespace and collapse empty / whitespace-only
|
|
* cells to `undefined`. The `''` → `undefined` mapping is what callers want
|
|
* for optional string fields.
|
|
*/
|
|
export function parseStringCell(raw: string): string | undefined {
|
|
const trimmed = raw.trim();
|
|
return trimmed ? trimmed : undefined;
|
|
}
|
|
|
|
/**
|
|
* Escape a value for safe placement inside a pipe-separated cell. Replaces
|
|
* any literal `|` with `\|` so the table layout stays intact. `parseRowCells`
|
|
* is the inverse and decodes the escape after identifying cell boundaries.
|
|
*/
|
|
export function escapeFenceCell(s: string): string {
|
|
return s.replace(/\|/g, '\\|');
|
|
}
|