(function () {
"use strict";
function otherSide(side) {
return side === "before" ? "after" : "before";
}
function nodePath(el) {
const segments = [];
let current = el;
while (true) {
const parent = current.parentElement;
if (!parent || !parent.classList.contains("node")) break;
const siblings = Array.from(parent.children).filter((c) => c.classList.contains("node"));
const kind = current.dataset.kind || "";
let occurrence = 0;
for (const sibling of siblings) {
if (sibling === current) break;
if (sibling.dataset.kind === kind) occurrence++;
}
segments.push(`${kind}:${occurrence + 1}`);
current = parent;
}
segments.reverse();
return segments.join("/");
}
function nextMatchIndex(texts, startIndex, query) {
if (!query || texts.length === 0) return -1;
const needle = query.toLowerCase();
const from = startIndex < 0 ? 0 : startIndex;
for (let step = 1; step <= texts.length; step++) {
const index = (from + step) % texts.length;
if (texts[index].toLowerCase().includes(needle)) return index;
}
return -1;
}
function pickRendering(names, wanted) {
if (names.length === 0) return -1;
const found = names.indexOf(wanted);
return found === -1 ? 0 : found;
}
if (typeof module !== "undefined") {
module.exports = { otherSide, nodePath, nextMatchIndex, pickRendering };
}
if (typeof document === "undefined") return;
const SIDES = ["before", "after"];
const panels = {
before: document.querySelector('.tree-panels .panel[data-side="before"]'),
after: document.querySelector('.tree-panels .panel[data-side="after"]'),
};
const statusLine = document.getElementById("status-line");
const issueLink = document.getElementById("file-issue");
const toggleIdenticalButton = document.getElementById("toggle-identical");
const helpOverlay = document.getElementById("help-overlay");
const fixtureName = document.body.dataset.fixture || "";
const repo = document.body.dataset.repo || "";
let focusedSide = "before";
const selected = { before: null, after: null };
let counterpartEl = null;
let searchQuery = { before: "", after: "" };
function visibleNodes(side) {
const panel = panels[side];
if (!panel) return [];
return Array.from(panel.querySelectorAll(".node")).filter(
(el) => el.getClientRects().length > 0
);
}
function setStatus(message) {
statusLine.textContent = message || "";
}
function clearCounterpartHighlight() {
if (counterpartEl) {
counterpartEl.classList.remove("counterpart");
counterpartEl = null;
}
}
function select(side, el, opts) {
opts = opts || {};
if (!el) return;
const previous = selected[side];
if (previous) previous.classList.remove("selected");
el.classList.add("selected");
selected[side] = el;
if (opts.setFocus !== false) {
setFocusedSide(side, { select: false });
}
if (opts.scroll !== false) {
el.scrollIntoView({ block: "center" });
}
clearCounterpartHighlight();
const matchId = el.dataset.match;
if (matchId) {
const match = document.getElementById(matchId);
if (match) {
match.classList.add("counterpart");
counterpartEl = match;
if (opts.scrollCounterpart) {
match.scrollIntoView({ block: "center" });
}
}
}
updateIssueLink(side, el);
}
function setFocusedSide(side, opts) {
opts = opts || {};
focusedSide = side;
for (const s of SIDES) {
if (panels[s]) panels[s].dataset.focused = s === side ? "true" : "false";
}
if (opts.select !== false && !selected[side]) {
const nodes = visibleNodes(side);
if (nodes.length > 0) select(side, nodes[0], { setFocus: false, scroll: false });
}
}
function updateIssueLink(side, el) {
const kind = el.dataset.kind || "";
const path = nodePath(el);
let status = "unmarked";
for (const cls of el.classList) {
if (cls.startsWith("status-")) status = cls.slice("status-".length);
}
const title = `human_mapping: ${fixtureName} - ${side} ${kind}`;
const body = [
`Fixture: ${fixtureName}`,
`Side: ${side}`,
`Node kind: ${kind}`,
`Path: ${path || "(root)"}`,
`Current status: ${status}`,
"",
"Describe why you disagree with this mapping:",
].join("\n");
const url =
`https://github.com/${repo}/issues/new` +
`?title=${encodeURIComponent(title)}` +
`&body=${encodeURIComponent(body)}` +
`&labels=${encodeURIComponent("human-mapping")}`;
issueLink.href = url;
issueLink.removeAttribute("aria-disabled");
}
function moveCursor(side, delta) {
const nodes = visibleNodes(side);
if (nodes.length === 0) return;
let idx = nodes.indexOf(selected[side]);
if (idx === -1) idx = 0;
idx = Math.max(0, Math.min(nodes.length - 1, idx + delta));
select(side, nodes[idx]);
}
function jumpEdge(side, which) {
const nodes = visibleNodes(side);
if (nodes.length === 0) return;
select(side, which === "first" ? nodes[0] : nodes[nodes.length - 1]);
}
function collapseOrParent(side) {
const el = selected[side];
if (!el) return;
if (el.tagName === "DETAILS" && el.open) {
el.open = false;
return;
}
const parent = el.parentElement ? el.parentElement.closest(".node") : null;
if (parent) select(side, parent);
}
function expandOrChild(side) {
const el = selected[side];
if (!el) return;
if (el.tagName === "DETAILS") {
if (!el.open) {
el.open = true;
return;
}
const firstChild = el.querySelector(":scope > .node");
if (firstChild) select(side, firstChild);
}
}
function alignOtherPanel() {
const el = selected[focusedSide];
if (!el || !el.dataset.match) {
setStatus("No mapped counterpart for this node (deleted/inserted)");
return;
}
const other = otherSide(focusedSide);
const match = document.getElementById(el.dataset.match);
if (match) select(other, match, { setFocus: false, scrollCounterpart: false });
}
function runSearch(side, query) {
if (!query) return;
searchQuery[side] = query;
const nodes = visibleNodes(side);
if (nodes.length === 0) return;
const index = nextMatchIndex(
nodes.map((node) => node.textContent),
nodes.indexOf(selected[side]),
query
);
if (index === -1) {
setStatus(`No node containing "${query}" found in this panel`);
return;
}
select(side, nodes[index]);
setStatus(`Found "${query}"`);
}
function toggleHelp() {
helpOverlay.classList.toggle("hidden");
}
function reselectIfHidden() {
for (const side of SIDES) {
const el = selected[side];
if (el && el.getClientRects().length === 0) {
const nodes = visibleNodes(side);
if (nodes.length > 0) select(side, nodes[0], { scroll: false });
}
}
}
function setHideIdentical(hidden) {
document.body.classList.toggle("hide-identical", hidden);
toggleIdenticalButton.setAttribute("aria-pressed", hidden ? "true" : "false");
toggleIdenticalButton.textContent = hidden
? "Show identical matches"
: "Hide identical matches";
reselectIfHidden();
setStatus(
hidden ? "Showing only inserted/deleted/updated nodes and their ancestors" : ""
);
}
function toggleHideIdentical() {
setHideIdentical(!document.body.classList.contains("hide-identical"));
}
function promptSearch() {
const prompt = document.getElementById("search-prompt");
const input = document.getElementById("search-input");
prompt.classList.remove("hidden");
input.value = searchQuery[focusedSide] || "";
input.focus();
input.select();
}
function closeSearchPrompt() {
document.getElementById("search-prompt").classList.add("hidden");
}
for (const side of SIDES) {
const panel = panels[side];
if (!panel) continue;
panel.addEventListener("click", (event) => {
const target = event.target.closest(".node.leaf, summary");
if (!target || !panel.contains(target)) return;
const node = target.classList.contains("node") ? target : target.parentElement;
select(side, node);
});
}
toggleIdenticalButton.addEventListener("click", toggleHideIdentical);
document.addEventListener("keydown", (event) => {
const searchPrompt = document.getElementById("search-prompt");
if (!searchPrompt.classList.contains("hidden")) {
if (event.key === "Enter") {
runSearch(focusedSide, document.getElementById("search-input").value.trim());
closeSearchPrompt();
event.preventDefault();
} else if (event.key === "Escape") {
closeSearchPrompt();
event.preventDefault();
}
return;
}
switch (event.key) {
case "j":
case "ArrowDown":
moveCursor(focusedSide, 1);
break;
case "k":
case "ArrowUp":
moveCursor(focusedSide, -1);
break;
case "h":
case "ArrowLeft":
collapseOrParent(focusedSide);
break;
case "l":
case "ArrowRight":
expandOrChild(focusedSide);
break;
case "g":
jumpEdge(focusedSide, "first");
break;
case "G":
jumpEdge(focusedSide, "last");
break;
case "Tab":
setFocusedSide(otherSide(focusedSide));
event.preventDefault();
break;
case "a":
alignOtherPanel();
break;
case "i":
toggleHideIdentical();
break;
case "/":
promptSearch();
event.preventDefault();
break;
case "v":
cycleView();
break;
case "p":
cycleRendering();
break;
case "?":
toggleHelp();
break;
case "Escape":
helpOverlay.classList.add("hidden");
break;
default:
return;
}
});
const VIEWS = ["split", "code", "tree"];
const VIEW_STORAGE_KEY = "codediff-mapping-view";
const viewButtons = Array.from(
document.querySelectorAll(".view-switch button")
);
function setView(view) {
if (VIEWS.indexOf(view) === -1) view = "split";
document.body.dataset.view = view;
viewButtons.forEach((button) => {
button.setAttribute(
"aria-pressed",
button.dataset.view === view ? "true" : "false"
);
});
try {
window.localStorage.setItem(VIEW_STORAGE_KEY, view);
} catch (e) {
}
}
function cycleView() {
const current = VIEWS.indexOf(document.body.dataset.view);
setView(VIEWS[(current + 1) % VIEWS.length]);
}
viewButtons.forEach((button) => {
button.addEventListener("click", () => setView(button.dataset.view));
});
let storedView = "split";
try {
storedView = window.localStorage.getItem(VIEW_STORAGE_KEY) || "split";
} catch (e) {
}
setView(storedView);
let selectedSpans = [];
let counterpartSpans = [];
function clearCodeSelection() {
selectedSpans.forEach((el) => el.classList.remove("selected"));
counterpartSpans.forEach((el) => el.classList.remove("counterpart"));
selectedSpans = [];
counterpartSpans = [];
}
function spansForRange(id) {
if (!id) return [];
return Array.from(
document.querySelectorAll('.code .cd[data-range="' + id + '"]')
);
}
function selectCodeSpan(span) {
clearCodeSelection();
selectedSpans = spansForRange(span.dataset.range);
selectedSpans.forEach((el) => el.classList.add("selected"));
const counterpart = span.dataset.counterpart;
counterpartSpans = spansForRange(counterpart);
counterpartSpans.forEach((el) => el.classList.add("counterpart"));
if (counterpartSpans.length > 0) {
counterpartSpans[0].scrollIntoView({ block: "center" });
setStatus(span.dataset.range + " ↔ " + counterpart);
} else {
setStatus("no counterpart on the other side");
}
}
document.querySelectorAll(".code").forEach((code) => {
code.addEventListener("click", (event) => {
const span = event.target.closest(".cd");
if (span) {
selectCodeSpan(span);
} else {
clearCodeSelection();
}
});
});
const renderingPanels = Array.from(
document.querySelectorAll(".code-panels[data-painting]")
);
const renderingButtons = Array.from(
document.querySelectorAll(".painting-switch button")
);
const RENDERING_STORAGE_KEY = "codediff-mapping-rendering";
function renderingName(el) {
return el.dataset.paintingName || "";
}
function setRendering(name) {
const index = pickRendering(renderingPanels.map(renderingName), name);
if (index === -1) return;
const panel = renderingPanels[index];
const key = panel.dataset.painting;
renderingPanels.forEach((el) => {
el.classList.toggle("hidden", el !== panel);
});
renderingButtons.forEach((button) => {
button.setAttribute(
"aria-pressed",
button.dataset.painting === key ? "true" : "false"
);
});
clearCodeSelection();
}
function chooseRendering(name) {
setRendering(name);
const panel = renderingPanels.filter((el) => renderingName(el) === name)[0];
if (!panel) return;
try {
window.localStorage.setItem(RENDERING_STORAGE_KEY, name);
} catch (e) {
}
}
function cycleRendering() {
if (renderingPanels.length < 2) return;
let current = 0;
renderingPanels.forEach((el, index) => {
if (!el.classList.contains("hidden")) current = index;
});
const next = renderingPanels[(current + 1) % renderingPanels.length];
chooseRendering(renderingName(next));
setStatus("code view: " + renderingName(next));
}
renderingButtons.forEach((button) => {
button.addEventListener("click", () =>
chooseRendering(button.dataset.paintingName || "")
);
});
let storedRendering = "";
try {
storedRendering = window.localStorage.getItem(RENDERING_STORAGE_KEY) || "";
} catch (e) {
}
setRendering(storedRendering);
setFocusedSide("before");
})();