UNPKG

@maizzle/framework

Version:

Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.

133 lines (132 loc) 4.09 kB
import { parse } from "../utils/ast/parser.js"; import { walk } from "../utils/ast/walker.js"; import { serialize } from "../utils/ast/serializer.js"; import "../utils/ast/index.js"; import postcss from "postcss"; import safeParser from "postcss-safe-parser"; //#region src/transformers/safeSelectors.ts const DEFAULT_REPLACEMENTS = { ":": "-", "/": "-", "%": "pc", ".": "_", ",": "_", "#": "_", "[": "", "]": "", "(": "", ")": "", "{": "", "}": "", "!": "-i", "&": "and-", "<": "lt-", "=": "eq-", ">": "gt-", "|": "or-", "@": "at-", "?": "q-", "\\": "-", "\"": "-", "'": "-", "*": "-", "+": "-", ";": "-", "^": "-", "`": "-", "~": "-", "$": "-" }; function escapeForRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Replace escaped special characters in CSS selectors. * * Tailwind generates selectors like `.sm\:text-base`. This function * replaces the `\:` with `-` (or whatever the configured replacement is) * so the selector becomes `.sm-text-base`, which is safe for email clients. */ function processCssSelectors(css, replacements) { const selectorRegex = new RegExp(`\\\\(${Object.keys(replacements).map(escapeForRegex).join("|")})`, "g"); return postcss([(root) => { root.walkRules((rule) => { rule.selector = rule.selector.replace(selectorRegex, (matched, char, offset, str) => { /** * Yahoo Mail wraps content in a class literally named `&`, so * the selector `.\&` must be preserved. Detect it as a * `\&` that follows a `.` and ends the class atom * (space, combinator, comma, `{`, or end-of-str). */ if (char === "&" && str[offset - 1] === ".") { const next = str[offset + 2]; if (next === void 0 || /[\s,{>~+)]/.test(next)) return matched; } return replacements[char] ?? matched; }).replaceAll("\\2c ", "_"); }); }]).process(css, { parser: safeParser }).css; } /** * Replace unsafe special characters in a class attribute value. * * Splits on whitespace and replaces each char from the replacements map * in each class token individually. */ function processClassAttr(classStr, replacements) { return classStr.split(/\s+/).filter(Boolean).map((cls) => { for (const [from, to] of Object.entries(replacements)) cls = cls.split(from).join(to); return cls; }).join(" "); } /** * Safe selectors transformer. * * Replaces unsafe characters (`:`, `/`, `[`, `]`, etc.) in: * - CSS selectors inside `<style>` tags * - HTML `class` attributes * * This makes Tailwind utility classes like `sm:text-base` safe for * email clients that cannot handle escaped characters in class names. * * Enabled by default. Disable by setting `css.safe` to `false`. * Customize replacements by passing a `Record<string, string>` — user * values are merged on top of the defaults. * * @param html HTML string to transform. * @param config CSS config (see {@link CssConfig}). * @returns The transformed HTML string. * * @example * import { safeSelectors } from '@maizzle/framework' * * const out = safeSelectors('<div class="sm:text-base"></div>') */ function safeSelectors(html, config = {}) { return serialize(safeSelectorsDom(parse(html), config)); } /** * DOM-form of {@link safeSelectors} used by the internal transformer pipeline. * Takes a parsed DOM, returns a parsed DOM — avoids redundant * serialize/parse round-trips when chained with other transformers. */ function safeSelectorsDom(dom, config = {}) { const option = config.safe ?? true; if (!option) return dom; const replacements = option && typeof option === "object" ? { ...DEFAULT_REPLACEMENTS, ...option } : DEFAULT_REPLACEMENTS; walk(dom, (node) => { const el = node; if (el.name === "style" && el.children?.length) { const text = el.children.find((c) => c.type === "text"); if (text?.data?.trim()) text.data = processCssSelectors(text.data, replacements); } if ("attribs" in el && el.attribs?.class) el.attribs.class = processClassAttr(el.attribs.class, replacements); }); return dom; } //#endregion export { safeSelectors, safeSelectorsDom }; //# sourceMappingURL=safeSelectors.js.map