From 0bfc6c107bf05e95cfac7a95824dc83edd823e49 Mon Sep 17 00:00:00 2001 From: Olav Date: Wed, 2 Jul 2025 00:00:16 +0200 Subject: [PATCH 1/4] selector & --- cssr.js | 103 ++++++++++++++++++++++++++++++++++--------------- selectors.html | 58 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 31 deletions(-) create mode 100644 selectors.html diff --git a/cssr.js b/cssr.js index 768198d..e73c38d 100644 --- a/cssr.js +++ b/cssr.js @@ -1,49 +1,90 @@ (() => { if (document.__cssr_mo) return; - let styleEl = document.head.appendChild(document.createElement("style")); - let gen = new Set(); - let newRules = []; - let processNode = (node) => { - (node.getAttribute("class") || "") - .split(/\s+/) - .filter((t) => t.includes(":")) - .forEach((token) => { - if (gen.has(token)) return; - - let [prop, val] = token.split(/:(.+)/); - let cssValue = val.replace(/_/g, " "); - - if (!CSS.supports(prop, cssValue)) { - console.warn(`Unsupported CSS: ${prop}: ${cssValue}`); - return; + let styleElement = document.head.appendChild(document.createElement("style")); + let generatedTokens = new Set(); + let pendingRules = []; + + let escapeClassSelector = (className) => { + return "." + CSS.escape(className); + }; + + let parseDeclarations = (styleString) => { + if (!styleString) return []; + return styleString + .split(";") + .map((decl) => { + let [property, value] = decl.split(/:(.+)/); + if (!property || !value) return null; + return [property.trim(), value.replace(/_/g, " ").trim()]; + }) + .filter(Boolean); + }; + + let queueRule = (selector, declarations, token) => { + if (declarations.length === 0) return; + let ruleBody = declarations + .map(([prop, val]) => { + if (!CSS.supports || !CSS.supports(prop, val)) { + console.warn(`(Warning) Possibly unsupported CSS: ${prop}: ${val}`); } + return `${prop}: ${val};`; + }) + .join(" "); + pendingRules.push(`${selector} { ${ruleBody} }`); + generatedTokens.add(token); + }; - let rule = `.${CSS.escape(token)} { ${prop}: ${cssValue}; }`; - newRules.push(rule); - gen.add(token); - }); + let processClassToken = (token) => { + if (generatedTokens.has(token)) return; + if (!token.includes(":")) return; + if (token.startsWith("&")) { + let firstColon = token.indexOf(":"); + let selectorPart = token.slice(0, firstColon); + let stylePart = token.slice(firstColon + 1); + let baseSelector = escapeClassSelector(token); + let restSelector = selectorPart + .slice(1) + .replace(/_>_/g, " > ") + .replace(/_/g, " "); + let fullSelector = baseSelector + restSelector; + let declarations = parseDeclarations(stylePart); + queueRule(fullSelector, declarations, token); + } else { + let firstColon = token.indexOf(":"); + let property = token.slice(0, firstColon); + let value = token.slice(firstColon + 1).replace(/_/g, " "); + let selector = escapeClassSelector(token); + queueRule(selector, [[property, value]], token); + } + }; + + let processNode = (node) => { + let classAttr = node.getAttribute("class") || ""; + let tokens = classAttr.split(/\s+/).filter((t) => t.length > 0); + tokens.forEach(processClassToken); + flushRules(); }; - let updateStyle = () => { - if (newRules.length > 0) { - styleEl.textContent += "\n" + newRules.join("\n"); - newRules = []; + let flushRules = () => { + if (pendingRules.length > 0) { + styleElement.textContent += "\n" + pendingRules.join("\n"); + pendingRules = []; } }; let processTree = (root) => { - root.querySelectorAll('[class*=":"]').forEach((node) => processNode(node)); - updateStyle(); + root.querySelectorAll('[class*=":"]').forEach(processNode); + flushRules(); }; - document.__cssr_mo = new MutationObserver((muts) => { - muts.forEach((m) => - m.addedNodes.forEach( - (n) => n.nodeType === 1 && (processNode(n), processTree(n)) + document.__cssr_mo = new MutationObserver((mutations) => { + mutations.forEach((mutation) => + mutation.addedNodes.forEach( + (node) => node.nodeType === 1 && (processNode(node), processTree(node)) ) ); - updateStyle(); + flushRules(); }); document.addEventListener("DOMContentLoaded", () => { diff --git a/selectors.html b/selectors.html new file mode 100644 index 0000000..19cd30d --- /dev/null +++ b/selectors.html @@ -0,0 +1,58 @@ + + + + + + Runtime CSS Generator with Regex Token Extraction + + + + + +
+ Styled box +
+
+ Calc box +
+
Border box
+ + + +
+
+
+
+ Title +
+ Subtitle +

Subsubtitle

+
+
+ +
+ Title +
+ Subtitle +

Subsubtitle

+
+
+ + From b28c1d560e74a0991e4b730552444f4c06e5c542 Mon Sep 17 00:00:00 2001 From: Olav Date: Wed, 2 Jul 2025 00:03:36 +0200 Subject: [PATCH 2/4] cssar namechange --- cssr.js => cssar.js | 6 +++--- index.html | 4 ++-- selectors.html | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename cssr.js => cssar.js (95%) diff --git a/cssr.js b/cssar.js similarity index 95% rename from cssr.js rename to cssar.js index e73c38d..6cc1bf2 100644 --- a/cssr.js +++ b/cssar.js @@ -1,5 +1,5 @@ (() => { - if (document.__cssr_mo) return; + if (document.__cssar_mo) return; let styleElement = document.head.appendChild(document.createElement("style")); let generatedTokens = new Set(); @@ -78,7 +78,7 @@ flushRules(); }; - document.__cssr_mo = new MutationObserver((mutations) => { + document.__cssar_mo = new MutationObserver((mutations) => { mutations.forEach((mutation) => mutation.addedNodes.forEach( (node) => node.nodeType === 1 && (processNode(node), processTree(node)) @@ -89,7 +89,7 @@ document.addEventListener("DOMContentLoaded", () => { processTree(document.body); - document.__cssr_mo.observe(document.body, { + document.__cssar_mo.observe(document.body, { childList: true, subtree: true, }); diff --git a/index.html b/index.html index fd5c059..c8eb28b 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Runtime CSS Generator with Regex Token Extraction + Runtime CSS Generator - + diff --git a/selectors.html b/selectors.html index 19cd30d..ecb495b 100644 --- a/selectors.html +++ b/selectors.html @@ -3,7 +3,7 @@ - Runtime CSS Generator with Regex Token Extraction + Runtime CSS Generator - +
Date: Mon, 7 Jul 2025 16:55:48 +0200 Subject: [PATCH 3/4] sophisticated handletoken --- cssar.js | 186 ++++++++++++++++++++++++++++++------------------- selectors.html | 36 ++++++++-- 2 files changed, 144 insertions(+), 78 deletions(-) diff --git a/cssar.js b/cssar.js index 6cc1bf2..7030dde 100644 --- a/cssar.js +++ b/cssar.js @@ -1,97 +1,141 @@ (() => { if (document.__cssar_mo) return; - let styleElement = document.head.appendChild(document.createElement("style")); - let generatedTokens = new Set(); - let pendingRules = []; + const styleEl = document.head.appendChild(document.createElement('style')); + const seen = new Set(); + const rules = []; - let escapeClassSelector = (className) => { - return "." + CSS.escape(className); - }; + const escapeClass = cls => '.' + CSS.escape(cls); - let parseDeclarations = (styleString) => { - if (!styleString) return []; - return styleString - .split(";") - .map((decl) => { - let [property, value] = decl.split(/:(.+)/); - if (!property || !value) return null; - return [property.trim(), value.replace(/_/g, " ").trim()]; - }) - .filter(Boolean); - }; + const parseDecls = str => + str + ? str + .split(';') + .map(d => d.split(/:(.+)/)) + .filter(([p, v]) => p && v != null) + .map(([p, v]) => { + let val = v.replace(/_/g, ' ').trim(); + // space‑wrap math ops between non‑spaces + val = val.replace(/(\S)([+\-*/])(\S)/g, '$1 $2 $3'); + return [p.trim(), val]; + }) + : []; - let queueRule = (selector, declarations, token) => { - if (declarations.length === 0) return; - let ruleBody = declarations - .map(([prop, val]) => { - if (!CSS.supports || !CSS.supports(prop, val)) { - console.warn(`(Warning) Possibly unsupported CSS: ${prop}: ${val}`); - } - return `${prop}: ${val};`; - }) - .join(" "); - pendingRules.push(`${selector} { ${ruleBody} }`); - generatedTokens.add(token); + const addRule = (sel, decls, tok) => { + if (!decls.length || seen.has(tok)) return; + rules.push(`${sel}{${decls.map(([p, v])=>`${p}:${v};`).join('')}}`); + seen.add(tok); }; - let processClassToken = (token) => { - if (generatedTokens.has(token)) return; - if (!token.includes(":")) return; - if (token.startsWith("&")) { - let firstColon = token.indexOf(":"); - let selectorPart = token.slice(0, firstColon); - let stylePart = token.slice(firstColon + 1); - let baseSelector = escapeClassSelector(token); - let restSelector = selectorPart - .slice(1) - .replace(/_>_/g, " > ") - .replace(/_/g, " "); - let fullSelector = baseSelector + restSelector; - let declarations = parseDeclarations(stylePart); - queueRule(fullSelector, declarations, token); + const handleToken = (token) => { + // 1) Skip if already seen or not a CSS token + if (seen.has(token) || !token.includes(':')) return; + + // 2) Which pseudos we support + const pseudos = ['hover','active','focus','visited','disabled','required']; + + // 3) Split on the *first* colon only + // e.g. ['hover', '#submitBtn:background-color:green;color:white'] + const [selPart, rest] = token.split(/:(.+)/); + + if (selPart.startsWith('&')) { + // ─── Nested selector branch ─── + // Strip leading &, handle > + ~ and underscores + const raw = selPart.slice(1); + const path = raw + .replace(/([>+~])/g, ' $1 ') + .replace(/_/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + addRule( + escapeClass(token) + path, + parseDecls(rest), + token + ); + + } else if (pseudos.includes(selPart)) { + // ─── Pseudo‑class branch ─── + // rest might be: + // a) property:value… (apply to the element itself) + // b) selector:property:value… (apply to a nested selector) + const [maybeSel, declsPart] = rest.split(/:(.+)/); + + if (/^[#.]/.test(maybeSel) || maybeSel.startsWith('&')) { + // 3a) Pseudo on a nested selector + const raw = maybeSel.startsWith('&') + ? maybeSel.slice(1) + : maybeSel; + const path = raw + .replace(/([>+~])/g, ' $1 ') + .replace(/_/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + addRule( + // .:hover
+ `${escapeClass(token)}:${selPart}${path}`, + parseDecls(declsPart), + token + ); + } else { + // 3b) Pseudo on the element itself + addRule( + // .:hover + `${escapeClass(token)}:${selPart}`, + parseDecls(rest), + token + ); + } + } else { - let firstColon = token.indexOf(":"); - let property = token.slice(0, firstColon); - let value = token.slice(firstColon + 1).replace(/_/g, " "); - let selector = escapeClassSelector(token); - queueRule(selector, [[property, value]], token); + // ─── Simple property:value branch ─── + // single declaration—do math‑spacing & underscore conversion + const property = selPart.trim(); + let value = rest + .replace(/_/g, ' ') + .replace(/(\S)([+\-*/])(\S)/g, '$1 $2 $3') + .trim(); + + addRule( + escapeClass(token), + [[property, value]], + token + ); } }; + - let processNode = (node) => { - let classAttr = node.getAttribute("class") || ""; - let tokens = classAttr.split(/\s+/).filter((t) => t.length > 0); - tokens.forEach(processClassToken); - flushRules(); - }; + const processNode = n => + (n.className || '') + .split(/\s+/) + .filter(Boolean) + .forEach(handleToken); - let flushRules = () => { - if (pendingRules.length > 0) { - styleElement.textContent += "\n" + pendingRules.join("\n"); - pendingRules = []; - } - }; - - let processTree = (root) => { + const processTree = root => root.querySelectorAll('[class*=":"]').forEach(processNode); - flushRules(); + + const flush = () => { + if (!rules.length) return; + styleEl.textContent += '\n' + rules.join('\n'); + rules.length = 0; }; - document.__cssar_mo = new MutationObserver((mutations) => { - mutations.forEach((mutation) => - mutation.addedNodes.forEach( - (node) => node.nodeType === 1 && (processNode(node), processTree(node)) + document.__cssar_mo = new MutationObserver(muts => { + muts.forEach(m => + m.addedNodes.forEach(n => + n.nodeType === 1 && (processNode(n), processTree(n)) ) ); - flushRules(); + flush(); }); - document.addEventListener("DOMContentLoaded", () => { + document.addEventListener('DOMContentLoaded', () => { processTree(document.body); document.__cssar_mo.observe(document.body, { childList: true, - subtree: true, + subtree: true }); + flush(); }); })(); diff --git a/selectors.html b/selectors.html index ecb495b..dd45f5d 100644 --- a/selectors.html +++ b/selectors.html @@ -23,7 +23,7 @@
Calc box
@@ -31,7 +31,7 @@ @@ -39,7 +39,7 @@


-
+
Title
Subtitle @@ -47,12 +47,34 @@

Subsubtitle

-
- Title +
+ Title222
- Subtitle -

Subsubtitle

+ Subtitle222 +

Subsubtitle222

+
+
+ + + +
+ Title222 +
+ Subtitle222 +

Subsubtitle222

+ +
Submit
+
+ + From 9638b606c2e2d6b9374fbd70c56c882bb3185037 Mon Sep 17 00:00:00 2001 From: Olav Date: Tue, 8 Jul 2025 22:51:25 +0200 Subject: [PATCH 4/4] cssar test --- cssar.js | 172 +++++++++++------------- selectors.html | 349 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 372 insertions(+), 149 deletions(-) diff --git a/cssar.js b/cssar.js index 7030dde..48ba56f 100644 --- a/cssar.js +++ b/cssar.js @@ -1,140 +1,124 @@ (() => { if (document.__cssar_mo) return; - const styleEl = document.head.appendChild(document.createElement('style')); + const styleEl = document.head.appendChild(document.createElement("style")); const seen = new Set(); const rules = []; - const escapeClass = cls => '.' + CSS.escape(cls); + const escapeClass = (cls) => "." + CSS.escape(cls); - const parseDecls = str => + const parseDecls = (str) => str ? str - .split(';') - .map(d => d.split(/:(.+)/)) + .split(";") + .map((d) => d.split(/:(.+)/)) .filter(([p, v]) => p && v != null) .map(([p, v]) => { - let val = v.replace(/_/g, ' ').trim(); + let val = v.replace(/_/g, " ").trim(); // space‑wrap math ops between non‑spaces - val = val.replace(/(\S)([+\-*/])(\S)/g, '$1 $2 $3'); + val = val.replace(/(\S)([+\-*/])(\S)/g, "$1 $2 $3"); return [p.trim(), val]; }) : []; const addRule = (sel, decls, tok) => { if (!decls.length || seen.has(tok)) return; - rules.push(`${sel}{${decls.map(([p, v])=>`${p}:${v};`).join('')}}`); + rules.push(`${sel}{${decls.map(([p, v]) => `${p}:${v};`).join("")}}`); seen.add(tok); }; const handleToken = (token) => { - // 1) Skip if already seen or not a CSS token - if (seen.has(token) || !token.includes(':')) return; - - // 2) Which pseudos we support - const pseudos = ['hover','active','focus','visited','disabled','required']; - - // 3) Split on the *first* colon only - // e.g. ['hover', '#submitBtn:background-color:green;color:white'] - const [selPart, rest] = token.split(/:(.+)/); - - if (selPart.startsWith('&')) { - // ─── Nested selector branch ─── - // Strip leading &, handle > + ~ and underscores - const raw = selPart.slice(1); + if (seen.has(token) || !token.includes(":")) return; + + // our supported pseudos + const pseudos = new Set([ + "hover", + "active", + "focus", + "visited", + "disabled", + "required", + ]); + + // split into segments on every colon + const parts = token.split(":"); + let idx = 0; + + let selectorPrefix = ""; // will become something like ".\&\>div:hover" + let property, value; + + // 1) Does the first part look like a nested selector? + const first = parts[idx]; + const isSelector = + first.startsWith("&") || first.startsWith("#") || first.startsWith("."); + if (isSelector) { + // build the selector path from that first segment + const raw = first.startsWith("&") ? first.slice(1) : first; const path = raw - .replace(/([>+~])/g, ' $1 ') - .replace(/_/g, ' ') - .replace(/\s+/g, ' ') + .replace(/([>+~])/g, " $1 ") + .replace(/_/g, " ") + .replace(/\s+/g, " ") .trim(); - - addRule( - escapeClass(token) + path, - parseDecls(rest), - token - ); - - } else if (pseudos.includes(selPart)) { - // ─── Pseudo‑class branch ─── - // rest might be: - // a) property:value… (apply to the element itself) - // b) selector:property:value… (apply to a nested selector) - const [maybeSel, declsPart] = rest.split(/:(.+)/); - - if (/^[#.]/.test(maybeSel) || maybeSel.startsWith('&')) { - // 3a) Pseudo on a nested selector - const raw = maybeSel.startsWith('&') - ? maybeSel.slice(1) - : maybeSel; - const path = raw - .replace(/([>+~])/g, ' $1 ') - .replace(/_/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - - addRule( - // .:hover
- `${escapeClass(token)}:${selPart}${path}`, - parseDecls(declsPart), - token - ); - } else { - // 3b) Pseudo on the element itself - addRule( - // .:hover - `${escapeClass(token)}:${selPart}`, - parseDecls(rest), - token - ); - } - - } else { - // ─── Simple property:value branch ─── - // single declaration—do math‑spacing & underscore conversion - const property = selPart.trim(); - let value = rest - .replace(/_/g, ' ') - .replace(/(\S)([+\-*/])(\S)/g, '$1 $2 $3') - .trim(); - - addRule( - escapeClass(token), - [[property, value]], - token - ); + selectorPrefix = escapeClass(token) + path; + idx++; + } + + // pseudo? + let pseudo = ""; + const maybePseudo = parts[idx]; + if (pseudos.has(maybePseudo)) { + pseudo = maybePseudo; + idx++; } + + // 3) We **must** now have property and value + if (parts.length - idx < 2) { + return; + } + property = parts[idx++]; + value = parts.slice(idx).join(":"); + + // If there was no selectorPrefix, we apply to the element itself: + let fullSelector = selectorPrefix || escapeClass(token); + if (pseudo) { + fullSelector += `:${pseudo}`; + } + + // 5) Normalize the value: underscores -> spaces, math ops spacing + let normalized = value + .replace(/_/g, " ") + .replace(/(\S)([+\-*/])(\S)/g, "$1 $2 $3") + .trim(); + + addRule(fullSelector, [[property.trim(), normalized]], token); }; - - const processNode = n => - (n.className || '') - .split(/\s+/) - .filter(Boolean) - .forEach(handleToken); + const processNode = (n) => + (n.className || "").split(/\s+/).filter(Boolean).forEach(handleToken); - const processTree = root => + const processTree = (root) => root.querySelectorAll('[class*=":"]').forEach(processNode); const flush = () => { if (!rules.length) return; - styleEl.textContent += '\n' + rules.join('\n'); + styleEl.textContent += "\n" + rules.join("\n"); rules.length = 0; }; - document.__cssar_mo = new MutationObserver(muts => { - muts.forEach(m => - m.addedNodes.forEach(n => - n.nodeType === 1 && (processNode(n), processTree(n)) + document.__cssar_mo = new MutationObserver((muts) => { + muts.forEach((m) => + m.addedNodes.forEach( + (n) => n.nodeType === 1 && (processNode(n), processTree(n)) ) ); flush(); }); - document.addEventListener('DOMContentLoaded', () => { + document.addEventListener("DOMContentLoaded", () => { processTree(document.body); document.__cssar_mo.observe(document.body, { childList: true, - subtree: true + subtree: true, }); flush(); }); diff --git a/selectors.html b/selectors.html index dd45f5d..6d563b2 100644 --- a/selectors.html +++ b/selectors.html @@ -2,79 +2,318 @@ - - Runtime CSS Generator - + CSSAR Tests - + + + -
- Styled box +

CSSAR Tests

+ +
Test 1: blue text
+ +
+ Test 2: margins 5/10/15/20
-
- Calc box + +
Test 3: padding-left = 20px
+ +
    +
  • Item A
  • +
  • Test 4: orange
  • +
+ +
+

Test 5: lightgray bg

-
Border box
- - - -
-
-
-
- Title -
- Subtitle -

Subsubtitle

+ +
+
+ Test 6: width = calc(50% - 20px)
-
- Title222 -
- Subtitle222 -

Subsubtitle222

-
+
+ +
-
- - Test -

Paragraph 1 in the div.

-

Paragraph 2 in the div.

-

Paragraph 3 in the div.

-
+
+ Test 8: red on lightblue
-
- Title222 -
- Subtitle222 -

Subsubtitle222

+
+

Heading

+

Test 9: italic

+
-
Submit
+
+

Test 10: bold

+
-
+ +
+

Spec 1: nested should override → green

+
+ +
+

Spec 2: ID should override class → pink

- +

+ Spec 3: inline-style should override loader → navy +

+ +
+ Spec 4: last declaration wins → blue +
+ +
+ +