/* * figures: sax * * SAX event stepper on features/parsing/sax_interface.md. The page contains the final state as static HTML (input, * all nine events, the finished tree); this script adds the controls and redraws the three lanes for each step: * the consumed part of the input, the events called so far, and the state of one of two handlers (a DOM builder * that keeps a stack of open containers, or a handler that keeps only the value at /b). "return false at this * event" stops the parse at the current event: the later events are not called and the rest of the input is not read. */ (function () { "use strict"; var SRC = '{"a":[1,true],"b":"x"}'; // tok: [start, end) of the token that makes the parser call the event var EVENTS = [ { call: "start_object(-1)", cls: "os", tok: [0, 1], op: "so" }, { call: 'key("a")', cls: "k", tok: [1, 4], op: "key", v: "a" }, { call: "start_array(-1)", cls: "as", tok: [5, 6], op: "sa" }, { call: "number_unsigned(1)", cls: "v", tok: [6, 7], op: "val", v: { t: "n", v: "1" } }, { call: "boolean(true)", cls: "v", tok: [8, 12], op: "val", v: { t: "b", v: "true" } }, { call: "end_array()", cls: "ae", tok: [12, 13], op: "end" }, { call: 'key("b")', cls: "k", tok: [14, 17], op: "key", v: "b" }, { call: 'string("x")', cls: "v", tok: [18, 21], op: "val", v: { t: "s", v: '"x"' } }, { call: "end_object()", cls: "oe", tok: [21, 22], op: "end" } ]; var N = EVENTS.length; function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } // ---- handler A: DOM builder (like json_sax_dom_parser) ---- function domState(steps) { var root = null, stack = [], last = null; function place(v) { if (!root) { root = v; return; } var top = stack[stack.length - 1]; if (top.t === "a") { top.e.push(v); } else { top.m[top.m.length - 1][1] = v; } } for (var i = 0; i < steps; ++i) { var ev = EVENTS[i], v; if (ev.op === "so" || ev.op === "sa") { v = ev.op === "so" ? { t: "o", m: [] } : { t: "a", e: [] }; var top = stack[stack.length - 1]; v.ptr = !top ? "" : top.ptr + "/" + (top.t === "a" ? top.e.length : top.m[top.m.length - 1][0]); place(v); stack.push(v); last = v; } else if (ev.op === "key") { v = { t: "z", v: "null" }; // the DOM parser adds null at the key and fills it in later stack[stack.length - 1].m.push([ev.v, v]); last = v; } else if (ev.op === "val") { v = { t: ev.v.t, v: ev.v.v }; place(v); last = v; } else { last = stack.pop(); } } return { root: root, stack: stack, last: last }; } function pretty(v, ind, last, out, prefix, suffix) { var pad = new Array(ind + 1).join(" "); var mark = function (s) { return v === last ? '' + s + "" : s; }; if (v.t === "o" || v.t === "a") { var items = v.t === "o" ? v.m : v.e, open = v.t === "o" ? "{" : "[", close = v.t === "o" ? "}" : "]"; if (!items.length) { out.push(pad + prefix + mark(open + close) + suffix); return; } out.push(pad + prefix + mark(open)); for (var i = 0; i < items.length; ++i) { var comma = i < items.length - 1 ? "," : ""; if (v.t === "o") { pretty(items[i][1], ind + 1, last, out, esc('"' + items[i][0] + '": '), comma); } else { pretty(items[i], ind + 1, last, out, "", comma); } } out.push(pad + mark(close) + suffix); } else { out.push(pad + prefix + mark(esc(v.v)) + suffix); } } function renderDom(steps, stopped) { var st = domState(steps), lines = []; var tree = st.root ? (pretty(st.root, 0, st.last, lines, "", ""), lines.join("\n")) : '(nothing built yet)'; var chips = st.stack.length ? st.stack.map(function (c) { return '
' + (c.t === "o" ? "{}" : "[]") + " " + esc(c.ptr === "" ? "root" : c.ptr) + "' + tree + "" +
'stack of open containers
' + note + "
"; } // ---- handler B: keep only the value at /b ---- function renderKeep(steps, stopped) { var frames = [], ptr = "", kept = null, keptAt = 0, action = ""; var where = function () { return frames.map(function (f) { return "/" + (f.t === "a" ? f.i : f.k); }).join(""); }; for (var i = 0; i < steps; ++i) { var ev = EVENTS[i], top = frames[frames.length - 1]; if (ev.op === "key") { top.k = ev.v; ptr = where(); action = "remembers the key"; continue; } if (ev.op === "end") { ptr = frames.pop().loc; action = "leaves the container"; continue; } ptr = where(); action = ptr === "/b" ? "keeps it" : "ignores it"; if (top && top.t === "a") { top.i++; } if (ev.op === "so") { frames.push({ t: "o", k: "", loc: ptr }); } else if (ev.op === "sa") { frames.push({ t: "a", i: 0, loc: ptr }); } else if (ptr === "/b") { kept = ev.v.v; keptAt = i + 1; } } var note; if (steps === 0) { note = "The handler keeps only the current position and, once it is found, one value."; } else if (stopped) { note = kept !== null ? "The value is found, so stopping here saves reading the rest of the input." : "Stopped before /b was read: nothing is kept."; } else { note = "At " + (ptr === "" ? "the root" : ptr) + " the handler " + action + "."; } if (!stopped && steps === keptAt && kept !== null) { note += " A handler that needs nothing else could return false here."; } return '" + (steps ? esc(ptr === "" ? "(root)" : ptr) : "-") + "' + esc(kept) + "" : 'none') + "' + note + " No tree is built.
"; } function init(root) { if (root.getAttribute("data-nj-sax-ready")) { return; } root.setAttribute("data-nj-sax-ready", "1"); var src = root.querySelector(".nj-sax__src"); var items = root.querySelectorAll(".nj-sax__events li"); var head = root.querySelector(".nj-sax__lane--state .nj-sax__h"); var state = root.querySelector(".nj-sax__state"); var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; var step = 0, mode = "dom", stopAt = 0, timer = null; // controls var bar = document.createElement("div"); bar.className = "nj-sax__controls"; bar.innerHTML = 'parse does" : "handler: keep only the value at /b";
state.innerHTML = mode === "dom" ? renderDom(step, stopped()) : renderKeep(step, stopped());
bar.querySelectorAll("[data-m]").forEach(function (b) { b.setAttribute("aria-pressed", String(b.getAttribute("data-m") === mode)); });
btn("prev").disabled = step === 0;
btn("reset").disabled = step === 0;
btn("next").disabled = step === N || stopped();
stopBox.disabled = step === 0;
stopBox.checked = stopped();
var playing = timer !== null;
btn("play").textContent = playing ? "Pause" : "Play";
btn("play").setAttribute("aria-label", playing ? "Pause" : "Play all events");
var msg;
if (step === 0) { msg = "Before the first event: nothing is read yet."; }
else if (stopped()) { msg = "Event " + step + " of " + N + ": " + cur.call + " returned false. Parsing stops, sax_parse returns false, and the rest of the input is not read."; }
else if (step === N) { msg = "Event " + N + " of " + N + ": " + cur.call + ". The input is read completely and sax_parse returns true."; }
else { msg = "Event " + step + " of " + N + ": the parser read " + SRC.slice(cur.tok[0], cur.tok[1]) + " and called " + cur.call + "."; }
status.textContent = msg;
}
function stop() { if (timer !== null) { clearInterval(timer); timer = null; } }
function go(s) {
step = Math.max(0, Math.min(N, s));
if (stopAt && step < stopAt) { stopAt = 0; }
render();
}
bar.addEventListener("click", function (e) {
var b = e.target.closest("button");
if (!b) { return; }
var m = b.getAttribute("data-m");
if (m) { mode = m; render(); return; }
var a = b.getAttribute("data-a");
if (a === "play") {
if (timer !== null) { stop(); render(); return; }
if (step === N || stopped()) { stopAt = 0; step = 0; }
timer = setInterval(function () {
if (step >= N || stopped()) { stop(); render(); return; }
go(step + 1);
if (step >= N) { stop(); render(); }
}, reduce ? 1400 : 900);
go(step + 1);
return;
}
stop();
if (a === "reset") { stopAt = 0; go(0); }
else if (a === "prev") { go(step - 1); }
else if (a === "next") { go(step + 1); }
});
stopBox.addEventListener("change", function () {
stop();
stopAt = stopBox.checked ? step : 0;
render();
});
root.classList.add("nj-sax--live");
render();
}
function setup() {
document.querySelectorAll("[data-nj-sax]").forEach(init);
}
if (typeof window.document$ !== "undefined" && window.document$ && typeof window.document$.subscribe === "function") {
window.document$.subscribe(setup);
} else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", setup);
} else {
setup();
}
})();