Remove dynamic style system and tighten inline style checks

This commit is contained in:
thePR0M3TH3AN committed 2025-10-14 08:48:17 -04:00
1 parent 861a9036d3
commit dfa4a98f0a
6 files changed
+40 -295

No files matched your search

-253
View File
@@ -1,253 +0,0 @@
const STYLE_TAG_ID = "bitvid-style-system";
const sheetRegistry = new WeakMap();
const styleCache = new Map();
const elementClassRegistry = new WeakMap();
const classDefinitionRegistry = new Map();
let classSequence = 0;
function resolveHTMLElementConstructor(documentRef) {
return (
documentRef?.defaultView?.HTMLElement ||
globalThis.HTMLElement ||
null
);
}
function isElementCandidate(element) {
return element && typeof element === "object";
}
function isHTMLElement(element, documentRef) {
const Constructor = resolveHTMLElementConstructor(documentRef);
if (!Constructor) {
return false;
}
return element instanceof Constructor;
}
function ensureDocument(element) {
if (element && element.ownerDocument) {
return element.ownerDocument;
}
if (typeof document !== "undefined") {
return document;
}
return null;
}
function ensureStyleSheet(documentRef) {
if (!documentRef) {
return null;
}
if (sheetRegistry.has(documentRef)) {
return sheetRegistry.get(documentRef);
}
const root = documentRef.documentElement || documentRef.body;
const head = documentRef.head || documentRef.getElementsByTagName("head")[0];
if (!head && root) {
// If the document does not yet have a <head>, create one so styles attach cleanly.
const createdHead = documentRef.createElement("head");
documentRef.insertBefore(createdHead, root);
}
const container = documentRef.getElementById(STYLE_TAG_ID);
const StyleElementConstructor =
documentRef.defaultView?.HTMLStyleElement ||
globalThis.HTMLStyleElement ||
null;
const isStyleElement =
(StyleElementConstructor && container instanceof StyleElementConstructor) ||
(!StyleElementConstructor && container?.tagName === "STYLE");
const styleElement = isStyleElement
? container
: documentRef.createElement("style");
styleElement.type = "text/css";
styleElement.id = STYLE_TAG_ID;
if (!styleElement.parentNode) {
(documentRef.head || documentRef.documentElement)?.appendChild(styleElement);
}
const sheet = styleElement.sheet;
sheetRegistry.set(documentRef, sheet);
return sheet;
}
function toCssPropertyName(property) {
if (!property) {
return "";
}
if (property.startsWith("--")) {
return property;
}
return property
.replace(/([A-Z])/g, "-$1")
.replace(/_/g, "-")
.toLowerCase();
}
function serializeStyleObject(styleObject) {
return Object.keys(styleObject)
.sort()
.map((key) => `${key}:${String(styleObject[key])}`)
.join(";");
}
function createCssRule(styleObject) {
return Object.keys(styleObject)
.sort()
.map((key) => `${toCssPropertyName(key)}: ${styleObject[key]};`)
.join(" ");
}
function getSlotKey(slot) {
if (!slot) {
return "default";
}
return String(slot);
}
export function applyDynamicStyles(element, styleObject = {}, options = {}) {
if (!isElementCandidate(element)) {
return null;
}
const doc = ensureDocument(element);
if (!doc || !isHTMLElement(element, doc)) {
return null;
}
const entries = Object.entries(styleObject).filter(([, value]) => value !== null && value !== undefined);
if (!entries.length) {
return null;
}
const normalized = entries.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
const sheet = ensureStyleSheet(doc);
if (!sheet) {
return null;
}
const cacheKey = serializeStyleObject(normalized);
let className = styleCache.get(cacheKey);
if (!className) {
classSequence += 1;
className = `bvds-${classSequence.toString(36)}`;
const cssRule = createCssRule(normalized);
try {
sheet.insertRule(`.${className} { ${cssRule} }`, sheet.cssRules.length);
} catch (error) {
console.warn("Failed to insert dynamic style", { cssRule, error });
return null;
}
styleCache.set(cacheKey, className);
classDefinitionRegistry.set(className, { ...normalized });
}
let slotRegistry = elementClassRegistry.get(element);
if (!slotRegistry) {
slotRegistry = new Map();
elementClassRegistry.set(element, slotRegistry);
}
const slot = getSlotKey(options.slot);
const previousClass = slotRegistry.get(slot);
if (previousClass && previousClass !== className) {
element.classList.remove(previousClass);
}
if (!element.classList.contains(className)) {
element.classList.add(className);
}
slotRegistry.set(slot, className);
return className;
}
export function removeDynamicStyles(element, options = {}) {
if (!isElementCandidate(element)) {
return;
}
const doc = ensureDocument(element);
if (!doc || !isHTMLElement(element, doc)) {
return;
}
const slot = getSlotKey(options.slot);
const slotRegistry = elementClassRegistry.get(element);
if (!slotRegistry) {
return;
}
const className = slotRegistry.get(slot);
if (className) {
element.classList.remove(className);
slotRegistry.delete(slot);
}
if (!slotRegistry.size) {
elementClassRegistry.delete(element);
}
}
export function clearAllDynamicStyles(element) {
if (!isElementCandidate(element)) {
return;
}
const doc = ensureDocument(element);
if (!doc || !isHTMLElement(element, doc)) {
return;
}
const slotRegistry = elementClassRegistry.get(element);
if (!slotRegistry) {
return;
}
for (const className of slotRegistry.values()) {
element.classList.remove(className);
}
elementClassRegistry.delete(element);
}
export function getAppliedDynamicStyles(element, options = {}) {
if (!isElementCandidate(element)) {
return null;
}
const doc = ensureDocument(element);
if (!doc || !isHTMLElement(element, doc)) {
return null;
}
const slot = getSlotKey(options.slot);
const slotRegistry = elementClassRegistry.get(element);
if (!slotRegistry) {
return null;
}
const className = slotRegistry.get(slot);
if (!className) {
return null;
}
const definition = classDefinitionRegistry.get(className);
if (!definition) {
return null;
}
return { ...definition };
}
+2 -4
View File
@@ -32,7 +32,6 @@ const DIST_SCAN_PREFIXES = new Set([
const DIST_SCAN_PREFIXES_ARRAY = [...DIST_SCAN_PREFIXES];
const IGNORED_FILES = new Set([
"js/ui/styleSystem.js",
"css/tailwind.generated.css",
"js/webtorrent.min.js",
"js/webtorrent.min.js.map",
@@ -45,6 +44,7 @@ const VIOLATION_ALLOWLIST = new Map([
"torrent/dist/beacon.vendor.js",
new Set(["Direct .style usage", "style.cssText usage"]),
],
["js/ui/utils/positionFloatingPanel.js", new Set(["Direct .style usage"])],
]);
const TEXT_EXTENSIONS = new Set([
@@ -220,9 +220,7 @@ async function main() {
`${violation.file}:${violation.line} — ${violation.label}\n ${violation.snippet}\n`
);
}
console.error(
"Inline styles are disallowed. Move styles into tokens, CSS, or styleSystem."
);
console.error("Inline styles are disallowed. Move styles into tokens or CSS.");
process.exitCode = 1;
return;
}
+22 -14
View File
@@ -3,11 +3,16 @@ import { beforeEach, afterEach, describe, test } from 'node:test';
import { advanceSlide, createCarousel, destroyCarousel } from '../js/blog/carousel.js';
import { mountProgressBar } from '../js/blog/progressBar.js';
import {
createBlogDomEnvironment,
createSpy,
readDynamicStyleRules,
} from './test-helpers/blog-dom.mjs';
import { createBlogDomEnvironment, createSpy } from './test-helpers/blog-dom.mjs';
function assertHasClasses(element, classes, messagePrefix = 'element') {
classes.forEach((className) => {
assert.ok(
element.classList.contains(className),
`${messagePrefix} should include the “${className}” class`,
);
});
}
function assertNoInlineStyleAttributes(elements, messagePrefix = 'element') {
elements.forEach((element, index) => {
@@ -208,8 +213,12 @@ describe('progress bar', () => {
const wrapper = container.querySelector('.blog-carousel__progress');
assert.ok(wrapper, 'wrapper element should be appended');
assertHasClasses(wrapper, ['blog-carousel__progress', 'blog-progress', 'mt-2'], 'progress wrapper');
assert.strictEqual(wrapper.dataset.carouselProgress, 'true');
assert.strictEqual(wrapper.getAttribute('data-carousel-progress'), 'true');
const meter = wrapper.querySelector('progress.progress--blog');
assert.ok(meter, 'progress meter should be created');
assertHasClasses(meter, ['progress', 'progress--blog'], 'progress meter');
assert.strictEqual(meter.max, 100, 'progress meter should expose a 0-100 range');
assert.strictEqual(meter.value, 0, 'progress meter should start at zero');
assert.strictEqual(meter.dataset.state, 'idle');
@@ -220,12 +229,12 @@ describe('progress bar', () => {
'Carousel progress 0% complete',
'progress meter should emit accessible status text',
);
assert.strictEqual(meter.getAttribute('data-progress-meter'), 'true');
assert.strictEqual(meter.dataset.progressMeter, 'true');
assert.strictEqual(meter.getAttribute('data-variant'), 'blog');
progress.reset();
assert.strictEqual(readDynamicStyleRules(env.document), '', 'no dynamic style rules should be injected');
assert.strictEqual(env.document.getElementById('bitvid-style-system'), null);
assertNoInlineStyleAttributes([container, wrapper, meter], 'progress element');
});
@@ -249,9 +258,6 @@ describe('progress bar', () => {
assert.strictEqual(meter.value, 100);
assert.strictEqual(meter.dataset.state, 'complete');
assert.strictEqual(meter.dataset.progress, '100');
assert.strictEqual(readDynamicStyleRules(env.document), '', 'no dynamic style rules should be injected');
assert.strictEqual(env.document.getElementById('bitvid-style-system'), null);
});
test('manual controls support stop, restart, and cleanup', () => {
@@ -281,9 +287,11 @@ describe('progress bar', () => {
progress.destroy();
assert.strictEqual(container.children.length, 0, 'destroy should remove wrapper from DOM');
assert.strictEqual(readDynamicStyleRules(env.document), '', 'no dynamic style rules should be injected');
assert.strictEqual(env.document.getElementById('bitvid-style-system'), null);
assert.strictEqual(
container.querySelector('[data-carousel-progress]'),
null,
'destroy should remove declarative progress markup',
);
assertNoInlineStyleAttributes([container], 'progress container');
});
});
+2 -2
View File
@@ -188,8 +188,8 @@ test("MediaLoader clears unsupported lazy targets without inline styles", async
]);
assert.equal(
div.style.backgroundImage,
"",
div.hasAttribute("style"),
false,
"background image styles should not be injected at runtime",
);
assert.ok(!observer.observed.has(div));
+14 -6
View File
@@ -3,6 +3,14 @@ import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import positionFloatingPanel from "../js/ui/utils/positionFloatingPanel.js";
function getInlineStyleValue(element, property) {
const styleAttr = element.getAttribute("style") || "";
const escapedProperty = property.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
const pattern = new RegExp(`(?:^|;)\\s*${escapedProperty}\\s*:\\s*([^;]+)`, "i");
const match = styleAttr.match(pattern);
return match ? match[1].trim() : "";
}
test("flips to top when bottom placement collides with viewport", () => {
const dom = new JSDOM(
`
@@ -54,9 +62,9 @@ test("flips to top when bottom placement collides with viewport", () => {
positioner.update();
assert.equal(panel.dataset.floatingPlacement, "top");
assert.equal(panel.style.position, "fixed");
assert.equal(panel.style.getPropertyValue("--floating-fallback-top"), "132px");
assert.equal(panel.style.getPropertyValue("--floating-fallback-left"), "100px");
assert.equal(getInlineStyleValue(panel, "position"), "fixed");
assert.equal(getInlineStyleValue(panel, "--floating-fallback-top"), "132px");
assert.equal(getInlineStyleValue(panel, "--floating-fallback-left"), "100px");
positioner.destroy();
assert.equal(panel.dataset.floatingPanel, undefined);
@@ -118,7 +126,7 @@ test("respects RTL alignment and clamps within the viewport", () => {
// In RTL mode, start alignment hugs the right edge of the trigger.
assert.equal(panel.dataset.floatingPlacement, "bottom");
assert.equal(panel.dataset.floatingDir, "rtl");
assert.equal(panel.style.getPropertyValue("--floating-fallback-left"), "140px");
assert.equal(getInlineStyleValue(panel, "--floating-fallback-left"), "140px");
positioner.destroy();
});
@@ -182,12 +190,12 @@ test("updates when scroll containers move the trigger", async () => {
panel.dataset.state = "open";
positioner.update();
assert.equal(panel.style.getPropertyValue("--floating-fallback-top"), "152px");
assert.equal(getInlineStyleValue(panel, "--floating-fallback-top"), "152px");
assert.ok(scrollHandler, "scroll listener should be registered");
anchorTop = 60;
scrollHandler?.({ type: "scroll" });
assert.equal(panel.style.getPropertyValue("--floating-fallback-top"), "112px");
assert.equal(getInlineStyleValue(panel, "--floating-fallback-top"), "112px");
positioner.destroy();
window.addEventListener = originalAddEventListener;
-16
View File
@@ -157,22 +157,6 @@ export function installInlineStyleGuard(document) {
};
}
export function readDynamicStyleRules(document) {
const styleElement = document.getElementById('bitvid-style-system');
const sheet = styleElement?.sheet;
if (!sheet) {
return '';
}
try {
return Array.from(sheet.cssRules)
.map((rule) => rule.cssText)
.join('\n');
} catch (error) {
return '';
}
}
export function createBlogDomEnvironment(html) {
const dom = new JSDOM(html, { url: 'https://example.com' });
const { window } = dom;