diff --git a/public/app.js b/public/app.js
index 62bcb5896..7ab784756 100644
--- a/public/app.js
+++ b/public/app.js
@@ -17,6 +17,16 @@ let allCommands = [];
// CONTENT LOADING
// ============================================
+function escapeHtml(value) {
+ if (typeof value !== "string") return "";
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
async function loadContent() {
try {
const [commandsRes, patternsRes] = await Promise.all([
@@ -96,13 +106,13 @@ function renderPatternsWithTabs(patterns, antipatterns) {
const tabsHTML = patterns
.map((category, i) => ``)
+ >${escapeHtml(category.name)}`)
.join("");
// Build panels with WAI-ARIA attributes
@@ -112,7 +122,7 @@ function renderPatternsWithTabs(patterns, antipatterns) {
return `
Don't
- ${antiItems.map((item) => `- ${item}
`).join("")}
+ ${antiItems.map((item) => `- ${escapeHtml(item)}
`).join("")}
Do
- ${category.items.map((item) => `- ${item}
`).join("")}
+ ${category.items.map((item) => `- ${escapeHtml(item)}
`).join("")}
@@ -167,7 +177,9 @@ function renderPatternsWithTabs(patterns, antipatterns) {
p.classList.remove('active');
p.setAttribute('hidden', '');
});
- const activePanel = container.querySelector(`[data-panel="${tabName}"]`);
+ const escapedName = CSS.escape(tabName);
+ const activePanel = container.querySelector(`[data-panel="${escapedName}"]`);
+ if (!activePanel) return;
activePanel.classList.add('active');
activePanel.removeAttribute('hidden');
};
@@ -229,10 +241,20 @@ document.addEventListener("click", (e) => {
const copyBtn = e.target.closest("[data-copy]");
if (copyBtn) {
const textToCopy = copyBtn.dataset.copy;
- navigator.clipboard.writeText(textToCopy).then(() => {
+ const onCopied = () => {
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 1500);
- });
+ };
+ if (navigator.clipboard?.writeText) {
+ navigator.clipboard.writeText(textToCopy).then(onCopied).catch(() => {});
+ } else {
+ // Fallback for non-HTTPS or older browsers
+ const ta = Object.assign(document.createElement('textarea'), { value: textToCopy, style: 'position:fixed;left:-9999px' });
+ document.body.appendChild(ta);
+ ta.select();
+ try { document.execCommand('copy'); onCopied(); } catch {}
+ ta.remove();
+ }
}
});