UNPKG

@maizzle/framework

Version:

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

112 lines (111 loc) 4.64 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 juice from "juice"; //#region src/transformers/inlineCss.ts /** Juice's built-in code blocks, captured before any per-call mutation. */ const DEFAULT_CODE_BLOCKS = { ...juice.codeBlocks }; /** * Inline CSS from `<style>` tags into `style` attributes on matching elements. * * @param html HTML string to transform. * @param options Juice options plus Maizzle-specific extras. * @returns The transformed HTML string. * * @example * import { inlineCss } from '@maizzle/framework' * * const out = inlineCss('<style>.red{color:red}</style><p class="red">x</p>', { * removeStyleTags: true, * }) */ function inlineCss(html, options = {}) { return serialize(inlineCssDom(parse(html), options)); } /** * DOM-form of {@link inlineCss} used by the internal transformer pipeline. * Takes a parsed DOM, returns a parsed DOM — avoids the redundant * serialize/parse round-trips when chained with other transformers. */ function inlineCssDom(dom, options = {}) { const { preferUnitlessValues = true, safelist, customCSS = "", styleToAttribute, excludedProperties, widthElements, heightElements, codeBlocks, ...juicePassthrough } = options; juice.styleToAttribute = styleToAttribute ?? {}; juice.excludedProperties = ["--tw-shadow", ...excludedProperties ?? []]; juice.widthElements = (widthElements ?? ["img", "video"]).map((i) => i.toUpperCase()); juice.heightElements = (heightElements ?? ["img", "video"]).map((i) => i.toUpperCase()); juice.codeBlocks = { ...DEFAULT_CODE_BLOCKS }; if (codeBlocks && typeof codeBlocks === "object") Object.entries(codeBlocks).forEach(([key, value]) => { if (value.start && value.end) juice.codeBlocks[key] = value; }); /** * Handle style tags with embed attributes. We add a marker attribute * that persists through the pipeline, then restore data-embed from * it after Juice runs. `amp-custom` (AMP4Email's CSS attribute) * is treated the same as embed: contents are preserved, * never inlined. */ walk(dom, (node) => { const el = node; if (el.name === "style" && el.attribs) { /** * `amp-custom` → tell juice to skip via data-embed, but don't mirror * back to `embed` (user wrote amp-custom, that's what stays in * output). */ if ("amp-custom" in el.attribs && !("data-embed" in el.attribs)) el.attribs["data-embed"] = ""; /** * Sync data-embed ↔ embed. Use `in` so presence-only attrs * (<style embed> → attribs.embed === '') still count. */ if ("embed" in el.attribs && !("data-embed" in el.attribs)) el.attribs["data-embed"] = ""; if ("data-embed" in el.attribs && !("embed" in el.attribs) && !("amp-custom" in el.attribs)) el.attribs.embed = ""; if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = ""; } }); const serialized = serialize(dom); let inlinedHtml; try { inlinedHtml = juice(serialized, { removeStyleTags: juicePassthrough.removeStyleTags ?? false, removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true, applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true, applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true, preservedSelectors: safelist ?? [], ...customCSS ? { extraCss: customCSS } : {}, inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? false, ...juicePassthrough }); } catch { return dom; } const result = parse(inlinedHtml); if (preferUnitlessValues) walk(result, (node) => { const el = node; if (el.attribs?.style) el.attribs.style = el.attribs.style.replace(/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g, "0"); }); /** * Restore `embed` from our marker so the purge step can detect * these tags and skip them. Drop `data-embed` (juice's name) * since it's redundant once `embed` is back, and let purge * strip `embed` itself at the end of its run. */ walk(result, (node) => { const el = node; if (el.name === "style" && el.attribs && "data-maizzle-embed" in el.attribs) { /** * Only restore `embed` when the original signal was embed/data-embed — * an `amp-custom` style was tagged for juice's benefit only and * must not pick up a stray `embed` attribute in the rendered * output. */ if (!("amp-custom" in el.attribs)) el.attribs.embed = ""; delete el.attribs["data-embed"]; delete el.attribs["data-maizzle-embed"]; } }); return result; } //#endregion export { inlineCss, inlineCssDom }; //# sourceMappingURL=inlineCss.js.map