Skip to content

Commit

Permalink
Mt/css (#133)
Browse files Browse the repository at this point in the history
* restructured global windon.__vtbot

* reworked

* Added changeset

* remove unused vars

* Switched to duck typing to support multiple module loadersv(e.g. main document and iframe document)

* fixed document access

* deriveCSSSelector: accept undefined elements

* bumps

* bumps

* formatting
  • Loading branch information
martrapp authored Jun 28, 2024
1 parent 77060fe commit f5f1b9c
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 54 deletions.
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

0 comments on commit f5f1b9c

Please sign in to comment.