quranic-universal-aligner / src /ui /static /ref-preview.js
hetchyy's picture
deploy
33b7a36 verified
Raw
History Blame Contribute Delete
10.3 kB
// ref-preview.js — instant client-side ref→Arabic-text preview.
//
// Loads the Digital Khatt word bundle ({dk_words, verse_word_counts}) once per
// session and resolves a typed reference to display HTML with zero server
// round-trips, so the segment text updates live as the user types a ref.
//
// Pure helpers mirror the inspector
// (inspector/.../tabs/segments/utils/data/references.ts) and the server
// normalizer (pipeline.py::_normalize_ref) so the live preview matches the
// authoritative HTML the server returns on commit. Verse markers use the same
// glyph as segments.py::format_verse_marker ('۝' + Arabic-Indic numeral).
(function () {
'use strict';
var ARABIC_DIGITS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
function toArabicNumeral(n) {
return String(n).split('').map(function (d) {
var i = parseInt(d, 10);
return isNaN(i) ? d : ARABIC_DIGITS[i];
}).join('');
}
// Normalize a user-typed ref to canonical "s:a:w-s:a:w" form, or null if
// invalid. Mirrors pipeline.py::_normalize_ref (minus special codes, which
// aren't in dk_words and yield no client preview). `vwc` is the flat
// verse_word_counts map ("s:a" -> count).
function normalizeRef(raw, vwc) {
if (raw == null) return null;
raw = String(raw).trim();
if (!raw) return null;
vwc = vwc || {};
function vwCount(surah, ayah) {
var n = vwc[surah + ':' + ayah];
return (typeof n === 'number') ? n : 0;
}
function num(parts) {
var out = [];
for (var i = 0; i < parts.length; i++) {
var v = parseInt(parts[i], 10);
if (isNaN(v) || String(v) !== String(parts[i]).trim()) return null;
out.push(v);
}
return out;
}
if (raw.indexOf('-') !== -1) {
var halves = raw.split('-');
if (halves.length !== 2) return null;
var sp = halves[0].split(':');
var ep = halves[1].split(':');
// Verse range: "76:1-76:2" -> "76:1:1-76:2:N"
if (sp.length === 2 && ep.length === 2) {
var vr = num(sp.concat(ep));
if (!vr) return null;
var n = vwCount(vr[2], vr[3]);
if (n === 0) return null;
return vr[0] + ':' + vr[1] + ':1-' + vr[2] + ':' + vr[3] + ':' + n;
}
if (sp.length < 3) return null;
// Short forms: "2:255:1-6" / "2:255:1-255:6"
if (ep.length === 1) ep = [sp[0], sp[1], ep[0]];
else if (ep.length === 2) ep = [sp[0], ep[0], ep[1]];
var s = num(sp.slice(0, 3));
var e = num(ep.slice(0, 3));
if (!s || !e) return null;
return s[0] + ':' + s[1] + ':' + s[2] + '-' + e[0] + ':' + e[1] + ':' + e[2];
}
var parts = raw.split(':');
// Whole verse: "76:5" -> "76:5:1-76:5:N"
if (parts.length === 2) {
var wv = num(parts);
if (!wv) return null;
var nw = vwCount(wv[0], wv[1]);
if (nw === 0) return null;
return wv[0] + ':' + wv[1] + ':1-' + wv[0] + ':' + wv[1] + ':' + nw;
}
// Single word: "2:255:5" -> "2:255:5-2:255:5"
if (parts.length < 3) return null;
var sw = num(parts.slice(0, 3));
if (!sw) return null;
var r = sw[0] + ':' + sw[1] + ':' + sw[2];
return r + '-' + r;
}
// Parse a canonical "s:a:w-s:a:w" ref into endpoints, or null.
function parseSegRef(ref) {
if (!ref) return null;
var halves = ref.split('-');
if (halves.length !== 2 || !halves[0] || !halves[1]) return null;
var s = halves[0].split(':'), e = halves[1].split(':');
if (s.length < 3 || e.length < 3) return null;
var p = {
surah: +s[0], ayah_from: +s[1], word_from: +s[2],
ayah_to: +e[1], word_to: +e[2]
};
for (var k in p) {
if (!isFinite(p[k])) return null;
}
return p;
}
// Walk dk_words from the start endpoint through the end endpoint
// (inclusive). Returns space-joined display text, or '' for malformed input.
// Mirrors references.ts::dkTextForRef.
function dkTextForRef(ref, dkWords, vwc) {
if (!ref || !dkWords) return '';
var halves = ref.split('-');
if (halves.length !== 2 || !halves[0] || !halves[1]) return '';
var sP = halves[0].split(':'), eP = halves[1].split(':');
if (sP.length !== 3 || eP.length !== 3) return '';
var sSu = +sP[0], sAy = +sP[1], sW = +sP[2];
var eSu = +eP[0], eAy = +eP[1], eW = +eP[2];
if (![sSu, sAy, sW, eSu, eAy, eW].every(isFinite)) return '';
var maxAyah = (typeof window !== 'undefined' && window.QL_REF_MAX_AYAH) || 300;
var su = sSu;
var words = [];
var ay = sAy, w = sW;
function lte() {
return su < eSu || (su === eSu && (ay < eAy || (ay === eAy && w <= eW)));
}
while (lte()) {
var t = dkWords[su + ':' + ay + ':' + w];
if (t) words.push(t);
w++;
var max = vwc ? (vwc[su + ':' + ay] || 0) : 0;
if (w > max) {
w = 1;
ay++;
if (ay > maxAyah) break;
}
}
return words.join(' ');
}
// Insert verse-end markers at verse boundaries. Mirrors
// references.ts::_addVerseMarkers + segments.py::format_verse_marker.
function addVerseMarkers(text, ref, vwc) {
if (!text || !ref) return text || '';
var p = parseSegRef(normalizeRef(ref, vwc));
if (!p || !vwc) return text;
var words = text.split(/\s+/).filter(Boolean);
var out = [];
var ay = p.ayah_from, w = p.word_from;
for (var i = 0; i < words.length; i++) {
var word = words[i];
if (word == null) continue;
out.push(word);
if (!/[؀-ٯ]/.test(word)) continue;
var total = vwc[p.surah + ':' + ay] || 0;
if (total > 0 && w >= total) {
out.push('۝' + toArabicNumeral(ay));
ay++;
w = 1;
} else {
w++;
}
}
return out.join(' ');
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Build the inner HTML for a segment's .segment-text from a typed ref.
// Words are wrapped in <span class="word"> (no data-pos — added
// authoritatively by the server patch on commit); verse markers are bare
// tokens, matching segments.py::get_text_with_markers. Returns '' for
// empty / invalid / special refs (specials aren't in dk_words).
function previewHtml(rawRef) {
var refs = getRefs();
if (!refs) return '';
var norm = normalizeRef(rawRef, refs.verse_word_counts);
if (!norm) return '';
var text = dkTextForRef(norm, refs.dk_words, refs.verse_word_counts);
if (!text) return '';
var marked = addVerseMarkers(text, norm, refs.verse_word_counts);
return marked.split(/\s+/).filter(Boolean).map(function (tok) {
if (tok.charAt(0) === '۝') return tok; // bare verse marker
return '<span class="word">' + escapeHtml(tok) + '</span>';
}).join(' ');
}
// ---- Bundle loader (browser only) ---------------------------------------
var _refs = null; // in-memory cache
var _inflight = null; // shared load promise
function getRefs() {
return _refs;
}
function _sessionKey(version) {
return 'ql-refs:' + version;
}
function loadRefs() {
if (_refs) return Promise.resolve(_refs);
if (_inflight) return _inflight;
if (typeof window === 'undefined' || typeof fetch === 'undefined') {
return Promise.resolve(null);
}
var url = window.QL_REFS_URL;
var version = window.QL_REFS_VERSION || '';
if (!url) return Promise.resolve(null);
// sessionStorage cache (evict stale-hash entries)
try {
var cached = window.sessionStorage.getItem(_sessionKey(version));
if (cached) {
_refs = JSON.parse(cached);
return Promise.resolve(_refs);
}
for (var i = window.sessionStorage.length - 1; i >= 0; i--) {
var k = window.sessionStorage.key(i);
if (k && k.indexOf('ql-refs:') === 0) window.sessionStorage.removeItem(k);
}
} catch (e) { /* sessionStorage unavailable — fall through to fetch */ }
_inflight = fetch(url + (version ? '?v=' + encodeURIComponent(version) : ''))
.then(function (r) { return r.json(); })
.then(function (data) {
_refs = data;
try {
window.sessionStorage.setItem(_sessionKey(version), JSON.stringify(data));
} catch (e) { /* quota — in-memory cache still serves the session */ }
return _refs;
})
.catch(function (err) {
console.warn('[ref-preview] bundle load failed', err);
_inflight = null;
return null;
});
return _inflight;
}
var QLRefPreview = {
toArabicNumeral: toArabicNumeral,
normalizeRef: normalizeRef,
parseSegRef: parseSegRef,
dkTextForRef: dkTextForRef,
addVerseMarkers: addVerseMarkers,
previewHtml: previewHtml,
loadRefs: loadRefs,
getRefs: getRefs
};
if (typeof window !== 'undefined') {
window.QLRefPreview = QLRefPreview;
// Warm the cache so the bundle is ready before the first ref edit.
if (typeof window.addEventListener === 'function') {
loadRefs();
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = QLRefPreview; // node — parity/unit tests
}
})();