/*
Unit tests for text normalize covering its documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the text suite's contract for test-file scaffold coverage.
*/
import { test, expect } from "vitest";
import {
DEFAULT_NORMALIZE_CONFIG,
SIGNAL_IDS,
SIGNAL_REGISTRY,
resolveNormalizeConfig,
normalizeText,
normalizeHtml,
normalizeJson,
normalizeXml,
normalizeMarkdown,
detectResidual,
createDevNormalizeMiddleware,
type NormalizeConfig,
} from "../text-normalize.ts";
// All special/invisible inputs use \u escapes so the test is canonical and the
// module's matchers must hit the real codepoints. Expected outputs are ASCII.
const ALL_ON = DEFAULT_NORMALIZE_CONFIG;
function only(id: (typeof SIGNAL_IDS)[number]): NormalizeConfig {
const signals = {} as NormalizeConfig["signals"];
for (const s of SIGNAL_IDS) signals[s] = s === id;
return { enabled: true, signals };
}
// --- char signals -----------------------------------------------------------
test("dashes: every special dash becomes a hyphen, spaces preserved", () => {
const input = "a—b — c–d‒e―f‑g−h";
expect(normalizeText(input, ALL_ON)).toBe("a-b - c-d-e-f-g-h");
});
test("quotes: curly + guillemets become straight", () => {
const input = "“hi” «yo» it’s ‘a’ „de“";
expect(normalizeText(input, ALL_ON)).toBe(`"hi" "yo" it's 'a' "de"`);
});
test("quotes: U+02BC modifier letter apostrophe becomes straight (RFC-1070)", () => {
const input = "it\u02BCs";
expect(normalizeText(input, ALL_ON)).toBe("it's");
});
test("ellipsis: single char becomes three dots", () => {
expect(normalizeText("wait…", ALL_ON)).toBe("wait...");
});
test("spaces: nbsp and exotic spaces become a regular space (no collapsing)", () => {
const input = "a b c d e f g";
expect(normalizeText(input, ALL_ON)).toBe("a b c d e f g");
});
test("zero-width cruft is removed", () => {
const input = "abcdefg";
expect(normalizeText(input, ALL_ON)).toBe("abcdefg");
});
test("ZWJ inside an emoji sequence is preserved; standalone ZWJ is stripped", () => {
const family = "\u{1F469}\u{1F469}\u{1F466}"; // woman+woman+boy
expect(normalizeText(family, ALL_ON)).toBe(family);
expect(normalizeText("ab", ALL_ON)).toBe("ab");
});
// --- html entities -----------------------------------------------------------
test("typographic entities are decoded then normalized; structural entities survive", () => {
const input = "A—B C… & <tag> "q"";
expect(normalizeText(input, ALL_ON)).toBe("A-B C... & <tag> "q"");
});
test("numeric typographic entities decode; structural numeric entities do not", () => {
// — = em dash (typographic) ; & = & (structural)
expect(normalizeText("x—y & z", ALL_ON)).toBe("x-y & z");
});
test("entity lens respects the dash toggle (decode keeps em-dash when dashes off)", () => {
const cfg: NormalizeConfig = { enabled: true, signals: { ...ALL_ON.signals, dashes: false } };
expect(normalizeText("a—b", cfg)).toBe("a—b");
});
// --- per-signal isolation ----------------------------------------------------
test("disabling spaces keeps nbsp while other signals still fire", () => {
const cfg: NormalizeConfig = { enabled: true, signals: { ...ALL_ON.signals, spaces: false } };
expect(normalizeText("a b—c", cfg)).toBe("a b-c");
});
test("master switch off is a no-op", () => {
const off: NormalizeConfig = { enabled: false, signals: { ...ALL_ON.signals } };
const input = "a—b“c”";
expect(normalizeText(input, off)).toBe(input);
});
// --- html --------------------------------------------------------------------
test("html: text nodes normalized, code/script/pre skipped", () => {
const input = "
he said “hi”—ok
a—bx—y
";
expect(normalizeHtml(input, ALL_ON)).toBe(`he said "hi"-ok
a—bx—y
`);
});
test("html: whitelisted attribute normalized and quote re-escaped", () => {
const input = `
`;
expect(normalizeHtml(input, ALL_ON)).toBe(`
`);
});
test("html: non-whitelisted attribute (href) is left untouched", () => {
const input = `t—t`;
expect(normalizeHtml(input, ALL_ON)).toBe(`t-t`);
});
test("html: inline JSON-LD string values normalized and stay valid JSON", () => {
const input = '';
const out = normalizeHtml(input, ALL_ON);
const json = out.replace(/^