@maizzle/framework
Version:
Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.
112 lines (111 loc) • 4.64 kB
JavaScript
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