feat(theme): add style switcher and centralize diff logic

Extract duplicated diff rendering logic into shared/diff-page.js
Implement theme switcher component across all templates
This commit is contained in:
2026-04-08 14:39:39 +08:00
parent 90792b276e
commit e9fcd411a7
9 changed files with 642 additions and 1271 deletions

0
.codex Normal file
View File

View File

@@ -83,6 +83,27 @@
.view-option.active {
@apply text-cyber-cyan border-cyber-cyan bg-cyber-cyan/10 shadow-[inset_0_0_10px_rgba(0,240,255,0.2)];
}
.style-switcher {
@apply flex items-center gap-3;
}
.style-switcher-label {
@apply text-xs text-gray-500 tracking-widest;
}
.style-switcher-select {
background: #050505;
border: 1px solid #333;
color: #fcee0a;
padding: 0.25rem 0.75rem;
font-family: "Fira Code", monospace;
font-size: 0.75rem;
letter-spacing: 0.12em;
outline: none;
}
.style-switcher-select:hover,
.style-switcher-select:focus {
border-color: #00f0ff;
box-shadow: 0 0 12px rgba(0, 240, 255, 0.15);
}
/* 面板边框发光效果 */
.cyber-panel {
@@ -313,6 +334,8 @@ function initCyberware() {
UNIFIED
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -351,184 +374,40 @@ function initCyberware() {
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
<!-- 核心逻辑 -->
<script src="../shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
return languageSelect.value;
window.initDiffPage({
themeId: "cyberpunk",
currentThemePath: "cyberpunk/index.html",
switcherLabel: "SKIN_",
switcherAriaLabel: "Switch cyberpunk skin",
fileLabels: {
left: "ORIGINAL",
right: "MODIFIED",
},
example: {
left: `function initCyberware() {\n console.log("Booting optics...");\n return "Ready";\n}`,
right: `function initCyberware() {\n console.log("Booting Kiroshi optics v3.0...");\n connectToNetwork();\n return "Online";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">SYS_ERR: ${error.message}</div>`,
renderError: () =>
'<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">RENDER_FAILED</div>',
blankResult:
'<div class="p-10 text-center font-mono text-gray-500 tracking-widest">DATA_MATCH // NO_DIFF_FOUND</div>',
blankStats: "<span class='text-gray-500'>SYNCED</span>",
generateErrorStats: "ERR",
renderErrorStats: "ERR",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "<span class='text-gray-500'>SYNCED // NO_CHANGES</span>";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"ORIGINAL",
"MODIFIED",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">SYS_ERR: ${e.message}</div>`;
diffStats.textContent = "ERR";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
diffOutput.innerHTML = `<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">RENDER_FAILED</div>`;
diffStats.textContent = "ERR";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `<span class='text-cyber-cyan'>+${added} INS</span> <span class='text-gray-600 mx-2'>|</span> <span class='text-cyber-pink'>-${deleted} DEL</span>`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML =
"<span class='text-gray-500'>SYNCED // NO_CHANGES</span>";
} else {
diffStats.innerHTML = `<span class='text-cyber-cyan'>+${addedLines} INS</span> <span class='text-gray-600 mx-2'>|</span> <span class='text-cyber-pink'>-${deletedLines} DEL</span>`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="p-10 text-center font-mono text-gray-500 tracking-widest">DATA_MATCH // NO_DIFF_FOUND</div>';
diffStats.innerHTML = "<span class='text-gray-500'>SYNCED</span>";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function initCyberware() {\n console.log("Booting optics...");\n return "Ready";\n}`;
rightTextarea.value = `function initCyberware() {\n console.log("Booting Kiroshi optics v3.0...");\n connectToNetwork();\n return "Online";\n}`;
languageSelect.value = "javascript";
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
)
renderDiff();
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
setActiveView(e.currentTarget.getAttribute("data-view"));
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", renderDiff);
})();
</script>
</body>
</html>

View File

@@ -72,6 +72,26 @@
.view-option.active {
@apply border-t-win-borderDark border-l-win-borderDark border-b-win-borderLight border-r-win-borderLight pt-[5px] pb-[3px] pl-[17px] pr-[15px] bg-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABZJREFUeNpi2rVrdz8DAwMDMwIGAAgwAEYQAWtO1y8gAAAAAElFTkSuQmCC')] /* 经典棋盘格背景 */;
}
.style-switcher {
@apply flex items-center gap-2;
}
.style-switcher-label {
@apply text-lg;
}
.style-switcher-select {
background: #ffffff;
border: 2px solid;
border-top-color: #000000;
border-left-color: #000000;
border-bottom-color: #ffffff;
border-right-color: #ffffff;
color: #000000;
cursor: pointer;
font-family: "VT323", monospace;
font-size: 1.125rem;
outline: none;
padding: 0.125rem 0.5rem;
}
/* JS 动态插入的空状态提示 */
.empty-message {
@apply p-8 text-center font-retro text-xl text-gray-600 bg-white;
@@ -269,6 +289,8 @@ function hello() {
UNIFIED
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -312,196 +334,38 @@ function hello() {
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
<!-- 核心逻辑 (保持不变,仅修改了统计信息的输出格式以符合复古风格) -->
<script src="../shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
return languageSelect.value;
window.initDiffPage({
themeId: "exe",
currentThemePath: "exe/index.html",
switcherLabel: "SKIN:",
switcherAriaLabel: "Switch retro EXE skin",
fileLabels: {
left: "ORIGINAL",
right: "MODIFIED",
},
example: {
left: `function hello() {\n console.log("Hello World");\n return "Hi";\n}`,
right: `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="empty-message" style="color:red;">ERROR: ${error.message}</div>`,
renderError: () => '<div class="empty-message">RENDER FAILED.</div>',
blankResult: '<div class="empty-message">FILES ARE IDENTICAL.</div>',
blankStats: "NO_CHANGES",
generateErrorStats: "ERR",
renderErrorStats: "ERR",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "NO_CHANGES_DETECTED";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"ORIGINAL",
"MODIFIED",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="empty-message" style="color:red;">ERROR: ${e.message}</div>`;
diffStats.textContent = "ERR";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
console.warn(e);
diffOutput.innerHTML = `<div class="empty-message">RENDER FAILED.</div>`;
diffStats.textContent = "ERR";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `[ +${added} INS | -${deleted} DEL ]`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML = "NO_CHANGES_DETECTED";
} else {
// 复古终端风格的统计输出
diffStats.innerHTML = `[ +${addedLines} INS | -${deletedLines} DEL ]`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="empty-message">FILES ARE IDENTICAL.</div>';
diffStats.innerHTML = "NO_CHANGES";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function hello() {\n console.log("Hello World");\n return "Hi";\n}`;
rightTextarea.value = `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`;
languageSelect.value = "javascript";
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
const view = e.currentTarget.getAttribute("data-view");
setActiveView(view);
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", () => {
renderDiff();
});
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
setTimeout(renderDiff, 10);
}
})();
</script>
</body>
</html>

View File

@@ -43,6 +43,15 @@
.view-option.active {
@apply bg-white text-blue-700 shadow-sm font-semibold;
}
.style-switcher {
@apply flex items-center gap-2 bg-slate-100 px-4 py-1.5 rounded-full;
}
.style-switcher-label {
@apply text-sm font-medium text-slate-700;
}
.style-switcher-select {
@apply bg-white border border-slate-300 rounded-full px-3 py-1 text-sm font-medium text-slate-800 outline-none cursor-pointer shadow-sm hover:border-blue-500 transition-colors;
}
/* JS 动态插入的空状态提示 */
.empty-message {
@apply p-10 text-center text-slate-500 bg-slate-50/50;
@@ -197,6 +206,8 @@ function hello() {
📋 行内
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -254,197 +265,38 @@ function hello() {
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
<!-- 核心逻辑 (保持不变) -->
<script src="./shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
return languageSelect.value;
window.initDiffPage({
themeId: "light",
currentThemePath: "index.html",
switcherLabel: "🎨 风格",
switcherAriaLabel: "切换 Diff Checker 风格",
fileLabels: {
left: "原始",
right: "修改",
},
example: {
left: `function hello() {\n console.log("Hello World");\n return "Hi";\n}`,
right: `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="empty-message" style="color:#ef4444;">生成差异时出错: ${error.message}</div>`,
renderError: () =>
'<div class="empty-message">渲染差异失败,请检查输入</div>',
blankResult:
'<div class="empty-message">两个文本完全相同,没有差异</div>',
blankStats: "<span class='text-slate-500'>⚖️ 无差异</span>",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "<span class='text-emerald-600 font-medium'>✅ 内容相同</span>";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"原始",
"修改",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="empty-message" style="color:#ef4444;">生成差异时出错: ${e.message}</div>`;
diffStats.textContent = "";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
console.warn(e);
diffOutput.innerHTML = `<div class="empty-message">渲染差异失败,请检查输入</div>`;
diffStats.textContent = "";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `<span class='text-emerald-600 font-medium'>+${added} 行添加</span> &nbsp;|&nbsp; <span class='text-rose-500 font-medium'>-${deleted} 行删除</span>`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML =
"<span class='text-emerald-600 font-medium'>✅ 内容相同</span>";
} else {
diffStats.innerHTML = `<span class='text-emerald-600 font-medium'>+${addedLines} 行添加</span> &nbsp;|&nbsp; <span class='text-rose-500 font-medium'>-${deletedLines} 行删除</span>`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="empty-message">两个文本完全相同,没有差异</div>';
diffStats.innerHTML =
"<span class='text-slate-500'>⚖️ 无差异</span>";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function hello() {\n console.log("Hello World");\n return "Hi";\n}`;
rightTextarea.value = `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`;
languageSelect.value = "javascript";
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
const view = e.currentTarget.getAttribute("data-view");
setActiveView(view);
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", () => {
renderDiff();
});
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
setTimeout(renderDiff, 10);
}
})();
</script>
</body>
</html>

View File

@@ -81,6 +81,21 @@
.view-option.active {
@apply bg-white/80 text-blue-600 shadow-sm border border-white/80;
}
.style-switcher {
@apply flex items-center gap-2 bg-white/40 p-1.5 rounded-xl border border-white/50;
}
.style-switcher-label {
@apply text-sm font-medium text-slate-500 pl-2;
}
.style-switcher-select {
background: transparent;
border: 0;
color: #334155;
font-size: 0.875rem;
font-weight: 500;
outline: none;
padding: 0.25rem 0.75rem 0.25rem 0.25rem;
}
}
/* ==========================================
@@ -338,6 +353,8 @@ function hello() {
Unified
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -416,183 +433,40 @@ function hello() {
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
<!-- 核心逻辑 -->
<script src="../shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
return languageSelect.value;
window.initDiffPage({
themeId: "macos",
currentThemePath: "macos/index.html",
switcherLabel: "Theme",
switcherAriaLabel: "Switch macOS glass theme",
fileLabels: {
left: "Original",
right: "Modified",
},
example: {
left: `function hello() {\n console.log("Hello World");\n return "Hi";\n}`,
right: `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="p-12 text-center text-red-500 text-sm">Error: ${error.message}</div>`,
renderError: () =>
'<div class="p-12 text-center text-red-500 text-sm">Render Failed</div>',
blankResult:
'<div class="p-12 text-center text-slate-400 text-sm">Files are identical.</div>',
blankStats: "Identical",
generateErrorStats: "Error",
renderErrorStats: "Error",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "No Changes";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"Original",
"Modified",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="p-12 text-center text-red-500 text-sm">Error: ${e.message}</div>`;
diffStats.textContent = "Error";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
diffOutput.innerHTML = `<div class="p-12 text-center text-red-500 text-sm">Render Failed</div>`;
diffStats.textContent = "Error";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `<span class='text-green-600'>+${added}</span> &nbsp;|&nbsp; <span class='text-red-500'>-${deleted}</span>`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML = "No Changes";
} else {
diffStats.innerHTML = `<span class='text-green-600'>+${addedLines}</span> &nbsp;|&nbsp; <span class='text-red-500'>-${deletedLines}</span>`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="p-12 text-center text-slate-400 text-sm">Files are identical.</div>';
diffStats.innerHTML = "Identical";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function hello() {\n console.log("Hello World");\n return "Hi";\n}`;
rightTextarea.value = `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`;
languageSelect.value = "javascript";
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
)
renderDiff();
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
setActiveView(e.currentTarget.getAttribute("data-view"));
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", renderDiff);
})();
</script>
</body>
</html>

View File

@@ -75,6 +75,26 @@
.view-option.active {
@apply text-black bg-matrix-green shadow-[0_0_10px_#00FF41];
}
.style-switcher {
@apply flex items-center gap-2;
}
.style-switcher-label {
@apply text-sm tracking-widest text-matrix-darkGreen;
}
.style-switcher-select {
background: #000;
border: 1px solid #008f11;
color: #00ff41;
padding: 0.25rem 0.75rem;
font-family: "Share Tech Mono", monospace;
font-size: 0.875rem;
outline: none;
}
.style-switcher-select:hover,
.style-switcher-select:focus {
border-color: #00ff41;
box-shadow: 0 0 10px #00ff41;
}
}
/* ==========================================
@@ -325,6 +345,8 @@ function enterMatrix() {
UNIFIED
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -418,185 +440,40 @@ function enterMatrix() {
});
</script>
<!-- 核心对比逻辑 -->
<script src="../shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
return languageSelect.value;
window.initDiffPage({
themeId: "matrix",
currentThemePath: "matrix/index.html",
switcherLabel: "MODE:",
switcherAriaLabel: "Switch Matrix skin",
fileLabels: {
left: "SOURCE",
right: "TARGET",
},
example: {
left: `function enterMatrix() {\n console.log("Follow the white rabbit.");\n return "Ignorance is bliss.";\n}`,
right: `function enterMatrix() {\n console.log("There is no spoon.");\n decodeConstruct();\n return "I know Kung Fu.";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="p-12 text-center text-matrix-red tracking-widest">FATAL_ERROR: ${error.message}</div>`,
renderError: () =>
'<div class="p-12 text-center text-matrix-red tracking-widest">RENDER_FAILURE</div>',
blankResult:
'<div class="p-12 text-center text-matrix-darkGreen tracking-widest">DATA_STREAMS_ARE_IDENTICAL.</div>',
blankStats: "<span class='text-matrix-darkGreen'>SYNCED</span>",
generateErrorStats: "ERR",
renderErrorStats: "ERR",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "<span class='text-matrix-darkGreen'>MATCH_FOUND // NO_ANOMALIES</span>";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"SOURCE",
"TARGET",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="p-12 text-center text-matrix-red tracking-widest">FATAL_ERROR: ${e.message}</div>`;
diffStats.textContent = "ERR";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
diffOutput.innerHTML = `<div class="p-12 text-center text-matrix-red tracking-widest">RENDER_FAILURE</div>`;
diffStats.textContent = "ERR";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `<span class='text-matrix-green'>+${added} INJECTED</span> <span class='text-matrix-darkGreen mx-2'>|</span> <span class='text-matrix-red'>-${deleted} PURGED</span>`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML =
"<span class='text-matrix-darkGreen'>MATCH_FOUND // NO_ANOMALIES</span>";
} else {
diffStats.innerHTML = `<span class='text-matrix-green'>+${addedLines} INJECTED</span> <span class='text-matrix-darkGreen mx-2'>|</span> <span class='text-matrix-red'>-${deletedLines} PURGED</span>`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="p-12 text-center text-matrix-darkGreen tracking-widest">DATA_STREAMS_ARE_IDENTICAL.</div>';
diffStats.innerHTML =
"<span class='text-matrix-darkGreen'>SYNCED</span>";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function enterMatrix() {\n console.log("Follow the white rabbit.");\n return "Ignorance is bliss.";\n}`;
rightTextarea.value = `function enterMatrix() {\n console.log("There is no spoon.");\n decodeConstruct();\n return "I know Kung Fu.";\n}`;
languageSelect.value = "javascript";
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
)
renderDiff();
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
setActiveView(e.currentTarget.getAttribute("data-view"));
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", renderDiff);
})();
</script>
</body>
</html>

View File

@@ -92,6 +92,15 @@
.view-option.active {
@apply text-notion-text font-medium bg-notion-hover;
}
.style-switcher {
@apply flex items-center gap-2;
}
.style-switcher-label {
@apply text-sm text-notion-gray;
}
.style-switcher-select {
@apply bg-transparent hover:bg-notion-hover px-2 py-1 rounded-[4px] text-sm text-notion-text outline-none cursor-pointer transition-colors appearance-none;
}
/* Notion 代码块输入框 */
.notion-input-wrapper {
@@ -279,6 +288,10 @@ function hello() {
Unified view
</button>
</div>
<div class="w-[1px] h-4 bg-notion-border hidden md:block"></div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -330,184 +343,38 @@ function hello() {
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
<!-- 核心逻辑 -->
<script src="../shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
return languageSelect.value;
window.initDiffPage({
themeId: "notion",
currentThemePath: "notion/index.html",
switcherLabel: "Style",
switcherAriaLabel: "Switch Notion style",
fileLabels: {
left: "Original",
right: "Modified",
},
example: {
left: `function hello() {\n console.log("Hello World");\n return "Hi";\n}`,
right: `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="p-8 text-center text-[#eb5757] text-sm">Error: ${error.message}</div>`,
renderError: () =>
'<div class="p-8 text-center text-[#eb5757] text-sm">Render Failed</div>',
blankResult:
'<div class="p-8 text-center text-notion-lightGray text-sm">Files are identical.</div>',
blankStats: "Identical",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "No changes";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"Original",
"Modified",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="p-8 text-center text-[#eb5757] text-sm">Error: ${e.message}</div>`;
diffStats.textContent = "";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
diffOutput.innerHTML = `<div class="p-8 text-center text-[#eb5757] text-sm">Render Failed</div>`;
diffStats.textContent = "";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `<span style="color:#448361">${added} additions</span>, <span style="color:#e03e3e">${deleted} deletions</span>`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML = "No changes";
} else {
// Notion 风格的极简统计文本
diffStats.innerHTML = `<span style="color:#448361">${addedLines} additions</span>, <span style="color:#e03e3e">${deletedLines} deletions</span>`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="p-8 text-center text-notion-lightGray text-sm">Files are identical.</div>';
diffStats.innerHTML = "Identical";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function hello() {\n console.log("Hello World");\n return "Hi";\n}`;
rightTextarea.value = `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`;
languageSelect.value = "javascript";
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
)
renderDiff();
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
setActiveView(e.currentTarget.getAttribute("data-view"));
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", renderDiff);
})();
</script>
</body>
</html>

293
shared/diff-page.js Normal file
View File

@@ -0,0 +1,293 @@
(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);

View File

@@ -23,11 +23,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Righteous&family=
@message_part::reasoning-1_2
Space+Mono:ital,wght@0,400;0,700;1,400&display=swap"
href="https://fonts.googleapis.com/css2?family=Righteous&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap"
rel="stylesheet"
/>
@@ -90,6 +86,26 @@ Space+Mono:ital,wght@0,400;0,700;1,400&display=swap"
.view-option.active {
@apply text-vapor-bg bg-vapor-purple shadow-[0_0_15px_#b967ff];
}
.style-switcher {
@apply flex items-center gap-3;
}
.style-switcher-label {
@apply font-vapor text-sm tracking-widest text-vapor-yellow drop-shadow-[0_0_5px_#fffb96];
}
.style-switcher-select {
background: #120458;
border: 2px solid #b967ff;
color: #01cdfe;
padding: 0.375rem 0.75rem;
font-family: "Righteous", cursive;
font-size: 0.875rem;
outline: none;
}
.style-switcher-select:hover,
.style-switcher-select:focus {
border-color: #01cdfe;
box-shadow: 0 0 10px #01cdfe;
}
}
/* ==========================================
@@ -377,6 +393,8 @@ function playSynthwave() {
MONO
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -425,193 +443,40 @@ function playSynthwave() {
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
<!-- 核心对比逻辑 -->
<script src="../shared/diff-page.js"></script>
<script>
(function () {
"use strict";
const leftTextarea = document.getElementById("leftTextarea");
const rightTextarea = document.getElementById("rightTextarea");
const compareBtn = document.getElementById("compareBtn");
const swapBtn = document.getElementById("swapBtn");
const exampleBtn = document.getElementById("exampleBtn");
const clearLeftBtn = document.getElementById("clearLeftBtn");
const clearRightBtn = document.getElementById("clearRightBtn");
const languageSelect = document.getElementById("languageSelect");
const diffOutput = document.getElementById("diff-output");
const diffStats = document.getElementById("diffStats");
const viewOptions = document.querySelectorAll(".view-option");
let currentView = "side-by-side";
function getSelectedLanguage() {
let val = languageSelect.value;
// 映射回真实的语言名称
if (val === "javascript") return "javascript";
if (val === "typescript") return "typescript";
if (val === "html") return "html";
if (val === "css") return "css";
if (val === "json") return "json";
if (val === "python") return "python";
return "plaintext";
window.initDiffPage({
themeId: "synthwave",
currentThemePath: "synthwave/index.html",
switcherLabel: "SKIN:",
switcherAriaLabel: "Switch synthwave skin",
fileLabels: {
left: "CASSETTE_A",
right: "CASSETTE_B",
},
example: {
left: `function playSynthwave() {\n console.log("Loading VHS tape...");\n return "Nostalgia";\n}`,
right: `function playSynthwave() {\n console.log("Loading LaserDisc...");\n enableNeonGlow();\n return "A E S T H E T I C S";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="p-12 text-center font-vapor text-vapor-pink tracking-widest text-xl">GLITCH_ERROR: ${error.message}</div>`,
renderError: () =>
'<div class="p-12 text-center font-vapor text-vapor-pink tracking-widest text-xl">TAPE_JAMMED</div>',
blankResult:
'<div class="p-12 text-center font-vapor text-vapor-cyan tracking-widest text-xl">TRACKS_ARE_IDENTICAL</div>',
blankStats: "<span class='text-vapor-cyan'>SYNCED</span>",
generateErrorStats: "ERR",
renderErrorStats: "ERR",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "<span class='text-vapor-cyan drop-shadow-[0_0_5px_#01cdfe]'>VIBES_SYNCED</span>";
}
function getHighlightConfig() {
const lang = getSelectedLanguage();
if (lang === "plaintext") return false;
return { enabled: true, language: lang };
}
function generateUnifiedDiff(original, modified) {
return Diff.createTwoFilesPatch(
"CASSETTE_A",
"CASSETTE_B",
original || "",
modified || "",
"",
"",
{ context: 4 },
);
}
function renderDiff() {
const leftText = leftTextarea.value;
const rightText = rightTextarea.value;
let diffString;
try {
diffString = generateUnifiedDiff(leftText, rightText);
} catch (e) {
diffOutput.innerHTML = `<div class="p-12 text-center font-vapor text-vapor-pink tracking-widest text-xl">GLITCH_ERROR: ${e.message}</div>`;
diffStats.textContent = "ERR";
return;
}
const configuration = {
drawFileList: false,
matching: "lines",
outputFormat: currentView,
highlight: getHighlightConfig(),
renderNothingWhenEmpty: false,
};
let diffHtml = "";
try {
diffHtml = Diff2Html.html(diffString, configuration);
} catch (e) {
diffOutput.innerHTML = `<div class="p-12 text-center font-vapor text-vapor-pink tracking-widest text-xl">TAPE_JAMMED</div>`;
diffStats.textContent = "ERR";
return;
}
diffOutput.innerHTML = diffHtml;
const selectedLang = getSelectedLanguage();
if (selectedLang !== "plaintext") {
const codeBlocks = diffOutput.querySelectorAll("code");
if (codeBlocks.length > 0 && window.hljs) {
codeBlocks.forEach((block) => {
block.classList.forEach((cls) => {
if (cls.startsWith("language-")) block.classList.remove(cls);
return `<span class='text-vapor-cyan drop-shadow-[0_0_5px_#01cdfe]'>+${added} UPLOADED</span> <span class='text-vapor-purple mx-2'>|</span> <span class='text-vapor-pink drop-shadow-[0_0_5px_#ff71ce]'>-${deleted} ERASED</span>`;
},
});
block.classList.add(`language-${selectedLang}`);
if (block.dataset.highlighted) {
delete block.dataset.highlighted;
}
try {
hljs.highlightElement(block);
} catch (e) {}
});
}
}
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
if (addedLines === 0 && deletedLines === 0) {
diffStats.innerHTML =
"<span class='text-vapor-cyan drop-shadow-[0_0_5px_#01cdfe]'>VIBES_SYNCED</span>";
} else {
diffStats.innerHTML = `<span class='text-vapor-cyan drop-shadow-[0_0_5px_#01cdfe]'>+${addedLines} UPLOADED</span> <span class='text-vapor-purple mx-2'>|</span> <span class='text-vapor-pink drop-shadow-[0_0_5px_#ff71ce]'>-${deletedLines} ERASED</span>`;
}
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
diffOutput.innerHTML =
'<div class="p-12 text-center font-vapor text-vapor-cyan tracking-widest text-xl">TRACKS_ARE_IDENTICAL</div>';
diffStats.innerHTML = "<span class='text-vapor-cyan'>SYNCED</span>";
}
}
function setActiveView(view) {
currentView = view;
viewOptions.forEach((btn) => {
const val = btn.getAttribute("data-view");
if (val === view) {
btn.classList.add("active");
} else {
btn.classList.remove("active");
}
});
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
) {
renderDiff();
}
}
function loadExample() {
leftTextarea.value = `function playSynthwave() {\n console.log("Loading VHS tape...");\n return "Nostalgia";\n}`;
rightTextarea.value = `function playSynthwave() {\n console.log("Loading LaserDisc...");\n enableNeonGlow();\n return "A E S T H E T I C S";\n}`;
// 恢复下拉框的显示值
languageSelect.selectedIndex = 0;
renderDiff();
}
function swapTexts() {
const temp = leftTextarea.value;
leftTextarea.value = rightTextarea.value;
rightTextarea.value = temp;
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", () => {
if (
diffOutput.querySelector(".d2h-wrapper") ||
diffOutput.children.length > 0
)
renderDiff();
});
viewOptions.forEach((btn) => {
btn.addEventListener("click", (e) => {
setActiveView(e.currentTarget.getAttribute("data-view"));
});
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderDiff();
}
});
window.addEventListener("DOMContentLoaded", renderDiff);
})();
</script>
</body>
</html>