@maizzle/framework 6.0.0-rc.20 → 6.0.0-rc.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +53 -43
  3. package/dist/build.js.map +1 -1
  4. package/dist/components/Body.vue +1 -1
  5. package/dist/components/CodeBlock.vue +1 -1
  6. package/dist/components/CodeInline.vue +72 -2
  7. package/dist/components/Column.vue +2 -1
  8. package/dist/components/Container.vue +1 -11
  9. package/dist/components/Img.vue +199 -4
  10. package/dist/components/Preheader.vue +33 -5
  11. package/dist/components/Section.vue +9 -14
  12. package/dist/components/Text.vue +1 -1
  13. package/dist/composables/defineConfig.d.ts +3 -4
  14. package/dist/composables/defineConfig.d.ts.map +1 -1
  15. package/dist/composables/defineConfig.js +3 -4
  16. package/dist/composables/defineConfig.js.map +1 -1
  17. package/dist/composables/renderContext.d.ts +1 -2
  18. package/dist/composables/renderContext.d.ts.map +1 -1
  19. package/dist/composables/renderContext.js.map +1 -1
  20. package/dist/composables/useCurrentTemplate.d.ts +31 -0
  21. package/dist/composables/useCurrentTemplate.d.ts.map +1 -0
  22. package/dist/composables/useCurrentTemplate.js +35 -0
  23. package/dist/composables/useCurrentTemplate.js.map +1 -0
  24. package/dist/composables/useEvent.d.ts +1 -1
  25. package/dist/composables/useEvent.js +1 -1
  26. package/dist/composables/useEvent.js.map +1 -1
  27. package/dist/composables/usePreheader.d.ts +6 -5
  28. package/dist/composables/usePreheader.d.ts.map +1 -1
  29. package/dist/composables/usePreheader.js +3 -3
  30. package/dist/composables/usePreheader.js.map +1 -1
  31. package/dist/composables/useTransformers.d.ts +1 -1
  32. package/dist/composables/useTransformers.js +1 -1
  33. package/dist/composables/useTransformers.js.map +1 -1
  34. package/dist/events/index.d.ts +20 -8
  35. package/dist/events/index.d.ts.map +1 -1
  36. package/dist/events/index.js +5 -8
  37. package/dist/events/index.js.map +1 -1
  38. package/dist/index.d.ts +3 -2
  39. package/dist/index.js +3 -2
  40. package/dist/render/createRenderer.js +4 -3
  41. package/dist/render/createRenderer.js.map +1 -1
  42. package/dist/serve.d.ts.map +1 -1
  43. package/dist/serve.js +3 -2
  44. package/dist/serve.js.map +1 -1
  45. package/dist/transformers/addAttributes.d.ts +18 -8
  46. package/dist/transformers/addAttributes.d.ts.map +1 -1
  47. package/dist/transformers/addAttributes.js +22 -8
  48. package/dist/transformers/addAttributes.js.map +1 -1
  49. package/dist/transformers/columnWidth.d.ts.map +1 -1
  50. package/dist/transformers/columnWidth.js +136 -150
  51. package/dist/transformers/columnWidth.js.map +1 -1
  52. package/dist/transformers/entities.d.ts.map +1 -1
  53. package/dist/transformers/entities.js +1 -0
  54. package/dist/transformers/entities.js.map +1 -1
  55. package/dist/transformers/index.d.ts.map +1 -1
  56. package/dist/transformers/index.js +7 -5
  57. package/dist/transformers/index.js.map +1 -1
  58. package/dist/transformers/inlineCss.js +2 -7
  59. package/dist/transformers/inlineCss.js.map +1 -1
  60. package/dist/transformers/inlineLink.js +2 -2
  61. package/dist/transformers/inlineLink.js.map +1 -1
  62. package/dist/transformers/minifyCodeInline.d.ts +29 -0
  63. package/dist/transformers/minifyCodeInline.d.ts.map +1 -0
  64. package/dist/transformers/minifyCodeInline.js +36 -0
  65. package/dist/transformers/minifyCodeInline.js.map +1 -0
  66. package/dist/transformers/msoPlaceholders.d.ts +10 -5
  67. package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
  68. package/dist/transformers/msoPlaceholders.js +38 -7
  69. package/dist/transformers/msoPlaceholders.js.map +1 -1
  70. package/dist/transformers/safeSelectors.d.ts +37 -0
  71. package/dist/transformers/safeSelectors.d.ts.map +1 -0
  72. package/dist/transformers/{safeClassNames.js → safeSelectors.js} +24 -5
  73. package/dist/transformers/safeSelectors.js.map +1 -0
  74. package/dist/transformers/shorthandCss.js +38 -7
  75. package/dist/transformers/shorthandCss.js.map +1 -1
  76. package/dist/transformers/tailwindComponent.js +1 -1
  77. package/dist/transformers/tailwindComponent.js.map +1 -1
  78. package/dist/types/config.d.ts +19 -9
  79. package/dist/types/config.d.ts.map +1 -1
  80. package/dist/utils/ast/serializer.d.ts.map +1 -1
  81. package/dist/utils/ast/serializer.js +27 -17
  82. package/dist/utils/ast/serializer.js.map +1 -1
  83. package/dist/utils/cssBox.d.ts +42 -0
  84. package/dist/utils/cssBox.d.ts.map +1 -0
  85. package/dist/utils/cssBox.js +156 -0
  86. package/dist/utils/cssBox.js.map +1 -0
  87. package/dist/utils/watchPaths.d.ts +11 -0
  88. package/dist/utils/watchPaths.d.ts.map +1 -0
  89. package/dist/utils/watchPaths.js +19 -0
  90. package/dist/utils/watchPaths.js.map +1 -0
  91. package/package.json +1 -1
  92. package/dist/transformers/safeClassNames.d.ts +0 -22
  93. package/dist/transformers/safeClassNames.d.ts.map +0 -1
  94. package/dist/transformers/safeClassNames.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/transformers/index.ts"],"sourcesContent":["import { parse, serialize } from '../utils/ast/index.ts'\nimport { inlineLinkDom } from './inlineLink.ts'\nimport { tailwindComponent } from './tailwindComponent.ts'\nimport { tailwindcss } from './tailwindcss.ts'\nimport { safeClassNames } from './safeClassNames.ts'\nimport { attributeToStyleDom } from './attributeToStyle.ts'\nimport { inlineCssDom } from './inlineCss.ts'\nimport { msoPlaceholders } from './msoPlaceholders.ts'\nimport { columnWidth } from './columnWidth.ts'\nimport { removeAttributesDom } from './removeAttributes.ts'\nimport { shorthandCssDom } from './shorthandCss.ts'\nimport { sixHexDom } from './sixHex.ts'\nimport { addAttributes } from './addAttributes.ts'\nimport { filtersDom } from './filters/index.ts'\nimport { baseDom } from './base.ts'\nimport { entitiesDom } from './entities.ts'\nimport { urlQueryDom } from './urlQuery.ts'\nimport { purgeCssDom } from './purgeCss.ts'\nimport { replaceStrings } from './replaceStrings.ts'\nimport { format } from './format.ts'\nimport { minify } from './minify.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\nimport type { TailwindBlock } from '../composables/renderContext.ts'\n\n/**\n * Run all Maizzle transformers on the rendered HTML.\n *\n * The HTML is parsed into a DOM once at the start and passed through all\n * DOM-based transformers as a shared `ChildNode[]`. After all DOM transformers\n * complete, the DOM is serialized back to a string exactly once.\n *\n * String-only transformers (those that rely on external tools that require a\n * raw HTML string) then run on the serialized output.\n *\n * Transformers run in a specific order:\n * 0. Inline link stylesheets — replace `<link rel=\"stylesheet\">` with `<style>` tags\n * 1. Tailwind CSS — compile CSS, lower syntax, optimize (cleanup + merge media queries)\n * 2. Safe class names\n * 3. Attribute to style\n * 4. CSS inliner\n * 5. Remove attributes\n * 6. Shorthand CSS\n * 7. Six-digit HEX\n * 8. Add attributes\n * 9. Filters\n * 10. Base URL\n * 11. URL query\n * 12. Purge CSS (serializes/parses internally around email-comb)\n * 13. Entities\n * + Vue-generated comments stripped here (on serialized string)\n * 14. Replace strings\n * 15. Prettify\n * 16. Minify\n */\nexport async function runTransformers(\n html: string,\n config: MaizzleConfig,\n filePath?: string,\n doctype?: string,\n tailwindBlocks?: TailwindBlock[],\n): Promise<string> {\n // Per-transformer skip map — only honored when useTransformers is an object.\n // Whole-pipeline opt-out (`useTransformers === false`) is handled upstream\n // in build.ts / render so we never reach this function in that case.\n //\n // A toggle set to `true` *force-enables* its transformer for this run by\n // layering on the matching config slice (e.g. `prettify: true` sets\n // `html.format = true`). This only applies to transformers whose\n // enable flag is a plain boolean — data-driven ones (filters,\n // baseURL, urlQuery, etc.) need actual config values, so a\n // bare `true` for those is a no-op.\n const toggles = typeof config.useTransformers === 'object' ? config.useTransformers : null\n const enabled = (key: keyof NonNullable<typeof toggles>) => toggles?.[key] !== false\n\n let effective = config\n if (toggles) {\n const cssOver: Record<string, unknown> = {}\n const htmlOver: Record<string, unknown> = {}\n if (toggles.inlineCss === true) cssOver.inline = true\n if (toggles.purgeCss === true) cssOver.purge = true\n if (toggles.safeClassNames === true) cssOver.safe = true\n if (toggles.shorthandCss === true) cssOver.shorthand = true\n if (toggles.sixHex === true) cssOver.sixHex = true\n if (toggles.prettify === true) htmlOver.format = true\n if (toggles.minify === true) htmlOver.minify = true\n if (toggles.entities === true) htmlOver.decodeEntities = true\n\n if (Object.keys(cssOver).length || Object.keys(htmlOver).length) {\n effective = {\n ...config,\n css: { ...config.css, ...cssOver },\n html: { ...config.html, ...htmlOver },\n }\n }\n }\n\n // Parse once — all DOM transformers share this array\n let dom = parse(html)\n\n // 0. Inline <link> stylesheets\n dom = await inlineLinkDom(dom, filePath)\n\n // 0.5. <Tailwind> component — compile per-block scoped CSS, inject into <head>\n if (tailwindBlocks?.length) {\n dom = await tailwindComponent(dom, tailwindBlocks, effective, filePath)\n }\n\n // 1. Tailwind CSS — always runs first\n dom = await tailwindcss(dom, effective, filePath)\n\n // 2. Safe class names\n if (enabled('safeClassNames')) dom = safeClassNames(dom, effective.css)\n\n // 3. Attribute to style\n if (enabled('attributeToStyle') && typeof effective.css?.inline === 'object' && effective.css.inline.attributeToStyle) {\n dom = attributeToStyleDom(dom, effective.css.inline.attributeToStyle)\n }\n\n // 4. CSS inliner (serializes/parses internally around juice)\n if (enabled('inlineCss') && effective.css?.inline) {\n const inlineOptions = typeof effective.css.inline === 'object' ? effective.css.inline : {}\n dom = inlineCssDom(dom, inlineOptions)\n }\n\n // 4.5. Resolve MSO placeholders (table width + td style) from inlined CSS\n dom = msoPlaceholders(dom)\n\n // 4.6. Resolve Column min-width placeholders from nearest sized ancestor\n dom = columnWidth(dom)\n\n // 5. Remove attributes\n if (enabled('removeAttributes')) {\n const removeRules = effective.html?.attributes?.remove\n dom = removeAttributesDom(dom, Array.isArray(removeRules) ? removeRules : [])\n }\n\n // 6. Shorthand CSS\n if (enabled('shorthandCss') && effective.css?.shorthand) {\n const shorthandOptions = typeof effective.css.shorthand === 'object' ? effective.css.shorthand : {}\n dom = shorthandCssDom(dom, shorthandOptions)\n }\n\n // 7. Six-digit HEX\n if (enabled('sixHex') && effective.css?.sixHex !== false) dom = sixHexDom(dom)\n\n // 8. Add attributes\n if (enabled('addAttributes')) dom = addAttributes(dom, effective.html?.attributes)\n\n // 9. Filters\n if (enabled('filters')) dom = filtersDom(dom, effective.filters)\n\n // 10. Base URL (serializes/parses internally for VML/MSO regex passes)\n if (enabled('baseURL') && effective.url?.base) dom = baseDom(dom, effective.url.base)\n\n // 11. URL query\n if (enabled('urlQuery') && effective.url?.query && Object.keys(effective.url.query).length > 0) {\n const { _options: queryOptions, ...queryParams } = effective.url.query as Record<string, unknown>\n dom = urlQueryDom(dom, queryParams, (queryOptions ?? {}) as import('../types/config.ts').UrlQueryOptions)\n }\n\n // 12. Remove unused CSS (serializes/parses internally around email-comb)\n if (enabled('purgeCss') && effective.css?.purge) {\n const purgeOptions = typeof effective.css.purge === 'object' ? effective.css.purge : {}\n dom = purgeCssDom(dom, purgeOptions)\n }\n\n // 13. Entities\n if (enabled('entities')) dom = entitiesDom(dom, effective.html?.decodeEntities)\n\n // Serialize once — remaining transformers operate on the HTML string\n const isXhtml = doctype ? /xhtml/i.test(doctype) : false\n let result = serialize(dom, { selfClosingTags: isXhtml })\n\n // 14. Replace strings\n if (enabled('replaceStrings')) result = replaceStrings(result, effective)\n\n // 15. Format — skipped when `minify` is enabled\n const minifyWillRun = enabled('minify') && !!effective.html?.minify\n if (enabled('prettify') && !minifyWillRun && effective.html?.format) {\n const formatOptions = typeof effective.html.format === 'object' ? effective.html.format : {}\n result = await format(result, formatOptions)\n }\n\n // 16. Minify\n if (enabled('minify') && effective.html?.minify) {\n const minifyOptions = typeof effective.html.minify === 'object' ? effective.html.minify : {}\n result = minify(result, minifyOptions)\n }\n\n // Strip self-closing slashes for HTML5 doctypes, but preserve content\n // inside MSO conditional comments (which are XML-ish and case/syntax sensitive).\n if (!isXhtml) {\n result = result.replace(\n /<!--\\[if [^\\]]*\\]>[\\s\\S]*?<!\\[endif\\]-->|( \\/>)/g,\n (match, selfClose) => selfClose ? '>' : match,\n )\n }\n\n return result\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/transformers/index.ts"],"sourcesContent":["import { parse, serialize } from '../utils/ast/index.ts'\nimport { inlineLinkDom } from './inlineLink.ts'\nimport { tailwindComponent } from './tailwindComponent.ts'\nimport { tailwindcss } from './tailwindcss.ts'\nimport { safeSelectorsDom } from './safeSelectors.ts'\nimport { attributeToStyleDom } from './attributeToStyle.ts'\nimport { inlineCssDom } from './inlineCss.ts'\nimport { msoPlaceholders } from './msoPlaceholders.ts'\nimport { columnWidth } from './columnWidth.ts'\nimport { removeAttributesDom } from './removeAttributes.ts'\nimport { shorthandCssDom } from './shorthandCss.ts'\nimport { sixHexDom } from './sixHex.ts'\nimport { addAttributesDom } from './addAttributes.ts'\nimport { filtersDom } from './filters/index.ts'\nimport { baseDom } from './base.ts'\nimport { entitiesDom } from './entities.ts'\nimport { urlQueryDom } from './urlQuery.ts'\nimport { purgeCssDom } from './purgeCss.ts'\nimport { replaceStrings } from './replaceStrings.ts'\nimport { format } from './format.ts'\nimport { minifyCodeInline } from './minifyCodeInline.ts'\nimport { minify } from './minify.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\nimport type { TailwindBlock } from '../composables/renderContext.ts'\n\n/**\n * Run all Maizzle transformers on the rendered HTML.\n *\n * The HTML is parsed into a DOM once at the start and passed through all\n * DOM-based transformers as a shared `ChildNode[]`. After all DOM transformers\n * complete, the DOM is serialized back to a string exactly once.\n *\n * String-only transformers (those that rely on external tools that require a\n * raw HTML string) then run on the serialized output.\n *\n * Transformers run in a specific order:\n * 0. Inline link stylesheets — replace `<link rel=\"stylesheet\">` with `<style>` tags\n * 1. Tailwind CSS — compile CSS, lower syntax, optimize (cleanup + merge media queries)\n * 2. Safe class names\n * 3. Attribute to style\n * 4. CSS inliner\n * 5. Remove attributes\n * 6. Shorthand CSS\n * 7. Six-digit HEX\n * 8. Add attributes\n * 9. Filters\n * 10. Base URL\n * 11. URL query\n * 12. Purge CSS (serializes/parses internally around email-comb)\n * 13. Entities\n * + Vue-generated comments stripped here (on serialized string)\n * 14. Replace strings\n * 15. Prettify\n * 16. Minify\n */\nexport async function runTransformers(\n html: string,\n config: MaizzleConfig,\n filePath?: string,\n doctype?: string,\n tailwindBlocks?: TailwindBlock[],\n): Promise<string> {\n // Per-transformer skip map — only honored when useTransformers is an object.\n // Whole-pipeline opt-out (`useTransformers === false`) is handled upstream\n // in build.ts / render so we never reach this function in that case.\n //\n // A toggle set to `true` *force-enables* its transformer for this run by\n // layering on the matching config slice (e.g. `prettify: true` sets\n // `html.format = true`). This only applies to transformers whose\n // enable flag is a plain boolean — data-driven ones (filters,\n // baseURL, urlQuery, etc.) need actual config values, so a\n // bare `true` for those is a no-op.\n const toggles = typeof config.useTransformers === 'object' ? config.useTransformers : null\n const enabled = (key: keyof NonNullable<typeof toggles>) => toggles?.[key] !== false\n\n let effective = config\n if (toggles) {\n const cssOver: Record<string, unknown> = {}\n const htmlOver: Record<string, unknown> = {}\n if (toggles.inlineCss === true) cssOver.inline = true\n if (toggles.purgeCss === true) cssOver.purge = true\n if (toggles.safeSelectors === true) cssOver.safe = true\n if (toggles.shorthandCss === true) cssOver.shorthand = true\n if (toggles.sixHex === true) cssOver.sixHex = true\n if (toggles.prettify === true) htmlOver.format = true\n if (toggles.minify === true) htmlOver.minify = true\n if (toggles.entities === true) htmlOver.decodeEntities = true\n\n if (Object.keys(cssOver).length || Object.keys(htmlOver).length) {\n effective = {\n ...config,\n css: { ...config.css, ...cssOver },\n html: { ...config.html, ...htmlOver },\n }\n }\n }\n\n // Parse once — all DOM transformers share this array\n let dom = parse(html)\n\n // 0. Inline <link> stylesheets\n dom = await inlineLinkDom(dom, filePath)\n\n // 0.5. <Tailwind> component — compile per-block scoped CSS, inject into <head>\n if (tailwindBlocks?.length) {\n dom = await tailwindComponent(dom, tailwindBlocks, effective, filePath)\n }\n\n // 1. Tailwind CSS — always runs first\n dom = await tailwindcss(dom, effective, filePath)\n\n // 2. Safe class names\n if (enabled('safeSelectors')) dom = safeSelectorsDom(dom, effective.css)\n\n // 3. Attribute to style\n if (enabled('attributeToStyle') && typeof effective.css?.inline === 'object' && effective.css.inline.attributeToStyle) {\n dom = attributeToStyleDom(dom, effective.css.inline.attributeToStyle)\n }\n\n // 4. CSS inliner (serializes/parses internally around juice)\n if (enabled('inlineCss') && effective.css?.inline) {\n const inlineOptions = typeof effective.css.inline === 'object' ? effective.css.inline : {}\n dom = inlineCssDom(dom, inlineOptions)\n }\n\n // 4.5. Resolve MSO placeholders (table width + td style) from inlined CSS\n dom = msoPlaceholders(dom)\n\n // 4.6. Resolve Column min-width placeholders from nearest sized ancestor\n dom = columnWidth(dom)\n\n // 5. Remove attributes\n if (enabled('removeAttributes')) {\n const removeRules = effective.html?.attributes?.remove\n dom = removeAttributesDom(dom, Array.isArray(removeRules) ? removeRules : [])\n }\n\n // 6. Shorthand CSS\n if (enabled('shorthandCss') && effective.css?.shorthand) {\n const shorthandOptions = typeof effective.css.shorthand === 'object' ? effective.css.shorthand : {}\n dom = shorthandCssDom(dom, shorthandOptions)\n }\n\n // 7. Six-digit HEX\n if (enabled('sixHex') && effective.css?.sixHex !== false) dom = sixHexDom(dom)\n\n // 8. Add attributes\n if (enabled('addAttributes')) dom = addAttributesDom(dom, effective.html?.attributes)\n\n // 9. Filters\n if (enabled('filters')) dom = filtersDom(dom, effective.filters)\n\n // 10. Base URL (serializes/parses internally for VML/MSO regex passes)\n if (enabled('baseURL') && effective.url?.base) dom = baseDom(dom, effective.url.base)\n\n // 11. URL query\n if (enabled('urlQuery') && effective.url?.query && Object.keys(effective.url.query).length > 0) {\n const { _options: queryOptions, ...queryParams } = effective.url.query as Record<string, unknown>\n dom = urlQueryDom(dom, queryParams, (queryOptions ?? {}) as import('../types/config.ts').UrlQueryOptions)\n }\n\n // 12. Remove unused CSS (serializes/parses internally around email-comb)\n if (enabled('purgeCss') && effective.css?.purge) {\n const purgeOptions = typeof effective.css.purge === 'object' ? effective.css.purge : {}\n dom = purgeCssDom(dom, purgeOptions)\n }\n\n // 13. Entities\n if (enabled('entities')) dom = entitiesDom(dom, effective.html?.decodeEntities)\n\n // Serialize once — remaining transformers operate on the HTML string\n const isXhtml = doctype ? /xhtml/i.test(doctype) : false\n let result = serialize(dom, { selfClosingTags: isXhtml })\n\n // 14. Replace strings\n if (enabled('replaceStrings')) result = replaceStrings(result, effective)\n\n // 15. Format — skipped when `minify` is enabled\n const minifyWillRun = enabled('minify') && !!effective.html?.minify\n if (enabled('prettify') && !minifyWillRun && effective.html?.format) {\n const formatOptions = typeof effective.html.format === 'object' ? effective.html.format : {}\n result = await format(result, formatOptions)\n }\n\n // 16. Minify\n if (enabled('minify') && effective.html?.minify) {\n const minifyOptions = typeof effective.html.minify === 'object' ? effective.html.minify : {}\n result = minify(result, minifyOptions)\n }\n\n // Strip self-closing slashes for HTML5 doctypes, but preserve content\n // inside MSO conditional comments (which are XML-ish and case/syntax sensitive).\n // MUST run BEFORE minifyCodeInline: at this point, CodeInline's shiki output\n // is still marker-encoded (§MZLT§/§MZGT§), so any ` />` in the highlighted\n // source code (e.g. a Vue self-closing tag) hasn't materialized yet and\n // can't be mistakenly stripped from inside a `<code>` element.\n if (!isXhtml) {\n result = result.replace(\n /<!--\\[if [^\\]]*\\]>[\\s\\S]*?<!\\[endif\\]-->|( \\/>)/g,\n (match, selfClose) => selfClose ? '>' : match,\n )\n }\n\n // 16.5. Strip whitespace inside `data-minify-inline` markers (CodeInline's\n // Shiki output, etc.). Runs after format/minify so it cleans up the\n // pretty-printer's indentation between sibling tags.\n result = minifyCodeInline(result)\n\n return result\n}\n"],"mappings":"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"}
@@ -60,14 +60,9 @@ function inlineCssDom(dom, options = {}) {
60
60
  return dom;
61
61
  }
62
62
  const result = parse(inlinedHtml);
63
- walk(result, (node) => {
63
+ if (preferUnitlessValues) walk(result, (node) => {
64
64
  const el = node;
65
- if (el.attribs?.style) {
66
- let style = el.attribs.style.replace(/:\s*/g, ": ").replace(/;\s*/g, "; ").trimEnd();
67
- if (!style.endsWith(";")) style += ";";
68
- if (preferUnitlessValues) style = style.replace(/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g, "0");
69
- el.attribs.style = style;
70
- }
65
+ 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");
71
66
  });
72
67
  /**
73
68
  * Restore `embed` from our marker so the purge step can detect
@@ -1 +1 @@
1
- {"version":3,"file":"inlineCss.js","names":[],"sources":["../../src/transformers/inlineCss.ts"],"sourcesContent":["import juice from 'juice'\nimport { walk, parse, serialize } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Options as JuiceOptions } from 'juice'\n\n/**\n * Options for the `inlineCss` transformer. Accepts every Juice option plus a\n * handful of Maizzle-specific extras.\n */\nexport interface InlineCssOptions extends JuiceOptions {\n /**\n * Convert `0px`, `0em` etc. to `0` in inline styles.\n *\n * @default true\n */\n preferUnitlessValues?: boolean\n /**\n * CSS selectors to preserve in `<style>` tags, even after inlining.\n * Mapped to Juice's `preservedSelectors` option.\n *\n * @default []\n */\n safelist?: string[]\n /**\n * Additional CSS string to inline alongside `<style>` tag contents.\n * Mapped to Juice's `extraCss` option.\n */\n customCSS?: string\n /**\n * Duplicate CSS properties to HTML attributes.\n * Mapped to Juice's static `styleToAttribute` property.\n */\n styleToAttribute?: Record<string, string>\n /**\n * CSS properties to exclude from inlining.\n * Mapped to Juice's static `excludedProperties` property.\n */\n excludedProperties?: string[]\n /**\n * Elements that can receive `width` HTML attributes.\n * Mapped to Juice's static `widthElements` property.\n *\n * @default ['img', 'video']\n */\n widthElements?: string[]\n /**\n * Elements that can receive `height` HTML attributes.\n * Mapped to Juice's static `heightElements` property.\n *\n * @default ['img', 'video']\n */\n heightElements?: string[]\n /**\n * Template language code blocks to preserve during inlining.\n * Mapped to Juice's static `codeBlocks` property.\n */\n codeBlocks?: Record<string, { start: string; end: string }>\n}\n\n/**\n * Inline CSS from `<style>` tags into `style` attributes on matching elements.\n *\n * @param html HTML string to transform.\n * @param options Juice options plus Maizzle-specific extras.\n * @returns The transformed HTML string.\n *\n * @example\n * import { inlineCss } from '@maizzle/framework'\n *\n * const out = inlineCss('<style>.red{color:red}</style><p class=\"red\">x</p>', {\n * removeStyleTags: true,\n * })\n */\nexport function inlineCss(html: string, options: InlineCssOptions = {}): string {\n return serialize(inlineCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link inlineCss} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids the redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function inlineCssDom(dom: ChildNode[], options: InlineCssOptions = {}): ChildNode[] {\n const {\n preferUnitlessValues = true,\n safelist,\n customCSS = '',\n styleToAttribute,\n excludedProperties,\n widthElements,\n heightElements,\n codeBlocks,\n ...juicePassthrough\n } = options\n\n // Configure Juice static properties\n juice.styleToAttribute = styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(excludedProperties ?? [])]\n juice.widthElements = (widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (codeBlocks && typeof codeBlocks === 'object') {\n Object.entries(codeBlocks).forEach(([key, value]) => {\n if (value.start && value.end) {\n juice.codeBlocks[key] = value\n }\n })\n }\n\n // Handle style tags with embed attributes.\n // We add a marker attribute that persists through the pipeline,\n // then restore data-embed from it after Juice runs.\n walk(dom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n // Sync data-embed ↔ embed. Use `in` so presence-only attrs\n // (<style embed> → attribs.embed === '') still count.\n if ('embed' in el.attribs && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n if ('data-embed' in el.attribs && !('embed' in el.attribs)) {\n el.attribs.embed = ''\n }\n\n // Add marker that persists through the pipeline\n if ('data-embed' in el.attribs) {\n el.attribs['data-maizzle-embed'] = ''\n }\n }\n })\n\n // Serialize for juice (juice requires a string)\n const serialized = serialize(dom)\n\n let inlinedHtml: string\n\n try {\n const juiceOptions: JuiceOptions = {\n removeStyleTags: juicePassthrough.removeStyleTags ?? false,\n removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true,\n applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true,\n applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true,\n preservedSelectors: safelist ?? [],\n ...customCSS ? { extraCss: customCSS } : {},\n inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? true,\n ...juicePassthrough,\n }\n\n inlinedHtml = juice(serialized, juiceOptions)\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n // Post-process for preferUnitlessValues\n const result = parse(inlinedHtml)\n\n walk(result, (node) => {\n const el = node as Element\n if (el.attribs?.style) {\n // Normalize style formatting: ensure spaces after : and ;\n let style = el.attribs.style\n .replace(/:\\s*/g, ': ')\n .replace(/;\\s*/g, '; ')\n .trimEnd()\n\n // Ensure trailing semicolon\n if (!style.endsWith(';')) {\n style += ';'\n }\n\n if (preferUnitlessValues) {\n style = style.replace(\n /\\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\\b/g,\n '0'\n )\n }\n\n el.attribs.style = style\n }\n })\n\n /**\n * Restore `embed` from our marker so the purge step can detect\n * these tags and skip them. Drop `data-embed` (juice's name)\n * since it's redundant once `embed` is back, and let purge\n * strip `embed` itself at the end of its run.\n */\n walk(result, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs && 'data-maizzle-embed' in el.attribs) {\n el.attribs.embed = ''\n delete el.attribs['data-embed']\n delete el.attribs['data-maizzle-embed']\n }\n })\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyEA,SAAgB,UAAU,MAAc,UAA4B,EAAE,EAAU;CAC9E,OAAO,UAAU,aAAa,MAAM,KAAK,EAAE,QAAQ,CAAC;;;;;;;AAQtD,SAAgB,aAAa,KAAkB,UAA4B,EAAE,EAAe;CAC1F,MAAM,EACJ,uBAAuB,MACvB,UACA,YAAY,IACZ,kBACA,oBACA,eACA,gBACA,YACA,GAAG,qBACD;CAGJ,MAAM,mBAAmB,oBAAoB,EAAE;CAC/C,MAAM,qBAAqB,CAAC,eAAe,GAAI,sBAAsB,EAAE,CAAE;CACzE,MAAM,iBAAiB,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;CACnF,MAAM,kBAAkB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;CAGrF,IAAI,cAAc,OAAO,eAAe,UACtC,OAAO,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;EACnD,IAAI,MAAM,SAAS,MAAM,KACvB,MAAM,WAAW,OAAO;GAE1B;CAMJ,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,GAAG,SAAS;GAGrC,IAAI,WAAW,GAAG,WAAW,EAAE,gBAAgB,GAAG,UAChD,GAAG,QAAQ,gBAAgB;GAE7B,IAAI,gBAAgB,GAAG,WAAW,EAAE,WAAW,GAAG,UAChD,GAAG,QAAQ,QAAQ;GAIrB,IAAI,gBAAgB,GAAG,SACrB,GAAG,QAAQ,wBAAwB;;GAGvC;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;CAEJ,IAAI;EAYF,cAAc,MAAM,YAAY;GAV9B,iBAAiB,iBAAiB,mBAAmB;GACrD,wBAAwB,iBAAiB,0BAA0B;GACnE,sBAAsB,iBAAiB,wBAAwB;GAC/D,uBAAuB,iBAAiB,yBAAyB;GACjE,oBAAoB,YAAY,EAAE;GAClC,GAAG,YAAY,EAAE,UAAU,WAAW,GAAG,EAAE;GAC3C,2BAA2B,iBAAiB,6BAA6B;GACzE,GAAG;GAGuC,CAAC;SACvC;EAEN,OAAO;;CAIT,MAAM,SAAS,MAAM,YAAY;CAEjC,KAAK,SAAS,SAAS;EACrB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,OAAO;GAErB,IAAI,QAAQ,GAAG,QAAQ,MACpB,QAAQ,SAAS,KAAK,CACtB,QAAQ,SAAS,KAAK,CACtB,SAAS;GAGZ,IAAI,CAAC,MAAM,SAAS,IAAI,EACtB,SAAS;GAGX,IAAI,sBACF,QAAQ,MAAM,QACZ,4DACA,IACD;GAGH,GAAG,QAAQ,QAAQ;;GAErB;;;;;;;CAQF,KAAK,SAAS,SAAS;EACrB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,GAAG,WAAW,wBAAwB,GAAG,SAAS;GAC3E,GAAG,QAAQ,QAAQ;GACnB,OAAO,GAAG,QAAQ;GAClB,OAAO,GAAG,QAAQ;;GAEpB;CAEF,OAAO"}
1
+ {"version":3,"file":"inlineCss.js","names":[],"sources":["../../src/transformers/inlineCss.ts"],"sourcesContent":["import juice from 'juice'\nimport { walk, parse, serialize } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Options as JuiceOptions } from 'juice'\n\n/**\n * Options for the `inlineCss` transformer. Accepts every Juice option plus a\n * handful of Maizzle-specific extras.\n */\nexport interface InlineCssOptions extends JuiceOptions {\n /**\n * Convert `0px`, `0em` etc. to `0` in inline styles.\n *\n * @default true\n */\n preferUnitlessValues?: boolean\n /**\n * CSS selectors to preserve in `<style>` tags, even after inlining.\n * Mapped to Juice's `preservedSelectors` option.\n *\n * @default []\n */\n safelist?: string[]\n /**\n * Additional CSS string to inline alongside `<style>` tag contents.\n * Mapped to Juice's `extraCss` option.\n */\n customCSS?: string\n /**\n * Duplicate CSS properties to HTML attributes.\n * Mapped to Juice's static `styleToAttribute` property.\n */\n styleToAttribute?: Record<string, string>\n /**\n * CSS properties to exclude from inlining.\n * Mapped to Juice's static `excludedProperties` property.\n */\n excludedProperties?: string[]\n /**\n * Elements that can receive `width` HTML attributes.\n * Mapped to Juice's static `widthElements` property.\n *\n * @default ['img', 'video']\n */\n widthElements?: string[]\n /**\n * Elements that can receive `height` HTML attributes.\n * Mapped to Juice's static `heightElements` property.\n *\n * @default ['img', 'video']\n */\n heightElements?: string[]\n /**\n * Template language code blocks to preserve during inlining.\n * Mapped to Juice's static `codeBlocks` property.\n */\n codeBlocks?: Record<string, { start: string; end: string }>\n}\n\n/**\n * Inline CSS from `<style>` tags into `style` attributes on matching elements.\n *\n * @param html HTML string to transform.\n * @param options Juice options plus Maizzle-specific extras.\n * @returns The transformed HTML string.\n *\n * @example\n * import { inlineCss } from '@maizzle/framework'\n *\n * const out = inlineCss('<style>.red{color:red}</style><p class=\"red\">x</p>', {\n * removeStyleTags: true,\n * })\n */\nexport function inlineCss(html: string, options: InlineCssOptions = {}): string {\n return serialize(inlineCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link inlineCss} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids the redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function inlineCssDom(dom: ChildNode[], options: InlineCssOptions = {}): ChildNode[] {\n const {\n preferUnitlessValues = true,\n safelist,\n customCSS = '',\n styleToAttribute,\n excludedProperties,\n widthElements,\n heightElements,\n codeBlocks,\n ...juicePassthrough\n } = options\n\n // Configure Juice static properties\n juice.styleToAttribute = styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(excludedProperties ?? [])]\n juice.widthElements = (widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (codeBlocks && typeof codeBlocks === 'object') {\n Object.entries(codeBlocks).forEach(([key, value]) => {\n if (value.start && value.end) {\n juice.codeBlocks[key] = value\n }\n })\n }\n\n // Handle style tags with embed attributes.\n // We add a marker attribute that persists through the pipeline,\n // then restore data-embed from it after Juice runs.\n walk(dom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n // Sync data-embed ↔ embed. Use `in` so presence-only attrs\n // (<style embed> → attribs.embed === '') still count.\n if ('embed' in el.attribs && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n if ('data-embed' in el.attribs && !('embed' in el.attribs)) {\n el.attribs.embed = ''\n }\n\n // Add marker that persists through the pipeline\n if ('data-embed' in el.attribs) {\n el.attribs['data-maizzle-embed'] = ''\n }\n }\n })\n\n // Serialize for juice (juice requires a string)\n const serialized = serialize(dom)\n\n let inlinedHtml: string\n\n try {\n const juiceOptions: JuiceOptions = {\n removeStyleTags: juicePassthrough.removeStyleTags ?? false,\n removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true,\n applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true,\n applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true,\n preservedSelectors: safelist ?? [],\n ...customCSS ? { extraCss: customCSS } : {},\n inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? true,\n ...juicePassthrough,\n }\n\n inlinedHtml = juice(serialized, juiceOptions)\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n const result = parse(inlinedHtml)\n\n if (preferUnitlessValues) {\n walk(result, (node) => {\n const el = node as Element\n if (el.attribs?.style) {\n el.attribs.style = el.attribs.style.replace(\n /\\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\\b/g,\n '0'\n )\n }\n })\n }\n\n /**\n * Restore `embed` from our marker so the purge step can detect\n * these tags and skip them. Drop `data-embed` (juice's name)\n * since it's redundant once `embed` is back, and let purge\n * strip `embed` itself at the end of its run.\n */\n walk(result, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs && 'data-maizzle-embed' in el.attribs) {\n el.attribs.embed = ''\n delete el.attribs['data-embed']\n delete el.attribs['data-maizzle-embed']\n }\n })\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyEA,SAAgB,UAAU,MAAc,UAA4B,EAAE,EAAU;CAC9E,OAAO,UAAU,aAAa,MAAM,KAAK,EAAE,QAAQ,CAAC;;;;;;;AAQtD,SAAgB,aAAa,KAAkB,UAA4B,EAAE,EAAe;CAC1F,MAAM,EACJ,uBAAuB,MACvB,UACA,YAAY,IACZ,kBACA,oBACA,eACA,gBACA,YACA,GAAG,qBACD;CAGJ,MAAM,mBAAmB,oBAAoB,EAAE;CAC/C,MAAM,qBAAqB,CAAC,eAAe,GAAI,sBAAsB,EAAE,CAAE;CACzE,MAAM,iBAAiB,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;CACnF,MAAM,kBAAkB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;CAGrF,IAAI,cAAc,OAAO,eAAe,UACtC,OAAO,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;EACnD,IAAI,MAAM,SAAS,MAAM,KACvB,MAAM,WAAW,OAAO;GAE1B;CAMJ,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,GAAG,SAAS;GAGrC,IAAI,WAAW,GAAG,WAAW,EAAE,gBAAgB,GAAG,UAChD,GAAG,QAAQ,gBAAgB;GAE7B,IAAI,gBAAgB,GAAG,WAAW,EAAE,WAAW,GAAG,UAChD,GAAG,QAAQ,QAAQ;GAIrB,IAAI,gBAAgB,GAAG,SACrB,GAAG,QAAQ,wBAAwB;;GAGvC;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;CAEJ,IAAI;EAYF,cAAc,MAAM,YAAY;GAV9B,iBAAiB,iBAAiB,mBAAmB;GACrD,wBAAwB,iBAAiB,0BAA0B;GACnE,sBAAsB,iBAAiB,wBAAwB;GAC/D,uBAAuB,iBAAiB,yBAAyB;GACjE,oBAAoB,YAAY,EAAE;GAClC,GAAG,YAAY,EAAE,UAAU,WAAW,GAAG,EAAE;GAC3C,2BAA2B,iBAAiB,6BAA6B;GACzE,GAAG;GAGuC,CAAC;SACvC;EAEN,OAAO;;CAGT,MAAM,SAAS,MAAM,YAAY;CAEjC,IAAI,sBACF,KAAK,SAAS,SAAS;EACrB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,OACd,GAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAClC,4DACA,IACD;GAEH;;;;;;;CASJ,KAAK,SAAS,SAAS;EACrB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,GAAG,WAAW,wBAAwB,GAAG,SAAS;GAC3E,GAAG,QAAQ,QAAQ;GACnB,OAAO,GAAG,QAAQ;GAClB,OAAO,GAAG,QAAQ;;GAEpB;CAEF,OAAO"}
@@ -1,4 +1,4 @@
1
- import { parse } from "../utils/ast/parser.js";
1
+ import { parse as parse$1 } from "../utils/ast/parser.js";
2
2
  import { walk } from "../utils/ast/walker.js";
3
3
  import { serialize } from "../utils/ast/serializer.js";
4
4
  import "../utils/ast/index.js";
@@ -28,7 +28,7 @@ import { dirname, resolve } from "node:path";
28
28
  * )
29
29
  */
30
30
  async function inlineLink(html, filePath) {
31
- return serialize(await inlineLinkDom(parse(html), filePath));
31
+ return serialize(await inlineLinkDom(parse$1(html), filePath));
32
32
  }
33
33
  /**
34
34
  * DOM-form of {@link inlineLink} used by the internal transformer pipeline.
@@ -1 +1 @@
1
- {"version":3,"file":"inlineLink.js","names":[],"sources":["../../src/transformers/inlineLink.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Inline `<link rel=\"stylesheet\">` tags as `<style>` tags.\n *\n * - Local file paths are inlined when `filePath` is provided (resolved\n * relative to it).\n * - Remote URLs (`http://` / `https://`) are only inlined when the link\n * carries an `inline` attribute, e.g. `<link rel=\"stylesheet\" inline href=\"…\">`.\n *\n * @param html HTML string to transform.\n * @param filePath Path of the source file the HTML came from, used as the\n * base for resolving relative `href` values. Required for\n * local-file inlining; remote `inline` links work without it.\n * @returns The transformed HTML string.\n *\n * @example\n * import { inlineLink } from '@maizzle/framework'\n *\n * const out = await inlineLink(\n * '<link rel=\"stylesheet\" href=\"./styles.css\">',\n * '/path/to/template.html',\n * )\n */\nexport async function inlineLink(html: string, filePath?: string): Promise<string> {\n return serialize(await inlineLinkDom(parse(html), filePath))\n}\n\n/**\n * DOM-form of {@link inlineLink} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport async function inlineLinkDom(dom: ChildNode[], filePath?: string): Promise<ChildNode[]> {\n const links: { node: Element; parent: ChildNode; index: number }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'link') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n if (attrs.rel !== 'stylesheet' || !attrs.href) return\n\n const parent = el.parent as ChildNode\n\n if (parent && 'children' in parent) {\n const index = (parent.children as ChildNode[]).indexOf(el)\n if (index !== -1) {\n links.push({ node: el, parent, index })\n }\n } else {\n // Top-level node\n const index = dom.indexOf(el)\n if (index !== -1) {\n links.push({ node: el, parent: null as any, index })\n }\n }\n })\n\n for (const { node, parent, index } of links) {\n const href = node.attribs.href\n const isRemote = href.startsWith('http://') || href.startsWith('https://')\n\n let css: string | undefined\n\n if (isRemote) {\n if (!('inline' in node.attribs)) continue\n\n try {\n const response = await fetch(href)\n css = await response.text()\n } catch {\n continue\n }\n } else {\n if (!filePath) continue\n\n try {\n const absolutePath = resolve(dirname(filePath), href)\n css = readFileSync(absolutePath, 'utf8')\n } catch {\n continue\n }\n }\n\n const styleNode = {\n type: 'tag',\n name: 'style',\n attribs: {},\n children: [{\n type: 'text',\n data: css,\n parent: null as any,\n }],\n parent: parent || null,\n } as any\n\n // Set parent reference on the text child\n styleNode.children[0].parent = styleNode\n\n const siblings = parent && 'children' in parent\n ? parent.children as ChildNode[]\n : dom\n\n siblings.splice(index, 1, styleNode)\n }\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,WAAW,MAAc,UAAoC;CACjF,OAAO,UAAU,MAAM,cAAc,MAAM,KAAK,EAAE,SAAS,CAAC;;;;;;;AAQ9D,eAAsB,cAAc,KAAkB,UAAyC;CAC7F,MAAM,QAA+D,EAAE;CAEvE,KAAK,MAAM,SAAS;EAClB,IAAK,KAAiB,SAAS,QAAQ;EAEvC,MAAM,KAAK;EACX,MAAM,QAAQ,GAAG,WAAW,EAAE;EAE9B,IAAI,MAAM,QAAQ,gBAAgB,CAAC,MAAM,MAAM;EAE/C,MAAM,SAAS,GAAG;EAElB,IAAI,UAAU,cAAc,QAAQ;GAClC,MAAM,QAAS,OAAO,SAAyB,QAAQ,GAAG;GAC1D,IAAI,UAAU,IACZ,MAAM,KAAK;IAAE,MAAM;IAAI;IAAQ;IAAO,CAAC;SAEpC;GAEL,MAAM,QAAQ,IAAI,QAAQ,GAAG;GAC7B,IAAI,UAAU,IACZ,MAAM,KAAK;IAAE,MAAM;IAAI,QAAQ;IAAa;IAAO,CAAC;;GAGxD;CAEF,KAAK,MAAM,EAAE,MAAM,QAAQ,WAAW,OAAO;EAC3C,MAAM,OAAO,KAAK,QAAQ;EAC1B,MAAM,WAAW,KAAK,WAAW,UAAU,IAAI,KAAK,WAAW,WAAW;EAE1E,IAAI;EAEJ,IAAI,UAAU;GACZ,IAAI,EAAE,YAAY,KAAK,UAAU;GAEjC,IAAI;IAEF,MAAM,OAAM,MADW,MAAM,KAAK,EACb,MAAM;WACrB;IACN;;SAEG;GACL,IAAI,CAAC,UAAU;GAEf,IAAI;IAEF,MAAM,aADe,QAAQ,QAAQ,SAAS,EAAE,KACjB,EAAE,OAAO;WAClC;IACN;;;EAIJ,MAAM,YAAY;GAChB,MAAM;GACN,MAAM;GACN,SAAS,EAAE;GACX,UAAU,CAAC;IACT,MAAM;IACN,MAAM;IACN,QAAQ;IACT,CAAC;GACF,QAAQ,UAAU;GACnB;EAGD,UAAU,SAAS,GAAG,SAAS;EAM/B,CAJiB,UAAU,cAAc,SACrC,OAAO,WACP,KAEK,OAAO,OAAO,GAAG,UAAU;;CAGtC,OAAO"}
1
+ {"version":3,"file":"inlineLink.js","names":["parse"],"sources":["../../src/transformers/inlineLink.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Inline `<link rel=\"stylesheet\">` tags as `<style>` tags.\n *\n * - Local file paths are inlined when `filePath` is provided (resolved\n * relative to it).\n * - Remote URLs (`http://` / `https://`) are only inlined when the link\n * carries an `inline` attribute, e.g. `<link rel=\"stylesheet\" inline href=\"…\">`.\n *\n * @param html HTML string to transform.\n * @param filePath Path of the source file the HTML came from, used as the\n * base for resolving relative `href` values. Required for\n * local-file inlining; remote `inline` links work without it.\n * @returns The transformed HTML string.\n *\n * @example\n * import { inlineLink } from '@maizzle/framework'\n *\n * const out = await inlineLink(\n * '<link rel=\"stylesheet\" href=\"./styles.css\">',\n * '/path/to/template.html',\n * )\n */\nexport async function inlineLink(html: string, filePath?: string): Promise<string> {\n return serialize(await inlineLinkDom(parse(html), filePath))\n}\n\n/**\n * DOM-form of {@link inlineLink} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport async function inlineLinkDom(dom: ChildNode[], filePath?: string): Promise<ChildNode[]> {\n const links: { node: Element; parent: ChildNode; index: number }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'link') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n if (attrs.rel !== 'stylesheet' || !attrs.href) return\n\n const parent = el.parent as ChildNode\n\n if (parent && 'children' in parent) {\n const index = (parent.children as ChildNode[]).indexOf(el)\n if (index !== -1) {\n links.push({ node: el, parent, index })\n }\n } else {\n // Top-level node\n const index = dom.indexOf(el)\n if (index !== -1) {\n links.push({ node: el, parent: null as any, index })\n }\n }\n })\n\n for (const { node, parent, index } of links) {\n const href = node.attribs.href\n const isRemote = href.startsWith('http://') || href.startsWith('https://')\n\n let css: string | undefined\n\n if (isRemote) {\n if (!('inline' in node.attribs)) continue\n\n try {\n const response = await fetch(href)\n css = await response.text()\n } catch {\n continue\n }\n } else {\n if (!filePath) continue\n\n try {\n const absolutePath = resolve(dirname(filePath), href)\n css = readFileSync(absolutePath, 'utf8')\n } catch {\n continue\n }\n }\n\n const styleNode = {\n type: 'tag',\n name: 'style',\n attribs: {},\n children: [{\n type: 'text',\n data: css,\n parent: null as any,\n }],\n parent: parent || null,\n } as any\n\n // Set parent reference on the text child\n styleNode.children[0].parent = styleNode\n\n const siblings = parent && 'children' in parent\n ? parent.children as ChildNode[]\n : dom\n\n siblings.splice(index, 1, styleNode)\n }\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,WAAW,MAAc,UAAoC;CACjF,OAAO,UAAU,MAAM,cAAcA,QAAM,KAAK,EAAE,SAAS,CAAC;;;;;;;AAQ9D,eAAsB,cAAc,KAAkB,UAAyC;CAC7F,MAAM,QAA+D,EAAE;CAEvE,KAAK,MAAM,SAAS;EAClB,IAAK,KAAiB,SAAS,QAAQ;EAEvC,MAAM,KAAK;EACX,MAAM,QAAQ,GAAG,WAAW,EAAE;EAE9B,IAAI,MAAM,QAAQ,gBAAgB,CAAC,MAAM,MAAM;EAE/C,MAAM,SAAS,GAAG;EAElB,IAAI,UAAU,cAAc,QAAQ;GAClC,MAAM,QAAS,OAAO,SAAyB,QAAQ,GAAG;GAC1D,IAAI,UAAU,IACZ,MAAM,KAAK;IAAE,MAAM;IAAI;IAAQ;IAAO,CAAC;SAEpC;GAEL,MAAM,QAAQ,IAAI,QAAQ,GAAG;GAC7B,IAAI,UAAU,IACZ,MAAM,KAAK;IAAE,MAAM;IAAI,QAAQ;IAAa;IAAO,CAAC;;GAGxD;CAEF,KAAK,MAAM,EAAE,MAAM,QAAQ,WAAW,OAAO;EAC3C,MAAM,OAAO,KAAK,QAAQ;EAC1B,MAAM,WAAW,KAAK,WAAW,UAAU,IAAI,KAAK,WAAW,WAAW;EAE1E,IAAI;EAEJ,IAAI,UAAU;GACZ,IAAI,EAAE,YAAY,KAAK,UAAU;GAEjC,IAAI;IAEF,MAAM,OAAM,MADW,MAAM,KAAK,EACb,MAAM;WACrB;IACN;;SAEG;GACL,IAAI,CAAC,UAAU;GAEf,IAAI;IAEF,MAAM,aADe,QAAQ,QAAQ,SAAS,EAAE,KACjB,EAAE,OAAO;WAClC;IACN;;;EAIJ,MAAM,YAAY;GAChB,MAAM;GACN,MAAM;GACN,SAAS,EAAE;GACX,UAAU,CAAC;IACT,MAAM;IACN,MAAM;IACN,QAAQ;IACT,CAAC;GACF,QAAQ,UAAU;GACnB;EAGD,UAAU,SAAS,GAAG,SAAS;EAM/B,CAJiB,UAAU,cAAc,SACrC,OAAO,WACP,KAEK,OAAO,OAAO,GAAG,UAAU;;CAGtC,OAAO"}
@@ -0,0 +1,29 @@
1
+ //#region src/transformers/minifyCodeInline.d.ts
2
+ /**
3
+ * Restore HTML inside elements marked `data-minify-inline`, then strip the
4
+ * marker attribute and trim formatter-injected whitespace.
5
+ *
6
+ * Named for its primary client, `<CodeInline theme="…">`. The component
7
+ * replaces shiki's structural `<`/`>` with private markers `§MZLT§` /
8
+ * `§MZGT§` so the format pass (oxfmt with `htmlWhitespaceSensitivity:
9
+ * 'ignore'`) can't see them as real angle brackets and reflow the
10
+ * chain of `<span>` tokens. Source-level entities like `&lt;` (a
11
+ * literal `<` in the user's code) are made of `&`, `l`, `t`, `;` —
12
+ * no real `<` — so they pass through this pipeline untouched and
13
+ * land in the browser as entities, rendering correctly as `<`.
14
+ *
15
+ * Runs unconditionally near the end of the pipeline so:
16
+ * 1. The markers always get decoded back to real `<` / `>`.
17
+ * 2. The `data-minify-inline` attribute never leaks to final HTML
18
+ * (whether or not the inner content had markers).
19
+ * 3. Whitespace the formatter injected around the inner content
20
+ * (e.g. between `<code>` and the text node) is trimmed so the
21
+ * inline element lands flush.
22
+ *
23
+ * The marker attribute is intentionally generic so any component facing
24
+ * the same formatter-vs-inline-structure problem can opt in.
25
+ */
26
+ declare function minifyCodeInline(html: string): string;
27
+ //#endregion
28
+ export { minifyCodeInline };
29
+ //# sourceMappingURL=minifyCodeInline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minifyCodeInline.d.ts","names":[],"sources":["../../src/transformers/minifyCodeInline.ts"],"mappings":";;AAwBA;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,CAAiB,IAAA"}
@@ -0,0 +1,36 @@
1
+ //#region src/transformers/minifyCodeInline.ts
2
+ /**
3
+ * Restore HTML inside elements marked `data-minify-inline`, then strip the
4
+ * marker attribute and trim formatter-injected whitespace.
5
+ *
6
+ * Named for its primary client, `<CodeInline theme="…">`. The component
7
+ * replaces shiki's structural `<`/`>` with private markers `§MZLT§` /
8
+ * `§MZGT§` so the format pass (oxfmt with `htmlWhitespaceSensitivity:
9
+ * 'ignore'`) can't see them as real angle brackets and reflow the
10
+ * chain of `<span>` tokens. Source-level entities like `&lt;` (a
11
+ * literal `<` in the user's code) are made of `&`, `l`, `t`, `;` —
12
+ * no real `<` — so they pass through this pipeline untouched and
13
+ * land in the browser as entities, rendering correctly as `<`.
14
+ *
15
+ * Runs unconditionally near the end of the pipeline so:
16
+ * 1. The markers always get decoded back to real `<` / `>`.
17
+ * 2. The `data-minify-inline` attribute never leaks to final HTML
18
+ * (whether or not the inner content had markers).
19
+ * 3. Whitespace the formatter injected around the inner content
20
+ * (e.g. between `<code>` and the text node) is trimmed so the
21
+ * inline element lands flush.
22
+ *
23
+ * The marker attribute is intentionally generic so any component facing
24
+ * the same formatter-vs-inline-structure problem can opt in.
25
+ */
26
+ function minifyCodeInline(html) {
27
+ if (!html.includes("data-minify-inline")) return html;
28
+ return html.replace(/<([a-zA-Z][\w-]*)([^>]*?)\s+data-minify-inline(?:="[^"]*")?([^>]*)>([\s\S]*?)<\/\1>/g, (_full, tag, before, after, contents) => {
29
+ const cleanedAttrs = `${before}${after}`.replace(/\s+/g, " ").trim();
30
+ return `${cleanedAttrs ? `<${tag} ${cleanedAttrs}>` : `<${tag}>`}${contents.replace(/§MZLT§/g, "<").replace(/§MZGT§/g, ">").trim()}</${tag}>`;
31
+ });
32
+ }
33
+ //#endregion
34
+ export { minifyCodeInline };
35
+
36
+ //# sourceMappingURL=minifyCodeInline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minifyCodeInline.js","names":[],"sources":["../../src/transformers/minifyCodeInline.ts"],"sourcesContent":["/**\n * Restore HTML inside elements marked `data-minify-inline`, then strip the\n * marker attribute and trim formatter-injected whitespace.\n *\n * Named for its primary client, `<CodeInline theme=\"…\">`. The component\n * replaces shiki's structural `<`/`>` with private markers `§MZLT§` /\n * `§MZGT§` so the format pass (oxfmt with `htmlWhitespaceSensitivity:\n * 'ignore'`) can't see them as real angle brackets and reflow the\n * chain of `<span>` tokens. Source-level entities like `&lt;` (a\n * literal `<` in the user's code) are made of `&`, `l`, `t`, `;` —\n * no real `<` — so they pass through this pipeline untouched and\n * land in the browser as entities, rendering correctly as `<`.\n *\n * Runs unconditionally near the end of the pipeline so:\n * 1. The markers always get decoded back to real `<` / `>`.\n * 2. The `data-minify-inline` attribute never leaks to final HTML\n * (whether or not the inner content had markers).\n * 3. Whitespace the formatter injected around the inner content\n * (e.g. between `<code>` and the text node) is trimmed so the\n * inline element lands flush.\n *\n * The marker attribute is intentionally generic so any component facing\n * the same formatter-vs-inline-structure problem can opt in.\n */\nexport function minifyCodeInline(html: string): string {\n if (!html.includes('data-minify-inline')) return html\n\n return html.replace(\n /<([a-zA-Z][\\w-]*)([^>]*?)\\s+data-minify-inline(?:=\"[^\"]*\")?([^>]*)>([\\s\\S]*?)<\\/\\1>/g,\n (_full, tag, before, after, contents) => {\n const cleanedAttrs = `${before}${after}`.replace(/\\s+/g, ' ').trim()\n const open = cleanedAttrs ? `<${tag} ${cleanedAttrs}>` : `<${tag}>`\n const decoded = contents\n .replace(/§MZLT§/g, '<')\n .replace(/§MZGT§/g, '>')\n .trim()\n return `${open}${decoded}</${tag}>`\n },\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,iBAAiB,MAAsB;CACrD,IAAI,CAAC,KAAK,SAAS,qBAAqB,EAAE,OAAO;CAEjD,OAAO,KAAK,QACV,yFACC,OAAO,KAAK,QAAQ,OAAO,aAAa;EACvC,MAAM,eAAe,GAAG,SAAS,QAAQ,QAAQ,QAAQ,IAAI,CAAC,MAAM;EAMpE,OAAO,GALM,eAAe,IAAI,IAAI,GAAG,aAAa,KAAK,IAAI,IAAI,KACjD,SACb,QAAQ,WAAW,IAAI,CACvB,QAAQ,WAAW,IAAI,CACvB,MACqB,CAAC,IAAI,IAAI;GAEpC"}
@@ -13,11 +13,16 @@ import { ChildNode } from "domhandler";
13
13
  * back to `data-maizzle-msow-fallback` (default `600px`) when the
14
14
  * value can't be parsed.
15
15
  *
16
- * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>`'s
17
- * MSO `<td>`. Source element is marked with `data-maizzle-mso-td-id`.
18
- * Copies every `padding*` declaration from inlined style and appends
19
- * the `data-maizzle-mso-style` value (the user's `msoStyle` prop).
20
- * Empty input resolves to '' so the placeholder collapses cleanly.
16
+ * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>` and
17
+ * `<Section>`'s MSO `<td>`. Source element is marked with
18
+ * `data-maizzle-mso-td-id`. Extracts from the inlined style:
19
+ * - `background-color` (always, when present) so Word paints the cell.
20
+ * - `padding*` (only when no horizontal border on the element, since
21
+ * Word drops div padding without a border and a copy would
22
+ * double-pad with one).
23
+ * Appends the `data-maizzle-mso-style` value (the user's `msoStyle`
24
+ * prop) last so it wins on duplicates. Empty input resolves to ''
25
+ * so the placeholder collapses cleanly.
21
26
  *
22
27
  * Single collect-walk + single substitute-walk: the same Container div
23
28
  * carries both marker kinds, so one element visit fills both maps.
@@ -1 +1 @@
1
- {"version":3,"file":"msoPlaceholders.d.ts","names":[],"sources":["../../src/transformers/msoPlaceholders.ts"],"mappings":";;;;;AA4CA;;;;;;;;;;;;;;;;;;;iBAAgB,eAAA,CAAgB,GAAA,EAAK,SAAA,KAAc,SAAA"}
1
+ {"version":3,"file":"msoPlaceholders.d.ts","names":[],"sources":["../../src/transformers/msoPlaceholders.ts"],"mappings":";;;;;AAkDA;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,eAAA,CAAgB,GAAA,EAAK,SAAA,KAAc,SAAA"}
@@ -1,10 +1,11 @@
1
1
  import { walk } from "../utils/ast/walker.js";
2
2
  import "../utils/ast/index.js";
3
+ import { horizontalBorderPx } from "../utils/cssBox.js";
4
+ import safeParser from "postcss-safe-parser";
3
5
  //#region src/transformers/msoPlaceholders.ts
4
6
  const RE_MAX_WIDTH = /(?:^|;\s*)max-width:\s*([^;]+)/i;
5
7
  const RE_WIDTH = /(?:^|;\s*)width:\s*([^;]+)/i;
6
8
  const RE_PERCENT = /^[\d.]+%$/;
7
- const PADDING_DECL_RE = /(?:^|;)\s*(padding(?:-[a-z-]+)?\s*:\s*[^;]+)/gi;
8
9
  function resolveWidth(value) {
9
10
  const trimmed = value.trim();
10
11
  if (RE_PERCENT.test(trimmed)) return trimmed;
@@ -31,11 +32,16 @@ function resolveWidth(value) {
31
32
  * back to `data-maizzle-msow-fallback` (default `600px`) when the
32
33
  * value can't be parsed.
33
34
  *
34
- * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>`'s
35
- * MSO `<td>`. Source element is marked with `data-maizzle-mso-td-id`.
36
- * Copies every `padding*` declaration from inlined style and appends
37
- * the `data-maizzle-mso-style` value (the user's `msoStyle` prop).
38
- * Empty input resolves to '' so the placeholder collapses cleanly.
35
+ * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>` and
36
+ * `<Section>`'s MSO `<td>`. Source element is marked with
37
+ * `data-maizzle-mso-td-id`. Extracts from the inlined style:
38
+ * - `background-color` (always, when present) so Word paints the cell.
39
+ * - `padding*` (only when no horizontal border on the element, since
40
+ * Word drops div padding without a border and a copy would
41
+ * double-pad with one).
42
+ * Appends the `data-maizzle-mso-style` value (the user's `msoStyle`
43
+ * prop) last so it wins on duplicates. Empty input resolves to ''
44
+ * so the placeholder collapses cleanly.
39
45
  *
40
46
  * Single collect-walk + single substitute-walk: the same Container div
41
47
  * carries both marker kinds, so one element visit fills both maps.
@@ -62,8 +68,33 @@ function msoPlaceholders(dom) {
62
68
  delete a["data-maizzle-mso-td-id"];
63
69
  const msoStyle = (a["data-maizzle-mso-style"] ?? "").trim().replace(/;\s*$/, "");
64
70
  delete a["data-maizzle-mso-style"];
71
+ /**
72
+ * Build the MSO td's inline style from three sources, in CSS priority order
73
+ * (earlier = lower, later wins on dupes):
74
+ *
75
+ * 1. `background-color` (always, when present) — Word paints the cell
76
+ * under any padding area or inline-block gap, not just the div.
77
+ *
78
+ * 2. `padding*` (hoisted only when no horizontal border) — Word drops
79
+ * div padding without a stabilizing border, so the td has to
80
+ * carry it. With a border, Word renders div padding and a td
81
+ * copy would double-pad.
82
+ *
83
+ * 3. The user's `mso-style` prop — last so it overrides anything the
84
+ * auto-hoist computed.
85
+ */
65
86
  const parts = [];
66
- if (style) for (const m of style.matchAll(PADDING_DECL_RE)) parts.push(m[1].trim());
87
+ if (style) {
88
+ const root = safeParser(style);
89
+ let bgDecl;
90
+ root.walkDecls("background-color", (d) => {
91
+ bgDecl = `background-color: ${d.value}${d.important ? " !important" : ""}`;
92
+ });
93
+ if (bgDecl) parts.push(bgDecl);
94
+ if (horizontalBorderPx(root) === 0) root.walkDecls((d) => {
95
+ if (/^padding(-|$)/.test(d.prop)) parts.push(`${d.prop}: ${d.value}${d.important ? " !important" : ""}`);
96
+ });
97
+ }
67
98
  if (msoStyle) parts.push(msoStyle);
68
99
  tdStyles.set(tdId, parts.length ? ` style="${parts.join("; ")}"` : "");
69
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"msoPlaceholders.js","names":["el"],"sources":["../../src/transformers/msoPlaceholders.ts"],"sourcesContent":["import { walk } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\n\nconst RE_MAX_WIDTH = /(?:^|;\\s*)max-width:\\s*([^;]+)/i\nconst RE_WIDTH = /(?:^|;\\s*)width:\\s*([^;]+)/i\nconst RE_PERCENT = /^[\\d.]+%$/\nconst PADDING_DECL_RE = /(?:^|;)\\s*(padding(?:-[a-z-]+)?\\s*:\\s*[^;]+)/gi\n\nfunction resolveWidth(value: string): string | null {\n const trimmed = value.trim()\n if (RE_PERCENT.test(trimmed)) return trimmed\n const m = trimmed.match(/^([\\d.]+)(px|rem|em|pt)?$/i)\n if (!m) return null\n const n = parseFloat(m[1])\n switch ((m[2] || 'px').toLowerCase()) {\n case 'px': return `${Math.round(n)}px`\n case 'rem':\n case 'em': return `${Math.round(n * 16)}px`\n case 'pt': return `${Math.round(n * 1.333)}px`\n default: return null\n }\n}\n\n/**\n * Resolve all `__MAIZZLE_MSO*__` placeholders inside MSO conditional comments\n * by reading inlined style + `data-*` markers on the paired elements.\n *\n * Two placeholder families:\n *\n * MSOW (`__MAIZZLE_MSOW_{id}__`) — emitted by `<Container>` and `<Section>`.\n * Source element is marked with `data-maizzle-msow-id`. Reads inlined\n * `max-width:` (falls back to `width:`) and normalizes to px. Falls\n * back to `data-maizzle-msow-fallback` (default `600px`) when the\n * value can't be parsed.\n *\n * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>`'s\n * MSO `<td>`. Source element is marked with `data-maizzle-mso-td-id`.\n * Copies every `padding*` declaration from inlined style and appends\n * the `data-maizzle-mso-style` value (the user's `msoStyle` prop).\n * Empty input resolves to '' so the placeholder collapses cleanly.\n *\n * Single collect-walk + single substitute-walk: the same Container div\n * carries both marker kinds, so one element visit fills both maps.\n */\nexport function msoPlaceholders(dom: ChildNode[]): ChildNode[] {\n const widths = new Map<string, string>()\n const tdStyles = new Map<string, string>()\n\n walk(dom, (node) => {\n const el = node as Element\n const a = el.attribs\n if (!a) return\n\n const msowId = a['data-maizzle-msow-id']\n const tdId = a['data-maizzle-mso-td-id']\n if (!msowId && !tdId) return\n\n const style = a.style ?? ''\n\n if (msowId) {\n delete a['data-maizzle-msow-id']\n const fallback = a['data-maizzle-msow-fallback'] ?? '600px'\n delete a['data-maizzle-msow-fallback']\n const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1]\n const resolved = raw ? resolveWidth(raw) : null\n widths.set(msowId, resolved ?? fallback)\n }\n\n if (tdId) {\n delete a['data-maizzle-mso-td-id']\n const msoStyle = (a['data-maizzle-mso-style'] ?? '').trim().replace(/;\\s*$/, '')\n delete a['data-maizzle-mso-style']\n\n const parts: string[] = []\n if (style) {\n for (const m of style.matchAll(PADDING_DECL_RE)) {\n parts.push(m[1].trim())\n }\n }\n if (msoStyle) parts.push(msoStyle)\n\n tdStyles.set(tdId, parts.length ? ` style=\"${parts.join('; ')}\"` : '')\n }\n })\n\n if (widths.size === 0 && tdStyles.size === 0) return dom\n\n walk(dom, (node) => {\n if (node.type !== 'comment') return\n let data = (node as any).data as string\n if (!data) return\n const hasMsow = widths.size > 0 && data.includes('__MAIZZLE_MSOW_')\n const hasTd = tdStyles.size > 0 && data.includes('__MAIZZLE_MSOTDSTYLE_')\n if (!hasMsow && !hasTd) return\n\n if (hasMsow) {\n for (const [id, val] of widths) {\n data = data.replaceAll(`__MAIZZLE_MSOW_${id}__`, val)\n }\n }\n if (hasTd) {\n for (const [id, val] of tdStyles) {\n data = data.replaceAll(`__MAIZZLE_MSOTDSTYLE_${id}__`, val)\n }\n }\n ;(node as any).data = data\n })\n\n return dom\n}\n"],"mappings":";;;AAGA,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAExB,SAAS,aAAa,OAA8B;CAClD,MAAM,UAAU,MAAM,MAAM;CAC5B,IAAI,WAAW,KAAK,QAAQ,EAAE,OAAO;CACrC,MAAM,IAAI,QAAQ,MAAM,6BAA6B;CACrD,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,IAAI,WAAW,EAAE,GAAG;CAC1B,SAAS,EAAE,MAAM,MAAM,aAAa,EAApC;EACE,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,EAAE,CAAC;EACnC,KAAK;EACL,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG,CAAC;EACxC,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;EAC3C,SAAS,OAAO;;;;;;;;;;;;;;;;;;;;;;;;AAyBpB,SAAgB,gBAAgB,KAA+B;CAC7D,MAAM,yBAAS,IAAI,KAAqB;CACxC,MAAM,2BAAW,IAAI,KAAqB;CAE1C,KAAK,MAAM,SAAS;EAElB,MAAM,IAAIA,KAAG;EACb,IAAI,CAAC,GAAG;EAER,MAAM,SAAS,EAAE;EACjB,MAAM,OAAO,EAAE;EACf,IAAI,CAAC,UAAU,CAAC,MAAM;EAEtB,MAAM,QAAQ,EAAE,SAAS;EAEzB,IAAI,QAAQ;GACV,OAAO,EAAE;GACT,MAAM,WAAW,EAAE,iCAAiC;GACpD,OAAO,EAAE;GACT,MAAM,MAAM,MAAM,MAAM,aAAa,GAAG,MAAM,MAAM,MAAM,SAAS,GAAG;GACtE,MAAM,WAAW,MAAM,aAAa,IAAI,GAAG;GAC3C,OAAO,IAAI,QAAQ,YAAY,SAAS;;EAG1C,IAAI,MAAM;GACR,OAAO,EAAE;GACT,MAAM,YAAY,EAAE,6BAA6B,IAAI,MAAM,CAAC,QAAQ,SAAS,GAAG;GAChF,OAAO,EAAE;GAET,MAAM,QAAkB,EAAE;GAC1B,IAAI,OACF,KAAK,MAAM,KAAK,MAAM,SAAS,gBAAgB,EAC7C,MAAM,KAAK,EAAE,GAAG,MAAM,CAAC;GAG3B,IAAI,UAAU,MAAM,KAAK,SAAS;GAElC,SAAS,IAAI,MAAM,MAAM,SAAS,WAAW,MAAM,KAAK,KAAK,CAAC,KAAK,GAAG;;GAExE;CAEF,IAAI,OAAO,SAAS,KAAK,SAAS,SAAS,GAAG,OAAO;CAErD,KAAK,MAAM,SAAS;EAClB,IAAI,KAAK,SAAS,WAAW;EAC7B,IAAI,OAAQ,KAAa;EACzB,IAAI,CAAC,MAAM;EACX,MAAM,UAAU,OAAO,OAAO,KAAK,KAAK,SAAS,kBAAkB;EACnE,MAAM,QAAQ,SAAS,OAAO,KAAK,KAAK,SAAS,wBAAwB;EACzE,IAAI,CAAC,WAAW,CAAC,OAAO;EAExB,IAAI,SACF,KAAK,MAAM,CAAC,IAAI,QAAQ,QACtB,OAAO,KAAK,WAAW,kBAAkB,GAAG,KAAK,IAAI;EAGzD,IAAI,OACF,KAAK,MAAM,CAAC,IAAI,QAAQ,UACtB,OAAO,KAAK,WAAW,wBAAwB,GAAG,KAAK,IAAI;EAG9D,KAAc,OAAO;GACtB;CAEF,OAAO"}
1
+ {"version":3,"file":"msoPlaceholders.js","names":["el"],"sources":["../../src/transformers/msoPlaceholders.ts"],"sourcesContent":["import safeParser from 'postcss-safe-parser'\nimport { walk } from '../utils/ast/index.ts'\nimport { horizontalBorderPx } from '../utils/cssBox.ts'\nimport type { ChildNode, Element } from 'domhandler'\n\nconst RE_MAX_WIDTH = /(?:^|;\\s*)max-width:\\s*([^;]+)/i\nconst RE_WIDTH = /(?:^|;\\s*)width:\\s*([^;]+)/i\nconst RE_PERCENT = /^[\\d.]+%$/\n\nfunction resolveWidth(value: string): string | null {\n const trimmed = value.trim()\n if (RE_PERCENT.test(trimmed)) return trimmed\n const m = trimmed.match(/^([\\d.]+)(px|rem|em|pt)?$/i)\n if (!m) return null\n const n = parseFloat(m[1])\n switch ((m[2] || 'px').toLowerCase()) {\n case 'px': return `${Math.round(n)}px`\n case 'rem':\n case 'em': return `${Math.round(n * 16)}px`\n case 'pt': return `${Math.round(n * 1.333)}px`\n default: return null\n }\n}\n\n/**\n * Resolve all `__MAIZZLE_MSO*__` placeholders inside MSO conditional comments\n * by reading inlined style + `data-*` markers on the paired elements.\n *\n * Two placeholder families:\n *\n * MSOW (`__MAIZZLE_MSOW_{id}__`) — emitted by `<Container>` and `<Section>`.\n * Source element is marked with `data-maizzle-msow-id`. Reads inlined\n * `max-width:` (falls back to `width:`) and normalizes to px. Falls\n * back to `data-maizzle-msow-fallback` (default `600px`) when the\n * value can't be parsed.\n *\n * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>` and\n * `<Section>`'s MSO `<td>`. Source element is marked with\n * `data-maizzle-mso-td-id`. Extracts from the inlined style:\n * - `background-color` (always, when present) so Word paints the cell.\n * - `padding*` (only when no horizontal border on the element, since\n * Word drops div padding without a border and a copy would\n * double-pad with one).\n * Appends the `data-maizzle-mso-style` value (the user's `msoStyle`\n * prop) last so it wins on duplicates. Empty input resolves to ''\n * so the placeholder collapses cleanly.\n *\n * Single collect-walk + single substitute-walk: the same Container div\n * carries both marker kinds, so one element visit fills both maps.\n */\nexport function msoPlaceholders(dom: ChildNode[]): ChildNode[] {\n const widths = new Map<string, string>()\n const tdStyles = new Map<string, string>()\n\n walk(dom, (node) => {\n const el = node as Element\n const a = el.attribs\n if (!a) return\n\n const msowId = a['data-maizzle-msow-id']\n const tdId = a['data-maizzle-mso-td-id']\n if (!msowId && !tdId) return\n\n const style = a.style ?? ''\n\n if (msowId) {\n delete a['data-maizzle-msow-id']\n const fallback = a['data-maizzle-msow-fallback'] ?? '600px'\n delete a['data-maizzle-msow-fallback']\n const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1]\n const resolved = raw ? resolveWidth(raw) : null\n widths.set(msowId, resolved ?? fallback)\n }\n\n if (tdId) {\n delete a['data-maizzle-mso-td-id']\n const msoStyle = (a['data-maizzle-mso-style'] ?? '').trim().replace(/;\\s*$/, '')\n delete a['data-maizzle-mso-style']\n\n /**\n * Build the MSO td's inline style from three sources, in CSS priority order\n * (earlier = lower, later wins on dupes):\n *\n * 1. `background-color` (always, when present) — Word paints the cell\n * under any padding area or inline-block gap, not just the div.\n *\n * 2. `padding*` (hoisted only when no horizontal border) — Word drops\n * div padding without a stabilizing border, so the td has to\n * carry it. With a border, Word renders div padding and a td\n * copy would double-pad.\n *\n * 3. The user's `mso-style` prop — last so it overrides anything the\n * auto-hoist computed.\n */\n const parts: string[] = []\n if (style) {\n const root = safeParser(style)\n\n let bgDecl: string | undefined\n root.walkDecls('background-color', (d) => {\n bgDecl = `background-color: ${d.value}${d.important ? ' !important' : ''}`\n })\n if (bgDecl) parts.push(bgDecl)\n\n if (horizontalBorderPx(root) === 0) {\n root.walkDecls((d) => {\n if (/^padding(-|$)/.test(d.prop)) {\n parts.push(`${d.prop}: ${d.value}${d.important ? ' !important' : ''}`)\n }\n })\n }\n }\n if (msoStyle) parts.push(msoStyle)\n\n tdStyles.set(tdId, parts.length ? ` style=\"${parts.join('; ')}\"` : '')\n }\n })\n\n if (widths.size === 0 && tdStyles.size === 0) return dom\n\n walk(dom, (node) => {\n if (node.type !== 'comment') return\n let data = (node as any).data as string\n if (!data) return\n const hasMsow = widths.size > 0 && data.includes('__MAIZZLE_MSOW_')\n const hasTd = tdStyles.size > 0 && data.includes('__MAIZZLE_MSOTDSTYLE_')\n if (!hasMsow && !hasTd) return\n\n if (hasMsow) {\n for (const [id, val] of widths) {\n data = data.replaceAll(`__MAIZZLE_MSOW_${id}__`, val)\n }\n }\n if (hasTd) {\n for (const [id, val] of tdStyles) {\n data = data.replaceAll(`__MAIZZLE_MSOTDSTYLE_${id}__`, val)\n }\n }\n ;(node as any).data = data\n })\n\n return dom\n}\n"],"mappings":";;;;;AAKA,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,aAAa;AAEnB,SAAS,aAAa,OAA8B;CAClD,MAAM,UAAU,MAAM,MAAM;CAC5B,IAAI,WAAW,KAAK,QAAQ,EAAE,OAAO;CACrC,MAAM,IAAI,QAAQ,MAAM,6BAA6B;CACrD,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,IAAI,WAAW,EAAE,GAAG;CAC1B,SAAS,EAAE,MAAM,MAAM,aAAa,EAApC;EACE,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,EAAE,CAAC;EACnC,KAAK;EACL,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG,CAAC;EACxC,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;EAC3C,SAAS,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BpB,SAAgB,gBAAgB,KAA+B;CAC7D,MAAM,yBAAS,IAAI,KAAqB;CACxC,MAAM,2BAAW,IAAI,KAAqB;CAE1C,KAAK,MAAM,SAAS;EAElB,MAAM,IAAIA,KAAG;EACb,IAAI,CAAC,GAAG;EAER,MAAM,SAAS,EAAE;EACjB,MAAM,OAAO,EAAE;EACf,IAAI,CAAC,UAAU,CAAC,MAAM;EAEtB,MAAM,QAAQ,EAAE,SAAS;EAEzB,IAAI,QAAQ;GACV,OAAO,EAAE;GACT,MAAM,WAAW,EAAE,iCAAiC;GACpD,OAAO,EAAE;GACT,MAAM,MAAM,MAAM,MAAM,aAAa,GAAG,MAAM,MAAM,MAAM,SAAS,GAAG;GACtE,MAAM,WAAW,MAAM,aAAa,IAAI,GAAG;GAC3C,OAAO,IAAI,QAAQ,YAAY,SAAS;;EAG1C,IAAI,MAAM;GACR,OAAO,EAAE;GACT,MAAM,YAAY,EAAE,6BAA6B,IAAI,MAAM,CAAC,QAAQ,SAAS,GAAG;GAChF,OAAO,EAAE;;;;;;;;;;;;;;;;GAiBT,MAAM,QAAkB,EAAE;GAC1B,IAAI,OAAO;IACT,MAAM,OAAO,WAAW,MAAM;IAE9B,IAAI;IACJ,KAAK,UAAU,qBAAqB,MAAM;KACxC,SAAS,qBAAqB,EAAE,QAAQ,EAAE,YAAY,gBAAgB;MACtE;IACF,IAAI,QAAQ,MAAM,KAAK,OAAO;IAE9B,IAAI,mBAAmB,KAAK,KAAK,GAC/B,KAAK,WAAW,MAAM;KACpB,IAAI,gBAAgB,KAAK,EAAE,KAAK,EAC9B,MAAM,KAAK,GAAG,EAAE,KAAK,IAAI,EAAE,QAAQ,EAAE,YAAY,gBAAgB,KAAK;MAExE;;GAGN,IAAI,UAAU,MAAM,KAAK,SAAS;GAElC,SAAS,IAAI,MAAM,MAAM,SAAS,WAAW,MAAM,KAAK,KAAK,CAAC,KAAK,GAAG;;GAExE;CAEF,IAAI,OAAO,SAAS,KAAK,SAAS,SAAS,GAAG,OAAO;CAErD,KAAK,MAAM,SAAS;EAClB,IAAI,KAAK,SAAS,WAAW;EAC7B,IAAI,OAAQ,KAAa;EACzB,IAAI,CAAC,MAAM;EACX,MAAM,UAAU,OAAO,OAAO,KAAK,KAAK,SAAS,kBAAkB;EACnE,MAAM,QAAQ,SAAS,OAAO,KAAK,KAAK,SAAS,wBAAwB;EACzE,IAAI,CAAC,WAAW,CAAC,OAAO;EAExB,IAAI,SACF,KAAK,MAAM,CAAC,IAAI,QAAQ,QACtB,OAAO,KAAK,WAAW,kBAAkB,GAAG,KAAK,IAAI;EAGzD,IAAI,OACF,KAAK,MAAM,CAAC,IAAI,QAAQ,UACtB,OAAO,KAAK,WAAW,wBAAwB,GAAG,KAAK,IAAI;EAG9D,KAAc,OAAO;GACtB;CAEF,OAAO"}
@@ -0,0 +1,37 @@
1
+ import { CssConfig } from "../types/config.js";
2
+ import { ChildNode } from "domhandler";
3
+
4
+ //#region src/transformers/safeSelectors.d.ts
5
+ /**
6
+ * Safe selectors transformer.
7
+ *
8
+ * Replaces unsafe characters (`:`, `/`, `[`, `]`, etc.) in:
9
+ * - CSS selectors inside `<style>` tags
10
+ * - HTML `class` attributes
11
+ *
12
+ * This makes Tailwind utility classes like `sm:text-base` safe for
13
+ * email clients that cannot handle escaped characters in class names.
14
+ *
15
+ * Enabled by default. Disable by setting `css.safe` to `false`.
16
+ * Customize replacements by passing a `Record<string, string>` — user
17
+ * values are merged on top of the defaults.
18
+ *
19
+ * @param html HTML string to transform.
20
+ * @param config CSS config (see {@link CssConfig}).
21
+ * @returns The transformed HTML string.
22
+ *
23
+ * @example
24
+ * import { safeSelectors } from '@maizzle/framework'
25
+ *
26
+ * const out = safeSelectors('<div class="sm:text-base"></div>')
27
+ */
28
+ declare function safeSelectors(html: string, config?: CssConfig): string;
29
+ /**
30
+ * DOM-form of {@link safeSelectors} used by the internal transformer pipeline.
31
+ * Takes a parsed DOM, returns a parsed DOM — avoids redundant
32
+ * serialize/parse round-trips when chained with other transformers.
33
+ */
34
+ declare function safeSelectorsDom(dom: ChildNode[], config?: CssConfig): ChildNode[];
35
+ //#endregion
36
+ export { safeSelectors, safeSelectorsDom };
37
+ //# sourceMappingURL=safeSelectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safeSelectors.d.ts","names":[],"sources":["../../src/transformers/safeSelectors.ts"],"mappings":";;;;;;AA+GA;;;;;;;;;AASA;;;;;;;;;;;;iBATgB,aAAA,CAAc,IAAA,UAAc,MAAA,GAAQ,SAAA;;;;;;iBASpC,gBAAA,CAAiB,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
@@ -1,8 +1,10 @@
1
+ import { parse } from "../utils/ast/parser.js";
1
2
  import { walk } from "../utils/ast/walker.js";
3
+ import { serialize } from "../utils/ast/serializer.js";
2
4
  import "../utils/ast/index.js";
3
5
  import postcss from "postcss";
4
6
  import safeParser from "postcss-safe-parser";
5
- //#region src/transformers/safeClassNames.ts
7
+ //#region src/transformers/safeSelectors.ts
6
8
  const DEFAULT_REPLACEMENTS = {
7
9
  ":": "-",
8
10
  "/": "-",
@@ -66,7 +68,7 @@ function processClassAttr(classStr, replacements) {
66
68
  }).join(" ");
67
69
  }
68
70
  /**
69
- * Safe class names transformer.
71
+ * Safe selectors transformer.
70
72
  *
71
73
  * Replaces unsafe characters (`:`, `/`, `[`, `]`, etc.) in:
72
74
  * - CSS selectors inside `<style>` tags
@@ -78,8 +80,25 @@ function processClassAttr(classStr, replacements) {
78
80
  * Enabled by default. Disable by setting `css.safe` to `false`.
79
81
  * Customize replacements by passing a `Record<string, string>` — user
80
82
  * values are merged on top of the defaults.
83
+ *
84
+ * @param html HTML string to transform.
85
+ * @param config CSS config (see {@link CssConfig}).
86
+ * @returns The transformed HTML string.
87
+ *
88
+ * @example
89
+ * import { safeSelectors } from '@maizzle/framework'
90
+ *
91
+ * const out = safeSelectors('<div class="sm:text-base"></div>')
92
+ */
93
+ function safeSelectors(html, config = {}) {
94
+ return serialize(safeSelectorsDom(parse(html), config));
95
+ }
96
+ /**
97
+ * DOM-form of {@link safeSelectors} used by the internal transformer pipeline.
98
+ * Takes a parsed DOM, returns a parsed DOM — avoids redundant
99
+ * serialize/parse round-trips when chained with other transformers.
81
100
  */
82
- function safeClassNames(dom, config = {}) {
101
+ function safeSelectorsDom(dom, config = {}) {
83
102
  const option = config.safe ?? true;
84
103
  if (!option) return dom;
85
104
  const replacements = option && typeof option === "object" ? {
@@ -97,6 +116,6 @@ function safeClassNames(dom, config = {}) {
97
116
  return dom;
98
117
  }
99
118
  //#endregion
100
- export { safeClassNames };
119
+ export { safeSelectors, safeSelectorsDom };
101
120
 
102
- //# sourceMappingURL=safeClassNames.js.map
121
+ //# sourceMappingURL=safeSelectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safeSelectors.js","names":[],"sources":["../../src/transformers/safeSelectors.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\nimport type { CssConfig } from '../types/config.ts'\n\nconst DEFAULT_REPLACEMENTS: Record<string, string> = {\n ':': '-',\n '/': '-',\n '%': 'pc',\n '.': '_',\n ',': '_',\n '#': '_',\n '[': '',\n ']': '',\n '(': '',\n ')': '',\n '{': '',\n '}': '',\n '!': '-i',\n '&': 'and-',\n '<': 'lt-',\n '=': 'eq-',\n '>': 'gt-',\n '|': 'or-',\n '@': 'at-',\n '?': 'q-',\n '\\\\': '-',\n '\"': '-',\n \"'\": '-',\n '*': '-',\n '+': '-',\n ';': '-',\n '^': '-',\n '`': '-',\n '~': '-',\n '$': '-',\n}\n\nfunction escapeForRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Replace escaped special characters in CSS selectors.\n *\n * Tailwind generates selectors like `.sm\\:text-base`. This function\n * replaces the `\\:` with `-` (or whatever the configured replacement is)\n * so the selector becomes `.sm-text-base`, which is safe for email clients.\n */\nfunction processCssSelectors(css: string, replacements: Record<string, string>): string {\n // Matches \\<char> in CSS selectors — e.g. \\: \\/ \\. \\[ etc.\n const selectorRegex = new RegExp(\n `\\\\\\\\(${Object.keys(replacements).map(escapeForRegex).join('|')})`,\n 'g',\n )\n\n return postcss([\n (root: postcss.Root) => {\n root.walkRules((rule: postcss.Rule) => {\n rule.selector = rule.selector\n .replace(selectorRegex, (_matched, char) => replacements[char] ?? _matched)\n // Handle CSS unicode escape for comma (\\2c → _)\n .replaceAll('\\\\2c ', '_')\n })\n },\n ]).process(css, { parser: safeParser }).css\n}\n\n/**\n * Replace unsafe special characters in a class attribute value.\n *\n * Splits on whitespace and replaces each char from the replacements map\n * in each class token individually.\n */\nfunction processClassAttr(classStr: string, replacements: Record<string, string>): string {\n return classStr\n .split(/\\s+/)\n .filter(Boolean)\n .map((cls) => {\n for (const [from, to] of Object.entries(replacements)) {\n cls = cls.split(from).join(to)\n }\n return cls\n })\n .join(' ')\n}\n\n/**\n * Safe selectors transformer.\n *\n * Replaces unsafe characters (`:`, `/`, `[`, `]`, etc.) in:\n * - CSS selectors inside `<style>` tags\n * - HTML `class` attributes\n *\n * This makes Tailwind utility classes like `sm:text-base` safe for\n * email clients that cannot handle escaped characters in class names.\n *\n * Enabled by default. Disable by setting `css.safe` to `false`.\n * Customize replacements by passing a `Record<string, string>` — user\n * values are merged on top of the defaults.\n *\n * @param html HTML string to transform.\n * @param config CSS config (see {@link CssConfig}).\n * @returns The transformed HTML string.\n *\n * @example\n * import { safeSelectors } from '@maizzle/framework'\n *\n * const out = safeSelectors('<div class=\"sm:text-base\"></div>')\n */\nexport function safeSelectors(html: string, config: CssConfig = {}): string {\n return serialize(safeSelectorsDom(parse(html), config))\n}\n\n/**\n * DOM-form of {@link safeSelectors} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function safeSelectorsDom(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.safe ?? true\n\n if (!option) return dom\n\n const replacements: Record<string, string> =\n option && typeof option === 'object'\n ? { ...DEFAULT_REPLACEMENTS, ...option }\n : DEFAULT_REPLACEMENTS\n\n walk(dom, (node) => {\n const el = node as Element\n\n // Process CSS selectors inside <style> tags\n if (el.name === 'style' && el.children?.length) {\n const text = el.children.find((c) => c.type === 'text') as any\n if (text?.data?.trim()) {\n text.data = processCssSelectors(text.data, replacements)\n }\n }\n\n // Replace special chars in class attributes\n if ('attribs' in el && el.attribs?.class) {\n el.attribs.class = processClassAttr(el.attribs.class, replacements)\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;AAMA,MAAM,uBAA+C;CACnD,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CACN,MAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACN;AAED,SAAS,eAAe,GAAmB;CACzC,OAAO,EAAE,QAAQ,uBAAuB,OAAO;;;;;;;;;AAUjD,SAAS,oBAAoB,KAAa,cAA8C;CAEtF,MAAM,gBAAgB,IAAI,OACxB,QAAQ,OAAO,KAAK,aAAa,CAAC,IAAI,eAAe,CAAC,KAAK,IAAI,CAAC,IAChE,IACD;CAED,OAAO,QAAQ,EACZ,SAAuB;EACtB,KAAK,WAAW,SAAuB;GACrC,KAAK,WAAW,KAAK,SAClB,QAAQ,gBAAgB,UAAU,SAAS,aAAa,SAAS,SAAS,CAE1E,WAAW,SAAS,IAAI;IAC3B;GAEL,CAAC,CAAC,QAAQ,KAAK,EAAE,QAAQ,YAAY,CAAC,CAAC;;;;;;;;AAS1C,SAAS,iBAAiB,UAAkB,cAA8C;CACxF,OAAO,SACJ,MAAM,MAAM,CACZ,OAAO,QAAQ,CACf,KAAK,QAAQ;EACZ,KAAK,MAAM,CAAC,MAAM,OAAO,OAAO,QAAQ,aAAa,EACnD,MAAM,IAAI,MAAM,KAAK,CAAC,KAAK,GAAG;EAEhC,OAAO;GACP,CACD,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;AA0Bd,SAAgB,cAAc,MAAc,SAAoB,EAAE,EAAU;CAC1E,OAAO,UAAU,iBAAiB,MAAM,KAAK,EAAE,OAAO,CAAC;;;;;;;AAQzD,SAAgB,iBAAiB,KAAkB,SAAoB,EAAE,EAAe;CACtF,MAAM,SAAS,OAAO,QAAQ;CAE9B,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,eACJ,UAAU,OAAO,WAAW,WACxB;EAAE,GAAG;EAAsB,GAAG;EAAQ,GACtC;CAEN,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EAGX,IAAI,GAAG,SAAS,WAAW,GAAG,UAAU,QAAQ;GAC9C,MAAM,OAAO,GAAG,SAAS,MAAM,MAAM,EAAE,SAAS,OAAO;GACvD,IAAI,MAAM,MAAM,MAAM,EACpB,KAAK,OAAO,oBAAoB,KAAK,MAAM,aAAa;;EAK5D,IAAI,aAAa,MAAM,GAAG,SAAS,OACjC,GAAG,QAAQ,QAAQ,iBAAiB,GAAG,QAAQ,OAAO,aAAa;GAErE;CAEF,OAAO"}
@@ -39,19 +39,50 @@ function shorthandCss(html, options = {}) {
39
39
  function shorthandCssDom(dom, options = {}) {
40
40
  const allowedTags = options.tags ?? [];
41
41
  const hasTagFilter = allowedTags.length > 0;
42
- walk(dom, (node) => {
43
- const el = node;
44
- if (!el.attribs?.style) return;
45
- if (hasTagFilter && !allowedTags.includes(el.name)) return;
46
- const styleValue = el.attribs.style;
42
+ /**
43
+ * Merge longhand within a single inline-style value. Returns the merged
44
+ * string when shorter, otherwise the original. Wraps the value in a
45
+ * dummy selector since postcss-merge-longhand operates on rules.
46
+ */
47
+ const mergeStyleValue = (styleValue) => {
47
48
  try {
48
49
  const { css } = postcss().use(mergeLonghand).process(`div { ${styleValue} }`, { parser: safeParser });
49
50
  const match = css.match(/div\s*\{\s*([^}]+)\s*\}/);
50
51
  if (match && match[1]) {
51
- const newStyle = match[1].trim();
52
- if (newStyle !== styleValue) el.attribs.style = newStyle;
52
+ const merged = match[1].trim();
53
+ if (merged !== styleValue) return merged;
53
54
  }
54
55
  } catch {}
56
+ return styleValue;
57
+ };
58
+ walk(dom, (node) => {
59
+ /**
60
+ * MSO conditional comments carry their own inline-style attributes
61
+ * (e.g. `<!--[if mso]><td style="…"><![endif]-->`) as opaque text.
62
+ * The element walker can't see them, so without this branch the td/
63
+ * v:rect styles inside comments stay longhand even when the visible
64
+ * div has already been merged. Match each `style="…"` substring,
65
+ * run it through mergeLonghand, splice back.
66
+ *
67
+ * Tag filter intentionally bypassed: the user can't address MSO td
68
+ * elements (they don't parse as elements), and these comments
69
+ * always wrap email-layout primitives anyway.
70
+ */
71
+ if (node.type === "comment") {
72
+ const data = node.data;
73
+ if (!data || !data.includes("style=\"")) return;
74
+ const newData = data.replace(/style="([^"]*)"/g, (full, value) => {
75
+ const merged = mergeStyleValue(value);
76
+ return merged === value ? full : `style="${merged}"`;
77
+ });
78
+ if (newData !== data) node.data = newData;
79
+ return;
80
+ }
81
+ const el = node;
82
+ if (!el.attribs?.style) return;
83
+ if (hasTagFilter && !allowedTags.includes(el.name)) return;
84
+ const merged = mergeStyleValue(el.attribs.style);
85
+ if (merged !== el.attribs.style) el.attribs.style = merged;
55
86
  });
56
87
  return dom;
57
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"shorthandCss.js","names":[],"sources":["../../src/transformers/shorthandCss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport mergeLonghand from 'postcss-merge-longhand'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Options for the `shorthandCss` transformer.\n */\nexport interface ShorthandCssOptions {\n /**\n * Restrict the transform to a list of HTML tag names. Omit to apply to\n * every element with a `style` attribute.\n *\n * @example ['td', 'div']\n */\n tags?: string[]\n}\n\n/**\n * Rewrite longhand CSS inside inline `style` attributes with shorthand\n * syntax. Works with margin, padding, and border when all sides are\n * specified.\n *\n * For example:\n * `margin-left: 2px; margin-right: 2px; margin-top: 4px; margin-bottom: 4px`\n * becomes:\n * `margin: 4px 2px`\n *\n * @param html HTML string to transform.\n * @param options Optional Maizzle options (`tags`).\n * @returns The transformed HTML string.\n *\n * @example\n * import { shorthandCss } from '@maizzle/framework'\n *\n * const out = shorthandCss(\n * '<p style=\"margin-top: 4px; margin-right: 2px; margin-bottom: 4px; margin-left: 2px;\">x</p>',\n * { tags: ['p'] },\n * )\n */\nexport function shorthandCss(html: string, options: ShorthandCssOptions = {}): string {\n return serialize(shorthandCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link shorthandCss} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function shorthandCssDom(dom: ChildNode[], options: ShorthandCssOptions = {}): ChildNode[] {\n const allowedTags = options.tags ?? []\n const hasTagFilter = allowedTags.length > 0\n\n walk(dom, (node) => {\n const el = node as Element\n\n // Skip if no attribs or no style\n if (!el.attribs?.style) {\n return\n }\n\n // Skip if tag filter is active and this tag is not allowed\n if (hasTagFilter && !allowedTags.includes(el.name)) {\n return\n }\n\n const styleValue = el.attribs.style\n\n try {\n // Process the style with postcss-merge-longhand\n // Wrap in a dummy selector since postcss needs a rule\n const { css } = postcss()\n .use(mergeLonghand)\n .process(`div { ${styleValue} }`, { parser: safeParser })\n\n // Extract the content between the braces\n const match = css.match(/div\\s*\\{\\s*([^}]+)\\s*\\}/)\n if (match && match[1]) {\n const newStyle = match[1].trim()\n if (newStyle !== styleValue) {\n el.attribs.style = newStyle\n }\n }\n } catch {\n // If processing fails, keep the original style\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,aAAa,MAAc,UAA+B,EAAE,EAAU;CACpF,OAAO,UAAU,gBAAgB,MAAM,KAAK,EAAE,QAAQ,CAAC;;;;;;;AAQzD,SAAgB,gBAAgB,KAAkB,UAA+B,EAAE,EAAe;CAChG,MAAM,cAAc,QAAQ,QAAQ,EAAE;CACtC,MAAM,eAAe,YAAY,SAAS;CAE1C,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EAGX,IAAI,CAAC,GAAG,SAAS,OACf;EAIF,IAAI,gBAAgB,CAAC,YAAY,SAAS,GAAG,KAAK,EAChD;EAGF,MAAM,aAAa,GAAG,QAAQ;EAE9B,IAAI;GAGF,MAAM,EAAE,QAAQ,SAAS,CACtB,IAAI,cAAc,CAClB,QAAQ,SAAS,WAAW,KAAK,EAAE,QAAQ,YAAY,CAAC;GAG3D,MAAM,QAAQ,IAAI,MAAM,0BAA0B;GAClD,IAAI,SAAS,MAAM,IAAI;IACrB,MAAM,WAAW,MAAM,GAAG,MAAM;IAChC,IAAI,aAAa,YACf,GAAG,QAAQ,QAAQ;;UAGjB;GAGR;CAEF,OAAO"}
1
+ {"version":3,"file":"shorthandCss.js","names":[],"sources":["../../src/transformers/shorthandCss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport mergeLonghand from 'postcss-merge-longhand'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Options for the `shorthandCss` transformer.\n */\nexport interface ShorthandCssOptions {\n /**\n * Restrict the transform to a list of HTML tag names. Omit to apply to\n * every element with a `style` attribute.\n *\n * @example ['td', 'div']\n */\n tags?: string[]\n}\n\n/**\n * Rewrite longhand CSS inside inline `style` attributes with shorthand\n * syntax. Works with margin, padding, and border when all sides are\n * specified.\n *\n * For example:\n * `margin-left: 2px; margin-right: 2px; margin-top: 4px; margin-bottom: 4px`\n * becomes:\n * `margin: 4px 2px`\n *\n * @param html HTML string to transform.\n * @param options Optional Maizzle options (`tags`).\n * @returns The transformed HTML string.\n *\n * @example\n * import { shorthandCss } from '@maizzle/framework'\n *\n * const out = shorthandCss(\n * '<p style=\"margin-top: 4px; margin-right: 2px; margin-bottom: 4px; margin-left: 2px;\">x</p>',\n * { tags: ['p'] },\n * )\n */\nexport function shorthandCss(html: string, options: ShorthandCssOptions = {}): string {\n return serialize(shorthandCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link shorthandCss} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function shorthandCssDom(dom: ChildNode[], options: ShorthandCssOptions = {}): ChildNode[] {\n const allowedTags = options.tags ?? []\n const hasTagFilter = allowedTags.length > 0\n\n /**\n * Merge longhand within a single inline-style value. Returns the merged\n * string when shorter, otherwise the original. Wraps the value in a\n * dummy selector since postcss-merge-longhand operates on rules.\n */\n const mergeStyleValue = (styleValue: string): string => {\n try {\n const { css } = postcss()\n .use(mergeLonghand)\n .process(`div { ${styleValue} }`, { parser: safeParser })\n const match = css.match(/div\\s*\\{\\s*([^}]+)\\s*\\}/)\n if (match && match[1]) {\n const merged = match[1].trim()\n if (merged !== styleValue) return merged\n }\n }\n catch {}\n return styleValue\n }\n\n walk(dom, (node) => {\n /**\n * MSO conditional comments carry their own inline-style attributes\n * (e.g. `<!--[if mso]><td style=\"…\"><![endif]-->`) as opaque text.\n * The element walker can't see them, so without this branch the td/\n * v:rect styles inside comments stay longhand even when the visible\n * div has already been merged. Match each `style=\"…\"` substring,\n * run it through mergeLonghand, splice back.\n *\n * Tag filter intentionally bypassed: the user can't address MSO td\n * elements (they don't parse as elements), and these comments\n * always wrap email-layout primitives anyway.\n */\n if (node.type === 'comment') {\n const data = (node as any).data as string\n if (!data || !data.includes('style=\"')) return\n const newData = data.replace(/style=\"([^\"]*)\"/g, (full, value) => {\n const merged = mergeStyleValue(value)\n return merged === value ? full : `style=\"${merged}\"`\n })\n if (newData !== data) (node as any).data = newData\n return\n }\n\n const el = node as Element\n\n if (!el.attribs?.style) return\n if (hasTagFilter && !allowedTags.includes(el.name)) return\n\n const merged = mergeStyleValue(el.attribs.style)\n if (merged !== el.attribs.style) el.attribs.style = merged\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,aAAa,MAAc,UAA+B,EAAE,EAAU;CACpF,OAAO,UAAU,gBAAgB,MAAM,KAAK,EAAE,QAAQ,CAAC;;;;;;;AAQzD,SAAgB,gBAAgB,KAAkB,UAA+B,EAAE,EAAe;CAChG,MAAM,cAAc,QAAQ,QAAQ,EAAE;CACtC,MAAM,eAAe,YAAY,SAAS;;;;;;CAO1C,MAAM,mBAAmB,eAA+B;EACtD,IAAI;GACF,MAAM,EAAE,QAAQ,SAAS,CACtB,IAAI,cAAc,CAClB,QAAQ,SAAS,WAAW,KAAK,EAAE,QAAQ,YAAY,CAAC;GAC3D,MAAM,QAAQ,IAAI,MAAM,0BAA0B;GAClD,IAAI,SAAS,MAAM,IAAI;IACrB,MAAM,SAAS,MAAM,GAAG,MAAM;IAC9B,IAAI,WAAW,YAAY,OAAO;;UAGhC;EACN,OAAO;;CAGT,KAAK,MAAM,SAAS;;;;;;;;;;;;;EAalB,IAAI,KAAK,SAAS,WAAW;GAC3B,MAAM,OAAQ,KAAa;GAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,SAAS,WAAU,EAAE;GACxC,MAAM,UAAU,KAAK,QAAQ,qBAAqB,MAAM,UAAU;IAChE,MAAM,SAAS,gBAAgB,MAAM;IACrC,OAAO,WAAW,QAAQ,OAAO,UAAU,OAAO;KAClD;GACF,IAAI,YAAY,MAAM,KAAc,OAAO;GAC3C;;EAGF,MAAM,KAAK;EAEX,IAAI,CAAC,GAAG,SAAS,OAAO;EACxB,IAAI,gBAAgB,CAAC,YAAY,SAAS,GAAG,KAAK,EAAE;EAEpD,MAAM,SAAS,gBAAgB,GAAG,QAAQ,MAAM;EAChD,IAAI,WAAW,GAAG,QAAQ,OAAO,GAAG,QAAQ,QAAQ;GACpD;CAEF,OAAO"}
@@ -3,7 +3,7 @@ import "../utils/ast/index.js";
3
3
  import { compileTailwindCss } from "../utils/compileTailwindCss.js";
4
4
  import { resolve } from "node:path";
5
5
  //#region src/transformers/tailwindComponent.ts
6
- const DEFAULT_SEED = "@import \"@maizzle/tailwindcss\";";
6
+ const DEFAULT_SEED = "@import \"@maizzle/tailwindcss\" source(none);";
7
7
  const OPEN_RE = /^mz-tw:(\S+)$/;
8
8
  const CLOSE_RE = /^\/mz-tw:(\S+)$/;
9
9
  /**