Compare commits

...

16 Commits

10 changed files with 359 additions and 117 deletions
+2
View File
@@ -10,6 +10,8 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
WEB_EXT_IGNORE_FILES: scripts/**
steps:
- uses: actions/checkout@v4
+68 -29
View File
@@ -121,7 +121,7 @@ var controllerButtonDefs = {
faster: { label: "", className: "" },
advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" },
reset: { label: "", className: "" },
reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" },
settings: { label: "", className: "" },
pause: { label: "", className: "" },
@@ -776,18 +776,40 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
return normalizedEnabled;
}
function subtitleNudgeIconMarkup(isEnabled) {
function renderSubtitleNudgeIndicatorContent(target, isEnabled) {
if (!target) return;
var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff";
var custom =
tc.settings.customButtonIcons &&
tc.settings.customButtonIcons[action] &&
tc.settings.customButtonIcons[action].svg;
vscClearElement(target);
if (custom) {
var customWrap = vscCreateSvgWrap(
target.ownerDocument || document,
custom,
"vsc-btn-icon"
);
if (customWrap) {
target.appendChild(customWrap);
return;
}
}
if (typeof vscIconSvgString !== "function") {
return isEnabled ? "✓" : "×";
target.textContent = isEnabled ? "✓" : "×";
return;
}
var svg = vscIconSvgString(action, 14);
if (!svg) {
return isEnabled ? "✓" : "×";
target.textContent = isEnabled ? "✓" : "×";
return;
}
return (
'<span class="vsc-btn-icon" aria-hidden="true">' + svg + "</span>"
);
var wrap = vscCreateSvgWrap(target.ownerDocument || document, svg, "vsc-btn-icon");
if (wrap) {
target.appendChild(wrap);
return;
}
target.textContent = isEnabled ? "✓" : "×";
}
function updateSubtitleNudgeIndicator(video) {
@@ -795,11 +817,10 @@ function updateSubtitleNudgeIndicator(video) {
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled";
var mark = subtitleNudgeIconMarkup(isEnabled);
var indicator = video.vsc.subtitleNudgeIndicator;
if (indicator) {
indicator.innerHTML = mark;
renderSubtitleNudgeIndicatorContent(indicator, isEnabled);
indicator.dataset.enabled = isEnabled ? "true" : "false";
indicator.dataset.supported = "true";
indicator.title = title;
@@ -808,7 +829,7 @@ function updateSubtitleNudgeIndicator(video) {
var flashEl = video.vsc.nudgeFlashIndicator;
if (flashEl) {
flashEl.innerHTML = mark;
renderSubtitleNudgeIndicatorContent(flashEl, isEnabled);
flashEl.dataset.enabled = isEnabled ? "true" : "false";
flashEl.dataset.supported = "true";
flashEl.setAttribute("aria-label", title);
@@ -1348,12 +1369,15 @@ chrome.storage.sync.get(tc.settings, function (storage) {
tc.settings.customButtonIcons &&
tc.settings.customButtonIcons[act] &&
tc.settings.customButtonIcons[act].svg;
btn.innerHTML = "";
vscClearElement(btn);
if (svg) {
var cw = doc.createElement("span");
cw.className = "vsc-btn-icon";
cw.innerHTML = svg;
btn.appendChild(cw);
var cw = vscCreateSvgWrap(doc, svg, "vsc-btn-icon");
if (cw) {
btn.appendChild(cw);
} else {
var cdf = controllerButtonDefs[act];
btn.textContent = (cdf && cdf.label) || "?";
}
} else if (typeof vscIconWrap === "function") {
var wrap = vscIconWrap(doc, act, 14);
if (wrap) {
@@ -1367,6 +1391,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
btn.textContent = (cdf2 && cdf2.label) || "?";
}
});
updateSubtitleNudgeIndicator(video);
});
}
});
@@ -1395,10 +1420,12 @@ function createControllerButton(doc, action, label, className) {
tc.settings.customButtonIcons[action] &&
tc.settings.customButtonIcons[action].svg;
if (custom) {
var customWrap = doc.createElement("span");
customWrap.className = "vsc-btn-icon";
customWrap.innerHTML = custom;
button.appendChild(customWrap);
var customWrap = vscCreateSvgWrap(doc, custom, "vsc-btn-icon");
if (customWrap) {
button.appendChild(customWrap);
} else {
button.textContent = label || "?";
}
} else if (typeof vscIconWrap === "function") {
var wrap = vscIconWrap(doc, action, 14);
if (wrap) {
@@ -1939,14 +1966,20 @@ function defineVideoController() {
if (subtitleNudgeIndicator) {
updateSubtitleNudgeIndicator(this.video);
}
function blurAfterPointerTap(target, e) {
if (!target || typeof target.blur !== "function") return;
var pt = e.pointerType;
if (pt === "mouse" || pt === "touch" || (!pt && e.detail > 0)) {
requestAnimationFrame(function () {
target.blur();
});
}
}
dragHandle.addEventListener(
"mousedown",
(e) => {
runAction(
e.target.dataset["action"],
getKeyBindings(e.target.dataset["action"], "value"),
e
);
var dragAction = dragHandle.dataset.action;
runAction(dragAction, getKeyBindings(dragAction, "value"), e);
e.stopPropagation();
},
true
@@ -1955,11 +1988,9 @@ function defineVideoController() {
button.addEventListener(
"click",
(e) => {
runAction(
e.target.dataset["action"],
getKeyBindings(e.target.dataset["action"]),
e
);
var action = button.dataset.action;
runAction(action, getKeyBindings(action), e);
blurAfterPointerTap(button, e);
e.stopPropagation();
},
true
@@ -1974,6 +2005,7 @@ function defineVideoController() {
var newState = !isSubtitleNudgeEnabledForVideo(video);
setSubtitleNudgeEnabledForVideo(video, newState);
}
blurAfterPointerTap(subtitleNudgeIndicator, e);
e.stopPropagation();
},
true
@@ -2667,6 +2699,7 @@ function runAction(action, value, e) {
"mark",
"jump",
"drag",
"nudge",
"toggleSubtitleNudge",
"display"
];
@@ -2782,6 +2815,12 @@ function runAction(action, value, e) {
case "toggleSubtitleNudge":
setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue);
break;
case "nudge":
setSubtitleNudgeEnabledForVideo(
v,
!isSubtitleNudgeEnabledForVideo(v)
);
break;
}
});
log("runAction End", 5);
+2 -25
View File
@@ -31,32 +31,9 @@ function sanitizeLucideSvg(svgText) {
var t = String(svgText).replace(/\0/g, "").trim();
if (!/<svg[\s>]/i.test(t)) return null;
var doc = new DOMParser().parseFromString(t, "image/svg+xml");
var svg = doc.querySelector("svg");
if (doc.querySelector("parsererror")) return null;
var svg = vscSanitizeSvgTree(doc.querySelector("svg"));
if (!svg) return null;
svg.querySelectorAll("script").forEach(function (n) {
n.remove();
});
svg.querySelectorAll("style").forEach(function (n) {
n.remove();
});
svg.querySelectorAll("*").forEach(function (el) {
for (var i = el.attributes.length - 1; i >= 0; i--) {
var attr = el.attributes[i];
var name = attr.name.toLowerCase();
var val = attr.value;
if (name.indexOf("on") === 0) {
el.removeAttribute(attr.name);
continue;
}
if (
(name === "href" || name === "xlink:href") &&
/^javascript:/i.test(val)
) {
el.removeAttribute(attr.name);
}
}
});
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.removeAttribute("width");
svg.removeAttribute("height");
svg.setAttribute("width", "100%");
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Speeder",
"short_name": "Speeder",
"version": "5.1.1",
"version": "5.1.6",
"manifest_version": 2,
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
"homepage_url": "https://github.com/SoPat712/speeder",
+45
View File
@@ -545,6 +545,51 @@ label em {
flex-shrink: 0;
}
.cb-icon.cb-icon-nudge-pair {
width: auto;
min-width: 0;
padding: 0 4px;
gap: 4px;
background: transparent;
border: none;
}
.cb-nudge-chip {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
flex-shrink: 0;
color: #fff;
}
.cb-nudge-chip[data-nudge-state="on"] {
background: #4b9135;
border: 1px solid #6ec754;
}
.cb-nudge-chip[data-nudge-state="off"] {
background: #943e3e;
border: 1px solid #c06060;
}
.cb-nudge-chip .vsc-btn-icon svg,
.cb-nudge-chip svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.cb-nudge-sep {
font-size: 11px;
font-weight: 600;
opacity: 0.45;
color: var(--text);
flex-shrink: 0;
}
.row-lucide-pair select {
justify-self: end;
}
+21 -20
View File
@@ -272,11 +272,6 @@
</label>
<input id="hideWithControlsTimer" type="text" placeholder="2" />
</div>
<div class="row row-checkbox">
<label for="showPopupControlBar">Show popup control bar</label>
<input id="showPopupControlBar" type="checkbox" />
</div>
<div class="defaults-divider"></div>
<h4 class="defaults-sub-heading">Subtitle sync</h4>
@@ -350,7 +345,11 @@
Configure which buttons appear in the browser popup control bar.
</p>
</div>
<div class="row">
<div class="row row-checkbox">
<label for="showPopupControlBar">Show popup control bar</label>
<input id="showPopupControlBar" type="checkbox" />
</div>
<div class="row row-checkbox">
<label for="popupMatchHoverControls">Match hover controls</label>
<input id="popupMatchHoverControls" type="checkbox" />
</div>
@@ -383,9 +382,11 @@
rel="noopener noreferrer"
>Lucide</a
>
set (fetched from jsDelivr). Chosen SVGs are cached in local
storage and included in settings export.
<strong>Reset speed</strong> stays numeric text only.
set (fetched from jsDelivr). Custom icons are cached in local
storage and included when you export settings. Subtitle nudge
icons use two menu entries (enabled and disabled), not the bar
block id
<code>nudge</code>.
</p>
</div>
<div class="row row-lucide-pair">
@@ -677,6 +678,17 @@
<div id="status" role="status" aria-live="polite"></div>
</section>
<section id="faq" class="settings-card info-card">
<h4>Extension controls not appearing?</h4>
<p>
This extension only works with HTML5 audio and video. If the
controls never appear, you may be looking at Flash content instead.
Right-click the player to check: if the menu mentions Flash, that
is the issue. Most sites will fall back to HTML5 when Flash is not
available, so disabling Flash in the browser can help.
</p>
</section>
<footer class="support-footer settings-card">
<p>
If Speeder has been useful, consider supporting its development via
@@ -695,17 +707,6 @@
>.
</p>
</footer>
<section id="faq" class="settings-card info-card">
<h4>Extension controls not appearing?</h4>
<p>
This extension only works with HTML5 audio and video. If the
controls never appear, you may be looking at Flash content instead.
Right-click the player to check: if the menu mentions Flash, that
is the issue. Most sites will fall back to HTML5 when Flash is not
available, so disabling Flash in the browser can help.
</p>
</section>
</main>
</div>
</body>
+109 -22
View File
@@ -138,7 +138,7 @@ var controllerButtonDefs = {
faster: { icon: "+", name: "Increase speed" },
advance: { icon: "\u00BB", name: "Advance" },
display: { icon: "\u00D7", name: "Close controller" },
reset: { icon: "", name: "Reset speed" },
reset: { icon: "\u21BB", name: "Reset speed" },
fast: { icon: "\u2605", name: "Preferred speed" },
nudge: { icon: "\u2713", name: "Subtitle nudge" },
settings: { icon: "\u2699", name: "Settings" },
@@ -147,24 +147,76 @@ var controllerButtonDefs = {
mark: { icon: "\u2691", name: "Set marker" },
jump: { icon: "\u21E5", name: "Jump to marker" }
};
var popupExcludedButtonIds = new Set(["settings"]);
/** Lucide picker only — not control-bar blocks (chip uses subtitleNudgeOn/Off). */
var lucideSubtitleNudgeActionLabels = {
subtitleNudgeOn: "Subtitle nudge — enabled",
subtitleNudgeOff: "Subtitle nudge — disabled"
};
function sanitizePopupButtonOrder(buttonIds) {
if (!Array.isArray(buttonIds)) return [];
var seen = new Set();
return buttonIds.filter(function (id) {
if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
return false;
}
seen.add(id);
return true;
});
}
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
var customButtonIconsLive = {};
function fillControlBarIconElement(icon, buttonId) {
if (!icon || !buttonId) return;
var doc = icon.ownerDocument || document;
if (buttonId === "nudge") {
vscClearElement(icon);
icon.className = "cb-icon cb-icon-nudge-pair";
function nudgeChipMarkup(action) {
var c = customButtonIconsLive[action];
if (c && c.svg) return c.svg;
if (typeof vscIconSvgString === "function") {
return vscIconSvgString(action, 14) || "";
}
return "";
}
function appendChip(action, stateKey) {
var sp = document.createElement("span");
sp.className = "cb-nudge-chip";
sp.setAttribute("data-nudge-state", stateKey);
var inner = nudgeChipMarkup(action);
if (inner) {
var wrap = vscCreateSvgWrap(doc, inner, "vsc-btn-icon");
if (wrap) {
sp.appendChild(wrap);
}
}
icon.appendChild(sp);
}
appendChip("subtitleNudgeOn", "on");
var sep = document.createElement("span");
sep.className = "cb-nudge-sep";
sep.textContent = "/";
icon.appendChild(sep);
appendChip("subtitleNudgeOff", "off");
return;
}
icon.className = "cb-icon";
var custom = customButtonIconsLive[buttonId];
if (custom && custom.svg) {
icon.innerHTML = custom.svg;
return;
if (vscSetSvgContent(icon, custom.svg)) return;
}
if (typeof vscIconSvgString === "function") {
var svgHtml = vscIconSvgString(buttonId, 16);
if (svgHtml) {
icon.innerHTML = svgHtml;
return;
if (vscSetSvgContent(icon, svgHtml)) return;
}
}
vscClearElement(icon);
var def = controllerButtonDefs[buttonId];
icon.textContent = (def && def.icon) || "?";
}
@@ -713,7 +765,7 @@ function save_options() {
document.getElementById("showPopupControlBar").checked;
settings.popupMatchHoverControls =
document.getElementById("popupMatchHoverControls").checked;
settings.popupControllerButtons = getPopupControlBarOrder();
settings.popupControllerButtons = sanitizePopupButtonOrder(getPopupControlBarOrder());
// Collect site rules
settings.siteRules = [];
@@ -802,7 +854,9 @@ function save_options() {
ruleEl.querySelector(".site-showPopupControlBar").checked;
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
if (popupActiveZone) {
rule.popupControllerButtons = readControlBarOrder(popupActiveZone);
rule.popupControllerButtons = sanitizePopupButtonOrder(
readControlBarOrder(popupActiveZone)
);
}
}
@@ -1071,7 +1125,10 @@ function createSiteRule(rule) {
populateControlBarZones(
sitePopupActive,
sitePopupAvailable,
rule.popupControllerButtons
sanitizePopupButtonOrder(rule.popupControllerButtons),
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
} else if (
sitePopupActive &&
@@ -1081,7 +1138,10 @@ function createSiteRule(rule) {
populateControlBarZones(
sitePopupActive,
sitePopupAvailable,
getPopupControlBarOrder()
getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
}
}
@@ -1139,16 +1199,23 @@ function createControlBarBlock(buttonId) {
return block;
}
function populateControlBarZones(activeZone, availableZone, activeIds) {
activeZone.innerHTML = "";
availableZone.innerHTML = "";
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
vscClearElement(activeZone);
vscClearElement(availableZone);
var allowed = function (id) {
if (!controllerButtonDefs[id]) return false;
return typeof allowButtonId === "function" ? Boolean(allowButtonId(id)) : true;
};
activeIds.forEach(function (id) {
if (!allowed(id)) return;
var block = createControlBarBlock(id);
if (block) activeZone.appendChild(block);
});
Object.keys(controllerButtonDefs).forEach(function (id) {
if (!allowed(id)) return;
if (!activeIds.includes(id)) {
var block = createControlBarBlock(id);
if (block) availableZone.appendChild(block);
@@ -1176,15 +1243,21 @@ function getControlBarOrder() {
}
function populatePopupControlBarEditor(activeIds) {
var popupActiveIds = sanitizePopupButtonOrder(activeIds);
populateControlBarZones(
document.getElementById("popupControlBarActive"),
document.getElementById("popupControlBarAvailable"),
activeIds
popupActiveIds,
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
}
function getPopupControlBarOrder() {
return readControlBarOrder(document.getElementById("popupControlBarActive"));
return sanitizePopupButtonOrder(
readControlBarOrder(document.getElementById("popupControlBarActive"))
);
}
function updatePopupEditorDisabledState() {
@@ -1321,9 +1394,18 @@ function initLucideButtonIconsUI() {
if (!actionSel.dataset.lucideInit) {
actionSel.dataset.lucideInit = "1";
actionSel.innerHTML = "";
vscClearElement(actionSel);
Object.keys(controllerButtonDefs).forEach(function (aid) {
if (aid === "reset") return;
if (aid === "nudge") {
Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) {
var o2 = document.createElement("option");
o2.value = subId;
o2.textContent =
lucideSubtitleNudgeActionLabels[subId] + " (" + subId + ")";
actionSel.appendChild(o2);
});
return;
}
var o = document.createElement("option");
o.value = aid;
o.textContent =
@@ -1333,7 +1415,7 @@ function initLucideButtonIconsUI() {
}
function renderResults(slugs) {
resultsEl.innerHTML = "";
vscClearElement(resultsEl);
slugs.forEach(function (slug) {
var b = document.createElement("button");
b.type = "button";
@@ -1368,11 +1450,13 @@ function initLucideButtonIconsUI() {
.then(function (txt) {
var safe = sanitizeLucideSvg(txt);
if (!safe) throw new Error("Bad SVG");
previewEl.innerHTML = safe;
if (!vscSetSvgContent(previewEl, safe)) {
throw new Error("Preview render failed");
}
setLucideStatus("Preview: " + slug);
})
.catch(function (e) {
previewEl.innerHTML = "";
vscClearElement(previewEl);
setLucideStatus(
"Could not load: " + slug + " — " + e.message
);
@@ -1391,7 +1475,7 @@ function initLucideButtonIconsUI() {
.then(function (map) {
var q = searchInput.value;
if (!q.trim()) {
resultsEl.innerHTML = "";
vscClearElement(resultsEl);
return;
}
renderResults(searchLucideSlugs(map, q, 48));
@@ -1556,7 +1640,7 @@ function restore_options() {
? storage.siteRules
: tcDefaults.siteRules || [];
document.getElementById("siteRulesContainer").innerHTML = "";
vscClearElement(document.getElementById("siteRulesContainer"));
if (siteRules.length > 0) {
siteRules.forEach((rule) => {
if (rule && rule.pattern) {
@@ -1771,7 +1855,10 @@ document.addEventListener("DOMContentLoaded", function () {
populateControlBarZones(
popupActiveZone,
popupAvailableZone,
getPopupControlBarOrder()
getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
);
}
} else {
+32 -15
View File
@@ -8,8 +8,9 @@ document.addEventListener("DOMContentLoaded", function () {
faster: { label: "", className: "" },
advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" },
reset: { label: "", className: "" },
reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" },
nudge: { label: "", className: "" },
settings: { label: "", className: "" },
pause: { label: "", className: "" },
muted: { label: "", className: "" },
@@ -18,6 +19,7 @@ document.addEventListener("DOMContentLoaded", function () {
};
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
var popupExcludedButtonIds = new Set(["settings"]);
var storageDefaults = {
enabled: true,
showPopupControlBar: true,
@@ -64,25 +66,37 @@ document.addEventListener("DOMContentLoaded", function () {
}
function resolvePopupButtons(storage, siteRule) {
function sanitize(buttons) {
if (!Array.isArray(buttons)) return [];
var seen = new Set();
return buttons.filter(function (id) {
if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
return false;
}
seen.add(id);
return true;
});
}
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
return siteRule.popupControllerButtons;
return sanitize(siteRule.popupControllerButtons);
}
if (storage.popupMatchHoverControls) {
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
return siteRule.controllerButtons;
return sanitize(siteRule.controllerButtons);
}
if (Array.isArray(storage.controllerButtons)) {
return storage.controllerButtons;
return sanitize(storage.controllerButtons);
}
}
if (Array.isArray(storage.popupControllerButtons)) {
return storage.popupControllerButtons;
return sanitize(storage.popupControllerButtons);
}
return defaultButtons;
return sanitize(defaultButtons);
}
function setControlBarVisible(visible) {
@@ -209,7 +223,6 @@ document.addEventListener("DOMContentLoaded", function () {
var customMap = customIconsMap || {};
buttons.forEach(function (btnId) {
if (btnId === "nudge") return;
var def = controllerButtonDefs[btnId];
if (!def) return;
@@ -217,17 +230,21 @@ document.addEventListener("DOMContentLoaded", function () {
btn.dataset.action = btnId;
var customEntry = customMap[btnId];
if (customEntry && customEntry.svg) {
var customSpan = document.createElement("span");
customSpan.className = "vsc-btn-icon";
customSpan.innerHTML = customEntry.svg;
btn.appendChild(customSpan);
var customSpan = vscCreateSvgWrap(document, customEntry.svg, "vsc-btn-icon");
if (customSpan) {
btn.appendChild(customSpan);
} else {
btn.textContent = def.label || "?";
}
} else if (typeof vscIconSvgString === "function") {
var svgStr = vscIconSvgString(btnId, 16);
if (svgStr) {
var iconSpan = document.createElement("span");
iconSpan.className = "vsc-btn-icon";
iconSpan.innerHTML = svgStr;
btn.appendChild(iconSpan);
var iconSpan = vscCreateSvgWrap(document, svgStr, "vsc-btn-icon");
if (iconSpan) {
btn.appendChild(iconSpan);
} else {
btn.textContent = def.label || "?";
}
} else {
btn.textContent = def.label || "?";
}
+4 -1
View File
@@ -10,8 +10,11 @@
line-height: 1;
}
/* Show extra buttons on hover or keyboard :focus-visible only. Plain :focus-within
after a mouse click kept #controls visible while hover-only rules (e.g. draggable
margin) turned off when the pointer left the bar. */
#controller:hover #controls,
#controller:focus-within #controls,
#controller:focus-within:has(:focus-visible) #controls,
:host(:hover) #controls {
display: inline-flex;
vertical-align: middle;
+75 -4
View File
@@ -3,6 +3,7 @@
* Use stroke="currentColor" so buttons inherit foreground for monochrome UI.
*/
var VSC_ICON_SIZE_DEFAULT = 18;
var VSC_SVG_NS = "http://www.w3.org/2000/svg";
/** Inner SVG markup only (paths / shapes inside <svg>). */
var vscUiIconPaths = {
@@ -54,6 +55,79 @@ function vscIconSvgString(action, size) {
);
}
function vscClearElement(el) {
if (!el) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
function vscSanitizeSvgTree(svg) {
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
svg.querySelectorAll("script, style, foreignObject").forEach(function (n) {
n.remove();
});
svg.querySelectorAll("*").forEach(function (el) {
for (var i = el.attributes.length - 1; i >= 0; i--) {
var attr = el.attributes[i];
var name = attr.name.toLowerCase();
var val = attr.value;
if (name.indexOf("on") === 0) {
el.removeAttribute(attr.name);
continue;
}
if (
(name === "href" || name === "xlink:href") &&
/^\s*javascript:/i.test(val)
) {
el.removeAttribute(attr.name);
}
}
});
svg.setAttribute("xmlns", VSC_SVG_NS);
svg.setAttribute("aria-hidden", "true");
return svg;
}
function vscCreateSvgNode(doc, svgText) {
if (!doc || !svgText || typeof svgText !== "string") return null;
var clean = String(svgText).replace(/\0/g, "").trim();
if (!clean || !/<svg[\s>]/i.test(clean)) return null;
var parsed = new DOMParser().parseFromString(clean, "image/svg+xml");
if (parsed.querySelector("parsererror")) return null;
var svg = vscSanitizeSvgTree(parsed.querySelector("svg"));
if (!svg) return null;
return doc.importNode(svg, true);
}
function vscSetSvgContent(el, svgText) {
if (!el) return false;
vscClearElement(el);
var doc = el.ownerDocument || document;
var svg = vscCreateSvgNode(doc, svgText);
if (!svg) return false;
el.appendChild(svg);
return true;
}
function vscCreateSvgWrap(doc, svgText, className) {
if (!doc) return null;
var span = doc.createElement("span");
span.className = className || "vsc-btn-icon";
if (!vscSetSvgContent(span, svgText)) {
return null;
}
return span;
}
/**
* @param {Document} doc
* @param {string} action
@@ -62,8 +136,5 @@ function vscIconSvgString(action, size) {
function vscIconWrap(doc, action, size) {
var html = vscIconSvgString(action, size);
if (!html) return null;
var span = doc.createElement("span");
span.className = "vsc-btn-icon";
span.innerHTML = html;
return span;
return vscCreateSvgWrap(doc, html, "vsc-btn-icon");
}