UNPKG

@maizzle/framework

Version:

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

117 lines (116 loc) 4.01 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 { defu as defu$1 } from "defu"; //#region src/transformers/addAttributes.ts /** * Default attributes to add to elements. */ const DEFAULT_ATTRIBUTES = { table: { cellpadding: 0, cellspacing: 0, role: "none" }, img: { alt: "" } }; /** * Add attributes transformer. * * Automatically adds attributes to HTML elements based on CSS selectors. * * Default attributes (can be disabled by setting `attributes.add` to false): * - table: cellpadding="0", cellspacing="0", role="none" * - img: alt="" * * Supports tag, class, id, and attribute selectors. * Multiple selectors can be specified by comma-separating them. * * @param html HTML string to transform. * @param config Attributes config (see {@link AttributesConfig}). * @returns The transformed HTML string. * * @example * import { addAttributes } from '@maizzle/framework' * * const out = addAttributes('<div></div>', { * add: { * div: { role: 'article' }, * '.test': { editable: true }, * '#header': { 'data-id': 'main' }, * 'div, p': { class: 'content' }, * }, * }) */ function addAttributes(html, config = {}) { return serialize(addAttributesDom(parse(html), config)); } /** * DOM-form of {@link addAttributes} 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 addAttributesDom(dom, config = {}) { const addConfig = config.add; if (addConfig === false) return dom; const attributesToAdd = defu$1(typeof addConfig === "object" ? addConfig : {}, DEFAULT_ATTRIBUTES); for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) { if (attributes === false) continue; const selectors = selectorPattern.split(",").map((s) => s.trim()); walk(dom, (node) => { const el = node; if (!el.name) return; if (selectors.some((selector) => elementMatches(el, selector))) { if (!el.attribs) el.attribs = {}; for (const [attrName, attrValue] of Object.entries(attributes)) { if (attrValue === false) continue; if (attrName === "class" && el.attribs.class) { const existingClasses = el.attribs.class.split(/\s+/).filter(Boolean); const newClasses = String(attrValue).split(/\s+/).filter(Boolean); const mergedClasses = [...new Set([...existingClasses, ...newClasses])]; if (mergedClasses.join(" ") !== el.attribs.class) el.attribs.class = mergedClasses.join(" "); } else if (!(attrName in el.attribs)) el.attribs[attrName] = String(attrValue); } } }); } return dom; } /** * Check if an element matches a CSS selector. * Supports: tag, .class, #id, [attribute], [attribute=value] */ function elementMatches(el, selector) { selector = selector.trim(); const attrMatch = selector.match(/^\[([^\]=]+)(?:=([^\]]*))?\]$/); if (attrMatch) { const [, attrName, attrValue] = attrMatch; if (attrValue === void 0) return attrName in el.attribs; else return el.attribs?.[attrName] === attrValue; } if (selector.startsWith(".")) { const className = selector.slice(1); return (el.attribs?.class?.split(/\s+/) || []).includes(className); } if (selector.startsWith("#")) { const id = selector.slice(1); return el.attribs?.id === id; } const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\[([^\]]+)\]$/i); if (tagAttrMatch) { const [, tagName, attrPart] = tagAttrMatch; if (el.name !== tagName) return false; const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/); if (attrEqMatch) { const [, attrName, attrValue] = attrEqMatch; if (attrValue === void 0) return attrName in el.attribs; else return el.attribs?.[attrName] === attrValue; } return false; } return el.name === selector; } //#endregion export { addAttributes, addAttributesDom }; //# sourceMappingURL=addAttributes.js.map