import { transform } from "lightningcss"; /** * Obsidian loads every plugin's stylesheet into one document, and Tailwind * puts its utilities into a cascade layer called `utilities` in each of them. * Layers with the same name merge, so another Tailwind plugin's * `.hidden{display:none!important}` and this plugin's * `@container … .\@min-\[30rem\]\:inline-block{…!important}` end up in the * same layer with the same specificity, and whichever plugin loaded last * wins. Prefixing every selector with `:is(.beaver-root, .beaver-root *)` adds a * class of specificity that no unscoped utility can match, whatever the load * order, while still reaching the view root itself and the portal layer, * both of which carry `.beaver-root`. * * Tailwind's own theme variables stay on `:root, :host`: moved onto the view * root they would sit above Obsidian's `body` values, and `--color-accent` * is a name both sides use — the accent would turn into the hover grey. */ const ROOT = "beaver-root"; const SCOPE = { kind: "is", selectors: [ [{ name: ROOT, type: "class" }], [ { name: ROOT, type: "class" }, { type: "combinator", value: "descendant" }, { type: "universal" }, ], ], type: "pseudo-class", }; function isPseudo(selector, kind) { return ( selector.length === 1 && selector[0].type === "pseudo-class" && selector[0].kind === kind ); } function mentions(selector, name) { for (const part of selector) { if (part.type === "class" && part.name === name) { return true; } if (part.selectors?.some((inner) => mentions(inner, name))) { return true; } } return false; } function scopeSelector(selector) { if (isPseudo(selector, "root") || isPseudo(selector, "host")) { return selector; } if (mentions(selector, ROOT)) { return selector; } // The scope goes on the subject — the last compound — not the first: // `.theme-dark .hue-chip` must keep matching with `.theme-dark` on body, // outside the root. let at = 0; for (let i = 0; i < selector.length; i += 1) { if (selector[i].type === "combinator") { at = i + 1; } } const head = selector.slice(0, at); const subject = selector.slice(at); // `*` and type selectors must open a compound; :is() goes in their place // or right after them. if (subject[0]?.type === "universal") { return [...head, SCOPE, ...subject.slice(1)]; } if (subject[0]?.type === "type") { return [...head, subject[0], SCOPE, ...subject.slice(1)]; } return [...head, SCOPE, ...subject]; } /** Rewrites a built stylesheet so its rules only reach the plugin's own DOM. */ export function scopeCss(code, { minify = false } = {}) { // The selector visitor, not the rule one: handing a whole rule back makes // lightningcss re-read its declarations, and some of Tailwind's it cannot. const result = transform({ code: Buffer.from(code), filename: "styles.css", minify, visitor: { Selector: scopeSelector }, }); return result.code.toString(); }