Extract duplicated diff rendering logic into shared/diff-page.js Implement theme switcher component across all templates
294 lines
8.6 KiB
JavaScript
294 lines
8.6 KiB
JavaScript
(function (global) {
|
|
"use strict";
|
|
|
|
const DEFAULT_THEMES = [
|
|
{ id: "light", label: "轻量", path: "index.html" },
|
|
{ id: "cyberpunk", label: "赛博", path: "cyberpunk/index.html" },
|
|
{ id: "macos", label: "macOS", path: "macos/index.html" },
|
|
{ id: "matrix", label: "Matrix", path: "matrix/index.html" },
|
|
{ id: "notion", label: "Notion", path: "notion/index.html" },
|
|
{ id: "synthwave", label: "蒸汽波", path: "synthwave/index.html" },
|
|
{ id: "exe", label: "EXE", path: "exe/index.html" },
|
|
];
|
|
|
|
function resolveThemeUrl(targetPath, currentPath) {
|
|
try {
|
|
const url = new URL(global.location.href);
|
|
if (url.pathname.endsWith(currentPath)) {
|
|
url.pathname =
|
|
url.pathname.slice(0, -currentPath.length) + targetPath;
|
|
return url.toString();
|
|
}
|
|
return new URL(targetPath, global.location.href).toString();
|
|
} catch (error) {
|
|
return targetPath;
|
|
}
|
|
}
|
|
|
|
function initStyleSwitcher(config) {
|
|
const host = document.querySelector(config.hostSelector || "[data-style-switcher]");
|
|
if (!host) {
|
|
return;
|
|
}
|
|
|
|
const themes = config.themes || DEFAULT_THEMES;
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "style-switcher";
|
|
|
|
const label = document.createElement("label");
|
|
label.className = "style-switcher-label";
|
|
label.textContent = config.switcherLabel || "风格";
|
|
|
|
const select = document.createElement("select");
|
|
select.className = "style-switcher-select";
|
|
select.setAttribute(
|
|
"aria-label",
|
|
config.switcherAriaLabel || "切换界面风格",
|
|
);
|
|
|
|
themes.forEach((theme) => {
|
|
const option = document.createElement("option");
|
|
option.value = resolveThemeUrl(theme.path, config.currentThemePath);
|
|
option.textContent = theme.label;
|
|
option.selected = theme.id === config.themeId;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
select.addEventListener("change", () => {
|
|
global.location.href = select.value;
|
|
});
|
|
|
|
wrapper.appendChild(label);
|
|
wrapper.appendChild(select);
|
|
host.replaceChildren(wrapper);
|
|
}
|
|
|
|
function getElement(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function initDiffPage(config) {
|
|
const leftTextarea = getElement(config.ids?.leftTextarea || "leftTextarea");
|
|
const rightTextarea = getElement(
|
|
config.ids?.rightTextarea || "rightTextarea",
|
|
);
|
|
const compareBtn = getElement(config.ids?.compareBtn || "compareBtn");
|
|
const swapBtn = getElement(config.ids?.swapBtn || "swapBtn");
|
|
const exampleBtn = getElement(config.ids?.exampleBtn || "exampleBtn");
|
|
const clearLeftBtn = getElement(config.ids?.clearLeftBtn || "clearLeftBtn");
|
|
const clearRightBtn = getElement(
|
|
config.ids?.clearRightBtn || "clearRightBtn",
|
|
);
|
|
const languageSelect = getElement(
|
|
config.ids?.languageSelect || "languageSelect",
|
|
);
|
|
const diffOutput = getElement(config.ids?.diffOutput || "diff-output");
|
|
const diffStats = getElement(config.ids?.diffStats || "diffStats");
|
|
const viewOptions = document.querySelectorAll(
|
|
config.selectors?.viewOptions || ".view-option",
|
|
);
|
|
|
|
if (
|
|
!leftTextarea ||
|
|
!rightTextarea ||
|
|
!compareBtn ||
|
|
!swapBtn ||
|
|
!exampleBtn ||
|
|
!clearLeftBtn ||
|
|
!clearRightBtn ||
|
|
!languageSelect ||
|
|
!diffOutput ||
|
|
!diffStats ||
|
|
viewOptions.length === 0
|
|
) {
|
|
throw new Error("Diff page initialization failed: missing required DOM nodes.");
|
|
}
|
|
|
|
const messages = config.messages || {};
|
|
const example = config.example || {};
|
|
const normalizeLanguage =
|
|
config.normalizeLanguage || ((language) => language);
|
|
const formatStats =
|
|
config.formatStats ||
|
|
((result) => {
|
|
if (result.identical) {
|
|
return "No changes";
|
|
}
|
|
return `+${result.added} / -${result.deleted}`;
|
|
});
|
|
|
|
let currentView =
|
|
config.initialView ||
|
|
Array.from(viewOptions).find((button) => button.classList.contains("active"))
|
|
?.getAttribute("data-view") ||
|
|
"side-by-side";
|
|
|
|
function setOutput(html, statsHtml) {
|
|
diffOutput.innerHTML = html;
|
|
diffStats.innerHTML = statsHtml || "";
|
|
}
|
|
|
|
function getSelectedLanguage() {
|
|
return normalizeLanguage(languageSelect.value);
|
|
}
|
|
|
|
function getHighlightConfig() {
|
|
const language = getSelectedLanguage();
|
|
if (language === "plaintext") {
|
|
return false;
|
|
}
|
|
return { enabled: true, language: language };
|
|
}
|
|
|
|
function generateUnifiedDiff(original, modified) {
|
|
return Diff.createTwoFilesPatch(
|
|
config.fileLabels?.left || "Original",
|
|
config.fileLabels?.right || "Modified",
|
|
original || "",
|
|
modified || "",
|
|
"",
|
|
"",
|
|
{ context: config.contextLines || 4 },
|
|
);
|
|
}
|
|
|
|
function applyHighlighting() {
|
|
const selectedLanguage = getSelectedLanguage();
|
|
if (selectedLanguage === "plaintext" || !global.hljs) {
|
|
return;
|
|
}
|
|
|
|
const codeBlocks = diffOutput.querySelectorAll("code");
|
|
codeBlocks.forEach((block) => {
|
|
block.classList.forEach((className) => {
|
|
if (className.startsWith("language-")) {
|
|
block.classList.remove(className);
|
|
}
|
|
});
|
|
block.classList.add("language-" + selectedLanguage);
|
|
delete block.dataset.highlighted;
|
|
try {
|
|
global.hljs.highlightElement(block);
|
|
} catch (error) {}
|
|
});
|
|
}
|
|
|
|
function renderDiff() {
|
|
let diffString = "";
|
|
try {
|
|
diffString = generateUnifiedDiff(leftTextarea.value, rightTextarea.value);
|
|
} catch (error) {
|
|
setOutput(
|
|
typeof messages.generateError === "function"
|
|
? messages.generateError(error)
|
|
: "<div>Diff generation failed.</div>",
|
|
messages.generateErrorStats || "",
|
|
);
|
|
return;
|
|
}
|
|
|
|
let diffHtml = "";
|
|
try {
|
|
diffHtml = Diff2Html.html(diffString, {
|
|
drawFileList: false,
|
|
matching: "lines",
|
|
outputFormat: currentView,
|
|
highlight: getHighlightConfig(),
|
|
renderNothingWhenEmpty: false,
|
|
});
|
|
} catch (error) {
|
|
setOutput(
|
|
typeof messages.renderError === "function"
|
|
? messages.renderError(error)
|
|
: "<div>Diff rendering failed.</div>",
|
|
messages.renderErrorStats || "",
|
|
);
|
|
return;
|
|
}
|
|
|
|
diffOutput.innerHTML = diffHtml;
|
|
applyHighlighting();
|
|
|
|
const added = diffOutput.querySelectorAll(".d2h-ins").length;
|
|
const deleted = diffOutput.querySelectorAll(".d2h-del").length;
|
|
const identical = added === 0 && deleted === 0;
|
|
|
|
diffStats.innerHTML = formatStats({
|
|
added: added,
|
|
deleted: deleted,
|
|
identical: identical,
|
|
});
|
|
|
|
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
|
setOutput(messages.blankResult || "<div>Files are identical.</div>", messages.blankStats || "");
|
|
}
|
|
}
|
|
|
|
function setActiveView(view, shouldRender) {
|
|
currentView = view;
|
|
viewOptions.forEach((button) => {
|
|
button.classList.toggle(
|
|
"active",
|
|
button.getAttribute("data-view") === view,
|
|
);
|
|
});
|
|
|
|
if (shouldRender !== false) {
|
|
renderDiff();
|
|
}
|
|
}
|
|
|
|
function loadExample() {
|
|
leftTextarea.value = example.left || "";
|
|
rightTextarea.value = example.right || "";
|
|
if (example.language) {
|
|
languageSelect.value = example.language;
|
|
}
|
|
renderDiff();
|
|
}
|
|
|
|
function swapTexts() {
|
|
const currentLeft = leftTextarea.value;
|
|
leftTextarea.value = rightTextarea.value;
|
|
rightTextarea.value = currentLeft;
|
|
renderDiff();
|
|
}
|
|
|
|
function clearLeft() {
|
|
leftTextarea.value = "";
|
|
renderDiff();
|
|
}
|
|
|
|
function clearRight() {
|
|
rightTextarea.value = "";
|
|
renderDiff();
|
|
}
|
|
|
|
compareBtn.addEventListener("click", renderDiff);
|
|
swapBtn.addEventListener("click", swapTexts);
|
|
exampleBtn.addEventListener("click", loadExample);
|
|
clearLeftBtn.addEventListener("click", clearLeft);
|
|
clearRightBtn.addEventListener("click", clearRight);
|
|
languageSelect.addEventListener("change", renderDiff);
|
|
|
|
viewOptions.forEach((button) => {
|
|
button.addEventListener("click", (event) => {
|
|
setActiveView(event.currentTarget.getAttribute("data-view"));
|
|
});
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
event.preventDefault();
|
|
renderDiff();
|
|
}
|
|
});
|
|
|
|
initStyleSwitcher(config);
|
|
setActiveView(currentView, false);
|
|
renderDiff();
|
|
}
|
|
|
|
global.initDiffPage = initDiffPage;
|
|
})(window);
|