manuscript: Baum's own words shown in green

This commit is contained in:
gator 2026-09-22 08:34:01 +00:00
parent aa8597bedd
commit cb0f3a6b1f

View file

@ -24,6 +24,10 @@
.para.sel{border-color:var(--accent);background:#10141d}
.para.changed{border-left:3px solid #3fae74}
.para .pn{position:absolute;left:-30px;top:14px;font:11px var(--mono,monospace);color:var(--muted)}
.orig{color:#5fd38d}
.new{color:var(--ink)}
.legend{font-size:12px;color:var(--muted);margin:0 0 8px}
.legend .orig{font-weight:600}
mark{border-radius:4px;padding:0 2px;cursor:pointer;color:inherit}
mark.open{background:#5a4a12}
mark.kept{background:#2a2f3a}
@ -189,14 +193,55 @@
POLL = setTimeout(function () { openChapter(CH.n, PAGE); }, 4000);
}
// Which words of the paragraph are Baum's. Longest common subsequence on
// words (punctuation ignored for the match, kept in the output).
function tok(t) { return String(t || "").split(/(\s+)/).filter(function (x) { return x.length; }); }
function key(w) { return w.toLowerCase().replace(/[^a-z0-9']/g, ""); }
function originalMask(p) {
if (p.text === p.original) return null;
var a = tok(p.original).filter(function (x) { return !/^\s+$/.test(x); }), b = tok(p.text);
var bw = b.map(function (x, i) { return /^\s+$/.test(x) ? null : i; }).filter(function (x) { return x !== null; });
var A = a.map(key), B = bw.map(function (i) { return key(b[i]); });
var n = A.length, m = B.length, L = [];
for (var i = 0; i <= n; i++) { L.push(new Uint16Array(m + 1)); }
for (i = n - 1; i >= 0; i--) for (var j = m - 1; j >= 0; j--) L[i][j] = A[i] === B[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
var mask = b.map(function () { return false; });
i = 0; j = 0;
while (i < n && j < m) {
if (A[i] === B[j]) { mask[bw[j]] = true; i++; j++; }
else if (L[i + 1][j] >= L[i][j + 1]) i++; else j++;
}
for (var k = 0; k < b.length; k++) if (/^\s+$/.test(b[k])) mask[k] = (k > 0 && mask[k - 1]) && (k + 1 < b.length && mask[k + 1]);
return { parts: b, mask: mask };
}
function greened(p) {
var om = originalMask(p);
if (!om) return '<span class="orig">' + esc(p.text) + '</span>';
var out = "", cur = null, buf = "";
om.parts.forEach(function (w, i) {
var cls = om.mask[i] ? "orig" : "new";
if (cls !== cur) { if (cur !== null) out += '<span class="' + cur + '">' + esc(buf) + '</span>'; cur = cls; buf = ""; }
buf += w;
});
if (cur !== null) out += '<span class="' + cur + '">' + esc(buf) + '</span>';
return out;
}
function marked(p) {
var text = p.text, html = esc(text), refs = REFS.filter(function (r) { return r.para === p.i; });
var text = p.text, html = greened(p), refs = REFS.filter(function (r) { return r.para === p.i; });
refs.forEach(function (r) {
var q = esc(r.quote), at = html.indexOf(q);
if (at >= 0) html = html.slice(0, at) + '<mark class="' + esc(r.status) + '" data-ref="' + esc(r.id) + '" title="' + esc(r.why || "") + '">' + q + '</mark>' + html.slice(at + q.length);
html = markIn(html, r.quote, esc(r.status), esc(r.id), esc(r.why || ""));
});
return html;
}
// Wrapping a quote in <mark> inside the green spans: the quote is found in
// the plain text; the coloured html is searched for the same run of words.
function markIn(html, q, cls, id, title) {
var plain = q.split(/\s+/).map(function (w) { return esc(w).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }).join('(?:\\s|<\\/?span[^>]*>)+');
var rx = new RegExp(plain);
var m = rx.exec(html);
if (!m) return html;
return html.slice(0, m.index) + '<mark class="' + cls + '" data-ref="' + id + '" title="' + title + '">' + m[0] + '</mark>' + html.slice(m.index + m[0].length);
}
function paintChapter() {
$("chtitle").textContent = (CH.n ? "Chapter " + CH.n + " — " : "") + CH.title;
@ -218,7 +263,7 @@
$("pagebar").innerHTML = bar.replace('left in the chapter</span>', 'left in the chapter' + printSpan + '</span>');
var platesHere = PLATES.filter(function (x) { return pp.length && x.pg >= Math.min.apply(null, pp) && x.pg <= Math.max.apply(null, pp); });
var sceneOf = function (i) { var sc = SCENES.find(function (x) { return i >= x.from && i <= x.to; }); return sc ? '<span class="tiny" style="display:block;color:#f5c04b">Scene ' + sc.n + (sc.title ? ' &middot; ' + esc(sc.title) : '') + '</span>' : ''; };
$("paras").innerHTML = (platesHere.length ? '<div style="display:flex;gap:10px;flex-wrap:wrap;margin:0 0 12px">' + platesHere.map(function (x) { return '<a href="' + esc(x.url) + '" target="_blank" title="' + esc(x.alt) + ' (Denslow, 1900, p.' + x.pg + ')"><img src="' + esc(x.url) + '" style="height:150px;border-radius:6px;background:#fff" alt="' + esc(x.alt) + '"></a>'; }).join("") + '</div>' : '') + here.map(function (p) {
$("paras").innerHTML = '<p class="legend"><span class="orig">Green is Baum\'s own text, word for word.</span> White is what has been written for this adaptation.</p>' + (platesHere.length ? '<div style="display:flex;gap:10px;flex-wrap:wrap;margin:0 0 12px">' + platesHere.map(function (x) { return '<a href="' + esc(x.url) + '" target="_blank" title="' + esc(x.alt) + ' (Denslow, 1900, p.' + x.pg + ')"><img src="' + esc(x.url) + '" style="height:150px;border-radius:6px;background:#fff" alt="' + esc(x.alt) + '"></a>'; }).join("") + '</div>' : '') + here.map(function (p) {
var changed = p.text !== p.original;
return (SCENES.some(function (x) { return x.from === p.i; }) ? sceneOf(p.i) : '') + '<div class="para' + (changed ? ' changed' : '') + (SEL === p.i ? ' sel' : '') + '" data-para="' + p.i + '" title="' + (p.print_page ? 'first edition p. ' + p.print_page : '') + '"><span class="pn">' + (p.i + 1) + (p.print_page ? '<br>p' + p.print_page : '') + '</span>' + marked(p)
+ (changed ? '<div class="was"><b>As Baum wrote it:</b> ' + esc(p.original) + '<br><span class="tiny">changed ' + esc(when(p.changed_at)) + ' by ' + esc(p.changed_by || "") + ' &middot; ' + esc(p.how || "") + ' &middot; revision ' + p.rev + '</span></div>' : '')