scoped-rem
Version:
Makes CSS rem units relative to a custom root font size.
81 lines (79 loc) • 3.01 kB
JavaScript
import postcss from "postcss";
import valueParser from "postcss-value-parser";
import { z } from "zod";
//#region src/index.ts
const VARNAME_DEFAULT = "rem-relative-base";
const VARSELECTOR_DEFAULT = ":root";
const ScopedRemOptionsSchema = z.object({
varname: z.string().optional().default(VARNAME_DEFAULT),
rootval: z.string().optional(),
varselector: z.string().optional().default(VARSELECTOR_DEFAULT),
precision: z.coerce.number().int().min(0).max(100).optional()
});
function parseQueryOptions(query) {
const params = new URLSearchParams(query);
if (!params.has("rem-scoped")) return null;
const rawOptions = {
varname: params.get("varname") || void 0,
rootval: params.get("rootval") || void 0,
varselector: params.get("varselector") || void 0,
precision: params.get("precision") || void 0
};
try {
return ScopedRemOptionsSchema.parse(rawOptions);
} catch (error) {
if (error instanceof z.ZodError) throw new Error(`[scoped-rem] Invalid query options: ${error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")}`);
throw error;
}
}
function generateCssVarDeclaration(options) {
const { rootval } = options;
if (!rootval) return "";
const varname = options.varname || VARNAME_DEFAULT;
return `${options.varselector || VARSELECTOR_DEFAULT} { --${varname}: ${rootval}; }`;
}
const remRegex = /"[^"]+"|'[^']+'|url\([^)]+\)|--[\w-]+|(-?\d*\.?\d+)rem/gi;
/**
* @param css CSS source code
* @param filename will be passed to PostCSS
* @returns The transformed CSS
*/
function transformRemWithPostCSS(css, filename, options) {
const varname = options.varname || VARNAME_DEFAULT;
return postcss([{
postcssPlugin: "scoped-rem-transform",
Declaration(decl) {
const parsed = valueParser(decl.value);
let modified = false;
parsed.walk((node) => {
if (node.type === "word" && node.value.endsWith("rem")) {
const numStr = node.value.match(remRegex)?.[0];
if (!numStr) return;
const numValue = (() => {
if (options.precision !== void 0) {
if (options.precision < 0 || options.precision > 100) throw new Error(`[scoped-rem] Invalid precision value: ${options.precision}. It should be between 0 and 100.`);
return parseFloat((parseFloat(numStr) || 0).toFixed(options.precision));
} else return parseFloat(numStr) || 0;
})();
if (numValue === 0) {
node.value = "0";
modified = true;
return;
}
node.value = `calc(${numValue} * var(--${varname}))`;
modified = true;
}
});
if (modified) decl.value = parsed.toString();
}
}]).process(css, { from: filename }).css;
}
function transformCss(css, filename, options) {
const varDeclaration = generateCssVarDeclaration(options);
const transformedCss = transformRemWithPostCSS(css, filename, options);
if (!varDeclaration) return transformedCss;
return `${varDeclaration}\n${transformedCss}`;
}
//#endregion
export { VARNAME_DEFAULT, VARSELECTOR_DEFAULT, parseQueryOptions, transformCss };
//# sourceMappingURL=index.js.map