@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.7

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 (63) hide show
  1. package/dist/components/CodeBlock.vue +12 -19
  2. package/dist/components/Markdown.vue +70 -0
  3. package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
  4. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  5. package/dist/render/createRenderer.d.mts +2 -3
  6. package/dist/render/createRenderer.d.mts.map +1 -1
  7. package/dist/render/createRenderer.mjs +55 -4
  8. package/dist/render/createRenderer.mjs.map +1 -1
  9. package/dist/serve.d.mts.map +1 -1
  10. package/dist/serve.mjs +83 -3
  11. package/dist/serve.mjs.map +1 -1
  12. package/dist/server/compatibility.d.mts +1 -2
  13. package/dist/server/compatibility.d.mts.map +1 -1
  14. package/dist/server/compatibility.mjs +15 -15
  15. package/dist/server/compatibility.mjs.map +1 -1
  16. package/dist/server/email.d.mts +17 -0
  17. package/dist/server/email.d.mts.map +1 -0
  18. package/dist/server/email.mjs +40 -0
  19. package/dist/server/email.mjs.map +1 -0
  20. package/dist/server/ui/App.vue +204 -68
  21. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  22. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  23. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  24. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  25. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  26. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  27. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  28. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  29. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  30. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  31. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  32. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  33. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  34. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  35. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  36. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  37. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  38. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  39. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  40. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  41. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  42. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  43. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  44. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  45. package/dist/server/ui/main.css +20 -20
  46. package/dist/server/ui/pages/Home.vue +12 -5
  47. package/dist/server/ui/pages/Preview.vue +369 -150
  48. package/dist/transformers/inlineCSS.mjs +9 -0
  49. package/dist/transformers/inlineCSS.mjs.map +1 -1
  50. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  51. package/dist/transformers/purgeCSS.mjs +67 -1
  52. package/dist/transformers/purgeCSS.mjs.map +1 -1
  53. package/dist/transformers/tailwindcss.mjs +3 -7
  54. package/dist/transformers/tailwindcss.mjs.map +1 -1
  55. package/dist/types/config.d.mts +38 -4
  56. package/dist/types/config.d.mts.map +1 -1
  57. package/dist/types/index.d.mts +2 -2
  58. package/package.json +7 -3
  59. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  60. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  61. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  62. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
  63. /package/dist/components/{Preview.vue → Preheader.vue} +0 -0
@@ -45,6 +45,7 @@ function inlineCSS(dom, config = {}) {
45
45
  if (el.name === "style" && el.attribs) {
46
46
  if (el.attribs.embed && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
47
47
  if (el.attribs["data-embed"] && !("embed" in el.attribs)) el.attribs.embed = "";
48
+ if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = "";
48
49
  }
49
50
  });
50
51
  const serialized = serialize(dom);
@@ -71,6 +72,14 @@ function inlineCSS(dom, config = {}) {
71
72
  const el = node;
72
73
  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");
73
74
  });
75
+ walk(result, (node) => {
76
+ const el = node;
77
+ if (el.name === "style" && el.attribs && "data-maizzle-embed" in el.attribs) {
78
+ el.attribs["data-embed"] = "";
79
+ el.attribs.embed = "";
80
+ delete el.attribs["data-maizzle-embed"];
81
+ }
82
+ });
74
83
  return result;
75
84
  }
76
85
 
@@ -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\ninterface InlineCssOptions {\n removeStyleTags?: boolean\n removeInlinedSelectors?: boolean\n preferUnitlessValues?: boolean\n safelist?: string[]\n styleToAttribute?: Record<string, string>\n applyWidthAttributes?: boolean\n applyHeightAttributes?: boolean\n widthElements?: string[]\n heightElements?: string[]\n excludedProperties?: string[]\n codeBlocks?: Record<string, { start: string; end: string }>\n customCSS?: string\n}\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 *\n * Options:\n * - removeStyleTags: Remove style tags after inlining (default: false)\n * - removeInlinedSelectors: Remove classes after they've been inlined (default: true)\n * - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)\n * - safelist: Selectors that should not be removed after inlining\n * - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)\n * - applyWidthAttributes: Add width attributes based on inline CSS (default: true)\n * - applyHeightAttributes: Add height attributes based on inline CSS (default: true)\n * - widthElements: Elements that can receive width attributes (default: ['img', 'video'])\n * - heightElements: Elements that can receive height attributes (default: ['img', 'video'])\n * - excludedProperties: CSS properties to exclude from inlining\n * - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })\n * - customCSS: Additional CSS to inline\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: InlineCssOptions = typeof inline === 'object' ? inline : {}\n\n const removeStyleTags = options.removeStyleTags ?? false\n const customCSS = options.customCSS ?? ''\n\n // Configure Juice static properties\n juice.styleToAttribute = options.styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(options.excludedProperties ?? [])]\n juice.widthElements = (options.widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (options.heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (options.codeBlocks && typeof options.codeBlocks === 'object') {\n Object.entries(options.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 walk(dom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n // Add data-embed to style tags with embed attribute\n if (el.attribs.embed && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n // Add embed to style tags with data-embed attribute\n if (el.attribs['data-embed'] && !('embed' in el.attribs)) {\n el.attribs.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,\n removeInlinedSelectors: options.removeInlinedSelectors ?? true,\n preservedSelectors: options.safelist ?? [],\n applyWidthAttributes: options.applyWidthAttributes ?? true,\n applyHeightAttributes: options.applyHeightAttributes ?? true,\n }\n\n if (customCSS) {\n inlinedHtml = juice(serialized, { ...juiceOptions, extraCss: customCSS })\n } else {\n inlinedHtml = juice(serialized, juiceOptions)\n }\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n // Post-process for preferUnitlessValues\n const preferUnitlessValues = options.preferUnitlessValues ?? true\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 return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAIT,MAAM,UAA4B,OAAO,WAAW,WAAW,SAAS,EAAE;CAE1E,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,YAAY,QAAQ,aAAa;AAGvC,OAAM,mBAAmB,QAAQ,oBAAoB,EAAE;AACvD,OAAM,qBAAqB,CAAC,eAAe,GAAI,QAAQ,sBAAsB,EAAE,CAAE;AACjF,OAAM,iBAAiB,QAAQ,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAC3F,OAAM,kBAAkB,QAAQ,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAG7F,KAAI,QAAQ,cAAc,OAAO,QAAQ,eAAe,SACtD,QAAO,QAAQ,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AAC3D,MAAI,MAAM,SAAS,MAAM,IACvB,OAAM,WAAW,OAAO;GAE1B;AAIJ,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;AAG7B,OAAI,GAAG,QAAQ,iBAAiB,EAAE,WAAW,GAAG,SAC9C,IAAG,QAAQ,QAAQ;;GAGvB;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;AAEJ,KAAI;EACF,MAAM,eAA6B;GACjC;GACA,wBAAwB,QAAQ,0BAA0B;GAC1D,oBAAoB,QAAQ,YAAY,EAAE;GAC1C,sBAAsB,QAAQ,wBAAwB;GACtD,uBAAuB,QAAQ,yBAAyB;GACzD;AAED,MAAI,UACF,eAAc,MAAM,YAAY;GAAE,GAAG;GAAc,UAAU;GAAW,CAAC;MAEzE,eAAc,MAAM,YAAY,aAAa;SAEzC;AAEN,SAAO;;CAIT,MAAM,uBAAuB,QAAQ,wBAAwB;CAC7D,MAAM,SAAS,MAAM,YAAY;AAEjC,KAAI,qBACF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,MACd,IAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAClC,4DACA,IACD;GAEH;AAGJ,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\ninterface InlineCssOptions {\n removeStyleTags?: boolean\n removeInlinedSelectors?: boolean\n preferUnitlessValues?: boolean\n safelist?: string[]\n styleToAttribute?: Record<string, string>\n applyWidthAttributes?: boolean\n applyHeightAttributes?: boolean\n widthElements?: string[]\n heightElements?: string[]\n excludedProperties?: string[]\n codeBlocks?: Record<string, { start: string; end: string }>\n customCSS?: string\n}\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 *\n * Options:\n * - removeStyleTags: Remove style tags after inlining (default: false)\n * - removeInlinedSelectors: Remove classes after they've been inlined (default: true)\n * - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)\n * - safelist: Selectors that should not be removed after inlining\n * - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)\n * - applyWidthAttributes: Add width attributes based on inline CSS (default: true)\n * - applyHeightAttributes: Add height attributes based on inline CSS (default: true)\n * - widthElements: Elements that can receive width attributes (default: ['img', 'video'])\n * - heightElements: Elements that can receive height attributes (default: ['img', 'video'])\n * - excludedProperties: CSS properties to exclude from inlining\n * - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })\n * - customCSS: Additional CSS to inline\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: InlineCssOptions = typeof inline === 'object' ? inline : {}\n\n const removeStyleTags = options.removeStyleTags ?? false\n const customCSS = options.customCSS ?? ''\n\n // Configure Juice static properties\n juice.styleToAttribute = options.styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(options.excludedProperties ?? [])]\n juice.widthElements = (options.widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (options.heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (options.codeBlocks && typeof options.codeBlocks === 'object') {\n Object.entries(options.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,\n removeInlinedSelectors: options.removeInlinedSelectors ?? true,\n preservedSelectors: options.safelist ?? [],\n applyWidthAttributes: options.applyWidthAttributes ?? true,\n applyHeightAttributes: options.applyHeightAttributes ?? true,\n }\n\n if (customCSS) {\n inlinedHtml = juice(serialized, { ...juiceOptions, extraCss: customCSS })\n } else {\n inlinedHtml = juice(serialized, juiceOptions)\n }\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n // Post-process for preferUnitlessValues\n const preferUnitlessValues = options.preferUnitlessValues ?? true\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 // 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAIT,MAAM,UAA4B,OAAO,WAAW,WAAW,SAAS,EAAE;CAE1E,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,YAAY,QAAQ,aAAa;AAGvC,OAAM,mBAAmB,QAAQ,oBAAoB,EAAE;AACvD,OAAM,qBAAqB,CAAC,eAAe,GAAI,QAAQ,sBAAsB,EAAE,CAAE;AACjF,OAAM,iBAAiB,QAAQ,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAC3F,OAAM,kBAAkB,QAAQ,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAG7F,KAAI,QAAQ,cAAc,OAAO,QAAQ,eAAe,SACtD,QAAO,QAAQ,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AAC3D,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;EACF,MAAM,eAA6B;GACjC;GACA,wBAAwB,QAAQ,0BAA0B;GAC1D,oBAAoB,QAAQ,YAAY,EAAE;GAC1C,sBAAsB,QAAQ,wBAAwB;GACtD,uBAAuB,QAAQ,yBAAyB;GACzD;AAED,MAAI,UACF,eAAc,MAAM,YAAY;GAAE,GAAG;GAAc,UAAU;GAAW,CAAC;MAEzE,eAAc,MAAM,YAAY,aAAa;SAEzC;AAEN,SAAO;;CAIT,MAAM,uBAAuB,QAAQ,wBAAwB;CAC7D,MAAM,SAAS,MAAM,YAAY;AAEjC,KAAI,qBACF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,MACd,IAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAClC,4DACA,IACD;GAEH;AAKJ,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 +1 @@
1
- {"version":3,"file":"purgeCSS.d.mts","names":[],"sources":["../../src/transformers/purgeCSS.ts"],"mappings":";;;;;;AA+CA;;;;;;;;;;;;;iBAAgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
1
+ {"version":3,"file":"purgeCSS.d.mts","names":[],"sources":["../../src/transformers/purgeCSS.ts"],"mappings":";;;;;;AAkDA;;;;;;;;;;;;;iBAAgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
@@ -1,7 +1,11 @@
1
1
  import { parse } from "../utils/ast/parser.mjs";
2
+ import { walk } from "../utils/ast/walker.mjs";
2
3
  import { serialize } from "../utils/ast/serializer.mjs";
3
4
  import "../utils/ast/index.mjs";
4
5
  import { defu } from "defu";
6
+ import postcss from "postcss";
7
+ import safeParser from "postcss-safe-parser";
8
+ import { selectAll } from "css-select";
5
9
  import { comb } from "email-comb";
6
10
 
7
11
  //#region src/transformers/purgeCSS.ts
@@ -57,8 +61,70 @@ function purgeCSS(dom, config = {}) {
57
61
  ...restUserOptions,
58
62
  whitelist: [...DEFAULT_SAFELIST, ...userSafelist]
59
63
  }, DEFAULT_OPTIONS);
64
+ const safelist = [...DEFAULT_SAFELIST, ...userSafelist];
65
+ dom = deepPurge(dom, safelist);
60
66
  const { result } = comb(serialize(dom), options);
61
- return parse(result);
67
+ let purgedDom = parse(result);
68
+ walk(purgedDom, (node) => {
69
+ const el = node;
70
+ if (el.name === "style" && el.attribs) {
71
+ delete el.attribs["data-embed"];
72
+ delete el.attribs.embed;
73
+ }
74
+ });
75
+ return purgedDom;
76
+ }
77
+ /**
78
+ * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,
79
+ * then checks each selector against the DOM with css-select.
80
+ * Removes rules where no selector matches any element.
81
+ */
82
+ function isSafelisted(selector, safelist) {
83
+ return safelist.some((pattern) => {
84
+ if (pattern.startsWith("*") && pattern.endsWith("*")) return selector.includes(pattern.slice(1, -1));
85
+ if (pattern.endsWith("*")) return selector.startsWith(pattern.slice(0, -1));
86
+ if (pattern.startsWith("*")) return selector.endsWith(pattern.slice(1));
87
+ return selector === pattern;
88
+ });
89
+ }
90
+ function deepPurge(dom, safelist) {
91
+ walk(dom, (node) => {
92
+ const el = node;
93
+ if (el.name !== "style" || !el.attribs) return;
94
+ if ("data-embed" in el.attribs) return;
95
+ const textNode = el.children?.find((c) => c.type === "text");
96
+ if (!textNode?.data?.trim()) return;
97
+ const root = postcss.parse(textNode.data, { parser: safeParser });
98
+ root.walkRules((rule) => {
99
+ if (rule.parent?.type === "atrule") return;
100
+ const selectors = rule.selectors ?? [rule.selector];
101
+ const matched = selectors.filter((sel) => {
102
+ if (isSafelisted(sel, safelist)) return true;
103
+ if (/::[\w-]/.test(sel)) return true;
104
+ if (/(?<!:):(?!not\b|is\b|where\b|has\b)[\w-]/.test(sel.replace(/\\./g, ""))) return true;
105
+ try {
106
+ return selectAll(sel, dom).length > 0;
107
+ } catch {
108
+ return true;
109
+ }
110
+ });
111
+ if (matched.length === 0) rule.remove();
112
+ else if (matched.length < selectors.length) rule.selectors = matched;
113
+ });
114
+ root.walkAtRules((atRule) => {
115
+ if (atRule.nodes?.length === 0) atRule.remove();
116
+ });
117
+ const purgedCss = root.toString();
118
+ if (purgedCss.trim()) textNode.data = purgedCss;
119
+ else {
120
+ const parent = el.parent;
121
+ if (parent && "children" in parent) {
122
+ const idx = parent.children.indexOf(el);
123
+ if (idx !== -1) parent.children.splice(idx, 1);
124
+ }
125
+ }
126
+ });
127
+ return dom;
62
128
  }
63
129
 
64
130
  //#endregion
@@ -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 type { ChildNode } from 'domhandler'\nimport { parse, serialize } 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 const { result } = comb(serialize(dom), options)\n\n return parse(result)\n}\n"],"mappings":";;;;;;;AAMA,MAAM,mBAA6B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,kBAAkB;CACtB,SAAS,CACP;EAAE,OAAO;EAAM,OAAO;EAAM,EAC5B;EAAE,OAAO;EAAM,OAAO;EAAM,CAC7B;CACD,WAAW,CAAC,GAAG,iBAAiB;CACjC;;;;;;;;;;;;;;;;AAiBD,SAAgB,SAAS,KAAkB,SAAoB,EAAE,EAAe;CAC9E,MAAM,SAAS,OAAO;AAEtB,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,cAAc,OAAO,WAAW,WAAW,SAAS,EAAE;CAM5D,MAAM,eAAe,MAAM,QAAS,YAAoB,SAAS,GAC5D,YAAoB,WACrB,EAAE;CAEN,MAAM,EAAE,UAAU,UAAU,GAAG,oBAAoB;CAEnD,MAAM,UAAUA,KACd;EAAE,GAAG;EAAiB,WAAW,CAAC,GAAG,kBAAkB,GAAG,aAAa;EAAE,EACzE,gBACD;CAED,MAAM,EAAE,WAAW,KAAK,UAAU,IAAI,EAAE,QAAQ;AAEhD,QAAO,MAAM,OAAO"}
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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"}
@@ -115,15 +115,11 @@ async function tailwindcss(dom, config, filePath) {
115
115
  if (node.name !== "style") return;
116
116
  const el = node;
117
117
  const attrs = el.attribs || {};
118
- const markerAttr = [
119
- "raw",
120
- "embed",
121
- "data-embed"
122
- ].find((attr) => attr in attrs);
123
- if (markerAttr) {
124
- delete el.attribs[markerAttr];
118
+ if ("raw" in attrs) {
119
+ delete el.attribs.raw;
125
120
  return;
126
121
  }
122
+ if ("embed" in attrs || "data-embed" in attrs) return;
127
123
  const rawContent = el.children.filter((child) => child.type === "text").map((child) => child.data).join("");
128
124
  if (!rawContent.trim()) return;
129
125
  styleTags.push({
@@ -1 +1 @@
1
- {"version":3,"file":"tailwindcss.mjs","names":["pruneVars"],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport tailwindcssPostcss from '@tailwindcss/postcss'\nimport customProperties from 'postcss-custom-properties'\nimport postcssCalc from 'postcss-calc'\nimport pruneVars from '../plugins/postcss/pruneVars.ts'\nimport safeParser from 'postcss-safe-parser'\nimport { transform } from 'lightningcss'\nimport { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { tailwindCleanup } from '../plugins/postcss/tailwindCleanup.ts'\nimport { mergeMediaQueries } from '../plugins/postcss/mergeMediaQueries.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nfunction createProcessor(config: MaizzleConfig) {\n return postcss([\n tailwindcssPostcss({\n base: config.css?.base,\n transformAssetUrls: false,\n optimize: false, // we run Lightning CSS manually\n }),\n customProperties({\n preserve: false,\n }),\n postcssCalc({}),\n pruneVars(),\n ])\n}\n\n/**\n * Decode HTML entities that Vue SSR encodes inside <style> tags.\n *\n * Vue's renderToString HTML-encodes quotes and other characters\n * inside <style> tags within templates, breaking CSS like\n * `@import \"@maizzle/tailwindcss\"` → `@import &quot;...&quot;`\n */\nfunction decodeEntities(str: string): string {\n return str\n .replace(/&quot;/g, '\"')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n}\n\n/**\n * Check if CSS content uses Tailwind features that require source scanning.\n *\n * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source\n * directives. Plain CSS without Tailwind imports doesn't need scanning\n * and would pass through @source directives unconsumed.\n */\nfunction usesTailwind(css: string): boolean {\n return /(@import\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Lower modern CSS syntax using lightningcss.\n *\n * Targets IE 1 to maximize syntax lowering — converts modern features\n * like nesting, oklch(), color-mix(), @property, etc. into simple CSS\n * that email clients can understand.\n */\nfunction lowerSyntax(css: string): string {\n const result = transform({\n filename: 'email.css',\n code: Buffer.from(css),\n minify: false,\n targets: {\n ie: 4 << 5,\n },\n })\n\n return result.code.toString()\n}\n\n/**\n * Run cleanup and media query merging on the compiled CSS.\n *\n * Removes unwanted selectors (:host, :lang) and at-rules (@layer, @property),\n * then sorts and merges media queries.\n */\nasync function optimizeCss(css: string, config: MaizzleConfig): Promise<string> {\n const plugins: postcss.Plugin[] = [...tailwindCleanup(config)]\n\n const mediaPlugin = mergeMediaQueries(config)\n if (mediaPlugin) plugins.push(mediaPlugin)\n\n const result = await postcss(plugins).process(css, { from: undefined })\n\n return result.css\n}\n\n/**\n * Build @source directives for Tailwind CSS scanning.\n *\n * Configures two types of sources:\n * 1. Exclusions for output dir and user-configured paths\n * 2. Inline source with all class attribute values from the rendered DOM,\n * capturing classes from all components (built-in + user), dynamic\n * expressions, and the template itself — Tailwind's scanner handles\n * the actual class extraction from these raw values\n */\nfunction buildSourceDirectives(dom: ChildNode[], config: MaizzleConfig, fromDir: string): string {\n const directives: string[] = []\n\n // Exclude output dir and user-configured paths\n const excludePaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\n for (const p of excludePaths) {\n directives.push(`@source not \"${relative(fromDir, resolve(p))}\";`)\n }\n\n // Inline source: collect all class attribute values from the rendered DOM.\n // After Vue SSR, the DOM contains every class from every component\n // (built-in framework components, user components, dynamic bindings).\n // We pass these raw values to Tailwind's scanner via @source inline().\n const classes: string[] = []\n walk(dom, (n) => {\n const cls = (n as Element).attribs?.class\n if (cls) classes.push(cls)\n })\n\n if (classes.length) {\n directives.push(`@source inline(\"${classes.join(' ')}\");`)\n }\n\n return directives.join('\\n')\n}\n\n/**\n * Tailwind CSS transformer.\n *\n * Compiles CSS inside <style> tags in the DOM using\n * @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.\n *\n * Configures Tailwind sources to scan:\n * - Rendered class attributes (via `@source inline`) for all classes from all components\n * - User project files (via Tailwind's auto-detection from base/from path)\n *\n * User `@source` and `@source not directives` in style tags are preserved.\n * Source directives are only added to style tags that import Tailwind.\n *\n * Runs as the first transformer in the pipeline so that subsequent\n * transformers (inliner, purge, etc.) work with fully compiled CSS.\n */\nexport async function tailwindcss(dom: ChildNode[], config: MaizzleConfig, filePath?: string): Promise<ChildNode[]> {\n const styleTags: { node: Element; cssContent: string }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'style') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n // Skip marked style tags, but remove the marker attribute first\n const markerAttr = ['raw', 'embed', 'data-embed'].find(attr => attr in attrs)\n if (markerAttr) {\n delete el.attribs[markerAttr]\n return\n }\n\n // Get text content from children and decode HTML entities\n const rawContent = el.children\n .filter(child => child.type === 'text')\n .map(child => (child as any).data)\n .join('')\n\n if (!rawContent.trim()) return\n\n styleTags.push({ node: el, cssContent: decodeEntities(rawContent) })\n })\n\n if (!styleTags.length) return dom\n\n const fromPath = filePath ?? resolve(process.cwd(), 'template.vue')\n const fromDir = dirname(fromPath)\n\n // Only compute source directives if at least one style tag uses Tailwind\n const hasTailwindStyles = styleTags.some(({ cssContent }) => usesTailwind(cssContent))\n const sourceDirectives = hasTailwindStyles\n ? buildSourceDirectives(dom, config, fromDir)\n : ''\n\n // Create processor once — reused for all style tags in this template\n const processor = createProcessor(config)\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n\n // Only add source directives to style tags that import Tailwind —\n // plain CSS doesn't need them and @tailwindcss/postcss would leave\n // the directives unconsumed in the output\n const fullCss = usesTailwind(cssContent)\n ? `${cssContent}\\n${sourceDirectives}`\n : cssContent\n\n try {\n const result = await processor.process(\n fullCss,\n {\n from: `${fromPath}?style=${i}`,\n parser: safeParser,\n }\n )\n\n const lowered = lowerSyntax(result.css)\n const optimized = await optimizeCss(lowered, config)\n\n // Replace the style tag's children with the compiled CSS\n node.children = [{\n type: 'text',\n data: optimized,\n parent: node,\n } as any]\n } catch {\n // If CSS processing fails, still replace with decoded content\n // so HTML entities don't break the CSS\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tailwindcss.mjs","names":["pruneVars"],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport tailwindcssPostcss from '@tailwindcss/postcss'\nimport customProperties from 'postcss-custom-properties'\nimport postcssCalc from 'postcss-calc'\nimport pruneVars from '../plugins/postcss/pruneVars.ts'\nimport safeParser from 'postcss-safe-parser'\nimport { transform } from 'lightningcss'\nimport { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { tailwindCleanup } from '../plugins/postcss/tailwindCleanup.ts'\nimport { mergeMediaQueries } from '../plugins/postcss/mergeMediaQueries.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nfunction createProcessor(config: MaizzleConfig) {\n return postcss([\n tailwindcssPostcss({\n base: config.css?.base,\n transformAssetUrls: false,\n optimize: false, // we run Lightning CSS manually\n }),\n customProperties({\n preserve: false,\n }),\n postcssCalc({}),\n pruneVars(),\n ])\n}\n\n/**\n * Decode HTML entities that Vue SSR encodes inside <style> tags.\n *\n * Vue's renderToString HTML-encodes quotes and other characters\n * inside <style> tags within templates, breaking CSS like\n * `@import \"@maizzle/tailwindcss\"` → `@import &quot;...&quot;`\n */\nfunction decodeEntities(str: string): string {\n return str\n .replace(/&quot;/g, '\"')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n}\n\n/**\n * Check if CSS content uses Tailwind features that require source scanning.\n *\n * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source\n * directives. Plain CSS without Tailwind imports doesn't need scanning\n * and would pass through @source directives unconsumed.\n */\nfunction usesTailwind(css: string): boolean {\n return /(@import\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Lower modern CSS syntax using lightningcss.\n *\n * Targets IE 1 to maximize syntax lowering — converts modern features\n * like nesting, oklch(), color-mix(), @property, etc. into simple CSS\n * that email clients can understand.\n */\nfunction lowerSyntax(css: string): string {\n const result = transform({\n filename: 'email.css',\n code: Buffer.from(css),\n minify: false,\n targets: {\n ie: 4 << 5,\n },\n })\n\n return result.code.toString()\n}\n\n/**\n * Run cleanup and media query merging on the compiled CSS.\n *\n * Removes unwanted selectors (:host, :lang) and at-rules (@layer, @property),\n * then sorts and merges media queries.\n */\nasync function optimizeCss(css: string, config: MaizzleConfig): Promise<string> {\n const plugins: postcss.Plugin[] = [...tailwindCleanup(config)]\n\n const mediaPlugin = mergeMediaQueries(config)\n if (mediaPlugin) plugins.push(mediaPlugin)\n\n const result = await postcss(plugins).process(css, { from: undefined })\n\n return result.css\n}\n\n/**\n * Build @source directives for Tailwind CSS scanning.\n *\n * Configures two types of sources:\n * 1. Exclusions for output dir and user-configured paths\n * 2. Inline source with all class attribute values from the rendered DOM,\n * capturing classes from all components (built-in + user), dynamic\n * expressions, and the template itself — Tailwind's scanner handles\n * the actual class extraction from these raw values\n */\nfunction buildSourceDirectives(dom: ChildNode[], config: MaizzleConfig, fromDir: string): string {\n const directives: string[] = []\n\n // Exclude output dir and user-configured paths\n const excludePaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\n for (const p of excludePaths) {\n directives.push(`@source not \"${relative(fromDir, resolve(p))}\";`)\n }\n\n // Inline source: collect all class attribute values from the rendered DOM.\n // After Vue SSR, the DOM contains every class from every component\n // (built-in framework components, user components, dynamic bindings).\n // We pass these raw values to Tailwind's scanner via @source inline().\n const classes: string[] = []\n walk(dom, (n) => {\n const cls = (n as Element).attribs?.class\n if (cls) classes.push(cls)\n })\n\n if (classes.length) {\n directives.push(`@source inline(\"${classes.join(' ')}\");`)\n }\n\n return directives.join('\\n')\n}\n\n/**\n * Tailwind CSS transformer.\n *\n * Compiles CSS inside <style> tags in the DOM using\n * @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.\n *\n * Configures Tailwind sources to scan:\n * - Rendered class attributes (via `@source inline`) for all classes from all components\n * - User project files (via Tailwind's auto-detection from base/from path)\n *\n * User `@source` and `@source not directives` in style tags are preserved.\n * Source directives are only added to style tags that import Tailwind.\n *\n * Runs as the first transformer in the pipeline so that subsequent\n * transformers (inliner, purge, etc.) work with fully compiled CSS.\n */\nexport async function tailwindcss(dom: ChildNode[], config: MaizzleConfig, filePath?: string): Promise<ChildNode[]> {\n const styleTags: { node: Element; cssContent: string }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'style') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n // Skip marked style tags\n // Remove 'raw' marker but preserve 'embed'/'data-embed' for Juice\n if ('raw' in attrs) {\n delete el.attribs.raw\n return\n }\n\n if ('embed' in attrs || 'data-embed' in attrs) {\n return\n }\n\n // Get text content from children and decode HTML entities\n const rawContent = el.children\n .filter(child => child.type === 'text')\n .map(child => (child as any).data)\n .join('')\n\n if (!rawContent.trim()) return\n\n styleTags.push({ node: el, cssContent: decodeEntities(rawContent) })\n })\n\n if (!styleTags.length) return dom\n\n const fromPath = filePath ?? resolve(process.cwd(), 'template.vue')\n const fromDir = dirname(fromPath)\n\n // Only compute source directives if at least one style tag uses Tailwind\n const hasTailwindStyles = styleTags.some(({ cssContent }) => usesTailwind(cssContent))\n const sourceDirectives = hasTailwindStyles\n ? buildSourceDirectives(dom, config, fromDir)\n : ''\n\n // Create processor once — reused for all style tags in this template\n const processor = createProcessor(config)\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n\n // Only add source directives to style tags that import Tailwind —\n // plain CSS doesn't need them and @tailwindcss/postcss would leave\n // the directives unconsumed in the output\n const fullCss = usesTailwind(cssContent)\n ? `${cssContent}\\n${sourceDirectives}`\n : cssContent\n\n try {\n const result = await processor.process(\n fullCss,\n {\n from: `${fromPath}?style=${i}`,\n parser: safeParser,\n }\n )\n\n const lowered = lowerSyntax(result.css)\n const optimized = await optimizeCss(lowered, config)\n\n // Replace the style tag's children with the compiled CSS\n node.children = [{\n type: 'text',\n data: optimized,\n parent: node,\n } as any]\n } catch {\n // If CSS processing fails, still replace with decoded content\n // so HTML entities don't break the CSS\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { RemoveValue } from "../plugins/postcss/removeDeclarations.mjs";
2
2
  import * as oxfmt from "oxfmt";
3
- import * as unplugin_vue_markdown_types0 from "unplugin-vue-markdown/types";
3
+ import * as shiki from "shiki";
4
+ import { Options } from "unplugin-vue-markdown/types";
4
5
 
5
6
  //#region src/types/config.d.ts
6
7
  interface UrlQueryOptions {
@@ -336,6 +337,14 @@ interface HtmlConfig {
336
337
  }
337
338
  type FilterFunction = (str: string, value: string) => string;
338
339
  type FiltersConfig = false | Record<string, FilterFunction>;
340
+ interface MarkdownConfig extends Options {
341
+ /**
342
+ * The shiki theme to use for syntax highlighting in Markdown fenced code blocks.
343
+ *
344
+ * @default 'github-light'
345
+ */
346
+ shikiTheme?: shiki.BundledTheme;
347
+ }
339
348
  interface MaizzleConfig {
340
349
  /**
341
350
  * Root directory for the Maizzle email project.
@@ -352,8 +361,8 @@ interface MaizzleConfig {
352
361
  * })
353
362
  */
354
363
  root?: string;
355
- /** Options passed to `unplugin-vue-markdown` for Markdown template support. */
356
- markdown?: unplugin_vue_markdown_types0.Options;
364
+ /** Options for Markdown template support, extending `unplugin-vue-markdown`. */
365
+ markdown?: MarkdownConfig;
357
366
  /**
358
367
  * Glob patterns for email template files to process.
359
368
  *
@@ -431,6 +440,31 @@ interface MaizzleConfig {
431
440
  * }
432
441
  */
433
442
  watch?: string[];
443
+ /**
444
+ * Email sending configuration for the "Send test" feature in the dev UI.
445
+ *
446
+ * When not configured, falls back to Ethereal (free fake SMTP — emails
447
+ * are captured and viewable via a URL, never actually delivered).
448
+ *
449
+ * @example
450
+ * server: {
451
+ * email: {
452
+ * to: ['test@example.com'],
453
+ * from: 'dev@maizzle.test',
454
+ * transport: {
455
+ * host: 'smtp.mailtrap.io',
456
+ * port: 587,
457
+ * auth: { user: '...', pass: '...' },
458
+ * },
459
+ * },
460
+ * }
461
+ */
462
+ email?: {
463
+ /** Default recipient(s). */to?: string | string[]; /** Sender address. @default 'Maizzle <maizzle@ethereal.email>' */
464
+ from?: string; /** Default subject line. */
465
+ subject?: string; /** Nodemailer transport options (SMTP, SES, etc.). Omit to use Ethereal. */
466
+ transport?: Record<string, unknown>;
467
+ };
434
468
  };
435
469
  /** Tailwind CSS and email CSS optimization settings. */
436
470
  css?: CssConfig;
@@ -504,5 +538,5 @@ interface MaizzleConfig {
504
538
  [key: string]: any;
505
539
  }
506
540
  //#endregion
507
- export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
541
+ export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
508
542
  //# sourceMappingURL=config.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":"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"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
2
- export { type AttributesConfig, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
1
+ import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
2
+ export { type AttributesConfig, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type MarkdownConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-rc.6",
3
+ "version": "6.0.0-rc.7",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,6 @@
33
33
  "maizzle"
34
34
  ],
35
35
  "dependencies": {
36
- "maizzle": "latest",
37
36
  "@tailwindcss/postcss": "^4.1.18",
38
37
  "@tailwindcss/vite": "^4.1.18",
39
38
  "@unhead/vue": "^2.1.4",
@@ -49,11 +48,15 @@
49
48
  "domhandler": "^5.0.3",
50
49
  "email-comb": "^7.1.3",
51
50
  "html-crush": "^6.1.3",
51
+ "html-to-image": "^1.11.13",
52
52
  "htmlparser2": "^10.1.0",
53
53
  "is-url-superb": "^6.1.0",
54
54
  "jiti": "^2.6.1",
55
55
  "juice": "^11.1.1",
56
56
  "lucide-vue-next": "^1.0.0",
57
+ "maizzle": "latest",
58
+ "markdown-exit": "^1.0.0-beta.9",
59
+ "nodemailer": "^8.0.5",
57
60
  "ora": "^9.3.0",
58
61
  "oxfmt": "^0.35.0",
59
62
  "postcss": "^8.5.6",
@@ -73,7 +76,7 @@
73
76
  "typescript": "^5.9.3",
74
77
  "unplugin-auto-import": "^21.0.0",
75
78
  "unplugin-vue-components": "^31.0.0",
76
- "unplugin-vue-markdown": "^29.2.0",
79
+ "unplugin-vue-markdown": "^30.0.0",
77
80
  "vite": "^7.3.1",
78
81
  "vue": "^3.5.28",
79
82
  "vue-router": "^5.0.2"
@@ -81,6 +84,7 @@
81
84
  "devDependencies": {
82
85
  "@types/js-beautify": "^1.14.3",
83
86
  "@types/node": "^25.2.3",
87
+ "@types/nodemailer": "^8.0.0",
84
88
  "@types/postcss-safe-parser": "^5.0.4",
85
89
  "@vue/test-utils": "^2.4.6",
86
90
  "happy-dom": "^20.6.3",
@@ -1,30 +0,0 @@
1
- <script setup lang="ts">
2
- import type { SplitterResizeHandleEmits, SplitterResizeHandleProps } from "reka-ui"
3
- import type { HTMLAttributes } from "vue"
4
- import { reactiveOmit } from "@vueuse/core"
5
- import { GripVertical } from "lucide-vue-next"
6
- import { SplitterResizeHandle, useForwardPropsEmits } from "reka-ui"
7
- import { cn } from "@/lib/utils"
8
-
9
- const props = defineProps<SplitterResizeHandleProps & { class?: HTMLAttributes["class"], withHandle?: boolean }>()
10
- const emits = defineEmits<SplitterResizeHandleEmits>()
11
-
12
- const delegatedProps = reactiveOmit(props, "class", "withHandle")
13
- const forwarded = useForwardPropsEmits(delegatedProps, emits)
14
- </script>
15
-
16
- <template>
17
- <SplitterResizeHandle
18
- data-slot="resizable-handle"
19
- v-bind="forwarded"
20
- :class="cn('bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)"
21
- >
22
- <template v-if="props.withHandle">
23
- <div class="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
24
- <slot>
25
- <GripVertical class="size-2.5" />
26
- </slot>
27
- </div>
28
- </template>
29
- </SplitterResizeHandle>
30
- </template>
@@ -1,21 +0,0 @@
1
- <script setup lang="ts">
2
- import type { SplitterPanelEmits, SplitterPanelProps } from "reka-ui"
3
- import { SplitterPanel, useForwardExpose, useForwardPropsEmits } from "reka-ui"
4
-
5
- const props = defineProps<SplitterPanelProps>()
6
- const emits = defineEmits<SplitterPanelEmits>()
7
-
8
- const forwarded = useForwardPropsEmits(props, emits)
9
- const { forwardRef } = useForwardExpose()
10
- </script>
11
-
12
- <template>
13
- <SplitterPanel
14
- :ref="forwardRef"
15
- v-slot="slotProps"
16
- data-slot="resizable-panel"
17
- v-bind="forwarded"
18
- >
19
- <slot v-bind="slotProps" />
20
- </SplitterPanel>
21
- </template>
@@ -1,25 +0,0 @@
1
- <script setup lang="ts">
2
- import type { SplitterGroupEmits, SplitterGroupProps } from "reka-ui"
3
- import type { HTMLAttributes } from "vue"
4
- import { reactiveOmit } from "@vueuse/core"
5
- import { SplitterGroup, useForwardPropsEmits } from "reka-ui"
6
- import { cn } from "@/lib/utils"
7
-
8
- const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes["class"] }>()
9
- const emits = defineEmits<SplitterGroupEmits>()
10
-
11
- const delegatedProps = reactiveOmit(props, "class")
12
-
13
- const forwarded = useForwardPropsEmits(delegatedProps, emits)
14
- </script>
15
-
16
- <template>
17
- <SplitterGroup
18
- v-slot="slotProps"
19
- data-slot="resizable-panel-group"
20
- v-bind="forwarded"
21
- :class="cn('flex h-full w-full data-[orientation=vertical]:flex-col', props.class)"
22
- >
23
- <slot v-bind="slotProps" />
24
- </SplitterGroup>
25
- </template>
@@ -1,3 +0,0 @@
1
- export { default as ResizableHandle } from "./ResizableHandle.vue"
2
- export { default as ResizablePanel } from "./ResizablePanel.vue"
3
- export { default as ResizablePanelGroup } from "./ResizablePanelGroup.vue"
File without changes