/** * models/engine/registry.ts — the encoding registry and conversion directions. * * An encoding NAME is not proof of a distinct encoding: "Sarala" appears in two * unrelated font families (Akruti and ORTT), and Unicode font families such as * Noto Sans Oriya or Kalinga are NOT legacy encodings at all — they all render * the same Unicode text. Only the entries below exist, and a direction is only * offered when it has an implementation AND that implementation passed its * start-up self-test AND the administrator has not switched it off. */ import { LEGACY_TABLES } from "./tables"; import { EXPECTED_SHA256, fixtureInputs } from "./fixtures"; import { legacyToUnicode, lineSafe } from "./legacyToUnicode"; import { createHash } from "node:crypto"; export const ENGINE_VERSION = "1.0.0+upstream.76664f0"; export type EncodingId = | "unicode" | "akruti_sarala" | "sreelipi" | "apranta" | "ortt_sarala" | "sambad" | "gist" | "lipikar"; export type Encoding = { id: EncodingId; name: string; kind: "unicode" | "legacy"; note: string; }; export const ENCODINGS: Encoding[] = [ { id: "unicode", name: "Unicode Odia", kind: "unicode", note: "The international standard (U+0B00–U+0B7F). Works in Word, Google Docs, websites, email and phones with any Unicode Odia font such as Noto Sans Oriya or Kalinga." }, { id: "akruti_sarala", name: "Akruti Sarala", kind: "legacy", note: "Akruti family legacy encoding (AkrutiOriSarala and related fonts), common in Odisha DTP and printing." }, { id: "sreelipi", name: "Sreelipi", kind: "legacy", note: "Sreelipi (spelled “Shreelipi” upstream) legacy Odia font encoding, common in publishing and newspapers." }, { id: "apranta", name: "Apranta", kind: "legacy", note: "Apranta (spelled “Aprant” upstream) legacy Odia font encoding." }, { id: "ortt_sarala", name: "ORTT Sarala", kind: "legacy", note: "ORTT Sarala legacy encoding. Despite the shared word “Sarala”, its layout differs from Akruti Sarala." }, { id: "sambad", name: "Sambad", kind: "legacy", note: "Not yet available. Upstream once had a Sambad EML converter but removed it in July 2016; no verified mapping is in use." }, { id: "gist", name: "GIST", kind: "legacy", note: "Not yet available. No verified mapping has been sourced." }, { id: "lipikar", name: "Lipikar", kind: "legacy", note: "Not yet available. The exact encoding and version still need to be identified." }, ]; export type DirectionStatus = "active" | "beta" | "in_development" | "planned"; export type Direction = { id: string; source: EncodingId; target: EncodingId; /** What the code can do; whether it is OFFERED also depends on the self-test and the admin switch. */ implemented: boolean; status: DirectionStatus; statusNote: string; }; const LEGACY_IMPLEMENTED: EncodingId[] = ["akruti_sarala", "sreelipi", "apranta", "ortt_sarala"]; function buildDirections(): Direction[] { const out: Direction[] = []; for (const e of ENCODINGS) { if (e.kind !== "legacy") continue; const has = LEGACY_IMPLEMENTED.includes(e.id); out.push({ id: `${e.id}->unicode`, source: e.id, target: "unicode", implemented: has, status: has ? "beta" : "planned", statusNote: has ? "Port of the OdiaWikimedia converter, byte-identical to upstream on the self-test. Accuracy against independently proof-read reference text is still being verified." : "No verified mapping yet.", }); out.push({ id: `unicode->${e.id}`, source: "unicode", target: e.id, implemented: false, status: has ? "in_development" : "planned", statusNote: has ? "In development. Upstream provides no Unicode-to-legacy rules, and reversing the table is not safe (it is neither one-to-one nor context-free). Needs reference text typed in this font." : "No verified mapping yet.", }); } return out; } export const DIRECTIONS: Direction[] = buildDirections(); // --- Start-up self-test ----------------------------------------------------- /** * SHA-256 of each table's data (JSON of pairs, rules, chunkSize) as extracted * from upstream commit 76664f0. If a single mapping changes — by an edit or by * damage in transit — the hash no longer matches and the direction is withdrawn. */ const TABLE_SHA256: Record = { akruti_sarala: "2a4c2e76def466f445f1550b2804faf062ee6f920eaa9b0130e9d1cc6c43cd36", apranta: "8fdadee7232bdc28d5521d5083a0fae52a0c5537002ba2d7cb64aa6b85b261fb", ortt_sarala: "88c7a5c149de33f3736d8127f86927259f9d8ca13686e5bea24fdc99a54c5fa7", sreelipi: "bf8259a8a0a8ab9981911d14c3187415c90da0eeda224cb6f251fb16a7a46d3f", }; function tableHash(id: string): string { const t = LEGACY_TABLES[id]; return createHash("sha256").update(JSON.stringify({ pairs: t.pairs, rules: t.rules, chunkSize: t.chunkSize })).digest("hex"); } export type SelfTestResult = { encoding: string; cases: number; passed: number; lineSafe: boolean; integrity: boolean; ms: number; ok: boolean }; let selfTest: SelfTestResult[] | null = null; export function runSelfTest(): SelfTestResult[] { const results: SelfTestResult[] = []; for (const id of LEGACY_IMPLEMENTED) { const table = LEGACY_TABLES[id]; const t0 = performance.now(); const inputs = fixtureInputs(table); let outputs: string[] = []; let crashed = false; try { outputs = inputs.map((s) => legacyToUnicode(table, s)); } catch { crashed = true; } const matches = !crashed && createHash("sha256").update(JSON.stringify(outputs)).digest("hex") === EXPECTED_SHA256[id]; const safe = lineSafe(table); const integrity = tableHash(id) === TABLE_SHA256[id]; results.push({ encoding: id, cases: inputs.length, passed: matches ? inputs.length : 0, lineSafe: safe, integrity, ms: Math.round(performance.now() - t0), ok: integrity && safe && matches }); } selfTest = results; return results; } export function selfTestResults(): SelfTestResult[] { return selfTest ?? runSelfTest(); } export function selfTestPassed(encoding: EncodingId): boolean { return selfTestResults().some((r) => r.encoding === encoding && r.ok); } export function encodingName(id: string): string { return ENCODINGS.find((e) => e.id === id)?.name ?? id; } export function findDirection(source: string, target: string): Direction | undefined { return DIRECTIONS.find((d) => d.source === source && d.target === target); }