mirror of
https://github.com/def-peter/obsidian-aera-theme.git
synced 2026-07-22 05:00:27 +00:00
fix: enforce component selector boundaries
This commit is contained in:
parent
d7ea139264
commit
bb58006e24
2 changed files with 135 additions and 18 deletions
|
|
@ -2,10 +2,36 @@ import assert from "node:assert/strict";
|
|||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import { declarationsFor } from "./helpers/css.mjs";
|
||||
import {
|
||||
assertThemeStructure,
|
||||
declarationsFor,
|
||||
selectorsFor,
|
||||
} from "./helpers/css.mjs";
|
||||
|
||||
const css = await readFile(new URL("../theme.css", import.meta.url), "utf8");
|
||||
const body = declarationsFor(css, "body");
|
||||
const allowedSelectors = new Set([
|
||||
".theme-light",
|
||||
".theme-dark",
|
||||
"body",
|
||||
".markdown-rendered pre",
|
||||
]);
|
||||
const officialCalloutTypeVariables = [
|
||||
"--callout-bug",
|
||||
"--callout-default",
|
||||
"--callout-error",
|
||||
"--callout-example",
|
||||
"--callout-fail",
|
||||
"--callout-important",
|
||||
"--callout-info",
|
||||
"--callout-question",
|
||||
"--callout-success",
|
||||
"--callout-summary",
|
||||
"--callout-tip",
|
||||
"--callout-todo",
|
||||
"--callout-warning",
|
||||
"--callout-quote",
|
||||
];
|
||||
|
||||
function assertPrefixedDeclarations(prefixes, expected) {
|
||||
const actual = Object.fromEntries(
|
||||
|
|
@ -17,6 +43,42 @@ function assertPrefixedDeclarations(prefixes, expected) {
|
|||
assert.deepEqual(actual, expected);
|
||||
}
|
||||
|
||||
test("uses only the allowed low-specificity selectors", () => {
|
||||
assert.deepEqual(selectorsFor(css), allowedSelectors);
|
||||
});
|
||||
|
||||
test("rejects unexpected selectors in memory", () => {
|
||||
for (const selector of [".mobile-only", ".markdown-source-view"]) {
|
||||
assert.throws(
|
||||
() => assertThemeStructure(`${css}\n${selector} { color: red; }`),
|
||||
/unexpected theme selector/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects callout variables outside body in memory", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertThemeStructure(
|
||||
`${css}\n.callout[data-callout="warning"] { --callout-color: red; }`,
|
||||
),
|
||||
/--callout-color.*body/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects official callout type variables globally in memory", () => {
|
||||
for (const property of officialCalloutTypeVariables) {
|
||||
assert.throws(
|
||||
() => assertThemeStructure(`${css}\nbody { ${property}: red; }`),
|
||||
new RegExp(`${property}.*semantic type`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the compiled theme respects component structure boundaries", () => {
|
||||
assert.doesNotThrow(() => assertThemeStructure(css));
|
||||
});
|
||||
|
||||
test("styles callouts without replacing semantic type colors", () => {
|
||||
assertPrefixedDeclarations(["--callout-"], {
|
||||
"--callout-padding": "var(--size-4-3) var(--size-4-4)",
|
||||
|
|
@ -31,23 +93,8 @@ test("styles callouts without replacing semantic type colors", () => {
|
|||
"--callout-content-background": "transparent",
|
||||
});
|
||||
|
||||
for (const type of [
|
||||
"note",
|
||||
"info",
|
||||
"todo",
|
||||
"tip",
|
||||
"important",
|
||||
"success",
|
||||
"question",
|
||||
"warning",
|
||||
"failure",
|
||||
"danger",
|
||||
"error",
|
||||
"bug",
|
||||
"example",
|
||||
"quote",
|
||||
]) {
|
||||
assert.equal(body.has(`--callout-${type}`), false, `--callout-${type}`);
|
||||
for (const property of officialCalloutTypeVariables) {
|
||||
assert.equal(body.has(property), false, property);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,28 @@
|
|||
import postcss from "postcss";
|
||||
|
||||
const allowedThemeSelectors = new Set([
|
||||
".theme-light",
|
||||
".theme-dark",
|
||||
"body",
|
||||
".markdown-rendered pre",
|
||||
]);
|
||||
const officialCalloutTypeVariables = new Set([
|
||||
"--callout-bug",
|
||||
"--callout-default",
|
||||
"--callout-error",
|
||||
"--callout-example",
|
||||
"--callout-fail",
|
||||
"--callout-important",
|
||||
"--callout-info",
|
||||
"--callout-question",
|
||||
"--callout-success",
|
||||
"--callout-summary",
|
||||
"--callout-tip",
|
||||
"--callout-todo",
|
||||
"--callout-warning",
|
||||
"--callout-quote",
|
||||
]);
|
||||
|
||||
export function declarationsFor(css, selector) {
|
||||
const declarations = new Map();
|
||||
const root = postcss.parse(css);
|
||||
|
|
@ -14,3 +37,50 @@ export function declarationsFor(css, selector) {
|
|||
|
||||
return declarations;
|
||||
}
|
||||
|
||||
export function selectorsFor(css) {
|
||||
const selectors = new Set();
|
||||
|
||||
postcss.parse(css).walkRules((rule) => selectors.add(rule.selector));
|
||||
|
||||
return selectors;
|
||||
}
|
||||
|
||||
export function assertThemeStructure(css) {
|
||||
const root = postcss.parse(css);
|
||||
|
||||
root.walkDecls((declaration) => {
|
||||
if (officialCalloutTypeVariables.has(declaration.prop)) {
|
||||
throw new Error(
|
||||
`${declaration.prop} is a forbidden semantic type variable`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!declaration.prop.startsWith("--callout-")) return;
|
||||
|
||||
const selector =
|
||||
declaration.parent?.type === "rule"
|
||||
? declaration.parent.selector
|
||||
: "a non-rule context";
|
||||
if (selector !== "body") {
|
||||
throw new Error(
|
||||
`${declaration.prop} must be declared only on body; found ${selector}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const selectors = new Set();
|
||||
root.walkRules((rule) => selectors.add(rule.selector));
|
||||
const unexpected = [...selectors].filter(
|
||||
(selector) => !allowedThemeSelectors.has(selector),
|
||||
);
|
||||
const missing = [...allowedThemeSelectors].filter(
|
||||
(selector) => !selectors.has(selector),
|
||||
);
|
||||
|
||||
if (unexpected.length > 0 || missing.length > 0) {
|
||||
throw new Error(
|
||||
`unexpected theme selector set: unexpected [${unexpected.join(", ")}], missing [${missing.join(", ")}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue