/** * models/engine/convert.ts — the engine's public API. Pure functions: no SQL, * no storage, no logging of text. The text is converted in memory and returned. */ import { LEGACY_TABLES } from "./tables"; import { legacyToUnicode, unmappedChars } from "./legacyToUnicode"; import { DIRECTIONS, ENCODINGS, encodingName, selfTestPassed, type Direction, type EncodingId } from "./registry"; export type ConvertResult = | { ok: true; output: string; warnings: string[]; via?: string } | { ok: false; error: string }; /** Unicode-normalise Odia text to NFC. Applied to Unicode OUTPUT (on by default); legacy INPUT is never normalised, because its code points are font glyph slots, not characters. */ export function normalizeOdia(input: string): string { return input.normalize("NFC"); } export function getAvailableEncodings() { return ENCODINGS; } /** Directions that have code and passed the self-test. The admin switch is applied by the caller. */ export function getSupportedDirections(): Direction[] { return DIRECTIONS.filter((d) => d.implemented && selfTestPassed(d.source === "unicode" ? d.target : d.source)); } function isOdiaUnicode(cp: number) { return cp >= 0x0b00 && cp <= 0x0b7f; } export function getConversionWarnings(input: string, output: string, source: EncodingId): string[] { const warnings: string[] = []; if (source !== "unicode") { const table = LEGACY_TABLES[source]; let odiaIn = 0; for (const ch of input) if (isOdiaUnicode(ch.codePointAt(0) ?? 0)) odiaIn++; if (odiaIn > 0) warnings.push(`Your input already contains ${odiaIn} Unicode Odia character${odiaIn === 1 ? "" : "s"}. If the text is already Unicode it does not need converting; those characters were left as they were.`); if (table) { const missing = unmappedChars(table, input); if (missing.length) { warnings.push(`${missing.length} character${missing.length === 1 ? " has" : "s have"} no ${encodingName(source)} mapping and ${missing.length === 1 ? "was" : "were"} left unchanged: ${missing.map((c) => `“${c}” (U+${(c.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")})`).join(", ")}. Please proof-read around them.`); } } } return warnings; } /** Convert text between two encodings. Whitespace and line breaks are preserved; nothing is trimmed. */ export function convertText( input: string, source: EncodingId, target: EncodingId, options: { enabled: (directionId: string) => boolean; normalize?: boolean }, ): ConvertResult { if (source === target) return { ok: false, error: "Choose two different encodings." }; const supported = getSupportedDirections().filter((d) => options.enabled(d.id)); const direct = supported.find((d) => d.source === source && d.target === target); try { if (direct && direct.target === "unicode") { const table = LEGACY_TABLES[direct.source]; const raw = legacyToUnicode(table, input); const output = options.normalize === false ? raw : normalizeOdia(raw); return { ok: true, output, warnings: getConversionWarnings(input, output, source) }; } // Unicode → legacy and legacy → legacy (which goes through Unicode, and needs // BOTH halves verified) have no verified implementation yet. return { ok: false, error: `${encodingName(source)} → ${encodingName(target)} is not available yet.` }; } catch (err) { return { ok: false, error: err instanceof Error && err.message === "conversion-loop-limit" ? "This text could not be converted safely. Try a smaller part of it." : "The conversion failed." }; } } /** Light structural check: the conversion never adds or removes line breaks. */ export function validateConversion(input: string, output: string): { linesPreserved: boolean } { return { linesPreserved: input.split("\n").length === output.split("\n").length }; }