posthtml-safe-class-names
Version:
Replace escaped characters in HTML class names and CSS selectors.
131 lines (123 loc) • 3.82 kB
JavaScript
const postcss = require('postcss');
const safe = require('postcss-safe-parser');
const cssEscape = require('css.escape');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const postcss__default = /*#__PURE__*/_interopDefaultCompat(postcss);
const safe__default = /*#__PURE__*/_interopDefaultCompat(safe);
const cssEscape__default = /*#__PURE__*/_interopDefaultCompat(cssEscape);
function unescapeRegExp(string) {
return string.replace(/\\(.)/g, "$1");
}
const postcssSafeCss = (options = {}) => (root) => {
const replacementsRegex = new RegExp(`\\\\(${Object.keys(options.replacements).map(cssEscape__default).join("|")})`, "g");
root.walkRules((rule) => {
rule.selector = rule.selector.replace(replacementsRegex, (matched) => {
return options.replacements[unescapeRegExp(matched)];
}).replaceAll("\\2c ", "_");
});
};
function getClassCandidates(input, config) {
const result = [];
let currentIndex = 0;
config.sort((a, b) => b.heads.length - a.heads.length);
while (currentIndex < input.length) {
let foundMatch = false;
for (const { heads, tails } of config) {
if (input.slice(currentIndex, currentIndex + heads.length) === heads) {
const tailIndex = input.indexOf(tails, currentIndex + heads.length);
if (tailIndex !== -1) {
result.push(input.slice(currentIndex, tailIndex + tails.length));
currentIndex = tailIndex + tails.length;
foundMatch = true;
break;
}
}
}
if (!foundMatch) {
const nextHeadIndex = Math.min(...config.map(({ heads }) => input.indexOf(heads, currentIndex)).filter((index) => index !== -1));
if (nextHeadIndex === Number.POSITIVE_INFINITY) {
result.push(input.slice(currentIndex));
currentIndex = input.length;
} else {
result.push(input.slice(currentIndex, nextHeadIndex));
currentIndex = nextHeadIndex;
}
}
}
return result.map((i) => i.trim()).filter((i) => i.trim().length > 0);
}
const plugin = (options = {}) => (tree) => {
options.ignored = options.ignored || [
{
heads: "{{",
tails: "}}"
},
{
heads: "{{{",
tails: "}}}"
}
];
options.replacements = {
":": "-",
"/": "-",
"%": "pc",
".": "_",
",": "_",
"#": "_",
"[": "",
"]": "",
"(": "",
")": "",
"{": "",
"}": "",
"!": "i-",
"&": "and-",
"<": "lt-",
"=": "eq-",
">": "gt-",
"|": "or-",
"@": "at-",
"?": "q-",
"\\": "-",
'"': "-",
"'": "-",
"*": "-",
"+": "-",
";": "-",
"^": "-",
"`": "-",
"~": "-",
$: "-",
...options.replacements
};
const classAttrRegex = (needle) => new RegExp(`\\${needle}`, "g");
const process = (node) => {
if (node.tag === "style" && node.content) {
const content = Array.isArray(node.content) ? node.content.join("") : node.content;
node.content = postcss__default([
postcssSafeCss({
replacements: options.replacements
})
]).process(content, { parser: safe__default }).css;
}
if (node.attrs?.class) {
const classes = getClassCandidates(node.attrs.class, options.ignored);
const safeClasses = [];
for (let cls of classes) {
if (options.ignored.some(({ heads, tails }) => cls.startsWith(heads) || cls.endsWith(tails))) {
safeClasses.push(cls);
continue;
}
for (const [from, to] of Object.entries(options.replacements)) {
cls = cls.replace(classAttrRegex(from), to);
}
safeClasses.push(cls);
}
node.attrs.class = safeClasses.join(" ");
}
return node;
};
return tree.walk(process);
};
module.exports = plugin;
;