Skip to content

Instantly share code, notes, and snippets.

@chaance
Last active October 12, 2021 02:53
Show Gist options
  • Save chaance/5512901105daa52798e33749711a939e to your computer and use it in GitHub Desktop.
Save chaance/5512901105daa52798e33749711a939e to your computer and use it in GitHub Desktop.
This was the result of a brainfart I had today. I wanted a PostCSS plugin that game me some *very simple* scoping capabilities, and nothing I found quite did what I wanted. Yet to thoroughly test this, but putting it here for now so I don't forget about it.
const DIRECTIVES = [
/**
* If there is currently a local scope or no scope, the :root directive will
* revert to using only the top-level scope if one is defined.
*
* @example
*
* @simple-scope;
*
* .one {} --> .root--one {}
*
* @simple-scope title;
*
* .two {} --> .root--title--two {}
* .three {} --> .root--title--three {}
*
* @simple-scope :root;
*
* .four {} --> .root--four {}
*/
":root",
/**
* The :local directive sets the scope to ignore any top-level scope and only
* use the given scope for the cascading styles.
*
* @example
*
* @simple-scope;
*
* .one {} --> .root--one {}
*
* @simple-scope title;
*
* .two {} --> .root--title--two {}
* .three {} --> .root--title--three {}
*
* @simple-scope :local copy;
*
* .four {} --> .copy--four {}
*/
":local",
/**
* The :global directive removes all scopes and leaves the rules as-is.
*
* @example
*
* @simple-scope;
*
* .one {} --> .root--one {}
*
* @simple-scope :global;
*
* .two {} --> .two {}
*/
":global",
];
/**
* @param opts {{ rootScope?: string; separator?: string }}
* @returns
*/
function scoper(opts = {}) {
let { rootScope = "", separator = "--" } = opts;
rootScope = rootScope.trim();
separator = separator.trim();
/** @type {string} */
let currentScope;
let cache = {};
return {
postcssPlugin: "postcss-simple-scope",
AtRule: {
"simple-scope": (node) => {
/** @type {string} */
let scope;
/** @type {Array<string>} */
let params = node.params.split(" ");
/** @type {string} */
let directive;
/** @type {string} */
let blockScope;
for (let param of params) {
if (DIRECTIVES.includes(param)) {
directive = param;
} else {
blockScope = param;
}
}
// TODO: some validation, maybe?
if (directive === ":root") {
scope = rootScope;
} else if (directive === ":global") {
scope = "";
} else if (directive === ":local") {
scope = blockScope || "";
} else if (blockScope) {
scope = rootScope
? [rootScope, blockScope].join(separator)
: blockScope;
} else {
scope = rootScope;
}
currentScope = scope;
// remove the scope atrule
node.remove();
},
},
Rule: {
"*": (node) => {
if (!currentScope) {
return;
}
let newSelector = node.selector
.split(",")
.map((selector) => {
let scopedSelector = selector
.split(/\s+/)
.map((singleSelector) => {
// Note, this probably isn't very complete. I imagine there's
// some css parser that already reliably grabs individual class
// name selectors but this works well enough for now, I think.
if (singleSelector.startsWith(".")) {
singleSelector =
"." + currentScope + separator + singleSelector.slice(1);
}
return singleSelector;
})
.join(" ");
return scopedSelector.trim();
})
.join(", ");
cache[node.selector] = newSelector;
},
},
OnceExit(root) {
root.walkRules((node) => {
if (cache[node.selector]) {
node.selector = cache[node.selector];
}
});
},
};
}
module.exports = scoper;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment