This repository has been archived by the owner on Jan 4, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 85
Add CSS variables custom metrics #181
Merged
rviscomi
merged 4 commits into
HTTPArchive:master
from
LeaVerou:custom-metrics-css-variables
Jul 30, 2020
+261
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
//[css-variables] | ||
function analyzeVariables() { | ||
|
||
const PREFIX = "almanac-var2020-"; | ||
|
||
// Selector to find elements that are relevant to the graph | ||
const selector = `.${PREFIX}element, [style*="--"]`; | ||
|
||
// Extract a list of custom properties set by a value | ||
function extractValueProperties(value) { | ||
// https://drafts.csswg.org/css-syntax-3/#ident-token-diagram | ||
let ret = value.match(/var\(--[-\w\u{0080}-\u{FFFF}]+(?=[,)])/gui)?.map(p => p.slice(4)); | ||
|
||
if (ret) { | ||
// Drop duplicates | ||
return [...new Set(ret)]; | ||
} | ||
} | ||
|
||
let visited = new Set(); | ||
|
||
// Recursively walk a CSSStyleRule or CSSStyleDeclaration | ||
function walkRule(rule, ret) { | ||
if (!rule || visited.has(rule)) { | ||
return; | ||
} | ||
|
||
visited.add(rule); | ||
|
||
let style, selector; | ||
|
||
if (rule instanceof CSSStyleRule && rule.style) { | ||
style = rule.style; | ||
selector = rule.selectorText; | ||
} | ||
else if (rule instanceof CSSStyleDeclaration) { | ||
style = rule; | ||
selector = ""; | ||
} | ||
|
||
if (style) { | ||
let condition; | ||
// mirror properties to add. We add them afterwards, so we don't pointlessly traverse them | ||
let additions = {}; | ||
|
||
for (let property of style) { | ||
let value = style.getPropertyValue(property); | ||
|
||
let containsRef = value.indexOf("var(--") > -1; | ||
let setsVar = property.indexOf("--") === 0 && property.indexOf("--" + PREFIX) === -1; | ||
|
||
if (containsRef || setsVar) { | ||
if (!condition && rule.parentRule) { | ||
condition = []; | ||
let r = rule; | ||
|
||
while (r.parentRule?.conditionText) { | ||
r = r.parentRule; | ||
condition.push({ | ||
type: r instanceof CSSMediaRule? "media" : "supports", | ||
test: r.conditionText | ||
}); | ||
} | ||
} | ||
|
||
if (containsRef) { | ||
// Set mirror property so we can find it in the computed style | ||
additions["--" + PREFIX + property] = value.replace(/var\(--/g, PREFIX + "$&"); | ||
|
||
let properties = extractValueProperties(value); | ||
|
||
for (let prop of properties) { | ||
let info = ret[prop] = ret[prop] || {get: [], set: []}; | ||
info.get.push({ usedIn: property, value, selector, condition }); | ||
} | ||
} | ||
|
||
if (setsVar) { | ||
let info = ret[property] = ret[property] || {get: [], set: []}; | ||
info.set.push({ value, selector, condition }); | ||
} | ||
|
||
// Add class so we can find these later | ||
if (selector) { | ||
for (let el of document.querySelectorAll(selector)) { | ||
el.classList.add(`${PREFIX}element`); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Now that we're done, add the mirror properties | ||
for (let property in additions) { | ||
style.setProperty(property, additions[property]); | ||
} | ||
} | ||
|
||
if (rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule) { | ||
// rules with child rules, e.g. @media, @supports | ||
for (let r of rule.cssRules) { | ||
walkRule(r, ret); | ||
} | ||
} | ||
} | ||
|
||
// Return a subset of the DOM tree that contains variable reads or writes | ||
function buildGraph() { | ||
// Elements that contain variable reads or writes. | ||
let elements = new Set(document.querySelectorAll(selector)); | ||
let map = new Map(); // keep pointers to object for each element | ||
let ret = []; | ||
|
||
for (let element of elements) { | ||
map.set(element, {element, children: []}); | ||
} | ||
|
||
for (let element of elements) { | ||
let ancestor = element.parentNode.closest?.(selector); | ||
let obj = map.get(element); | ||
|
||
if (ancestor) { | ||
let o = map.get(ancestor); | ||
o.children.push(obj) | ||
} | ||
else { | ||
// Top-level | ||
ret.push(obj); | ||
} | ||
|
||
let cs = element.computedStyleMap(); | ||
let parentCS = element.parentNode.computedStyleMap?.(); | ||
let vars = extractVars(cs, parentCS); | ||
|
||
if (Object.keys(vars).length > 0) { | ||
obj.declarations = vars; | ||
} | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
// Extract custom property declarations from a computed style map | ||
// The schema of the returned object is: | ||
// {get: {--var1: [{property, value, computedValue}]}, set: {--var2: {value, type}}} | ||
function extractVars(cs, parentCS) { | ||
let ret = {}; | ||
let norefs = {}; | ||
|
||
for (let [property, [originalValue]] of cs) { | ||
// Do references first | ||
if (property.indexOf("--") === 0) { | ||
let value = originalValue + ""; | ||
|
||
// Skip inherited values | ||
if (parentCS && (parentCS.get(property) + "" === value + "")) { | ||
continue; // most likely inherited | ||
} | ||
|
||
if (property.indexOf("--" + PREFIX) === 0) { | ||
// Usage | ||
let originalProperty = property.replace("--" + PREFIX, ""); | ||
|
||
value = value.replace(RegExp(PREFIX + "var\\(--", "g"), "var(--"); | ||
let properties = extractValueProperties(value); | ||
let computed = cs.get(originalProperty) + ""; | ||
|
||
ret[originalProperty] = { | ||
value, | ||
references: properties | ||
} | ||
|
||
if (computed !== value) { | ||
ret[originalProperty].computed = computed; | ||
} | ||
} | ||
else { | ||
// Definition | ||
norefs[property] = {value}; | ||
|
||
if (originalValue + "" !== value) { | ||
norefs[property].computed = originalValue + ""; | ||
} | ||
|
||
// If value is of another type, we have Houdini P&V usage! | ||
if (!(originalValue instanceof CSSUnparsedValue)) { | ||
norefs[property].type = Object.prototype.toString.call(originalValue).slice(8, -1); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Merge static with ret | ||
for (let property in norefs) { | ||
if (!(property in ret)) { | ||
ret[property] = norefs[property]; | ||
} | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
let summary = {}; | ||
|
||
// Walk through stylesheet and add custom properties for every declaration that uses var() | ||
// This way we can retrieve them in the computed styles and build a dependency graph. | ||
// Otherwise, they get resolved before they hit the computed style. | ||
for (let stylesheet of document.styleSheets) { | ||
try { | ||
var rules = stylesheet.cssRules; | ||
} | ||
catch (e) {} | ||
|
||
if (rules) { | ||
for (let rule of rules) { | ||
walkRule(rule, summary); | ||
} | ||
} | ||
} | ||
|
||
// Do the same thing with inline styles | ||
for (let element of document.querySelectorAll('[style*="--"]')) { | ||
walkRule(element.style, summary); | ||
} | ||
|
||
let computed = buildGraph(); | ||
|
||
// Cleanup | ||
for (let el of document.querySelectorAll(`.${PREFIX}element`)) { | ||
el.classList.remove(`${PREFIX}element`); | ||
} | ||
|
||
return {summary, computed}; | ||
|
||
}; | ||
|
||
function serialize(data, separator) { | ||
return JSON.stringify(data, (key, value) => { | ||
if (value instanceof HTMLElement) { | ||
let str = value.tagName; | ||
|
||
if (value.classList.length > 0) { | ||
str += "." + [...value.classList].join(".") | ||
} | ||
|
||
if (value.id) { | ||
str += "#" + value.id; | ||
} | ||
|
||
return str; | ||
} | ||
|
||
// remove empty arrays | ||
if (Array.isArray(value) && value.length === 0) { | ||
return; | ||
} | ||
|
||
return value; | ||
}, separator); | ||
} | ||
|
||
return serialize(analyzeVariables()); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First thing that comes to mind is serialize the
obj
, save it in a Set, and test to see if each new object already exists before appending too.children
.Or check the
children
array itself:The object properties may not be serialized in the same order, so this may not be a very robust option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LeaVerou I'll merge this as-is to make sure that we get it into the sync tomorrow. If you're able to optimize this before the end of the month, feel free to file a PR and we'll try to fit it in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for merging!
The structure is built recursively, so at the time of that push,
obj
is not necessarily in its final state. But I could do something along those lines after the tree is built.