diff --git a/cssar.js b/cssar.js new file mode 100644 index 0000000..7030dde --- /dev/null +++ b/cssar.js @@ -0,0 +1,141 @@ +(() => { + if (document.__cssar_mo) return; + + const styleEl = document.head.appendChild(document.createElement('style')); + const seen = new Set(); + const rules = []; + + const escapeClass = cls => '.' + CSS.escape(cls); + + 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]; + }) + : []; + + 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); + }; + + 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 { + // ─── 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 + ); + } + }; + + + const processNode = n => + (n.className || '') + .split(/\s+/) + .filter(Boolean) + .forEach(handleToken); + + const processTree = root => + root.querySelectorAll('[class*=":"]').forEach(processNode); + + const flush = () => { + if (!rules.length) return; + 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)) + ) + ); + flush(); + }); + + document.addEventListener('DOMContentLoaded', () => { + processTree(document.body); + document.__cssar_mo.observe(document.body, { + childList: true, + subtree: true + }); + flush(); + }); +})(); diff --git a/cssr.js b/cssr.js deleted file mode 100644 index 768198d..0000000 --- a/cssr.js +++ /dev/null @@ -1,56 +0,0 @@ -(() => { - 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 rule = `.${CSS.escape(token)} { ${prop}: ${cssValue}; }`; - newRules.push(rule); - gen.add(token); - }); - }; - - let updateStyle = () => { - if (newRules.length > 0) { - styleEl.textContent += "\n" + newRules.join("\n"); - newRules = []; - } - }; - - let processTree = (root) => { - root.querySelectorAll('[class*=":"]').forEach((node) => processNode(node)); - updateStyle(); - }; - - document.__cssr_mo = new MutationObserver((muts) => { - muts.forEach((m) => - m.addedNodes.forEach( - (n) => n.nodeType === 1 && (processNode(n), processTree(n)) - ) - ); - updateStyle(); - }); - - document.addEventListener("DOMContentLoaded", () => { - processTree(document.body); - document.__cssr_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 new file mode 100644 index 0000000..dd45f5d --- /dev/null +++ b/selectors.html @@ -0,0 +1,80 @@ + + + + + + Runtime CSS Generator + + + + + +
+ Styled box +
+
+ Calc box +
+
Border box
+ + + +
+
+
+
+ Title +
+ Subtitle +

Subsubtitle

+
+
+ +
+ Title222 +
+ Subtitle222 +

Subsubtitle222

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

Subsubtitle222

+ +
Submit
+ +
+
+ + + +