96 lines
3.0 KiB
JavaScript
96 lines
3.0 KiB
JavaScript
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();
|
|
}
|