Files
PaperClipAI/packages/shared/src/validators/text.ts
T

74 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
export function normalizeEscapedLineBreaks(value: string): string {
return value
.replace(/\\r\\n/g, "\n")
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\n");
}
const FENCE_LINE = /^(\s{0,3})(```+|~~~+)/;
// A mid-line ATX heading marker: 1–6 `#`, a space, then heading text. The
// capture keeps the char immediately before the run so we can tell a real
// (collapsed) heading from an inline `#` token like `C#`/`F#`.
const MID_LINE_HEADING = /([^\n#])(#{1,6})([ \t])/g;
/**
* Heal a single line that carries an ATX heading marker mid-line. A plan/doc
* body whose paragraph and heading newlines were collapsed to a single line
* (a producer bug) renders as one giant heading with `##` markers
* showing inline (e.g. "…plan pack)## The goal, restatedYou picked…"). Insert a
* hard paragraph break before each such marker so the body self-heals into
* headings + paragraphs.
*
* A lone `#` fused to an alphanumeric on its left (`C#`, `F#`, `A#`) is left
* alone — that is an inline token, not a heading. Markers with 2–6 hashes are
* always healed: `word## ` never occurs as legitimate inline prose.
*/
function healMidLineHeadings(line: string): string {
return line.replace(MID_LINE_HEADING, (match, before: string, hashes: string, space: string) => {
if (hashes.length === 1 && /[A-Za-z0-9]/.test(before)) return match;
return `${before}\n\n${hashes}${space}`;
});
}
/**
* Defense-in-depth for the Plan pane: insert a paragraph break before
* ATX heading markers that appear mid-line, so a malformed single-line markdown
* body still renders as headings/paragraphs. Fenced code blocks are skipped so
* inline `## ` comments inside code (YAML, shell, diffs) are never rewritten.
*/
export function normalizeMidLineHeadings(value: string): string {
if (!value.includes("#")) return value;
const lines = value.split("\n");
let inFence = false;
let fenceChar = "";
let fenceLength = 0;
const out: string[] = [];
for (const line of lines) {
const fenceMatch = FENCE_LINE.exec(line);
if (fenceMatch) {
const marker = fenceMatch[2]!;
if (!inFence) {
inFence = true;
fenceChar = marker[0]!;
fenceLength = marker.length;
} else if (marker[0] === fenceChar && marker.length >= fenceLength) {
inFence = false;
fenceChar = "";
fenceLength = 0;
}
out.push(line);
continue;
}
out.push(inFence ? line : healMidLineHeadings(line));
}
return out.join("\n");
}
export function normalizeMarkdownBody(value: string): string {
return normalizeMidLineHeadings(normalizeEscapedLineBreaks(value));
}
export const multilineTextSchema = z.string().transform(normalizeEscapedLineBreaks);