UNPKG

@maizzle/framework

Version:

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

92 lines (91 loc) 3.35 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"; import mergeLonghand from "postcss-merge-longhand"; //#region src/transformers/shorthandCss.ts /** * Rewrite longhand CSS inside inline `style` attributes with shorthand * syntax. Works with margin, padding, and border when all sides are * specified. * * For example: * `margin-left: 2px; margin-right: 2px; margin-top: 4px; margin-bottom: 4px` * becomes: * `margin: 4px 2px` * * @param html HTML string to transform. * @param options Optional Maizzle options (`tags`). * @returns The transformed HTML string. * * @example * import { shorthandCss } from '@maizzle/framework' * * const out = shorthandCss( * '<p style="margin-top: 4px; margin-right: 2px; margin-bottom: 4px; margin-left: 2px;">x</p>', * { tags: ['p'] }, * ) */ function shorthandCss(html, options = {}) { return serialize(shorthandCssDom(parse(html), options)); } /** * DOM-form of {@link shorthandCss} 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 shorthandCssDom(dom, options = {}) { const allowedTags = options.tags ?? []; const hasTagFilter = allowedTags.length > 0; /** * Merge longhand within a single inline-style value. Returns the merged * string when shorter, otherwise the original. Wraps the value in a * dummy selector since postcss-merge-longhand operates on rules. */ const mergeStyleValue = (styleValue) => { try { const { css } = postcss().use(mergeLonghand).process(`div { ${styleValue} }`, { parser: safeParser }); const match = css.match(/div\s*\{\s*([^}]+)\s*\}/); if (match && match[1]) { const merged = match[1].trim(); if (merged !== styleValue) return merged; } } catch {} return styleValue; }; walk(dom, (node) => { /** * MSO conditional comments carry their own inline-style attributes * (e.g. `<!--[if mso]><td style="…"><![endif]-->`) as opaque text. * The element walker can't see them, so without this branch the td/ * v:rect styles inside comments stay longhand even when the visible * div has already been merged. Match each `style="…"` substring, * run it through mergeLonghand, splice back. * * Tag filter intentionally bypassed: the user can't address MSO td * elements (they don't parse as elements), and these comments * always wrap email-layout primitives anyway. */ if (node.type === "comment") { const data = node.data; if (!data || !data.includes("style=\"")) return; const newData = data.replace(/style="([^"]*)"/g, (full, value) => { const merged = mergeStyleValue(value); return merged === value ? full : `style="${merged}"`; }); if (newData !== data) node.data = newData; return; } const el = node; if (!el.attribs?.style) return; if (hasTagFilter && !allowedTags.includes(el.name)) return; const merged = mergeStyleValue(el.attribs.style); if (merged !== el.attribs.style) el.attribs.style = merged; }); return dom; } //#endregion export { shorthandCss, shorthandCssDom }; //# sourceMappingURL=shorthandCss.js.map