Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mt/css #133

Merged
merged 12 commits into from
Jun 28, 2024
5 changes: 5 additions & 0 deletions .changeset/thick-ladybugs-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro-vtbot': minor
---

Adds the long time planned support for CSSGroupingRules.
2 changes: 1 addition & 1 deletion components/Linter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const active = import.meta.env.DEV || production;
) {
const here = `in ${origin} DOM (${event[origin === 'old' ? 'from' : 'to'].pathname})`;

const namedElements = elementsWithStyleProperty('view-transition-name');
const namedElements = elementsWithStyleProperty(document, 'view-transition-name');
const warned = new Set<string>();
const ignore = new Set(
(
Expand Down
4 changes: 2 additions & 2 deletions components/VtBotDebug.astro
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const active = import.meta.env.DEV || production;
if (originalMap === undefined) return;

const bold = (s: string) => `**${s}**`;
const map = elementsWithStyleProperty('view-transition-name');
const map = elementsWithStyleProperty(document, 'view-transition-name');
[...map.values()].filter((set) => set.has(document.documentElement)).length === 0 &&
map.set('root', (map.get('root') ?? new Set()).add(document.documentElement));
const newMap = toCSSSelectorMap(map, 'new');
Expand Down Expand Up @@ -472,7 +472,7 @@ const active = import.meta.env.DEV || production;
logProperties(swapEvent);
console.groupEnd();
if (supportsViewTransitions) {
const map = elementsWithStyleProperty('view-transition-name');
const map = elementsWithStyleProperty(document, 'view-transition-name');
[...map.values()].filter((set) => set.has(document.documentElement)).length === 0 &&
map.set('root', (map.get('root') ?? new Set()).add(document.documentElement));
window.__vtbot.debug.originalMap = toCSSSelectorMap(map, 'old');
Expand Down
129 changes: 79 additions & 50 deletions components/css.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
// todos:
// check for different CSS rule types (beside CSSStyleRule)

const decodeDiv = document.createElement('div');

export const ILLEGAL_TRANSITION_NAMES = 'data-vtbot-illegal-transition-names';
export function astroContextIds() {

export function walkSheets(
sheets: CSSStyleSheet[],
withSheet?: (sheet: CSSStyleSheet) => void,
withStyleRule?: (rule: CSSStyleRule) => void,
afterSheet?: (sheet: CSSStyleSheet) => void
) {
sheets.forEach((sheet) => {
try {
withSheet && withSheet(sheet);
walkRules([...sheet.cssRules], withSheet, withStyleRule, afterSheet);
afterSheet && afterSheet(sheet);
} catch (e) {
console.log(`%c[vtbot] Can't analyze sheet at ${sheet.href}: ${e}`, 'color: #888');
}
});
}

export function walkRules(
rules: CSSRule[],
withSheet?: (sheet: CSSStyleSheet) => void,
withStyleRule?: (rule: CSSStyleRule) => void,
afterSheet?: (sheet: CSSStyleSheet) => void
) {
rules.forEach((rule) => {
if (rule.constructor.name === 'CSSStyleRule') {
withStyleRule && withStyleRule(rule as CSSStyleRule);
} else if ('cssRules' in rule) {
walkRules([...(rule.cssRules as CSSRuleList)], withSheet, withStyleRule, afterSheet);
} else if ('styleSheet' in rule) {
walkSheets([rule.styleSheet as CSSStyleSheet], withSheet, withStyleRule, afterSheet);
}
});
}

export function astroContextIds(doc = document) {
const inStyleSheets = new Set<string>();
const inElements = new Set<string>();

[...document.styleSheets].forEach((sheet) => {
[...sheet.cssRules].forEach((rule) => {
if (rule instanceof CSSStyleRule) {
[...rule.selectorText.matchAll(/data-astro-cid-(\w{8})/g)].forEach((match) =>
inStyleSheets.add(match[1]!)
);
[...rule.selectorText.matchAll(/\.astro-(\w{8})/g)].forEach((match) =>
inStyleSheets.add(match[1]!)
);
}
});
walkSheets([...doc.styleSheets], undefined, (r) => {
[...r.selectorText.matchAll(/data-astro-cid-(\w{8})/g)].forEach((match) =>
inStyleSheets.add(match[1]!)
);
[...r.selectorText.matchAll(/\.astro-(\w{8})/g)].forEach((match) =>
inStyleSheets.add(match[1]!)
);
});

const ASTRO_CID = 'astroCid';
[...document.querySelectorAll('*')].forEach((el) => {
[...doc.querySelectorAll('*')].forEach((el) => {
Object.keys((el as HTMLElement).dataset).forEach((key) => {
if (key.startsWith(ASTRO_CID)) {
inElements.add(key.substring(ASTRO_CID.length).toLowerCase().replace(/^-/g, ''));
Expand All @@ -39,42 +67,42 @@ export function astroContextIds() {
}

type SupportedCSSProperties = 'view-transition-name';
// finds all elements of a _the current document_ with a given _string_ property in a style sheet
// document.styleSheets does not seem to work for arbitrary documents
// finds all elements of _an active_ document with a given _string_ property in a style sheet.
// document.styleSheets does not work for documents that are not associated with a window
export function elementsWithPropertyInStylesheet(
doc: Document,
property: SupportedCSSProperties,
map: Map<string, Set<Element>> = new Map()
): Map<string, Set<Element>> {
[...document.styleSheets].forEach((sheet) => {
const style = sheet.ownerNode as HTMLElement;
const definedNames = new Set<string>();
const matches = style?.innerHTML
.replace(/@supports[^{]*\{/gu, '')
.matchAll(new RegExp(`${property}:\\s*([^;}]*)`, 'gu'));
[...matches].forEach((match) => definedNames.add(decode(property, match[1]!)));
try {
[...sheet.cssRules].forEach((rule) => {
if (rule instanceof CSSStyleRule) {
const name = rule.style[property as keyof CSSStyleDeclaration] as string;
if (name) {
definedNames.delete(name);
map.set(
name,
new Set([
...(map.get(name) ?? new Set()),
...document.querySelectorAll(rule.selectorText),
])
);
}
}
});
} catch (e) {
console.log(`%c[vtbot] Can't analyze sheet at ${sheet.href}: ${e}`, 'color: #888');
const definitions = new Map<CSSStyleSheet, Set<string>>();

walkSheets([...doc.styleSheets], (sheet) => {
const owner = sheet.ownerNode;
if (definitions.has(sheet)) return;
const set = new Set<string>();
definitions.set(sheet, set);
const text = (owner?.textContent ?? '').replace(/@supports[^;{]+/g, '');
const matches = text.matchAll(new RegExp(`${property}:\\s*([^;}]*)`, 'gu'));
[...matches].forEach((match) => set.add(decode(property, match[1]!)));
});

walkSheets([...doc.styleSheets], undefined, (rule) => {
const name = rule.style[property as keyof CSSStyleDeclaration] as string;
if (name) {
definitions.get(rule.parentStyleSheet!)?.delete(name);
map.set(
name,
new Set([...(map.get(name) ?? new Set()), ...doc.querySelectorAll(rule.selectorText)])
);
}
if (definedNames.size > 0) {
const illegalNames = [...definedNames].join(', ');
style.setAttribute(ILLEGAL_TRANSITION_NAMES, illegalNames);
map.set('', new Set([...(map.get('') ?? new Set()), style]));
});

definitions.forEach((set, sheet) => {
const styleElement = sheet.ownerNode as HTMLElement;
if (set.size > 0) {
const illegalNames = [...set].join(', ');
styleElement.setAttribute(ILLEGAL_TRANSITION_NAMES, illegalNames);
map.set('', new Set([...(map.get('') ?? new Set()), styleElement]));
}
});
return map;
Expand All @@ -100,15 +128,16 @@ export function elementsWithPropertyInStyleAttribute(
return map;
}

// finds all elements _of the current document_ with a given property
// finds all elements of _an active_ document with a given property
// in their style attribute or in a style sheet
export function elementsWithStyleProperty(
doc: Document,
property: SupportedCSSProperties,
map: Map<string, Set<Element>> = new Map()
): Map<string, Set<Element>> {
return elementsWithPropertyInStyleAttribute(
document,
doc,
property,
elementsWithPropertyInStylesheet(property, map)
elementsWithPropertyInStylesheet(doc, property, map)
);
}
2 changes: 1 addition & 1 deletion components/derive-css-selector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function deriveCSSSelector(element: Element, useIds = true) {
export function deriveCSSSelector(element?: Element, useIds = true) {
let path: string[] = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let selector = element.nodeName.toLowerCase();
Expand Down