@maizzle/framework 6.0.0-rc.12 → 6.0.0-rc.14

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 (99) hide show
  1. package/dist/build.mjs +4 -1
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/components/Button.vue +2 -2
  4. package/dist/components/CodeBlock.vue +2 -1
  5. package/dist/components/Column.vue +28 -22
  6. package/dist/components/Container.vue +47 -9
  7. package/dist/components/Font.vue +96 -0
  8. package/dist/components/Layout.vue +9 -4
  9. package/dist/components/Overlap.vue +75 -18
  10. package/dist/components/Row.vue +40 -19
  11. package/dist/components/Section.vue +35 -8
  12. package/dist/components/utils.d.mts +14 -1
  13. package/dist/components/utils.d.mts.map +1 -1
  14. package/dist/components/utils.mjs +32 -1
  15. package/dist/components/utils.mjs.map +1 -1
  16. package/dist/components/utils.ts +39 -0
  17. package/dist/composables/renderContext.d.mts +8 -1
  18. package/dist/composables/renderContext.d.mts.map +1 -1
  19. package/dist/composables/renderContext.mjs.map +1 -1
  20. package/dist/composables/useFont.d.mts +50 -0
  21. package/dist/composables/useFont.d.mts.map +1 -0
  22. package/dist/composables/useFont.mjs +93 -0
  23. package/dist/composables/useFont.mjs.map +1 -0
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.mjs +2 -1
  26. package/dist/plugins/postcss/quoteFontFamilies.d.mts +13 -0
  27. package/dist/plugins/postcss/quoteFontFamilies.d.mts.map +1 -0
  28. package/dist/plugins/postcss/quoteFontFamilies.mjs +84 -0
  29. package/dist/plugins/postcss/quoteFontFamilies.mjs.map +1 -0
  30. package/dist/render/createRenderer.mjs +8 -2
  31. package/dist/render/createRenderer.mjs.map +1 -1
  32. package/dist/render/injectFonts.d.mts +15 -0
  33. package/dist/render/injectFonts.d.mts.map +1 -0
  34. package/dist/render/injectFonts.mjs +46 -0
  35. package/dist/render/injectFonts.mjs.map +1 -0
  36. package/dist/serve.d.mts.map +1 -1
  37. package/dist/serve.mjs +28 -12
  38. package/dist/serve.mjs.map +1 -1
  39. package/dist/server/compatibility.d.mts +54 -2
  40. package/dist/server/compatibility.d.mts.map +1 -1
  41. package/dist/server/compatibility.mjs +890 -76
  42. package/dist/server/compatibility.mjs.map +1 -1
  43. package/dist/server/linter.d.mts +15 -2
  44. package/dist/server/linter.d.mts.map +1 -1
  45. package/dist/server/linter.mjs +194 -43
  46. package/dist/server/linter.mjs.map +1 -1
  47. package/dist/server/sfc-utils.d.mts +18 -0
  48. package/dist/server/sfc-utils.d.mts.map +1 -0
  49. package/dist/server/sfc-utils.mjs +184 -0
  50. package/dist/server/sfc-utils.mjs.map +1 -0
  51. package/dist/server/ui/App.vue +27 -50
  52. package/dist/server/ui/components/SidebarClose.vue +12 -0
  53. package/dist/server/ui/components/ui/command/Command.vue +1 -0
  54. package/dist/server/ui/components/ui/input/Input.vue +1 -1
  55. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
  56. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
  57. package/dist/server/ui/lib/emulated-dark-mode.ts +131 -0
  58. package/dist/server/ui/pages/Preview.vue +215 -156
  59. package/dist/transformers/addAttributes.mjs +10 -6
  60. package/dist/transformers/addAttributes.mjs.map +1 -1
  61. package/dist/transformers/columnWidth.d.mts +31 -0
  62. package/dist/transformers/columnWidth.d.mts.map +1 -0
  63. package/dist/transformers/columnWidth.mjs +166 -0
  64. package/dist/transformers/columnWidth.mjs.map +1 -0
  65. package/dist/transformers/index.d.mts.map +1 -1
  66. package/dist/transformers/index.mjs +4 -0
  67. package/dist/transformers/index.mjs.map +1 -1
  68. package/dist/transformers/inlineCSS.mjs +2 -2
  69. package/dist/transformers/inlineCSS.mjs.map +1 -1
  70. package/dist/transformers/msoWidthFromClass.d.mts +19 -0
  71. package/dist/transformers/msoWidthFromClass.d.mts.map +1 -0
  72. package/dist/transformers/msoWidthFromClass.mjs +61 -0
  73. package/dist/transformers/msoWidthFromClass.mjs.map +1 -0
  74. package/dist/transformers/purgeCSS.mjs +1 -1
  75. package/dist/transformers/purgeCSS.mjs.map +1 -1
  76. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  77. package/dist/transformers/tailwindcss.mjs +6 -16
  78. package/dist/transformers/tailwindcss.mjs.map +1 -1
  79. package/dist/types/config.d.mts +42 -2
  80. package/dist/types/config.d.mts.map +1 -1
  81. package/dist/types/index.d.mts +2 -2
  82. package/dist/utils/decodeStyleEntities.d.mts +15 -0
  83. package/dist/utils/decodeStyleEntities.d.mts.map +1 -0
  84. package/dist/utils/decodeStyleEntities.mjs +18 -0
  85. package/dist/utils/decodeStyleEntities.mjs.map +1 -0
  86. package/package.json +2 -3
  87. package/dist/_virtual/_rolldown/runtime.mjs +0 -32
  88. package/dist/node_modules/picomatch/index.mjs +0 -13
  89. package/dist/node_modules/picomatch/index.mjs.map +0 -1
  90. package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
  91. package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
  92. package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
  93. package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
  94. package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
  95. package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
  96. package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
  97. package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
  98. package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
  99. package/dist/node_modules/picomatch/lib/utils.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"addAttributes.mjs","names":["merge"],"sources":["../../src/transformers/addAttributes.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport type { AttributesConfig } from '../types/config.ts'\n\n/**\n * Default attributes to add to elements.\n */\nconst DEFAULT_ATTRIBUTES: Record<string, Record<string, string | boolean | number>> = {\n table: {\n cellpadding: 0,\n cellspacing: 0,\n role: 'none',\n },\n img: {\n alt: '',\n },\n}\n\n/**\n * Add attributes transformer.\n *\n * Automatically adds attributes to HTML elements based on CSS selectors.\n *\n * Default attributes (can be disabled by setting `attributes.add` to false):\n * - table: cellpadding=\"0\", cellspacing=\"0\", role=\"none\"\n * - img: alt=\"\"\n *\n * Supports tag, class, id, and attribute selectors.\n * Multiple selectors can be specified by comma-separating them.\n *\n * Examples:\n * ```js\n * attributes: {\n * add: {\n * div: { role: 'article' },\n * '.test': { editable: true },\n * '#header': { 'data-id': 'main' },\n * 'div, p': { class: 'content' },\n * }\n * }\n * ```\n */\nexport function addAttributes(dom: ChildNode[], config: AttributesConfig = {}): ChildNode[] {\n const addConfig = config.add\n\n // Disabled when explicitly set to false\n if (addConfig === false) {\n return dom\n }\n\n // Deep merge user attributes on top of defaults using defu\n const userAttributes = typeof addConfig === 'object' ? addConfig : {}\n const attributesToAdd = merge(userAttributes, DEFAULT_ATTRIBUTES) as Record<string, Record<string, string | boolean | number>>\n\n if (Object.keys(attributesToAdd).length === 0) {\n return dom\n }\n\n // Process each selector pattern\n for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {\n // Split by comma for multiple selectors\n const selectors = selectorPattern.split(',').map(s => s.trim())\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.name) return\n\n // Check if element matches any selector in the pattern\n const matches = selectors.some(selector => elementMatches(el, selector))\n\n if (matches) {\n // Initialize attribs if needed\n if (!el.attribs) {\n el.attribs = {}\n }\n\n for (const [attrName, attrValue] of Object.entries(attributes)) {\n // Special handling for class - merge instead of replace\n if (attrName === 'class' && el.attribs.class) {\n const existingClasses = el.attribs.class.split(/\\s+/).filter(Boolean)\n const newClasses = String(attrValue).split(/\\s+/).filter(Boolean)\n const mergedClasses = [...new Set([...existingClasses, ...newClasses])]\n if (mergedClasses.join(' ') !== el.attribs.class) {\n el.attribs.class = mergedClasses.join(' ')\n }\n } else {\n // Only add attribute if not already present\n if (!(attrName in el.attribs)) {\n el.attribs[attrName] = String(attrValue)\n }\n }\n }\n }\n })\n }\n\n return dom\n}\n\n/**\n * Check if an element matches a CSS selector.\n * Supports: tag, .class, #id, [attribute], [attribute=value]\n */\nfunction elementMatches(el: Element, selector: string): boolean {\n // Remove whitespace\n selector = selector.trim()\n\n // Check for attribute selector [attr] or [attr=value]\n const attrMatch = selector.match(/^\\[([^\\]=]+)(?:=([^\\]]*))?\\]$/)\n if (attrMatch) {\n const [, attrName, attrValue] = attrMatch\n if (attrValue === undefined) {\n // Just checking if attribute exists\n return attrName in (el.attribs || {})\n } else {\n // Check if attribute has specific value\n return el.attribs?.[attrName] === attrValue\n }\n }\n\n // Check for class selector .class\n if (selector.startsWith('.')) {\n const className = selector.slice(1)\n const classes = el.attribs?.class?.split(/\\s+/) || []\n return classes.includes(className)\n }\n\n // Check for id selector #id\n if (selector.startsWith('#')) {\n const id = selector.slice(1)\n return el.attribs?.id === id\n }\n\n // Check for tag selector (possibly with attribute)\n // Split tag from attribute if present, e.g., \"div[role=alert]\"\n const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\\[([^\\]]+)\\]$/i)\n if (tagAttrMatch) {\n const [, tagName, attrPart] = tagAttrMatch\n if (el.name !== tagName) return false\n\n // Parse attribute part: could be \"attr\" or \"attr=value\"\n const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/)\n if (attrEqMatch) {\n const [, attrName, attrValue] = attrEqMatch\n if (attrValue === undefined) {\n return attrName in (el.attribs || {})\n } else {\n return el.attribs?.[attrName] === attrValue\n }\n }\n return false\n }\n\n // Simple tag selector\n return el.name === selector\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,qBAAgF;CACpF,OAAO;EACL,aAAa;EACb,aAAa;EACb,MAAM;EACP;CACD,KAAK,EACH,KAAK,IACN;CACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,cAAc,KAAkB,SAA2B,EAAE,EAAe;CAC1F,MAAM,YAAY,OAAO;AAGzB,KAAI,cAAc,MAChB,QAAO;CAKT,MAAM,kBAAkBA,KADD,OAAO,cAAc,WAAW,YAAY,EAAE,EACvB,mBAAmB;AAEjE,KAAI,OAAO,KAAK,gBAAgB,CAAC,WAAW,EAC1C,QAAO;AAIT,MAAK,MAAM,CAAC,iBAAiB,eAAe,OAAO,QAAQ,gBAAgB,EAAE;EAE3E,MAAM,YAAY,gBAAgB,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;AAE/D,OAAK,MAAM,SAAS;GAClB,MAAM,KAAK;AACX,OAAI,CAAC,GAAG,KAAM;AAKd,OAFgB,UAAU,MAAK,aAAY,eAAe,IAAI,SAAS,CAAC,EAE3D;AAEX,QAAI,CAAC,GAAG,QACN,IAAG,UAAU,EAAE;AAGjB,SAAK,MAAM,CAAC,UAAU,cAAc,OAAO,QAAQ,WAAW,CAE5D,KAAI,aAAa,WAAW,GAAG,QAAQ,OAAO;KAC5C,MAAM,kBAAkB,GAAG,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,QAAQ;KACrE,MAAM,aAAa,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;KACjE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,WAAW,CAAC,CAAC;AACvE,SAAI,cAAc,KAAK,IAAI,KAAK,GAAG,QAAQ,MACzC,IAAG,QAAQ,QAAQ,cAAc,KAAK,IAAI;eAIxC,EAAE,YAAY,GAAG,SACnB,IAAG,QAAQ,YAAY,OAAO,UAAU;;IAKhD;;AAGJ,QAAO;;;;;;AAOT,SAAS,eAAe,IAAa,UAA2B;AAE9D,YAAW,SAAS,MAAM;CAG1B,MAAM,YAAY,SAAS,MAAM,gCAAgC;AACjE,KAAI,WAAW;EACb,MAAM,GAAG,UAAU,aAAa;AAChC,MAAI,cAAc,OAEhB,QAAO,aAAa,GAAG,WAAW,EAAE;MAGpC,QAAO,GAAG,UAAU,cAAc;;AAKtC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,YAAY,SAAS,MAAM,EAAE;AAEnC,UADgB,GAAG,SAAS,OAAO,MAAM,MAAM,IAAI,EAAE,EACtC,SAAS,UAAU;;AAIpC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,KAAK,SAAS,MAAM,EAAE;AAC5B,SAAO,GAAG,SAAS,OAAO;;CAK5B,MAAM,eAAe,SAAS,MAAM,kCAAkC;AACtE,KAAI,cAAc;EAChB,MAAM,GAAG,SAAS,YAAY;AAC9B,MAAI,GAAG,SAAS,QAAS,QAAO;EAGhC,MAAM,cAAc,SAAS,MAAM,sBAAsB;AACzD,MAAI,aAAa;GACf,MAAM,GAAG,UAAU,aAAa;AAChC,OAAI,cAAc,OAChB,QAAO,aAAa,GAAG,WAAW,EAAE;OAEpC,QAAO,GAAG,UAAU,cAAc;;AAGtC,SAAO;;AAIT,QAAO,GAAG,SAAS"}
1
+ {"version":3,"file":"addAttributes.mjs","names":["merge"],"sources":["../../src/transformers/addAttributes.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport type { AttributesConfig } from '../types/config.ts'\n\n/**\n * Default attributes to add to elements.\n */\nconst DEFAULT_ATTRIBUTES: Record<string, Record<string, string | boolean | number>> = {\n table: {\n cellpadding: 0,\n cellspacing: 0,\n role: 'none',\n },\n img: {\n alt: '',\n },\n}\n\n/**\n * Add attributes transformer.\n *\n * Automatically adds attributes to HTML elements based on CSS selectors.\n *\n * Default attributes (can be disabled by setting `attributes.add` to false):\n * - table: cellpadding=\"0\", cellspacing=\"0\", role=\"none\"\n * - img: alt=\"\"\n *\n * Supports tag, class, id, and attribute selectors.\n * Multiple selectors can be specified by comma-separating them.\n *\n * Examples:\n * ```js\n * attributes: {\n * add: {\n * div: { role: 'article' },\n * '.test': { editable: true },\n * '#header': { 'data-id': 'main' },\n * 'div, p': { class: 'content' },\n * }\n * }\n * ```\n */\nexport function addAttributes(dom: ChildNode[], config: AttributesConfig = {}): ChildNode[] {\n const addConfig = config.add\n\n // Disabled when explicitly set to false\n if (addConfig === false) {\n return dom\n }\n\n // Deep merge user attributes on top of defaults using defu\n const userAttributes = typeof addConfig === 'object' ? addConfig : {}\n const attributesToAdd = merge(userAttributes, DEFAULT_ATTRIBUTES) as Record<string, false | Record<string, false | string | boolean | number>>\n\n if (Object.keys(attributesToAdd).length === 0) {\n return dom\n }\n\n // Process each selector pattern\n for (const [selectorPattern, attributes] of Object.entries(attributesToAdd)) {\n // User opted out of this selector entirely (e.g. `table: false`)\n if (attributes === false) continue\n // Split by comma for multiple selectors\n const selectors = selectorPattern.split(',').map(s => s.trim())\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.name) return\n\n // Check if element matches any selector in the pattern\n const matches = selectors.some(selector => elementMatches(el, selector))\n\n if (matches) {\n // Initialize attribs if needed\n if (!el.attribs) {\n el.attribs = {}\n }\n\n for (const [attrName, attrValue] of Object.entries(attributes)) {\n // User opted out of this specific attribute (e.g. `role: false`)\n if (attrValue === false) continue\n // Special handling for class - merge instead of replace\n if (attrName === 'class' && el.attribs.class) {\n const existingClasses = el.attribs.class.split(/\\s+/).filter(Boolean)\n const newClasses = String(attrValue).split(/\\s+/).filter(Boolean)\n const mergedClasses = [...new Set([...existingClasses, ...newClasses])]\n if (mergedClasses.join(' ') !== el.attribs.class) {\n el.attribs.class = mergedClasses.join(' ')\n }\n } else {\n // Only add attribute if not already present\n if (!(attrName in el.attribs)) {\n el.attribs[attrName] = String(attrValue)\n }\n }\n }\n }\n })\n }\n\n return dom\n}\n\n/**\n * Check if an element matches a CSS selector.\n * Supports: tag, .class, #id, [attribute], [attribute=value]\n */\nfunction elementMatches(el: Element, selector: string): boolean {\n // Remove whitespace\n selector = selector.trim()\n\n // Check for attribute selector [attr] or [attr=value]\n const attrMatch = selector.match(/^\\[([^\\]=]+)(?:=([^\\]]*))?\\]$/)\n if (attrMatch) {\n const [, attrName, attrValue] = attrMatch\n if (attrValue === undefined) {\n // Just checking if attribute exists\n return attrName in (el.attribs || {})\n } else {\n // Check if attribute has specific value\n return el.attribs?.[attrName] === attrValue\n }\n }\n\n // Check for class selector .class\n if (selector.startsWith('.')) {\n const className = selector.slice(1)\n const classes = el.attribs?.class?.split(/\\s+/) || []\n return classes.includes(className)\n }\n\n // Check for id selector #id\n if (selector.startsWith('#')) {\n const id = selector.slice(1)\n return el.attribs?.id === id\n }\n\n // Check for tag selector (possibly with attribute)\n // Split tag from attribute if present, e.g., \"div[role=alert]\"\n const tagAttrMatch = selector.match(/^([a-z][a-z0-9]*)\\[([^\\]]+)\\]$/i)\n if (tagAttrMatch) {\n const [, tagName, attrPart] = tagAttrMatch\n if (el.name !== tagName) return false\n\n // Parse attribute part: could be \"attr\" or \"attr=value\"\n const attrEqMatch = attrPart.match(/^([^=]+)(?:=(.*))?$/)\n if (attrEqMatch) {\n const [, attrName, attrValue] = attrEqMatch\n if (attrValue === undefined) {\n return attrName in (el.attribs || {})\n } else {\n return el.attribs?.[attrName] === attrValue\n }\n }\n return false\n }\n\n // Simple tag selector\n return el.name === selector\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,qBAAgF;CACpF,OAAO;EACL,aAAa;EACb,aAAa;EACb,MAAM;EACP;CACD,KAAK,EACH,KAAK,IACN;CACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAgB,cAAc,KAAkB,SAA2B,EAAE,EAAe;CAC1F,MAAM,YAAY,OAAO;AAGzB,KAAI,cAAc,MAChB,QAAO;CAKT,MAAM,kBAAkBA,KADD,OAAO,cAAc,WAAW,YAAY,EAAE,EACvB,mBAAmB;AAEjE,KAAI,OAAO,KAAK,gBAAgB,CAAC,WAAW,EAC1C,QAAO;AAIT,MAAK,MAAM,CAAC,iBAAiB,eAAe,OAAO,QAAQ,gBAAgB,EAAE;AAE3E,MAAI,eAAe,MAAO;EAE1B,MAAM,YAAY,gBAAgB,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;AAE/D,OAAK,MAAM,SAAS;GAClB,MAAM,KAAK;AACX,OAAI,CAAC,GAAG,KAAM;AAKd,OAFgB,UAAU,MAAK,aAAY,eAAe,IAAI,SAAS,CAAC,EAE3D;AAEX,QAAI,CAAC,GAAG,QACN,IAAG,UAAU,EAAE;AAGjB,SAAK,MAAM,CAAC,UAAU,cAAc,OAAO,QAAQ,WAAW,EAAE;AAE9D,SAAI,cAAc,MAAO;AAEzB,SAAI,aAAa,WAAW,GAAG,QAAQ,OAAO;MAC5C,MAAM,kBAAkB,GAAG,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,QAAQ;MACrE,MAAM,aAAa,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;MACjE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,iBAAiB,GAAG,WAAW,CAAC,CAAC;AACvE,UAAI,cAAc,KAAK,IAAI,KAAK,GAAG,QAAQ,MACzC,IAAG,QAAQ,QAAQ,cAAc,KAAK,IAAI;gBAIxC,EAAE,YAAY,GAAG,SACnB,IAAG,QAAQ,YAAY,OAAO,UAAU;;;IAKhD;;AAGJ,QAAO;;;;;;AAOT,SAAS,eAAe,IAAa,UAA2B;AAE9D,YAAW,SAAS,MAAM;CAG1B,MAAM,YAAY,SAAS,MAAM,gCAAgC;AACjE,KAAI,WAAW;EACb,MAAM,GAAG,UAAU,aAAa;AAChC,MAAI,cAAc,OAEhB,QAAO,aAAa,GAAG,WAAW,EAAE;MAGpC,QAAO,GAAG,UAAU,cAAc;;AAKtC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,YAAY,SAAS,MAAM,EAAE;AAEnC,UADgB,GAAG,SAAS,OAAO,MAAM,MAAM,IAAI,EAAE,EACtC,SAAS,UAAU;;AAIpC,KAAI,SAAS,WAAW,IAAI,EAAE;EAC5B,MAAM,KAAK,SAAS,MAAM,EAAE;AAC5B,SAAO,GAAG,SAAS,OAAO;;CAK5B,MAAM,eAAe,SAAS,MAAM,kCAAkC;AACtE,KAAI,cAAc;EAChB,MAAM,GAAG,SAAS,YAAY;AAC9B,MAAI,GAAG,SAAS,QAAS,QAAO;EAGhC,MAAM,cAAc,SAAS,MAAM,sBAAsB;AACzD,MAAI,aAAa;GACf,MAAM,GAAG,UAAU,aAAa;AAChC,OAAI,cAAc,OAChB,QAAO,aAAa,GAAG,WAAW,EAAE;OAEpC,QAAO,GAAG,UAAU,cAAc;;AAGtC,SAAO;;AAIT,QAAO,GAAG,SAAS"}
@@ -0,0 +1,31 @@
1
+ import { ChildNode } from "domhandler";
2
+
3
+ //#region src/transformers/columnWidth.d.ts
4
+ /**
5
+ * Resolve `__MAIZZLE_COLW_{id}__` and `__MAIZZLE_OH_{id}__` placeholders.
6
+ *
7
+ * COLW (column width) — emitted by `<Column>` and `<Overlap>`. Walks up to
8
+ * the nearest ancestor marked with `data-maizzle-cw` (Container/Section/
9
+ * Row/another Column already resolved) and divides by `data-maizzle-cw-count`.
10
+ * If `data-maizzle-cw-self` is present, reads from the element's own inlined
11
+ * `max-width`/`width`/`min-width` instead of walking up — used by `<Overlap>`
12
+ * when it has its own width class/inline style.
13
+ *
14
+ * OH (overlap height) — emitted by `<Overlap>`. Reads `max-height`/`height`/
15
+ * `min-height` from the element's own inlined style.
16
+ *
17
+ * Resolution rules:
18
+ * - Style placeholders for `min-width`: replaced when resolvable, otherwise
19
+ * the entire `min-width` declaration is stripped.
20
+ * - Other style placeholders (Overlap td `width`, etc.): replaced when
21
+ * resolvable, otherwise replaced with the count-based fallback or `100%`.
22
+ * - Comment placeholders: same fallback chain.
23
+ *
24
+ * Resolved column widths are written back to `data-maizzle-cw` so nested
25
+ * rows cascade. All `data-maizzle-cw*` and `data-maizzle-oh-*` attrs are
26
+ * stripped at the end.
27
+ */
28
+ declare function columnWidth(dom: ChildNode[]): ChildNode[];
29
+ //#endregion
30
+ export { columnWidth };
31
+ //# sourceMappingURL=columnWidth.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columnWidth.d.mts","names":[],"sources":["../../src/transformers/columnWidth.ts"],"mappings":";;;;;AA4FA;;;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA,CAAY,GAAA,EAAK,SAAA,KAAc,SAAA"}
@@ -0,0 +1,166 @@
1
+ import { walk } from "../utils/ast/walker.mjs";
2
+ import "../utils/ast/index.mjs";
3
+
4
+ //#region src/transformers/columnWidth.ts
5
+ const RE_MAX_WIDTH = /(?:^|;\s*)max-width:\s*([^;]+)/i;
6
+ const RE_WIDTH = /(?:^|;\s*)width:\s*([^;]+)/i;
7
+ const RE_MIN_WIDTH = /(?:^|;\s*)min-width:\s*([^;]+)/i;
8
+ const RE_MAX_HEIGHT = /(?:^|;\s*)max-height:\s*([^;]+)/i;
9
+ const RE_HEIGHT = /(?:^|;\s*)height:\s*([^;]+)/i;
10
+ const RE_MIN_HEIGHT = /(?:^|;\s*)min-height:\s*([^;]+)/i;
11
+ const RE_PERCENT = /^[\d.]+%$/;
12
+ function resolveLength(value) {
13
+ const trimmed = value.trim();
14
+ if (RE_PERCENT.test(trimmed)) return trimmed;
15
+ const m = trimmed.match(/^([\d.]+)(px|rem|em|pt)?$/i);
16
+ if (!m) return null;
17
+ const n = parseFloat(m[1]);
18
+ switch ((m[2] || "px").toLowerCase()) {
19
+ case "px": return `${Math.round(n)}px`;
20
+ case "rem":
21
+ case "em": return `${Math.round(n * 16)}px`;
22
+ case "pt": return `${Math.round(n * 1.333)}px`;
23
+ default: return null;
24
+ }
25
+ }
26
+ function divideLength(value, divisor) {
27
+ const m = value.match(/^([\d.]+)(px|%)$/);
28
+ if (!m || divisor < 1) return null;
29
+ const n = parseFloat(m[1]);
30
+ return `${parseFloat((n / divisor).toFixed(2))}${m[2]}`;
31
+ }
32
+ function depth(node) {
33
+ let d = 0;
34
+ let cur = node.parent;
35
+ while (cur) {
36
+ d++;
37
+ cur = cur.parent ?? null;
38
+ }
39
+ return d;
40
+ }
41
+ function readWidthSource(el) {
42
+ const explicit = el.attribs?.["data-maizzle-cw"];
43
+ if (explicit) {
44
+ const r = resolveLength(explicit);
45
+ if (r) return r;
46
+ }
47
+ return readWidthFromStyle(el);
48
+ }
49
+ function readWidthFromStyle(el) {
50
+ const style = el.attribs?.style ?? "";
51
+ const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1] ?? style.match(RE_MIN_WIDTH)?.[1];
52
+ return raw ? resolveLength(raw) : null;
53
+ }
54
+ function readHeightFromStyle(el) {
55
+ const style = el.attribs?.style ?? "";
56
+ const raw = style.match(RE_MAX_HEIGHT)?.[1] ?? style.match(RE_HEIGHT)?.[1] ?? style.match(RE_MIN_HEIGHT)?.[1];
57
+ return raw ? resolveLength(raw) : null;
58
+ }
59
+ /**
60
+ * Resolve `__MAIZZLE_COLW_{id}__` and `__MAIZZLE_OH_{id}__` placeholders.
61
+ *
62
+ * COLW (column width) — emitted by `<Column>` and `<Overlap>`. Walks up to
63
+ * the nearest ancestor marked with `data-maizzle-cw` (Container/Section/
64
+ * Row/another Column already resolved) and divides by `data-maizzle-cw-count`.
65
+ * If `data-maizzle-cw-self` is present, reads from the element's own inlined
66
+ * `max-width`/`width`/`min-width` instead of walking up — used by `<Overlap>`
67
+ * when it has its own width class/inline style.
68
+ *
69
+ * OH (overlap height) — emitted by `<Overlap>`. Reads `max-height`/`height`/
70
+ * `min-height` from the element's own inlined style.
71
+ *
72
+ * Resolution rules:
73
+ * - Style placeholders for `min-width`: replaced when resolvable, otherwise
74
+ * the entire `min-width` declaration is stripped.
75
+ * - Other style placeholders (Overlap td `width`, etc.): replaced when
76
+ * resolvable, otherwise replaced with the count-based fallback or `100%`.
77
+ * - Comment placeholders: same fallback chain.
78
+ *
79
+ * Resolved column widths are written back to `data-maizzle-cw` so nested
80
+ * rows cascade. All `data-maizzle-cw*` and `data-maizzle-oh-*` attrs are
81
+ * stripped at the end.
82
+ */
83
+ function columnWidth(dom) {
84
+ const columns = [];
85
+ const heightTargets = [];
86
+ walk(dom, (node) => {
87
+ const el = node;
88
+ if (!el.attribs) return;
89
+ const id = el.attribs["data-maizzle-cw-id"];
90
+ if (id) {
91
+ const count = parseInt(el.attribs["data-maizzle-cw-count"] || "1", 10);
92
+ const self = "data-maizzle-cw-self" in el.attribs;
93
+ columns.push({
94
+ el,
95
+ id,
96
+ count,
97
+ d: depth(node),
98
+ self
99
+ });
100
+ }
101
+ const ohId = el.attribs["data-maizzle-oh-id"];
102
+ if (ohId) heightTargets.push({
103
+ el,
104
+ id: ohId
105
+ });
106
+ });
107
+ columns.sort((a, b) => a.d - b.d);
108
+ const widthResolutions = /* @__PURE__ */ new Map();
109
+ const widthFallbacks = /* @__PURE__ */ new Map();
110
+ for (const { id, count } of columns) widthFallbacks.set(id, `${Math.round(100 / Math.max(count, 1))}%`);
111
+ for (const { el, id, count, self } of columns) {
112
+ let sourceWidth = null;
113
+ if (self) sourceWidth = readWidthFromStyle(el);
114
+ else {
115
+ let cur = el.parent;
116
+ while (cur) {
117
+ const parentEl = cur;
118
+ if (parentEl.attribs && "data-maizzle-cw" in parentEl.attribs) {
119
+ sourceWidth = readWidthSource(parentEl);
120
+ break;
121
+ }
122
+ cur = cur.parent ?? null;
123
+ }
124
+ }
125
+ if (sourceWidth) {
126
+ const div = divideLength(sourceWidth, count);
127
+ if (div) {
128
+ widthResolutions.set(id, div);
129
+ el.attribs["data-maizzle-cw"] = div;
130
+ }
131
+ }
132
+ }
133
+ const heightResolutions = /* @__PURE__ */ new Map();
134
+ for (const { el, id } of heightTargets) {
135
+ const h = readHeightFromStyle(el);
136
+ if (h) heightResolutions.set(id, h);
137
+ }
138
+ walk(dom, (node) => {
139
+ if (node.type === "comment") {
140
+ const data = node.data;
141
+ if (!data || !data.includes("__MAIZZLE_COLW_") && !data.includes("__MAIZZLE_OH_")) return;
142
+ node.data = data.replace(/__MAIZZLE_COLW_([^_]+)__/g, (_m, mid) => widthResolutions.get(mid) ?? widthFallbacks.get(mid) ?? "100%").replace(/__MAIZZLE_OH_([^_]+)__/g, (_m, hid) => heightResolutions.get(hid) ?? "100%");
143
+ return;
144
+ }
145
+ const el = node;
146
+ if (!el.attribs) return;
147
+ const style = el.attribs.style;
148
+ if (style && (style.includes("__MAIZZLE_COLW_") || style.includes("__MAIZZLE_OH_"))) {
149
+ let next = style.replace(/(?:^|;\s*)min-width:\s*__MAIZZLE_COLW_([^_]+)__\s*;?/g, (_m, mid) => widthResolutions.has(mid) ? `; min-width: ${widthResolutions.get(mid)}` : "");
150
+ next = next.replace(/__MAIZZLE_COLW_([^_]+)__/g, (_m, mid) => widthResolutions.get(mid) ?? widthFallbacks.get(mid) ?? "100%").replace(/__MAIZZLE_OH_([^_]+)__/g, (_m, hid) => heightResolutions.get(hid) ?? "100%");
151
+ next = next.replace(/^;\s*/, "").replace(/;\s*$/, "").trim();
152
+ if (next) el.attribs.style = next;
153
+ else delete el.attribs.style;
154
+ }
155
+ delete el.attribs["data-maizzle-cw"];
156
+ delete el.attribs["data-maizzle-cw-id"];
157
+ delete el.attribs["data-maizzle-cw-count"];
158
+ delete el.attribs["data-maizzle-cw-self"];
159
+ delete el.attribs["data-maizzle-oh-id"];
160
+ });
161
+ return dom;
162
+ }
163
+
164
+ //#endregion
165
+ export { columnWidth };
166
+ //# sourceMappingURL=columnWidth.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columnWidth.mjs","names":[],"sources":["../../src/transformers/columnWidth.ts"],"sourcesContent":["import { walk } from '../utils/ast/index.ts'\nimport type { ChildNode, Element, ParentNode } from 'domhandler'\n\nconst RE_MAX_WIDTH = /(?:^|;\\s*)max-width:\\s*([^;]+)/i\nconst RE_WIDTH = /(?:^|;\\s*)width:\\s*([^;]+)/i\nconst RE_MIN_WIDTH = /(?:^|;\\s*)min-width:\\s*([^;]+)/i\nconst RE_MAX_HEIGHT = /(?:^|;\\s*)max-height:\\s*([^;]+)/i\nconst RE_HEIGHT = /(?:^|;\\s*)height:\\s*([^;]+)/i\nconst RE_MIN_HEIGHT = /(?:^|;\\s*)min-height:\\s*([^;]+)/i\nconst RE_PERCENT = /^[\\d.]+%$/\n\nfunction resolveLength(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\nfunction divideLength(value: string, divisor: number): string | null {\n const m = value.match(/^([\\d.]+)(px|%)$/)\n if (!m || divisor < 1) return null\n const n = parseFloat(m[1])\n return `${parseFloat((n / divisor).toFixed(2))}${m[2]}`\n}\n\nfunction depth(node: ChildNode): number {\n let d = 0\n let cur: ParentNode | null = node.parent\n while (cur) {\n d++\n cur = (cur as any).parent ?? null\n }\n return d\n}\n\nfunction readWidthSource(el: Element): string | null {\n const explicit = el.attribs?.['data-maizzle-cw']\n if (explicit) {\n const r = resolveLength(explicit)\n if (r) return r\n }\n return readWidthFromStyle(el)\n}\n\nfunction readWidthFromStyle(el: Element): string | null {\n const style = el.attribs?.style ?? ''\n const raw = style.match(RE_MAX_WIDTH)?.[1]\n ?? style.match(RE_WIDTH)?.[1]\n ?? style.match(RE_MIN_WIDTH)?.[1]\n return raw ? resolveLength(raw) : null\n}\n\nfunction readHeightFromStyle(el: Element): string | null {\n const style = el.attribs?.style ?? ''\n const raw = style.match(RE_MAX_HEIGHT)?.[1]\n ?? style.match(RE_HEIGHT)?.[1]\n ?? style.match(RE_MIN_HEIGHT)?.[1]\n return raw ? resolveLength(raw) : null\n}\n\n/**\n * Resolve `__MAIZZLE_COLW_{id}__` and `__MAIZZLE_OH_{id}__` placeholders.\n *\n * COLW (column width) — emitted by `<Column>` and `<Overlap>`. Walks up to\n * the nearest ancestor marked with `data-maizzle-cw` (Container/Section/\n * Row/another Column already resolved) and divides by `data-maizzle-cw-count`.\n * If `data-maizzle-cw-self` is present, reads from the element's own inlined\n * `max-width`/`width`/`min-width` instead of walking up — used by `<Overlap>`\n * when it has its own width class/inline style.\n *\n * OH (overlap height) — emitted by `<Overlap>`. Reads `max-height`/`height`/\n * `min-height` from the element's own inlined style.\n *\n * Resolution rules:\n * - Style placeholders for `min-width`: replaced when resolvable, otherwise\n * the entire `min-width` declaration is stripped.\n * - Other style placeholders (Overlap td `width`, etc.): replaced when\n * resolvable, otherwise replaced with the count-based fallback or `100%`.\n * - Comment placeholders: same fallback chain.\n *\n * Resolved column widths are written back to `data-maizzle-cw` so nested\n * rows cascade. All `data-maizzle-cw*` and `data-maizzle-oh-*` attrs are\n * stripped at the end.\n */\nexport function columnWidth(dom: ChildNode[]): ChildNode[] {\n const columns: { el: Element; id: string; count: number; d: number; self: boolean }[] = []\n const heightTargets: { el: Element; id: string }[] = []\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.attribs) return\n\n const id = el.attribs['data-maizzle-cw-id']\n if (id) {\n const count = parseInt(el.attribs['data-maizzle-cw-count'] || '1', 10)\n const self = 'data-maizzle-cw-self' in el.attribs\n columns.push({ el, id, count, d: depth(node), self })\n }\n\n const ohId = el.attribs['data-maizzle-oh-id']\n if (ohId) heightTargets.push({ el, id: ohId })\n })\n\n columns.sort((a, b) => a.d - b.d)\n\n const widthResolutions = new Map<string, string>()\n const widthFallbacks = new Map<string, string>()\n\n for (const { id, count } of columns) {\n widthFallbacks.set(id, `${Math.round(100 / Math.max(count, 1))}%`)\n }\n\n for (const { el, id, count, self } of columns) {\n let sourceWidth: string | null = null\n\n if (self) {\n sourceWidth = readWidthFromStyle(el)\n } else {\n let cur: ParentNode | null = el.parent\n while (cur) {\n const parentEl = cur as Element\n if (parentEl.attribs && 'data-maizzle-cw' in parentEl.attribs) {\n sourceWidth = readWidthSource(parentEl)\n break\n }\n cur = (cur as any).parent ?? null\n }\n }\n\n if (sourceWidth) {\n const div = divideLength(sourceWidth, count)\n if (div) {\n widthResolutions.set(id, div)\n el.attribs['data-maizzle-cw'] = div\n }\n }\n }\n\n const heightResolutions = new Map<string, string>()\n for (const { el, id } of heightTargets) {\n const h = readHeightFromStyle(el)\n if (h) heightResolutions.set(id, h)\n }\n\n walk(dom, (node) => {\n if (node.type === 'comment') {\n const data = (node as any).data as string\n if (!data || (!data.includes('__MAIZZLE_COLW_') && !data.includes('__MAIZZLE_OH_'))) return\n ;(node as any).data = data\n .replace(/__MAIZZLE_COLW_([^_]+)__/g,\n (_m, mid) => widthResolutions.get(mid) ?? widthFallbacks.get(mid) ?? '100%')\n .replace(/__MAIZZLE_OH_([^_]+)__/g,\n (_m, hid) => heightResolutions.get(hid) ?? '100%')\n return\n }\n\n const el = node as Element\n if (!el.attribs) return\n\n const style = el.attribs.style\n if (style && (style.includes('__MAIZZLE_COLW_') || style.includes('__MAIZZLE_OH_'))) {\n let next = style.replace(\n /(?:^|;\\s*)min-width:\\s*__MAIZZLE_COLW_([^_]+)__\\s*;?/g,\n (_m, mid) => widthResolutions.has(mid) ? `; min-width: ${widthResolutions.get(mid)}` : ''\n )\n next = next\n .replace(/__MAIZZLE_COLW_([^_]+)__/g,\n (_m, mid) => widthResolutions.get(mid) ?? widthFallbacks.get(mid) ?? '100%')\n .replace(/__MAIZZLE_OH_([^_]+)__/g,\n (_m, hid) => heightResolutions.get(hid) ?? '100%')\n next = next.replace(/^;\\s*/, '').replace(/;\\s*$/, '').trim()\n if (next) el.attribs.style = next\n else delete el.attribs.style\n }\n\n delete el.attribs['data-maizzle-cw']\n delete el.attribs['data-maizzle-cw-id']\n delete el.attribs['data-maizzle-cw-count']\n delete el.attribs['data-maizzle-cw-self']\n delete el.attribs['data-maizzle-oh-id']\n })\n\n return dom\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/transformers/index.ts"],"mappings":";;;;;AAkDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,eAAA,CACpB,IAAA,UACA,MAAA,EAAQ,aAAA,EACR,QAAA,WACA,OAAA,YACC,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/transformers/index.ts"],"mappings":";;;;;AAoDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,eAAA,CACpB,IAAA,UACA,MAAA,EAAQ,aAAA,EACR,QAAA,WACA,OAAA,YACC,OAAA"}
@@ -6,6 +6,8 @@ import { tailwindcss } from "./tailwindcss.mjs";
6
6
  import { safeClassNames } from "./safeClassNames.mjs";
7
7
  import { attributeToStyle } from "./attributeToStyle.mjs";
8
8
  import { inlineCSS } from "./inlineCSS.mjs";
9
+ import { msoWidthFromClass } from "./msoWidthFromClass.mjs";
10
+ import { columnWidth } from "./columnWidth.mjs";
9
11
  import { removeAttributes } from "./removeAttributes.mjs";
10
12
  import { shorthandCSS } from "./shorthandCSS.mjs";
11
13
  import { sixHex } from "./sixHex.mjs";
@@ -57,6 +59,8 @@ async function runTransformers(html, config, filePath, doctype) {
57
59
  dom = safeClassNames(dom, config.css);
58
60
  dom = attributeToStyle(dom, config.css);
59
61
  dom = inlineCSS(dom, config.css);
62
+ dom = msoWidthFromClass(dom);
63
+ dom = columnWidth(dom);
60
64
  dom = removeAttributes(dom, config.html?.attributes);
61
65
  dom = shorthandCSS(dom, config.css);
62
66
  dom = sixHex(dom, config.css);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/transformers/index.ts"],"sourcesContent":["import { parse, serialize } from '../utils/ast/index.ts'\nimport { inlineLink } from './inlineLink.ts'\nimport { tailwindcss } from './tailwindcss.ts'\nimport { safeClassNames } from './safeClassNames.ts'\nimport { attributeToStyle } from './attributeToStyle.ts'\nimport { inlineCSS } from './inlineCSS.ts'\nimport { removeAttributes } from './removeAttributes.ts'\nimport { shorthandCSS } from './shorthandCSS.ts'\nimport { sixHex } from './sixHex.ts'\nimport { addAttributes } from './addAttributes.ts'\nimport { filters } from './filters/index.ts'\nimport { base } from './base.ts'\nimport { entities } from './entities.ts'\nimport { urlQuery } from './urlQuery.ts'\nimport { purgeCSS } 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'\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): Promise<string> {\n // Parse once — all DOM transformers share this array\n let dom = parse(html)\n\n // 0. Inline <link> stylesheets\n dom = await inlineLink(dom, filePath)\n\n // 1. Tailwind CSS — always runs first\n dom = await tailwindcss(dom, config, filePath)\n\n // 2. Safe class names\n dom = safeClassNames(dom, config.css)\n\n // 3. Attribute to style\n dom = attributeToStyle(dom, config.css)\n\n // 4. CSS inliner (serializes/parses internally around juice)\n dom = inlineCSS(dom, config.css)\n\n // 5. Remove attributes\n dom = removeAttributes(dom, config.html?.attributes)\n\n // 6. Shorthand CSS\n dom = shorthandCSS(dom, config.css)\n\n // 7. Six-digit HEX\n dom = sixHex(dom, config.css)\n\n // 8. Add attributes\n dom = addAttributes(dom, config.html?.attributes)\n\n // 9. Filters\n dom = filters(dom, config.filters)\n\n // 10. Base URL (serializes/parses internally for VML/MSO regex passes)\n dom = base(dom, config.url)\n\n // 11. URL query\n dom = urlQuery(dom, config.url)\n\n // 12. Purge CSS (serializes/parses internally around email-comb)\n dom = purgeCSS(dom, config.css)\n\n // 13. Entities\n dom = entities(dom, config.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 // Remove Vue-generated comments after serializing\n result = result\n .replaceAll('<!--[-->', '')\n .replaceAll('<!--]-->', '')\n .replaceAll('<!--teleport start anchor-->', '')\n .replaceAll('<!--teleport anchor-->', '')\n .replaceAll('<!--teleport start-->', '')\n .replaceAll('<!--teleport end-->', '')\n\n // 14. Replace strings\n result = replaceStrings(result, config)\n\n // 15. Format\n result = await format(result, config)\n\n // 16. Minify\n result = minify(result, config)\n\n // Strip self-closing slashes for HTML5 doctypes\n if (!isXhtml) {\n result = result.replace(/ \\/>/g, '>')\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,eAAsB,gBACpB,MACA,QACA,UACA,SACiB;CAEjB,IAAI,MAAM,MAAM,KAAK;AAGrB,OAAM,MAAM,WAAW,KAAK,SAAS;AAGrC,OAAM,MAAM,YAAY,KAAK,QAAQ,SAAS;AAG9C,OAAM,eAAe,KAAK,OAAO,IAAI;AAGrC,OAAM,iBAAiB,KAAK,OAAO,IAAI;AAGvC,OAAM,UAAU,KAAK,OAAO,IAAI;AAGhC,OAAM,iBAAiB,KAAK,OAAO,MAAM,WAAW;AAGpD,OAAM,aAAa,KAAK,OAAO,IAAI;AAGnC,OAAM,OAAO,KAAK,OAAO,IAAI;AAG7B,OAAM,cAAc,KAAK,OAAO,MAAM,WAAW;AAGjD,OAAM,QAAQ,KAAK,OAAO,QAAQ;AAGlC,OAAM,KAAK,KAAK,OAAO,IAAI;AAG3B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,MAAM,eAAe;CAGhD,MAAM,UAAU,UAAU,SAAS,KAAK,QAAQ,GAAG;CACnD,IAAI,SAAS,UAAU,KAAK,EAAE,iBAAiB,SAAS,CAAC;AAGzD,UAAS,OACN,WAAW,YAAY,GAAG,CAC1B,WAAW,YAAY,GAAG,CAC1B,WAAW,gCAAgC,GAAG,CAC9C,WAAW,0BAA0B,GAAG,CACxC,WAAW,yBAAyB,GAAG,CACvC,WAAW,uBAAuB,GAAG;AAGxC,UAAS,eAAe,QAAQ,OAAO;AAGvC,UAAS,MAAM,OAAO,QAAQ,OAAO;AAGrC,UAAS,OAAO,QAAQ,OAAO;AAG/B,KAAI,CAAC,QACH,UAAS,OAAO,QAAQ,SAAS,IAAI;AAGvC,QAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/transformers/index.ts"],"sourcesContent":["import { parse, serialize } from '../utils/ast/index.ts'\nimport { inlineLink } from './inlineLink.ts'\nimport { tailwindcss } from './tailwindcss.ts'\nimport { safeClassNames } from './safeClassNames.ts'\nimport { attributeToStyle } from './attributeToStyle.ts'\nimport { inlineCSS } from './inlineCSS.ts'\nimport { msoWidthFromClass } from './msoWidthFromClass.ts'\nimport { columnWidth } from './columnWidth.ts'\nimport { removeAttributes } from './removeAttributes.ts'\nimport { shorthandCSS } from './shorthandCSS.ts'\nimport { sixHex } from './sixHex.ts'\nimport { addAttributes } from './addAttributes.ts'\nimport { filters } from './filters/index.ts'\nimport { base } from './base.ts'\nimport { entities } from './entities.ts'\nimport { urlQuery } from './urlQuery.ts'\nimport { purgeCSS } 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'\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): Promise<string> {\n // Parse once — all DOM transformers share this array\n let dom = parse(html)\n\n // 0. Inline <link> stylesheets\n dom = await inlineLink(dom, filePath)\n\n // 1. Tailwind CSS — always runs first\n dom = await tailwindcss(dom, config, filePath)\n\n // 2. Safe class names\n dom = safeClassNames(dom, config.css)\n\n // 3. Attribute to style\n dom = attributeToStyle(dom, config.css)\n\n // 4. CSS inliner (serializes/parses internally around juice)\n dom = inlineCSS(dom, config.css)\n\n // 4.5. Resolve MSO width placeholders from inlined max-width/width\n dom = msoWidthFromClass(dom)\n\n // 4.6. Resolve Column min-width placeholders from nearest sized ancestor\n dom = columnWidth(dom)\n\n // 5. Remove attributes\n dom = removeAttributes(dom, config.html?.attributes)\n\n // 6. Shorthand CSS\n dom = shorthandCSS(dom, config.css)\n\n // 7. Six-digit HEX\n dom = sixHex(dom, config.css)\n\n // 8. Add attributes\n dom = addAttributes(dom, config.html?.attributes)\n\n // 9. Filters\n dom = filters(dom, config.filters)\n\n // 10. Base URL (serializes/parses internally for VML/MSO regex passes)\n dom = base(dom, config.url)\n\n // 11. URL query\n dom = urlQuery(dom, config.url)\n\n // 12. Purge CSS (serializes/parses internally around email-comb)\n dom = purgeCSS(dom, config.css)\n\n // 13. Entities\n dom = entities(dom, config.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 // Remove Vue-generated comments after serializing\n result = result\n .replaceAll('<!--[-->', '')\n .replaceAll('<!--]-->', '')\n .replaceAll('<!--teleport start anchor-->', '')\n .replaceAll('<!--teleport anchor-->', '')\n .replaceAll('<!--teleport start-->', '')\n .replaceAll('<!--teleport end-->', '')\n\n // 14. Replace strings\n result = replaceStrings(result, config)\n\n // 15. Format\n result = await format(result, config)\n\n // 16. Minify\n result = minify(result, config)\n\n // Strip self-closing slashes for HTML5 doctypes\n if (!isXhtml) {\n result = result.replace(/ \\/>/g, '>')\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,eAAsB,gBACpB,MACA,QACA,UACA,SACiB;CAEjB,IAAI,MAAM,MAAM,KAAK;AAGrB,OAAM,MAAM,WAAW,KAAK,SAAS;AAGrC,OAAM,MAAM,YAAY,KAAK,QAAQ,SAAS;AAG9C,OAAM,eAAe,KAAK,OAAO,IAAI;AAGrC,OAAM,iBAAiB,KAAK,OAAO,IAAI;AAGvC,OAAM,UAAU,KAAK,OAAO,IAAI;AAGhC,OAAM,kBAAkB,IAAI;AAG5B,OAAM,YAAY,IAAI;AAGtB,OAAM,iBAAiB,KAAK,OAAO,MAAM,WAAW;AAGpD,OAAM,aAAa,KAAK,OAAO,IAAI;AAGnC,OAAM,OAAO,KAAK,OAAO,IAAI;AAG7B,OAAM,cAAc,KAAK,OAAO,MAAM,WAAW;AAGjD,OAAM,QAAQ,KAAK,OAAO,QAAQ;AAGlC,OAAM,KAAK,KAAK,OAAO,IAAI;AAG3B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,MAAM,eAAe;CAGhD,MAAM,UAAU,UAAU,SAAS,KAAK,QAAQ,GAAG;CACnD,IAAI,SAAS,UAAU,KAAK,EAAE,iBAAiB,SAAS,CAAC;AAGzD,UAAS,OACN,WAAW,YAAY,GAAG,CAC1B,WAAW,YAAY,GAAG,CAC1B,WAAW,gCAAgC,GAAG,CAC9C,WAAW,0BAA0B,GAAG,CACxC,WAAW,yBAAyB,GAAG,CACvC,WAAW,uBAAuB,GAAG;AAGxC,UAAS,eAAe,QAAQ,OAAO;AAGvC,UAAS,MAAM,OAAO,QAAQ,OAAO;AAGrC,UAAS,OAAO,QAAQ,OAAO;AAG/B,KAAI,CAAC,QACH,UAAS,OAAO,QAAQ,SAAS,IAAI;AAGvC,QAAO"}
@@ -28,8 +28,8 @@ function inlineCSS(dom, config = {}) {
28
28
  walk(dom, (node) => {
29
29
  const el = node;
30
30
  if (el.name === "style" && el.attribs) {
31
- if (el.attribs.embed && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
32
- if (el.attribs["data-embed"] && !("embed" in el.attribs)) el.attribs.embed = "";
31
+ if ("embed" in el.attribs && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
32
+ if ("data-embed" in el.attribs && !("embed" in el.attribs)) el.attribs.embed = "";
33
33
  if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = "";
34
34
  }
35
35
  });
@@ -1 +1 @@
1
- {"version":3,"file":"inlineCSS.mjs","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'\nimport type { CssConfig } from '../types/config.ts'\n\n/**\n * Inline CSS transformer.\n *\n * Inlines CSS from `<style>` tags into inline style attributes on HTML elements.\n * This is important for email client compatibility (especially Outlook on Windows).\n *\n * Enabled when `css.inline` is set to `true` or an object with options.\n * All Juice options are supported and passed through directly.\n */\nexport function inlineCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const inline = config.inline\n\n // Disabled when inline is falsy or not an object/truthy\n if (!inline) {\n return dom\n }\n\n // Build options from config\n const options = typeof inline === 'object' ? inline : {}\n\n // Separate Maizzle-specific options from Juice options\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\n if (el.attribs.embed && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n if (el.attribs['data-embed'] && !('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 // Restore data-embed from our marker, then remove the marker.\n // The purge step will handle final data-embed/embed removal.\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['data-embed'] = ''\n el.attribs.embed = ''\n delete el.attribs['data-maizzle-embed']\n }\n })\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAOT,MAAM,EACJ,uBAAuB,MACvB,UACA,YAAY,IACZ,kBACA,oBACA,eACA,gBACA,YACA,GAAG,qBAZW,OAAO,WAAW,WAAW,SAAS,EAAE;AAgBxD,OAAM,mBAAmB,oBAAoB,EAAE;AAC/C,OAAM,qBAAqB,CAAC,eAAe,GAAI,sBAAsB,EAAE,CAAE;AACzE,OAAM,iBAAiB,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AACnF,OAAM,kBAAkB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAGrF,KAAI,cAAc,OAAO,eAAe,SACtC,QAAO,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AACnD,MAAI,MAAM,SAAS,MAAM,IACvB,OAAM,WAAW,OAAO;GAE1B;AAMJ,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS;AAErC,OAAI,GAAG,QAAQ,SAAS,EAAE,gBAAgB,GAAG,SAC3C,IAAG,QAAQ,gBAAgB;AAE7B,OAAI,GAAG,QAAQ,iBAAiB,EAAE,WAAW,GAAG,SAC9C,IAAG,QAAQ,QAAQ;AAIrB,OAAI,gBAAgB,GAAG,QACrB,IAAG,QAAQ,wBAAwB;;GAGvC;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;AAEJ,KAAI;AAYF,gBAAc,MAAM,YAXe;GACjC,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;GACJ,CAE4C;SACvC;AAEN,SAAO;;CAIT,MAAM,SAAS,MAAM,YAAY;AAEjC,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,OAAO;GAErB,IAAI,QAAQ,GAAG,QAAQ,MACpB,QAAQ,SAAS,KAAK,CACtB,QAAQ,SAAS,KAAK,CACtB,SAAS;AAGZ,OAAI,CAAC,MAAM,SAAS,IAAI,CACtB,UAAS;AAGX,OAAI,qBACF,SAAQ,MAAM,QACZ,4DACA,IACD;AAGH,MAAG,QAAQ,QAAQ;;GAErB;AAIF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,WAAW,wBAAwB,GAAG,SAAS;AAC3E,MAAG,QAAQ,gBAAgB;AAC3B,MAAG,QAAQ,QAAQ;AACnB,UAAO,GAAG,QAAQ;;GAEpB;AAEF,QAAO"}
1
+ {"version":3,"file":"inlineCSS.mjs","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'\nimport type { CssConfig } from '../types/config.ts'\n\n/**\n * Inline CSS transformer.\n *\n * Inlines CSS from `<style>` tags into inline style attributes on HTML elements.\n * This is important for email client compatibility (especially Outlook on Windows).\n *\n * Enabled when `css.inline` is set to `true` or an object with options.\n * All Juice options are supported and passed through directly.\n */\nexport function inlineCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const inline = config.inline\n\n // Disabled when inline is falsy or not an object/truthy\n if (!inline) {\n return dom\n }\n\n // Build options from config\n const options = typeof inline === 'object' ? inline : {}\n\n // Separate Maizzle-specific options from Juice options\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 // Restore data-embed from our marker, then remove the marker.\n // The purge step will handle final data-embed/embed removal.\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['data-embed'] = ''\n el.attribs.embed = ''\n delete el.attribs['data-maizzle-embed']\n }\n })\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAOT,MAAM,EACJ,uBAAuB,MACvB,UACA,YAAY,IACZ,kBACA,oBACA,eACA,gBACA,YACA,GAAG,qBAZW,OAAO,WAAW,WAAW,SAAS,EAAE;AAgBxD,OAAM,mBAAmB,oBAAoB,EAAE;AAC/C,OAAM,qBAAqB,CAAC,eAAe,GAAI,sBAAsB,EAAE,CAAE;AACzE,OAAM,iBAAiB,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AACnF,OAAM,kBAAkB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAGrF,KAAI,cAAc,OAAO,eAAe,SACtC,QAAO,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AACnD,MAAI,MAAM,SAAS,MAAM,IACvB,OAAM,WAAW,OAAO;GAE1B;AAMJ,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS;AAGrC,OAAI,WAAW,GAAG,WAAW,EAAE,gBAAgB,GAAG,SAChD,IAAG,QAAQ,gBAAgB;AAE7B,OAAI,gBAAgB,GAAG,WAAW,EAAE,WAAW,GAAG,SAChD,IAAG,QAAQ,QAAQ;AAIrB,OAAI,gBAAgB,GAAG,QACrB,IAAG,QAAQ,wBAAwB;;GAGvC;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;AAEJ,KAAI;AAYF,gBAAc,MAAM,YAXe;GACjC,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;GACJ,CAE4C;SACvC;AAEN,SAAO;;CAIT,MAAM,SAAS,MAAM,YAAY;AAEjC,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,OAAO;GAErB,IAAI,QAAQ,GAAG,QAAQ,MACpB,QAAQ,SAAS,KAAK,CACtB,QAAQ,SAAS,KAAK,CACtB,SAAS;AAGZ,OAAI,CAAC,MAAM,SAAS,IAAI,CACtB,UAAS;AAGX,OAAI,qBACF,SAAQ,MAAM,QACZ,4DACA,IACD;AAGH,MAAG,QAAQ,QAAQ;;GAErB;AAIF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,WAAW,wBAAwB,GAAG,SAAS;AAC3E,MAAG,QAAQ,gBAAgB;AAC3B,MAAG,QAAQ,QAAQ;AACnB,UAAO,GAAG,QAAQ;;GAEpB;AAEF,QAAO"}
@@ -0,0 +1,19 @@
1
+ import { ChildNode } from "domhandler";
2
+
3
+ //#region src/transformers/msoWidthFromClass.d.ts
4
+ /**
5
+ * Resolve `__MAIZZLE_MSOW_{id}__` placeholders inside MSO conditional
6
+ * comments by reading the inlined `max-width` (or `width`) of the
7
+ * paired element marked with `data-maizzle-msow-id`.
8
+ *
9
+ * Used by `<Container>` and `<Section>` to derive Outlook's table width
10
+ * from the resolved Tailwind class or inline style on the inner div,
11
+ * after CSS inlining.
12
+ *
13
+ * Falls back to the value of `data-maizzle-msow-fallback` (default
14
+ * `600px`) when the value can't be parsed.
15
+ */
16
+ declare function msoWidthFromClass(dom: ChildNode[]): ChildNode[];
17
+ //#endregion
18
+ export { msoWidthFromClass };
19
+ //# sourceMappingURL=msoWidthFromClass.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msoWidthFromClass.d.mts","names":[],"sources":["../../src/transformers/msoWidthFromClass.ts"],"mappings":";;;;;AAkCA;;;;;;;;;;iBAAgB,iBAAA,CAAkB,GAAA,EAAK,SAAA,KAAc,SAAA"}
@@ -0,0 +1,61 @@
1
+ import { walk } from "../utils/ast/walker.mjs";
2
+ import "../utils/ast/index.mjs";
3
+
4
+ //#region src/transformers/msoWidthFromClass.ts
5
+ const RE_MAX_WIDTH = /(?:^|;\s*)max-width:\s*([^;]+)/i;
6
+ const RE_WIDTH = /(?:^|;\s*)width:\s*([^;]+)/i;
7
+ const RE_PERCENT = /^[\d.]+%$/;
8
+ function resolveWidth(value) {
9
+ const trimmed = value.trim();
10
+ if (RE_PERCENT.test(trimmed)) return trimmed;
11
+ const m = trimmed.match(/^([\d.]+)(px|rem|em|pt)?$/i);
12
+ if (!m) return null;
13
+ const n = parseFloat(m[1]);
14
+ switch ((m[2] || "px").toLowerCase()) {
15
+ case "px": return `${Math.round(n)}px`;
16
+ case "rem":
17
+ case "em": return `${Math.round(n * 16)}px`;
18
+ case "pt": return `${Math.round(n * 1.333)}px`;
19
+ default: return null;
20
+ }
21
+ }
22
+ /**
23
+ * Resolve `__MAIZZLE_MSOW_{id}__` placeholders inside MSO conditional
24
+ * comments by reading the inlined `max-width` (or `width`) of the
25
+ * paired element marked with `data-maizzle-msow-id`.
26
+ *
27
+ * Used by `<Container>` and `<Section>` to derive Outlook's table width
28
+ * from the resolved Tailwind class or inline style on the inner div,
29
+ * after CSS inlining.
30
+ *
31
+ * Falls back to the value of `data-maizzle-msow-fallback` (default
32
+ * `600px`) when the value can't be parsed.
33
+ */
34
+ function msoWidthFromClass(dom) {
35
+ const widths = /* @__PURE__ */ new Map();
36
+ walk(dom, (node) => {
37
+ const el = node;
38
+ const id = el.attribs?.["data-maizzle-msow-id"];
39
+ if (!id) return;
40
+ delete el.attribs["data-maizzle-msow-id"];
41
+ const fallback = el.attribs["data-maizzle-msow-fallback"] ?? "600px";
42
+ delete el.attribs["data-maizzle-msow-fallback"];
43
+ const style = el.attribs.style ?? "";
44
+ const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1];
45
+ const resolved = raw ? resolveWidth(raw) : null;
46
+ widths.set(id, resolved ?? fallback);
47
+ });
48
+ if (widths.size === 0) return dom;
49
+ walk(dom, (node) => {
50
+ if (node.type !== "comment") return;
51
+ let data = node.data;
52
+ if (!data.includes("__MAIZZLE_MSOW_")) return;
53
+ for (const [id, px] of widths) data = data.replaceAll(`__MAIZZLE_MSOW_${id}__`, px);
54
+ node.data = data;
55
+ });
56
+ return dom;
57
+ }
58
+
59
+ //#endregion
60
+ export { msoWidthFromClass };
61
+ //# sourceMappingURL=msoWidthFromClass.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msoWidthFromClass.mjs","names":[],"sources":["../../src/transformers/msoWidthFromClass.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.]+%$/\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 `__MAIZZLE_MSOW_{id}__` placeholders inside MSO conditional\n * comments by reading the inlined `max-width` (or `width`) of the\n * paired element marked with `data-maizzle-msow-id`.\n *\n * Used by `<Container>` and `<Section>` to derive Outlook's table width\n * from the resolved Tailwind class or inline style on the inner div,\n * after CSS inlining.\n *\n * Falls back to the value of `data-maizzle-msow-fallback` (default\n * `600px`) when the value can't be parsed.\n */\nexport function msoWidthFromClass(dom: ChildNode[]): ChildNode[] {\n const widths = new Map<string, string>()\n\n walk(dom, (node) => {\n const el = node as Element\n const id = el.attribs?.['data-maizzle-msow-id']\n if (!id) return\n delete el.attribs['data-maizzle-msow-id']\n\n const fallback = el.attribs['data-maizzle-msow-fallback'] ?? '600px'\n delete el.attribs['data-maizzle-msow-fallback']\n\n const style = el.attribs.style ?? ''\n const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1]\n const resolved = raw ? resolveWidth(raw) : null\n widths.set(id, resolved ?? fallback)\n })\n\n if (widths.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.includes('__MAIZZLE_MSOW_')) return\n for (const [id, px] of widths) {\n data = data.replaceAll(`__MAIZZLE_MSOW_${id}__`, px)\n }\n ;(node as any).data = data\n })\n\n return dom\n}\n"],"mappings":";;;;AAGA,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,aAAa;AAEnB,SAAS,aAAa,OAA8B;CAClD,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,WAAW,KAAK,QAAQ,CAAE,QAAO;CACrC,MAAM,IAAI,QAAQ,MAAM,6BAA6B;AACrD,KAAI,CAAC,EAAG,QAAO;CACf,MAAM,IAAI,WAAW,EAAE,GAAG;AAC1B,UAAS,EAAE,MAAM,MAAM,aAAa,EAApC;EACE,KAAK,KAAM,QAAO,GAAG,KAAK,MAAM,EAAE,CAAC;EACnC,KAAK;EACL,KAAK,KAAM,QAAO,GAAG,KAAK,MAAM,IAAI,GAAG,CAAC;EACxC,KAAK,KAAM,QAAO,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;EAC3C,QAAS,QAAO;;;;;;;;;;;;;;;AAgBpB,SAAgB,kBAAkB,KAA+B;CAC/D,MAAM,yBAAS,IAAI,KAAqB;AAExC,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EACX,MAAM,KAAK,GAAG,UAAU;AACxB,MAAI,CAAC,GAAI;AACT,SAAO,GAAG,QAAQ;EAElB,MAAM,WAAW,GAAG,QAAQ,iCAAiC;AAC7D,SAAO,GAAG,QAAQ;EAElB,MAAM,QAAQ,GAAG,QAAQ,SAAS;EAClC,MAAM,MAAM,MAAM,MAAM,aAAa,GAAG,MAAM,MAAM,MAAM,SAAS,GAAG;EACtE,MAAM,WAAW,MAAM,aAAa,IAAI,GAAG;AAC3C,SAAO,IAAI,IAAI,YAAY,SAAS;GACpC;AAEF,KAAI,OAAO,SAAS,EAAG,QAAO;AAE9B,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,UAAW;EAC7B,IAAI,OAAQ,KAAa;AACzB,MAAI,CAAC,KAAK,SAAS,kBAAkB,CAAE;AACvC,OAAK,MAAM,CAAC,IAAI,OAAO,OACrB,QAAO,KAAK,WAAW,kBAAkB,GAAG,KAAK,GAAG;AAErD,EAAC,KAAa,OAAO;GACtB;AAEF,QAAO"}
@@ -91,7 +91,7 @@ function deepPurge(dom, safelist) {
91
91
  walk(dom, (node) => {
92
92
  const el = node;
93
93
  if (el.name !== "style" || !el.attribs) return;
94
- if ("data-embed" in el.attribs) return;
94
+ if ("data-embed" in el.attribs || "embed" in el.attribs) return;
95
95
  const textNode = el.children?.find((c) => c.type === "text");
96
96
  if (!textNode?.data?.trim()) return;
97
97
  const root = postcss.parse(textNode.data, { parser: safeParser });
@@ -1 +1 @@
1
- {"version":3,"file":"purgeCSS.mjs","names":["merge"],"sources":["../../src/transformers/purgeCSS.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport { selectAll } from 'css-select'\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_SAFELIST: string[] = [\n '*body*', // Gmail\n '.gmail*', // Gmail\n '.apple*', // Apple Mail\n '.ios*', // Mail on iOS\n '.ox-*', // Open-Xchange\n '.outlook*', // Outlook.com\n '[data-ogs*', // Outlook.com\n '.bloop_container', // Airmail\n '.Singleton', // Apple Mail 10\n '.unused', // Notes 8\n '.moz-text-html', // Thunderbird\n '.mail-detail-content', // Comcast, Libero webmail\n '*edo*', // Edison (all)\n '#*', // Freenet uses #msgBody\n '.lang*', // Fenced code blocks\n]\n\nconst DEFAULT_OPTIONS = {\n backend: [\n { heads: '{{', tails: '}}' },\n { heads: '{%', tails: '%}' },\n ],\n whitelist: [...DEFAULT_SAFELIST],\n}\n\n/**\n * Remove unused CSS transformer.\n *\n * Uses `email-comb` to strip CSS selectors and corresponding class/id\n * references that are not matched anywhere in the HTML body.\n *\n * Enable by setting `css.purge: true` (or passing options).\n * The user-supplied options are merged on top of the defaults, so\n * `safelist` values are **appended** to the built-in safelist rather\n * than replacing it.\n *\n * Accepts `ChildNode[]` as input, serializes internally before passing\n * to email-comb (which requires a raw HTML string), then parses the\n * result back to `ChildNode[]` so it fits in the DOM pipeline.\n */\nexport function purgeCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.purge\n\n if (!option) return dom\n\n const userOptions = typeof option === 'object' ? option : {}\n\n // Merge user options on top of defaults.\n // defu merges objects deeply; for arrays it appends user values.\n // We want the user safelist appended to the default safelist,\n // so we build whitelist manually.\n const userSafelist = Array.isArray((userOptions as any).safelist)\n ? (userOptions as any).safelist as string[]\n : []\n\n const { safelist: _discard, ...restUserOptions } = userOptions as any\n\n const options = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n // Deep purge first: DOM-aware selector removal using PostCSS + css-select.\n // Runs before email-comb so that email-comb can clean up orphaned classes\n // in HTML attributes left behind by removed CSS rules.\n const safelist = [...DEFAULT_SAFELIST, ...userSafelist]\n dom = deepPurge(dom, safelist)\n\n const { result } = comb(serialize(dom), options)\n\n let purgedDom = parse(result)\n\n // Clean up data-embed/embed attributes — no longer needed after purging\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n delete el.attribs['data-embed']\n delete el.attribs.embed\n }\n })\n\n return purgedDom\n}\n\n/**\n * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,\n * then checks each selector against the DOM with css-select.\n * Removes rules where no selector matches any element.\n */\nfunction isSafelisted(selector: string, safelist: string[]): boolean {\n return safelist.some((pattern) => {\n if (pattern.startsWith('*') && pattern.endsWith('*')) {\n return selector.includes(pattern.slice(1, -1))\n }\n if (pattern.endsWith('*')) {\n return selector.startsWith(pattern.slice(0, -1))\n }\n if (pattern.startsWith('*')) {\n return selector.endsWith(pattern.slice(1))\n }\n return selector === pattern\n })\n}\n\nfunction deepPurge(dom: ChildNode[], safelist: string[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (el.name !== 'style' || !el.attribs) return\n if ('data-embed' in el.attribs) return\n\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data?.trim()) return\n\n const root = postcss.parse(textNode.data, { parser: safeParser })\n\n root.walkRules((rule) => {\n // Skip rules inside @media or other at-rules — those may target\n // states we can't match statically (hover, responsive, etc.)\n if (rule.parent?.type === 'atrule') return\n\n const selectors = rule.selectors ?? [rule.selector]\n const matched = selectors.filter((sel) => {\n // Keep safelisted selectors\n if (isSafelisted(sel, safelist)) return true\n\n // Skip pseudo-classes/elements that can't be matched statically.\n // Functional pseudos like :not(), :is(), :where(), :has() are\n // matchable by css-select, so we only skip dynamic/state ones.\n if (/::[\\w-]/.test(sel)) return true\n if (/(?<!:):(?!not\\b|is\\b|where\\b|has\\b)[\\w-]/.test(sel.replace(/\\\\./g, ''))) return true\n\n try {\n return selectAll(sel, dom).length > 0\n } catch {\n // If css-select can't parse the selector, keep it\n return true\n }\n })\n\n if (matched.length === 0) {\n rule.remove()\n } else if (matched.length < selectors.length) {\n rule.selectors = matched\n }\n })\n\n // Remove empty at-rules\n root.walkAtRules((atRule) => {\n if (atRule.nodes?.length === 0) {\n atRule.remove()\n }\n })\n\n const purgedCss = root.toString()\n\n if (purgedCss.trim()) {\n textNode.data = purgedCss\n } else {\n // Remove the style tag entirely if empty\n const parent = el.parent\n if (parent && 'children' in parent) {\n const idx = parent.children.indexOf(el as any)\n if (idx !== -1) parent.children.splice(idx, 1)\n }\n }\n })\n\n return dom\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"purgeCSS.mjs","names":["merge"],"sources":["../../src/transformers/purgeCSS.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport { selectAll } from 'css-select'\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_SAFELIST: string[] = [\n '*body*', // Gmail\n '.gmail*', // Gmail\n '.apple*', // Apple Mail\n '.ios*', // Mail on iOS\n '.ox-*', // Open-Xchange\n '.outlook*', // Outlook.com\n '[data-ogs*', // Outlook.com\n '.bloop_container', // Airmail\n '.Singleton', // Apple Mail 10\n '.unused', // Notes 8\n '.moz-text-html', // Thunderbird\n '.mail-detail-content', // Comcast, Libero webmail\n '*edo*', // Edison (all)\n '#*', // Freenet uses #msgBody\n '.lang*', // Fenced code blocks\n]\n\nconst DEFAULT_OPTIONS = {\n backend: [\n { heads: '{{', tails: '}}' },\n { heads: '{%', tails: '%}' },\n ],\n whitelist: [...DEFAULT_SAFELIST],\n}\n\n/**\n * Remove unused CSS transformer.\n *\n * Uses `email-comb` to strip CSS selectors and corresponding class/id\n * references that are not matched anywhere in the HTML body.\n *\n * Enable by setting `css.purge: true` (or passing options).\n * The user-supplied options are merged on top of the defaults, so\n * `safelist` values are **appended** to the built-in safelist rather\n * than replacing it.\n *\n * Accepts `ChildNode[]` as input, serializes internally before passing\n * to email-comb (which requires a raw HTML string), then parses the\n * result back to `ChildNode[]` so it fits in the DOM pipeline.\n */\nexport function purgeCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.purge\n\n if (!option) return dom\n\n const userOptions = typeof option === 'object' ? option : {}\n\n // Merge user options on top of defaults.\n // defu merges objects deeply; for arrays it appends user values.\n // We want the user safelist appended to the default safelist,\n // so we build whitelist manually.\n const userSafelist = Array.isArray((userOptions as any).safelist)\n ? (userOptions as any).safelist as string[]\n : []\n\n const { safelist: _discard, ...restUserOptions } = userOptions as any\n\n const options = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n // Deep purge first: DOM-aware selector removal using PostCSS + css-select.\n // Runs before email-comb so that email-comb can clean up orphaned classes\n // in HTML attributes left behind by removed CSS rules.\n const safelist = [...DEFAULT_SAFELIST, ...userSafelist]\n dom = deepPurge(dom, safelist)\n\n const { result } = comb(serialize(dom), options)\n\n let purgedDom = parse(result)\n\n // Clean up data-embed/embed attributes — no longer needed after purging\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n delete el.attribs['data-embed']\n delete el.attribs.embed\n }\n })\n\n return purgedDom\n}\n\n/**\n * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,\n * then checks each selector against the DOM with css-select.\n * Removes rules where no selector matches any element.\n */\nfunction isSafelisted(selector: string, safelist: string[]): boolean {\n return safelist.some((pattern) => {\n if (pattern.startsWith('*') && pattern.endsWith('*')) {\n return selector.includes(pattern.slice(1, -1))\n }\n if (pattern.endsWith('*')) {\n return selector.startsWith(pattern.slice(0, -1))\n }\n if (pattern.startsWith('*')) {\n return selector.endsWith(pattern.slice(1))\n }\n return selector === pattern\n })\n}\n\nfunction deepPurge(dom: ChildNode[], safelist: string[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (el.name !== 'style' || !el.attribs) return\n if ('data-embed' in el.attribs || 'embed' in el.attribs) return\n\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data?.trim()) return\n\n const root = postcss.parse(textNode.data, { parser: safeParser })\n\n root.walkRules((rule) => {\n // Skip rules inside @media or other at-rules — those may target\n // states we can't match statically (hover, responsive, etc.)\n if (rule.parent?.type === 'atrule') return\n\n const selectors = rule.selectors ?? [rule.selector]\n const matched = selectors.filter((sel) => {\n // Keep safelisted selectors\n if (isSafelisted(sel, safelist)) return true\n\n // Skip pseudo-classes/elements that can't be matched statically.\n // Functional pseudos like :not(), :is(), :where(), :has() are\n // matchable by css-select, so we only skip dynamic/state ones.\n if (/::[\\w-]/.test(sel)) return true\n if (/(?<!:):(?!not\\b|is\\b|where\\b|has\\b)[\\w-]/.test(sel.replace(/\\\\./g, ''))) return true\n\n try {\n return selectAll(sel, dom).length > 0\n } catch {\n // If css-select can't parse the selector, keep it\n return true\n }\n })\n\n if (matched.length === 0) {\n rule.remove()\n } else if (matched.length < selectors.length) {\n rule.selectors = matched\n }\n })\n\n // Remove empty at-rules\n root.walkAtRules((atRule) => {\n if (atRule.nodes?.length === 0) {\n atRule.remove()\n }\n })\n\n const purgedCss = root.toString()\n\n if (purgedCss.trim()) {\n textNode.data = purgedCss\n } else {\n // Remove the style tag entirely if empty\n const parent = el.parent\n if (parent && 'children' in parent) {\n const idx = parent.children.indexOf(el as any)\n if (idx !== -1) parent.children.splice(idx, 1)\n }\n }\n })\n\n return dom\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tailwindcss.d.mts","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"mappings":";;;;;;AAoJA;;;;;;;;;;;;;;iBAAsB,WAAA,CAAY,GAAA,EAAK,SAAA,IAAa,MAAA,EAAQ,aAAA,EAAe,QAAA,YAAoB,OAAA,CAAQ,SAAA"}
1
+ {"version":3,"file":"tailwindcss.d.mts","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"mappings":";;;;;;AAqIA;;;;;;;;;;;;;;iBAAsB,WAAA,CAAY,GAAA,EAAK,SAAA,IAAa,MAAA,EAAQ,aAAA,EAAe,QAAA,YAAoB,OAAA,CAAQ,SAAA"}
@@ -4,6 +4,8 @@ import resolveProps_default from "../plugins/postcss/resolveProps.mjs";
4
4
  import pruneVars_default from "../plugins/postcss/pruneVars.mjs";
5
5
  import { tailwindCleanup } from "../plugins/postcss/tailwindCleanup.mjs";
6
6
  import { mergeMediaQueries } from "../plugins/postcss/mergeMediaQueries.mjs";
7
+ import { quoteFontFamilies } from "../plugins/postcss/quoteFontFamilies.mjs";
8
+ import { decodeStyleEntities } from "../utils/decodeStyleEntities.mjs";
7
9
  import { dirname, relative, resolve } from "node:path";
8
10
  import postcss from "postcss";
9
11
  import tailwindcssPostcss from "@tailwindcss/postcss";
@@ -25,16 +27,6 @@ function createProcessor(config) {
25
27
  ]);
26
28
  }
27
29
  /**
28
- * Decode HTML entities that Vue SSR encodes inside <style> tags.
29
- *
30
- * Vue's renderToString HTML-encodes quotes and other characters
31
- * inside <style> tags within templates, breaking CSS like
32
- * `@import "@maizzle/tailwindcss"` → `@import &quot;...&quot;`
33
- */
34
- function decodeEntities(str) {
35
- return str.replace(/&quot;/g, "\"").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#39;/g, "'").replace(/&apos;/g, "'");
36
- }
37
- /**
38
30
  * Check if CSS content uses Tailwind features that require source scanning.
39
31
  *
40
32
  * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source
@@ -42,7 +34,7 @@ function decodeEntities(str) {
42
34
  * and would pass through @source directives unconsumed.
43
35
  */
44
36
  function usesTailwind(css) {
45
- return /(@import\s+["'](tailwindcss|@maizzle\/tailwindcss)|@tailwind\s)/.test(css);
37
+ return /((@import|@reference)\s+["'](tailwindcss|@maizzle\/tailwindcss)|@tailwind\s)/.test(css);
46
38
  }
47
39
  /**
48
40
  * Lower modern CSS syntax using lightningcss.
@@ -66,7 +58,7 @@ function lowerSyntax(css) {
66
58
  * then sorts and merges media queries.
67
59
  */
68
60
  async function optimizeCss(css, config) {
69
- const plugins = [...tailwindCleanup(config)];
61
+ const plugins = [...tailwindCleanup(config), quoteFontFamilies()];
70
62
  const mediaPlugin = mergeMediaQueries(config);
71
63
  if (mediaPlugin) plugins.push(mediaPlugin);
72
64
  return (await postcss(plugins).process(css, { from: void 0 })).css;
@@ -114,17 +106,15 @@ async function tailwindcss(dom, config, filePath) {
114
106
  walk(dom, (node) => {
115
107
  if (node.name !== "style") return;
116
108
  const el = node;
117
- const attrs = el.attribs || {};
118
- if ("raw" in attrs) {
109
+ if ("raw" in (el.attribs || {})) {
119
110
  delete el.attribs.raw;
120
111
  return;
121
112
  }
122
- if ("embed" in attrs || "data-embed" in attrs) return;
123
113
  const rawContent = el.children.filter((child) => child.type === "text").map((child) => child.data).join("");
124
114
  if (!rawContent.trim()) return;
125
115
  styleTags.push({
126
116
  node: el,
127
- cssContent: decodeEntities(rawContent)
117
+ cssContent: decodeStyleEntities(rawContent)
128
118
  });
129
119
  });
130
120
  if (!styleTags.length) return dom;