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

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 (86) hide show
  1. package/dist/components/Body.vue +105 -36
  2. package/dist/components/Button.vue +4 -1
  3. package/dist/components/CodeBlock.vue +11 -18
  4. package/dist/components/CodeInline.vue +6 -1
  5. package/dist/components/Column.vue +30 -5
  6. package/dist/components/Container.vue +10 -2
  7. package/dist/components/Divider.vue +28 -0
  8. package/dist/components/Head.vue +22 -0
  9. package/dist/components/Heading.vue +28 -0
  10. package/dist/components/Html.vue +98 -47
  11. package/dist/components/Layout.vue +93 -0
  12. package/dist/components/Link.vue +26 -0
  13. package/dist/components/Markdown.vue +83 -0
  14. package/dist/components/Outlook.vue +36 -0
  15. package/dist/components/Overlap.vue +25 -5
  16. package/dist/components/{Preview.vue → Preheader.vue} +1 -1
  17. package/dist/components/Row.vue +16 -5
  18. package/dist/components/Section.vue +83 -0
  19. package/dist/components/Text.vue +29 -0
  20. package/dist/components/Vml.vue +165 -13
  21. package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
  22. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  23. package/dist/render/createRenderer.d.mts +2 -3
  24. package/dist/render/createRenderer.d.mts.map +1 -1
  25. package/dist/render/createRenderer.mjs +67 -4
  26. package/dist/render/createRenderer.mjs.map +1 -1
  27. package/dist/serve.d.mts.map +1 -1
  28. package/dist/serve.mjs +84 -4
  29. package/dist/serve.mjs.map +1 -1
  30. package/dist/server/compatibility.d.mts +1 -2
  31. package/dist/server/compatibility.d.mts.map +1 -1
  32. package/dist/server/compatibility.mjs +30 -16
  33. package/dist/server/compatibility.mjs.map +1 -1
  34. package/dist/server/email.d.mts +17 -0
  35. package/dist/server/email.d.mts.map +1 -0
  36. package/dist/server/email.mjs +41 -0
  37. package/dist/server/email.mjs.map +1 -0
  38. package/dist/server/linter.d.mts +1 -2
  39. package/dist/server/linter.d.mts.map +1 -1
  40. package/dist/server/linter.mjs +60 -71
  41. package/dist/server/linter.mjs.map +1 -1
  42. package/dist/server/ui/App.vue +205 -69
  43. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  44. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  45. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  46. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  47. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  48. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  49. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  50. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  51. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  52. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  53. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  54. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  55. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  56. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  57. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  58. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  59. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  60. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  61. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  62. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  63. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  64. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  65. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  66. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  67. package/dist/server/ui/main.css +20 -20
  68. package/dist/server/ui/pages/Home.vue +12 -5
  69. package/dist/server/ui/pages/Preview.vue +495 -211
  70. package/dist/transformers/inlineCSS.d.mts +1 -14
  71. package/dist/transformers/inlineCSS.d.mts.map +1 -1
  72. package/dist/transformers/inlineCSS.mjs +25 -34
  73. package/dist/transformers/inlineCSS.mjs.map +1 -1
  74. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  75. package/dist/transformers/purgeCSS.mjs +67 -1
  76. package/dist/transformers/purgeCSS.mjs.map +1 -1
  77. package/dist/transformers/tailwindcss.mjs +3 -7
  78. package/dist/transformers/tailwindcss.mjs.map +1 -1
  79. package/dist/types/config.d.mts +47 -29
  80. package/dist/types/config.d.mts.map +1 -1
  81. package/dist/types/index.d.mts +2 -2
  82. package/package.json +7 -3
  83. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  84. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  85. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  86. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
@@ -1,56 +1,208 @@
1
1
  <script lang="ts">
2
2
  import { computed, createStaticVNode } from 'vue'
3
+ import type { PropType } from 'vue'
3
4
  import { normalizeToPixels } from './utils.ts'
4
5
 
5
6
  export default {
6
7
  name: 'Vml',
7
8
  props: {
9
+ /**
10
+ * Width of the VML rectangle.
11
+ *
12
+ * Accepts a number (treated as pixels) or a string with units.
13
+ *
14
+ * @default '600px'
15
+ */
8
16
  width: {
9
17
  type: [String, Number],
10
18
  default: '600px'
11
19
  },
20
+ /**
21
+ * Height of the VML rectangle.
22
+ *
23
+ * Accepts a number (treated as pixels) or a string with units.
24
+ * When not set, the rectangle auto-sizes to fit its content.
25
+ */
12
26
  height: {
13
27
  type: [String, Number],
14
28
  default: null
15
29
  },
30
+ /**
31
+ * VML fill type that controls how the background image is rendered.
32
+ *
33
+ * - `frame` — scale to fill the rectangle (default)
34
+ * - `tile` — repeat the image to fill the rectangle
35
+ * - `pattern` — tile at original size
36
+ * - `solid` — solid color fill, no image
37
+ * - `gradient` — linear gradient fill
38
+ * - `gradientradial` — radial gradient fill
39
+ *
40
+ * @default 'frame'
41
+ */
16
42
  type: {
17
- type: String,
43
+ type: String as PropType<'solid' | 'gradient' | 'gradientradial' | 'tile' | 'pattern' | 'frame'>,
18
44
  default: 'frame'
19
45
  },
20
- sizes: String,
21
- origin: String,
22
- position: String,
23
- aspect: String,
46
+ /**
47
+ * Comma-separated dimensions for the fill image.
48
+ *
49
+ * Controls the rendered size of the background image.
50
+ *
51
+ * @example '300px,200px'
52
+ */
53
+ sizes: {
54
+ type: String,
55
+ validator: (v: string) => /^[\d.]+(px|%|em|rem)?(,[\d.]+(px|%|em|rem)?)+$/.test(v.replace(/\s/g, ''))
56
+ },
57
+ /**
58
+ * Fill origin offset as comma-separated fractional values.
59
+ *
60
+ * Controls where the fill image anchors relative to the shape.
61
+ * Values are fractions of the shape's dimensions, where `0,0` is
62
+ * center and `-0.5,-0.5` is the top-left corner.
63
+ *
64
+ * Overridden by `backgroundPosition` if both are set.
65
+ *
66
+ * @example '0,0'
67
+ * @example '-0.5,-0.5'
68
+ */
69
+ origin: {
70
+ type: String,
71
+ validator: (v: string) => /^-?[\d.]+(,-?[\d.]+)+$/.test(v.replace(/\s/g, ''))
72
+ },
73
+ /**
74
+ * Fill position offset as comma-separated fractional values.
75
+ *
76
+ * Controls where the fill image is positioned within the shape.
77
+ * Values are fractions of the shape's dimensions, where `0,0` is
78
+ * center and `0.5,0.5` is the bottom-right corner.
79
+ *
80
+ * Overridden by `backgroundPosition` if both are set.
81
+ *
82
+ * @example '0,0'
83
+ * @example '0.5,0.5'
84
+ */
85
+ position: {
86
+ type: String,
87
+ validator: (v: string) => /^-?[\d.]+(,-?[\d.]+)+$/.test(v.replace(/\s/g, ''))
88
+ },
89
+ /**
90
+ * Background image position as `vertical,horizontal`.
91
+ *
92
+ * First value is the vertical axis: `top`, `center`, or `bottom`.
93
+ * Second value is the horizontal axis: `left`, `center`, or `right`.
94
+ *
95
+ * Convenience prop that maps to VML `origin` and `position` attributes.
96
+ *
97
+ * @example 'top,left'
98
+ * @example 'center,center'
99
+ */
100
+ backgroundPosition: {
101
+ type: String as PropType<
102
+ | 'top,left' | 'top,center' | 'top,right'
103
+ | 'center,left' | 'center,center' | 'center,right'
104
+ | 'bottom,left' | 'bottom,center' | 'bottom,right'
105
+ >,
106
+ validator: (v: string) => /^(top|center|bottom),(left|center|right)$/.test(v.replace(/\s/g, ''))
107
+ },
108
+ /**
109
+ * Aspect ratio constraint for the fill image.
110
+ *
111
+ * - `atleast` — image is at least as large as the shape
112
+ * - `atmost` — image is at most as large as the shape
113
+ */
114
+ aspect: {
115
+ type: String as PropType<'atleast' | 'atmost'>,
116
+ },
117
+ /**
118
+ * Fill color used for `solid` and `gradient` fill types.
119
+ *
120
+ * @example '#ffffff'
121
+ */
24
122
  color: String,
123
+ /**
124
+ * Text box inset (padding) as `top,right,bottom,left`.
125
+ *
126
+ * Controls the inner spacing of the `v:textbox` element.
127
+ *
128
+ * @default '0,0,0,0'
129
+ */
25
130
  inset: {
26
131
  type: String,
27
132
  default: '0,0,0,0'
28
133
  },
134
+ /**
135
+ * Whether the VML rectangle has a visible border.
136
+ *
137
+ * @default false
138
+ */
29
139
  stroke: {
30
- type: String,
31
- default: 'f'
140
+ type: [Boolean, String],
141
+ default: false
32
142
  },
143
+ /**
144
+ * Border color for the VML rectangle.
145
+ *
146
+ * Setting this also enables `stroke` automatically.
147
+ *
148
+ * @example '#000000'
149
+ */
33
150
  strokecolor: String,
151
+ /**
152
+ * Whether the VML rectangle has a fill.
153
+ *
154
+ * @default true
155
+ */
34
156
  fill: {
35
- type: String,
36
- default: 't'
157
+ type: [Boolean, String],
158
+ default: true
37
159
  },
160
+ /**
161
+ * Background color of the VML rectangle.
162
+ *
163
+ * Used as a fallback when the background image cannot be loaded.
164
+ *
165
+ * @default 'none'
166
+ * @example '#3b82f6'
167
+ */
38
168
  fillcolor: {
39
169
  type: String,
40
170
  default: 'none'
41
171
  },
172
+ /**
173
+ * URL of the background image.
174
+ *
175
+ * @default 'https://via.placeholder.com/600x400'
176
+ */
42
177
  src: {
43
178
  type: String,
44
179
  default: 'https://via.placeholder.com/600x400'
45
180
  }
46
181
  },
47
182
  setup(props, { slots }) {
183
+ const backgroundPositionMap: Record<string, string> = {
184
+ 'top,left': '-0.5,-0.5',
185
+ 'top,center': '0,-0.5',
186
+ 'top,right': '0.5,-0.5',
187
+ 'center,left': '-0.5,0',
188
+ 'center,center': '0,0',
189
+ 'center,right': '0.5,0',
190
+ 'bottom,left': '-0.5,0.5',
191
+ 'bottom,center': '0,0.5',
192
+ 'bottom,right': '0.5,0.5',
193
+ }
194
+
195
+ const resolvedOrigin = computed(() => props.origin ?? (props.backgroundPosition ? backgroundPositionMap[props.backgroundPosition.replace(/\s/g, '')] : undefined))
196
+ const resolvedPosition = computed(() => props.position ?? (props.backgroundPosition ? backgroundPositionMap[props.backgroundPosition.replace(/\s/g, '')] : undefined))
197
+
48
198
  const before = computed(() => {
49
199
  const width = normalizeToPixels(props.width)
50
200
 
201
+ const toBool = (v: boolean | string) => v === true || v === 'true' ? 'true' : 'false'
202
+
51
203
  const rectAttrs = [
52
- `fill="${props.fillcolor ? 't' : props.fill}"`,
53
- `stroke="${props.strokecolor ? 't' : props.stroke}"`,
204
+ `fill="${props.fillcolor ? 'true' : toBool(props.fill)}"`,
205
+ `stroke="${props.strokecolor ? 'true' : toBool(props.stroke)}"`,
54
206
  `style="width: ${width};${props.height ? ` height: ${normalizeToPixels(props.height)};` : ''}"`,
55
207
  props.strokecolor ? `strokecolor="${props.strokecolor}"` : '',
56
208
  props.fillcolor ? `fillcolor="${props.fillcolor}"` : ''
@@ -61,8 +213,8 @@ export default {
61
213
  `src="${props.src}"`,
62
214
  props.sizes ? `sizes="${props.sizes}"` : '',
63
215
  props.aspect ? `aspect="${props.aspect}"` : '',
64
- props.origin ? `origin="${props.origin}"` : '',
65
- props.position ? `position="${props.position}"` : '',
216
+ resolvedOrigin.value ? `origin="${resolvedOrigin.value}"` : '',
217
+ resolvedPosition.value ? `position="${resolvedPosition.value}"` : '',
66
218
  props.color ? `color="${props.color}"` : ''
67
219
  ].filter(Boolean).join(' ')
68
220
 
@@ -14,20 +14,29 @@ const DEFAULT_AT_RULES = ["layer", "property"];
14
14
  function tailwindCleanup(config) {
15
15
  const selectors = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS;
16
16
  const atRules = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES;
17
- return [{
18
- postcssPlugin: "tailwind-cleanup-selectors",
19
- Rule(rule) {
20
- const parts = rule.selector.split(",").map((s) => s.trim());
21
- const kept = parts.filter((p) => !selectors.some((s) => p === s || p.startsWith(`${s}(`)));
22
- if (kept.length === 0) rule.remove();
23
- else if (kept.length < parts.length) rule.selector = kept.join(", ");
17
+ return [
18
+ {
19
+ postcssPlugin: "tailwind-cleanup-selectors",
20
+ Rule(rule) {
21
+ const parts = rule.selector.split(",").map((s) => s.trim());
22
+ const kept = parts.filter((p) => !selectors.some((s) => p === s || p.startsWith(`${s}(`)));
23
+ if (kept.length === 0) rule.remove();
24
+ else if (kept.length < parts.length) rule.selector = kept.join(", ");
25
+ }
26
+ },
27
+ {
28
+ postcssPlugin: "tailwind-cleanup-at-rules",
29
+ AtRule(rule) {
30
+ if (atRules.includes(rule.name)) rule.remove();
31
+ }
32
+ },
33
+ {
34
+ postcssPlugin: "tailwind-cleanup-text-decoration",
35
+ Declaration(decl) {
36
+ if (decl.prop === "text-decoration-line") decl.prop = "text-decoration";
37
+ }
24
38
  }
25
- }, {
26
- postcssPlugin: "tailwind-cleanup-at-rules",
27
- AtRule(rule) {
28
- if (atRules.includes(rule.name)) rule.remove();
29
- }
30
- }];
39
+ ];
31
40
  }
32
41
 
33
42
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"tailwindCleanup.mjs","names":[],"sources":["../../../src/plugins/postcss/tailwindCleanup.ts"],"sourcesContent":["import postcss from 'postcss'\nimport type { MaizzleConfig } from '../../types/config.ts'\n\nconst DEFAULT_SELECTORS = [':host', ':lang']\nconst DEFAULT_AT_RULES = ['layer', 'property']\n\n/**\n * Removes CSS rules whose every comma-separated selector part starts with\n * one of the configured prefixes (e.g. ':host', ':lang'). Rules with mixed\n * selectors have the unwanted parts stripped.\n *\n * Also removes entire at-rules by name (e.g. '@layer', '@property').\n *\n * Intended to clean up Tailwind's compiled output after lightningcss has\n * flattened all modern CSS syntax.\n */\nexport function tailwindCleanup(config: MaizzleConfig): postcss.Plugin[] {\n const selectors: string[] = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS\n const atRules: string[] = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES\n\n return [\n {\n postcssPlugin: 'tailwind-cleanup-selectors',\n Rule(rule) {\n const parts = rule.selector.split(',').map(s => s.trim())\n const kept = parts.filter(p => !selectors.some(s => p === s || p.startsWith(`${s}(`)))\n if (kept.length === 0) {\n rule.remove()\n } else if (kept.length < parts.length) {\n rule.selector = kept.join(', ')\n }\n },\n },\n {\n postcssPlugin: 'tailwind-cleanup-at-rules',\n AtRule(rule) {\n if (atRules.includes(rule.name)) {\n rule.remove()\n }\n },\n },\n ]\n}\n"],"mappings":";AAGA,MAAM,oBAAoB,CAAC,SAAS,QAAQ;AAC5C,MAAM,mBAAmB,CAAC,SAAS,WAAW;;;;;;;;;;;AAY9C,SAAgB,gBAAgB,QAAyC;CACvE,MAAM,YAAsB,OAAO,SAAS,mBAAmB;CAC/D,MAAM,UAAoB,OAAO,SAAS,iBAAiB;AAE3D,QAAO,CACL;EACE,eAAe;EACf,KAAK,MAAM;GACT,MAAM,QAAQ,KAAK,SAAS,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;GACzD,MAAM,OAAO,MAAM,QAAO,MAAK,CAAC,UAAU,MAAK,MAAK,MAAM,KAAK,EAAE,WAAW,GAAG,EAAE,GAAG,CAAC,CAAC;AACtF,OAAI,KAAK,WAAW,EAClB,MAAK,QAAQ;YACJ,KAAK,SAAS,MAAM,OAC7B,MAAK,WAAW,KAAK,KAAK,KAAK;;EAGpC,EACD;EACE,eAAe;EACf,OAAO,MAAM;AACX,OAAI,QAAQ,SAAS,KAAK,KAAK,CAC7B,MAAK,QAAQ;;EAGlB,CACF"}
1
+ {"version":3,"file":"tailwindCleanup.mjs","names":[],"sources":["../../../src/plugins/postcss/tailwindCleanup.ts"],"sourcesContent":["import postcss from 'postcss'\nimport type { MaizzleConfig } from '../../types/config.ts'\n\nconst DEFAULT_SELECTORS = [':host', ':lang']\nconst DEFAULT_AT_RULES = ['layer', 'property']\n\n/**\n * Removes CSS rules whose every comma-separated selector part starts with\n * one of the configured prefixes (e.g. ':host', ':lang'). Rules with mixed\n * selectors have the unwanted parts stripped.\n *\n * Also removes entire at-rules by name (e.g. '@layer', '@property').\n *\n * Intended to clean up Tailwind's compiled output after lightningcss has\n * flattened all modern CSS syntax.\n */\nexport function tailwindCleanup(config: MaizzleConfig): postcss.Plugin[] {\n const selectors: string[] = config.postcss?.removeSelectors ?? DEFAULT_SELECTORS\n const atRules: string[] = config.postcss?.removeAtRules ?? DEFAULT_AT_RULES\n\n return [\n {\n postcssPlugin: 'tailwind-cleanup-selectors',\n Rule(rule) {\n const parts = rule.selector.split(',').map(s => s.trim())\n const kept = parts.filter(p => !selectors.some(s => p === s || p.startsWith(`${s}(`)))\n if (kept.length === 0) {\n rule.remove()\n } else if (kept.length < parts.length) {\n rule.selector = kept.join(', ')\n }\n },\n },\n {\n postcssPlugin: 'tailwind-cleanup-at-rules',\n AtRule(rule) {\n if (atRules.includes(rule.name)) {\n rule.remove()\n }\n },\n },\n {\n postcssPlugin: 'tailwind-cleanup-text-decoration',\n Declaration(decl) {\n if (decl.prop === 'text-decoration-line') {\n decl.prop = 'text-decoration'\n }\n },\n },\n ]\n}\n"],"mappings":";AAGA,MAAM,oBAAoB,CAAC,SAAS,QAAQ;AAC5C,MAAM,mBAAmB,CAAC,SAAS,WAAW;;;;;;;;;;;AAY9C,SAAgB,gBAAgB,QAAyC;CACvE,MAAM,YAAsB,OAAO,SAAS,mBAAmB;CAC/D,MAAM,UAAoB,OAAO,SAAS,iBAAiB;AAE3D,QAAO;EACL;GACE,eAAe;GACf,KAAK,MAAM;IACT,MAAM,QAAQ,KAAK,SAAS,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;IACzD,MAAM,OAAO,MAAM,QAAO,MAAK,CAAC,UAAU,MAAK,MAAK,MAAM,KAAK,EAAE,WAAW,GAAG,EAAE,GAAG,CAAC,CAAC;AACtF,QAAI,KAAK,WAAW,EAClB,MAAK,QAAQ;aACJ,KAAK,SAAS,MAAM,OAC7B,MAAK,WAAW,KAAK,KAAK,KAAK;;GAGpC;EACD;GACE,eAAe;GACf,OAAO,MAAM;AACX,QAAI,QAAQ,SAAS,KAAK,KAAK,CAC7B,MAAK,QAAQ;;GAGlB;EACD;GACE,eAAe;GACf,YAAY,MAAM;AAChB,QAAI,KAAK,SAAS,uBAChB,MAAK,OAAO;;GAGjB;EACF"}
@@ -1,7 +1,6 @@
1
- import { MaizzleConfig } from "../types/config.mjs";
1
+ import { MaizzleConfig, MarkdownConfig } from "../types/config.mjs";
2
2
  import { RenderContext } from "../composables/renderContext.mjs";
3
3
  import { Component } from "vue";
4
- import { Options } from "unplugin-vue-markdown/types";
5
4
 
6
5
  //#region src/render/createRenderer.d.ts
7
6
  interface RenderedTemplate {
@@ -21,7 +20,7 @@ interface CreateRendererOptions {
21
20
  /** Generate .d.ts files for auto-imports and components (default: false) */
22
21
  dts?: boolean;
23
22
  /** Options passed to unplugin-vue-markdown */
24
- markdown?: Options;
23
+ markdown?: MarkdownConfig;
25
24
  /** Root directory for resolving user component dirs and .d.ts output */
26
25
  root?: string;
27
26
  /** Additional component directories to register for auto-import */
@@ -1 +1 @@
1
- {"version":3,"file":"createRenderer.d.mts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;;UA2EiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;AAAA;AAAA,UAGG,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EAClE,UAAA,CAAW,QAAA,WAAmB,OAAA;EAC9B,aAAA,IAAiB,OAAA;EACjB,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAZC;EAchB,GAAA;EAbkB;EAelB,QAAA,GAAW,OAAA;EAdC;EAgBZ,IAAA;EAhByB;EAkBzB,aAAA;AAAA;;;;;;;iBASoB,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
1
+ {"version":3,"file":"createRenderer.d.mts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;UAyJiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;AAAA;AAAA,UAGG,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EAClE,UAAA,CAAW,QAAA,WAAmB,OAAA;EAC9B,aAAA,IAAiB,OAAA;EACjB,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAZC;EAchB,GAAA;EAbkB;EAelB,QAAA,GAAW,cAAA;EAdC;EAgBZ,IAAA;EAhByB;EAkBzB,aAAA;AAAA;;;;;;;iBASoB,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
@@ -1,6 +1,7 @@
1
1
  import { MaizzleConfigKey } from "../composables/useConfig.mjs";
2
2
  import { RenderContextKey } from "../composables/renderContext.mjs";
3
3
  import { isLaravel } from "../utils/detect.mjs";
4
+ import { readFileSync } from "node:fs";
4
5
  import { dirname, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { defu } from "defu";
@@ -36,8 +37,49 @@ function codeBlockExtract() {
36
37
  const stripped = content.replace(/^\n+/, "").replace(/\s+$/, "");
37
38
  if (!stripped) return _match;
38
39
  const minIndent = stripped.match(/^[ \t]*(?=\S)/gm)?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0;
39
- const dedented = minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped;
40
- return `<${tag}${attrs} encoded-code="${Buffer.from(dedented).toString("base64")}" />`;
40
+ return `<${tag}${attrs} code="${(minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" />`;
41
+ });
42
+ if (transformed !== code) return {
43
+ code: transformed,
44
+ map: null
45
+ };
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Vite plugin that pre-processes <Markdown> tags:
51
+ * - Extracts slot content, dedents it, and passes as :content prop
52
+ * - Resolves `src` prop to read file contents at build time
53
+ */
54
+ function markdownExtract() {
55
+ const re = /<(Markdown|markdown)((?:\s[^>]*?)?)>([\s\S]*?)<\/\1>/g;
56
+ const selfClosingRe = /<(Markdown|markdown)((?:\s[^>]*?\bsrc\s*=\s*"[^"]*"[^>]*?))\/>/g;
57
+ return {
58
+ name: "maizzle:markdown-extract",
59
+ enforce: "pre",
60
+ transform(code, id) {
61
+ if (!id.endsWith(".vue") && !id.endsWith(".md")) return;
62
+ if (!code.includes("Markdown") && !code.includes("markdown")) return;
63
+ let transformed = code;
64
+ transformed = transformed.replace(re, (_match, tag, attrs, content) => {
65
+ if (/(?:^|\s):content\b/.test(attrs) || /v-bind:content\b/.test(attrs)) return _match;
66
+ const stripped = content.replace(/^\n+/, "").replace(/\s+$/, "");
67
+ if (!stripped) return _match;
68
+ const minIndent = stripped.match(/^[ \t]*(?=\S)/gm)?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0;
69
+ return `<${tag}${attrs} content="${(minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" />`;
70
+ });
71
+ transformed = transformed.replace(selfClosingRe, (_match, tag, attrs) => {
72
+ const srcMatch = attrs.match(/\bsrc\s*=\s*"([^"]*)"/);
73
+ if (!srcMatch) return _match;
74
+ const srcPath = srcMatch[1];
75
+ const resolvedPath = resolve(dirname(id), srcPath);
76
+ let fileContent;
77
+ try {
78
+ fileContent = readFileSync(resolvedPath, "utf-8").trim();
79
+ } catch {
80
+ return _match;
81
+ }
82
+ return `<${tag}${attrs.replace(/\s*\bsrc\s*=\s*"[^"]*"/, "")} content="${fileContent.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" />`;
41
83
  });
42
84
  if (transformed !== code) return {
43
85
  code: transformed,
@@ -57,7 +99,8 @@ const vueRouterPkgDir = dirname(fileURLToPath(import.meta.resolve("vue-router/pa
57
99
  * Tailwind CSS compilation is handled by the transformer pipeline.
58
100
  */
59
101
  async function createRenderer(options = {}) {
60
- const { dts = false, markdown: markdownOptions, root = process.cwd(), componentDirs = [] } = options;
102
+ const { dts = false, markdown: markdownOptionsRaw, root = process.cwd(), componentDirs = [] } = options;
103
+ const { shikiTheme = "github-light", ...markdownOptions } = markdownOptionsRaw ?? {};
61
104
  const dtsDir = isLaravel() ? resolve(process.cwd(), "resources/js/types/maizzle") : resolve(root, ".maizzle");
62
105
  const VIRTUAL_SFC_ID = "virtual:maizzle-sfc.vue";
63
106
  let virtualSfcSource = "";
@@ -65,6 +108,7 @@ async function createRenderer(options = {}) {
65
108
  configFile: false,
66
109
  plugins: [
67
110
  codeBlockExtract(),
111
+ markdownExtract(),
68
112
  {
69
113
  name: "maizzle:virtual-sfc",
70
114
  resolveId(id) {
@@ -80,7 +124,26 @@ async function createRenderer(options = {}) {
80
124
  }),
81
125
  Markdown(defu(markdownOptions ?? {}, {
82
126
  headEnabled: true,
83
- wrapperDiv: false
127
+ wrapperDiv: false,
128
+ wrapperClasses: "prose",
129
+ markdownOptions: { async highlight(code, lang) {
130
+ const { codeToHtml } = await import("shiki");
131
+ return codeToHtml(code, {
132
+ lang,
133
+ theme: shikiTheme
134
+ });
135
+ } },
136
+ markdownSetup(md) {
137
+ const wrapPre = (html) => `<table class="w-full"><tr><td class="max-w-0 mso-padding-alt-4">${html}</td></tr></table>\n`;
138
+ const defaultFence = md.renderer.rules.fence;
139
+ md.renderer.rules.fence = (...args) => {
140
+ const result = defaultFence(...args);
141
+ if (typeof result === "string") return wrapPre(result);
142
+ return result.then(wrapPre);
143
+ };
144
+ const defaultCodeBlock = md.renderer.rules.code_block;
145
+ md.renderer.rules.code_block = (...args) => wrapPre(defaultCodeBlock(...args));
146
+ }
84
147
  })),
85
148
  AutoImport({
86
149
  dirs: [resolve(__dirname, "../composables"), resolve(__dirname, "../filters")],
@@ -1 +1 @@
1
- {"version":3,"file":"createRenderer.mjs","names":["merge"],"sources":["../../src/render/createRenderer.ts"],"sourcesContent":["import { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { isLaravel } from '../utils/detect.ts'\nimport { createServer } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport Markdown from 'unplugin-vue-markdown/vite'\nimport AutoImport from 'unplugin-auto-import/vite'\nimport Components from 'unplugin-vue-components/vite'\nimport { unheadVueComposablesImports } from '@unhead/vue'\nimport { defu as merge } from 'defu'\nimport { createSSRApp } from 'vue'\nimport { renderToString } from 'vue/server-renderer'\nimport { createHead, renderSSRHead } from '@unhead/vue/server'\nimport { MaizzleConfigKey } from '../composables/useConfig.ts'\nimport { RenderContextKey } from '../composables/renderContext.ts'\nimport type { Component, InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { Options as MarkdownOptions } from 'unplugin-vue-markdown/types'\nimport type { RenderContext } from '../composables/renderContext.ts'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Vite plugin that extracts raw slot content from <CodeBlock> tags\n * and passes it as a :code prop before Vue compiles the template.\n *\n * This lets users write HTML naturally inside CodeBlock slots without\n * Vue attempting to compile it as template syntax.\n */\nfunction codeBlockExtract() {\n // Matches <CodeBlock ...>content</CodeBlock> (and kebab-case <code-block>)\n const re = /<(CodeBlock|code-block)((?:\\s[^>]*?)?)>([\\s\\S]*?)<\\/\\1>/g\n\n return {\n name: 'maizzle:code-block-extract',\n enforce: 'pre' as const,\n transform(code: string, id: string) {\n if (!id.endsWith('.vue') && !id.endsWith('.md')) return\n if (!code.includes('CodeBlock') && !code.includes('code-block')) return\n\n const transformed = code.replace(re, (_match, tag, attrs, content) => {\n // Skip if already has a :code or v-bind:code prop\n if (/(?:^|\\s):code\\b/.test(attrs) || /v-bind:code\\b/.test(attrs)) return _match\n\n // Strip leading/trailing blank lines, then dedent based on\n // the minimum indent of non-empty lines (à la min-indent)\n const stripped = content.replace(/^\\n+/, '').replace(/\\s+$/, '')\n if (!stripped) return _match\n\n const minIndent = stripped.match(/^[ \\t]*(?=\\S)/gm)\n ?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0\n\n const dedented = minIndent > 0\n ? stripped.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, 'gm'), '')\n : stripped\n\n // Base64-encode so no characters can interfere with Vue's HTML parser.\n // The component decodes it back via Buffer.\n const encoded = Buffer.from(dedented).toString('base64')\n\n return `<${tag}${attrs} encoded-code=\"${encoded}\" />`\n })\n\n if (transformed !== code) {\n return { code: transformed, map: null }\n }\n },\n }\n}\n\nconst vuePkgDir = dirname(fileURLToPath(import.meta.resolve('vue/package.json')))\nconst vueServerRendererPkgDir = dirname(fileURLToPath(import.meta.resolve('@vue/server-renderer/package.json')))\nconst unheadVuePkgDir = resolve(dirname(fileURLToPath(import.meta.resolve('@unhead/vue'))), '..')\nconst vueRouterPkgDir = dirname(fileURLToPath(import.meta.resolve('vue-router/package.json')))\n\nexport interface RenderedTemplate {\n html: string\n doctype?: string\n templateConfig: MaizzleConfig\n sfcEventHandlers: RenderContext['sfcEventHandlers']\n plaintext?: RenderContext['plaintext']\n}\n\nexport interface Renderer {\n render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate>\n invalidate(filePath: string): Promise<void>\n invalidateAll(): Promise<void>\n close(): Promise<void>\n}\n\nexport interface CreateRendererOptions {\n /** Generate .d.ts files for auto-imports and components (default: false) */\n dts?: boolean\n /** Options passed to unplugin-vue-markdown */\n markdown?: MarkdownOptions\n /** Root directory for resolving user component dirs and .d.ts output */\n root?: string\n /** Additional component directories to register for auto-import */\n componentDirs?: string[]\n}\n\n/**\n * Lightweight Vite SSR loader for rendering Vue SFC email templates.\n *\n * Uses only Vue + unplugin for component/auto-import resolution.\n * Tailwind CSS compilation is handled by the transformer pipeline.\n */\nexport async function createRenderer(\n options: CreateRendererOptions = {},\n): Promise<Renderer> {\n const { dts = false, markdown: markdownOptions, root = process.cwd(), componentDirs = [] } = options\n\n const dtsDir = isLaravel()\n ? resolve(process.cwd(), 'resources/js/types/maizzle')\n : resolve(root, '.maizzle')\n\n const VIRTUAL_SFC_ID = 'virtual:maizzle-sfc.vue'\n let virtualSfcSource = ''\n\n const server = await createServer({\n configFile: false,\n plugins: [\n codeBlockExtract(),\n {\n name: 'maizzle:virtual-sfc',\n resolveId(id) {\n if (id === VIRTUAL_SFC_ID) return id\n },\n load(id) {\n if (id === VIRTUAL_SFC_ID) return virtualSfcSource\n },\n },\n vue({\n include: [/\\.vue$/, /\\.md$/],\n template: {\n transformAssetUrls: false,\n },\n }),\n Markdown(merge(markdownOptions ?? {}, {\n headEnabled: true,\n wrapperDiv: false,\n })),\n AutoImport({\n dirs: [\n resolve(__dirname, '../composables'),\n resolve(__dirname, '../filters'),\n ],\n imports: ['vue', unheadVueComposablesImports],\n dts: dts ? resolve(dtsDir, 'auto-imports.d.ts') : false,\n }),\n Components({\n extensions: ['vue', 'md'],\n include: [/\\.vue$/, /\\.vue\\?vue/, /\\.md$/],\n dirs: [\n resolve(__dirname, '../components'),\n resolve(root, 'components'),\n ...componentDirs,\n ],\n dts: dts ? resolve(dtsDir, 'components.d.ts') : false,\n }),\n ],\n resolve: {\n alias: {\n 'vue/server-renderer': resolve(vueServerRendererPkgDir, 'dist/server-renderer.esm-bundler.js'),\n 'vue': resolve(vuePkgDir, 'dist/vue.runtime.esm-bundler.js'),\n 'vue-router': vueRouterPkgDir,\n '@unhead/vue/server': resolve(unheadVuePkgDir, 'dist/server.mjs'),\n '@unhead/vue': resolve(unheadVuePkgDir, 'dist/index.mjs'),\n },\n },\n server: {\n middlewareMode: true,\n hmr: false,\n watch: null,\n fs: {\n allow: [process.cwd(), root, ...componentDirs, vuePkgDir, vueServerRendererPkgDir, unheadVuePkgDir, vueRouterPkgDir],\n },\n },\n appType: 'custom',\n logLevel: 'silent',\n optimizeDeps: {\n noDiscovery: true,\n },\n })\n\n return {\n async render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate> {\n let component: Component\n let configKey: InjectionKey<MaizzleConfig>\n let contextKey: InjectionKey<RenderContext>\n\n if (typeof input === 'string') {\n // String input goes through Vite — must use ssrLoadModule for injection keys\n // so they share the same module instance as the SFC\n const configModule = await server.ssrLoadModule(resolve(__dirname, '../composables/useConfig'))\n const contextModule = await server.ssrLoadModule(resolve(__dirname, '../composables/renderContext'))\n configKey = configModule.MaizzleConfigKey\n contextKey = contextModule.RenderContextKey\n\n if (input.includes('<template') || input.includes('<script')) {\n virtualSfcSource = input\n const mod = server.moduleGraph.getModuleById(VIRTUAL_SFC_ID)\n if (mod) server.moduleGraph.invalidateModule(mod)\n component = (await server.ssrLoadModule(VIRTUAL_SFC_ID)).default\n } else {\n component = (await server.ssrLoadModule(input)).default\n }\n } else {\n // Pre-compiled component — use directly imported keys\n component = input\n configKey = MaizzleConfigKey\n contextKey = RenderContextKey\n }\n\n const renderContext: RenderContext = {\n doctype: undefined,\n sfcConfig: undefined,\n sfcEventHandlers: [],\n }\n\n const head = createHead({ disableDefaults: true })\n const app = createSSRApp(component)\n app.use(head)\n app.provide(configKey, config)\n app.provide(contextKey, renderContext)\n\n const ssrContext: Record<string, any> = {}\n let html: string = await renderToString(app, ssrContext)\n\n const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)\n\n // Inject head entries into the rendered HTML\n if (htmlAttrs) {\n html = html.replace(/<html([^>]*)>/, `<html$1 ${htmlAttrs}>`)\n }\n if (headTags) {\n html = html.replace('</head>', `${headTags}\\n</head>`)\n }\n if (bodyAttrs) {\n html = html.replace(/<body([^>]*)>/, `<body$1 ${bodyAttrs}>`)\n }\n if (bodyTagsOpen) {\n html = html.replace(/<body([^>]*)>/, `<body$1>\\n${bodyTagsOpen}`)\n }\n if (bodyTags) {\n html = html.replace('</body>', `${bodyTags}\\n</body>`)\n }\n\n // Inject SSR teleport content into their target elements\n if (ssrContext.teleports) {\n const { parse: parseDom, serialize: serializeDom, walk } = await import('../utils/ast/index.ts')\n let dom = parseDom(html)\n\n for (const [rawTarget, content] of Object.entries(ssrContext.teleports) as [string, string][]) {\n if (!content) continue\n\n const prepend = rawTarget.endsWith(':start')\n const target = prepend ? rawTarget.slice(0, -6) : rawTarget\n const targetChildren = parseDom(content)\n\n walk(dom, (node) => {\n const el = node as import('domhandler').Element\n\n if (!el.name) return\n\n const matched\n = target === el.name\n || (target.startsWith('#') && el.attribs?.id === target.slice(1))\n || (target.startsWith('.') && el.attribs?.class?.split(/\\s+/).includes(target.slice(1)))\n\n if (matched) {\n for (const child of targetChildren) {\n child.parent = el as any\n }\n\n el.children = prepend\n ? [...targetChildren, ...(el.children || [])] as any\n : [...(el.children || []), ...targetChildren] as any\n }\n })\n }\n\n html = serializeDom(dom)\n }\n\n // Inject preview/preheader text from usePreviewText() composable\n if (renderContext.previewText) {\n const { text, fillerCount, shyCount } = renderContext.previewText\n const filler = '\\u2007\\u034F '.repeat(fillerCount)\n const shys = '\\u00AD '.repeat(shyCount)\n const previewHtml = `<div style=\"display:none\">${text}${filler}${shys}\\u00A0</div>`\n html = html.replace(/<body([^>]*)>/, `<body$1>${previewHtml}`)\n }\n\n return {\n html,\n doctype: renderContext.doctype,\n templateConfig: renderContext.sfcConfig ?? config,\n sfcEventHandlers: renderContext.sfcEventHandlers,\n plaintext: renderContext.plaintext,\n }\n },\n\n async invalidate(filePath: string): Promise<void> {\n const mod = await server.moduleGraph.getModuleByUrl(filePath)\n if (mod) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async invalidateAll(): Promise<void> {\n for (const mod of server.moduleGraph.idToModuleMap.values()) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async close(): Promise<void> {\n await server.close()\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"createRenderer.mjs","names":["merge"],"sources":["../../src/render/createRenderer.ts"],"sourcesContent":["import { dirname, resolve } from 'node:path'\nimport { readFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { isLaravel } from '../utils/detect.ts'\nimport { createServer } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport Markdown from 'unplugin-vue-markdown/vite'\nimport AutoImport from 'unplugin-auto-import/vite'\nimport Components from 'unplugin-vue-components/vite'\nimport { unheadVueComposablesImports } from '@unhead/vue'\nimport { defu as merge } from 'defu'\nimport { createSSRApp } from 'vue'\nimport { renderToString } from 'vue/server-renderer'\nimport { createHead, renderSSRHead } from '@unhead/vue/server'\nimport { MaizzleConfigKey } from '../composables/useConfig.ts'\nimport { RenderContextKey } from '../composables/renderContext.ts'\nimport type { Component, InjectionKey } from 'vue'\nimport type { MaizzleConfig, MarkdownConfig } from '../types/index.ts'\nimport type { MarkdownExit } from 'markdown-exit'\nimport type { RenderContext } from '../composables/renderContext.ts'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Vite plugin that extracts raw slot content from <CodeBlock> tags\n * and passes it as a :code prop before Vue compiles the template.\n *\n * This lets users write HTML naturally inside CodeBlock slots without\n * Vue attempting to compile it as template syntax.\n */\nfunction codeBlockExtract() {\n // Matches <CodeBlock ...>content</CodeBlock> (and kebab-case <code-block>)\n const re = /<(CodeBlock|code-block)((?:\\s[^>]*?)?)>([\\s\\S]*?)<\\/\\1>/g\n\n return {\n name: 'maizzle:code-block-extract',\n enforce: 'pre' as const,\n transform(code: string, id: string) {\n if (!id.endsWith('.vue') && !id.endsWith('.md')) return\n if (!code.includes('CodeBlock') && !code.includes('code-block')) return\n\n const transformed = code.replace(re, (_match, tag, attrs, content) => {\n // Skip if already has a :code or v-bind:code prop\n if (/(?:^|\\s):code\\b/.test(attrs) || /v-bind:code\\b/.test(attrs)) return _match\n\n // Strip leading/trailing blank lines, then dedent based on\n // the minimum indent of non-empty lines (à la min-indent)\n const stripped = content.replace(/^\\n+/, '').replace(/\\s+$/, '')\n if (!stripped) return _match\n\n const minIndent = stripped.match(/^[ \\t]*(?=\\S)/gm)\n ?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0\n\n const dedented = minIndent > 0\n ? stripped.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, 'gm'), '')\n : stripped\n\n // HTML-escape for safe embedding in a static attribute value.\n const escaped = dedented\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n\n return `<${tag}${attrs} code=\"${escaped}\" />`\n })\n\n if (transformed !== code) {\n return { code: transformed, map: null }\n }\n },\n }\n}\n\n/**\n * Vite plugin that pre-processes <Markdown> tags:\n * - Extracts slot content, dedents it, and passes as :content prop\n * - Resolves `src` prop to read file contents at build time\n */\nfunction markdownExtract() {\n const re = /<(Markdown|markdown)((?:\\s[^>]*?)?)>([\\s\\S]*?)<\\/\\1>/g\n const selfClosingRe = /<(Markdown|markdown)((?:\\s[^>]*?\\bsrc\\s*=\\s*\"[^\"]*\"[^>]*?))\\/>/g\n\n return {\n name: 'maizzle:markdown-extract',\n enforce: 'pre' as const,\n transform(code: string, id: string) {\n if (!id.endsWith('.vue') && !id.endsWith('.md')) return\n if (!code.includes('Markdown') && !code.includes('markdown')) return\n\n let transformed = code\n\n // Handle <Markdown>content</Markdown>\n transformed = transformed.replace(re, (_match, tag, attrs, content) => {\n if (/(?:^|\\s):content\\b/.test(attrs) || /v-bind:content\\b/.test(attrs)) return _match\n\n const stripped = content.replace(/^\\n+/, '').replace(/\\s+$/, '')\n if (!stripped) return _match\n\n const minIndent = stripped.match(/^[ \\t]*(?=\\S)/gm)\n ?.reduce((min: number, ws: string) => Math.min(min, ws.length), Infinity) ?? 0\n\n const dedented = minIndent > 0\n ? stripped.replace(new RegExp(`^[ \\\\t]{${minIndent}}`, 'gm'), '')\n : stripped\n\n const escaped = dedented\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n\n return `<${tag}${attrs} content=\"${escaped}\" />`\n })\n\n // Handle <Markdown src=\"./file.md\" /> — resolve and inline file content\n transformed = transformed.replace(selfClosingRe, (_match, tag, attrs) => {\n const srcMatch = attrs.match(/\\bsrc\\s*=\\s*\"([^\"]*)\"/)\n if (!srcMatch) return _match\n\n const srcPath = srcMatch[1]\n const resolvedPath = resolve(dirname(id), srcPath)\n\n let fileContent: string\n try {\n fileContent = readFileSync(resolvedPath, 'utf-8').trim()\n } catch {\n return _match\n }\n\n // Remove src prop, add content prop\n const cleanAttrs = attrs.replace(/\\s*\\bsrc\\s*=\\s*\"[^\"]*\"/, '')\n const escaped = fileContent\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n\n return `<${tag}${cleanAttrs} content=\"${escaped}\" />`\n })\n\n if (transformed !== code) {\n return { code: transformed, map: null }\n }\n },\n }\n}\n\nconst vuePkgDir = dirname(fileURLToPath(import.meta.resolve('vue/package.json')))\nconst vueServerRendererPkgDir = dirname(fileURLToPath(import.meta.resolve('@vue/server-renderer/package.json')))\nconst unheadVuePkgDir = resolve(dirname(fileURLToPath(import.meta.resolve('@unhead/vue'))), '..')\nconst vueRouterPkgDir = dirname(fileURLToPath(import.meta.resolve('vue-router/package.json')))\n\nexport interface RenderedTemplate {\n html: string\n doctype?: string\n templateConfig: MaizzleConfig\n sfcEventHandlers: RenderContext['sfcEventHandlers']\n plaintext?: RenderContext['plaintext']\n}\n\nexport interface Renderer {\n render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate>\n invalidate(filePath: string): Promise<void>\n invalidateAll(): Promise<void>\n close(): Promise<void>\n}\n\nexport interface CreateRendererOptions {\n /** Generate .d.ts files for auto-imports and components (default: false) */\n dts?: boolean\n /** Options passed to unplugin-vue-markdown */\n markdown?: MarkdownConfig\n /** Root directory for resolving user component dirs and .d.ts output */\n root?: string\n /** Additional component directories to register for auto-import */\n componentDirs?: string[]\n}\n\n/**\n * Lightweight Vite SSR loader for rendering Vue SFC email templates.\n *\n * Uses only Vue + unplugin for component/auto-import resolution.\n * Tailwind CSS compilation is handled by the transformer pipeline.\n */\nexport async function createRenderer(\n options: CreateRendererOptions = {},\n): Promise<Renderer> {\n const { dts = false, markdown: markdownOptionsRaw, root = process.cwd(), componentDirs = [] } = options\n const { shikiTheme = 'github-light', ...markdownOptions } = markdownOptionsRaw ?? {}\n\n const dtsDir = isLaravel()\n ? resolve(process.cwd(), 'resources/js/types/maizzle')\n : resolve(root, '.maizzle')\n\n const VIRTUAL_SFC_ID = 'virtual:maizzle-sfc.vue'\n let virtualSfcSource = ''\n\n const server = await createServer({\n configFile: false,\n plugins: [\n codeBlockExtract(),\n markdownExtract(),\n {\n name: 'maizzle:virtual-sfc',\n resolveId(id) {\n if (id === VIRTUAL_SFC_ID) return id\n },\n load(id) {\n if (id === VIRTUAL_SFC_ID) return virtualSfcSource\n },\n },\n vue({\n include: [/\\.vue$/, /\\.md$/],\n template: {\n transformAssetUrls: false,\n },\n }),\n Markdown(merge(markdownOptions ?? {}, {\n headEnabled: true,\n wrapperDiv: false,\n wrapperClasses: 'prose',\n markdownOptions: {\n async highlight(code: string, lang: string) {\n const { codeToHtml } = await import('shiki')\n return codeToHtml(code, { lang, theme: shikiTheme })\n },\n },\n markdownSetup(md: MarkdownExit) {\n const wrapPre = (html: string) =>\n `<table class=\"w-full\"><tr><td class=\"max-w-0 mso-padding-alt-4\">${html}</td></tr></table>\\n`\n\n const defaultFence = md.renderer.rules.fence!\n md.renderer.rules.fence = (...args) => {\n const result = defaultFence(...args)\n if (typeof result === 'string') return wrapPre(result)\n return result.then(wrapPre)\n }\n\n const defaultCodeBlock = md.renderer.rules.code_block!\n md.renderer.rules.code_block = (...args) => wrapPre(defaultCodeBlock(...args) as string)\n },\n })),\n AutoImport({\n dirs: [\n resolve(__dirname, '../composables'),\n resolve(__dirname, '../filters'),\n ],\n imports: ['vue', unheadVueComposablesImports],\n dts: dts ? resolve(dtsDir, 'auto-imports.d.ts') : false,\n }),\n Components({\n extensions: ['vue', 'md'],\n include: [/\\.vue$/, /\\.vue\\?vue/, /\\.md$/],\n dirs: [\n resolve(__dirname, '../components'),\n resolve(root, 'components'),\n ...componentDirs,\n ],\n dts: dts ? resolve(dtsDir, 'components.d.ts') : false,\n }),\n ],\n resolve: {\n alias: {\n 'vue/server-renderer': resolve(vueServerRendererPkgDir, 'dist/server-renderer.esm-bundler.js'),\n 'vue': resolve(vuePkgDir, 'dist/vue.runtime.esm-bundler.js'),\n 'vue-router': vueRouterPkgDir,\n '@unhead/vue/server': resolve(unheadVuePkgDir, 'dist/server.mjs'),\n '@unhead/vue': resolve(unheadVuePkgDir, 'dist/index.mjs'),\n },\n },\n server: {\n middlewareMode: true,\n hmr: false,\n watch: null,\n fs: {\n allow: [process.cwd(), root, ...componentDirs, vuePkgDir, vueServerRendererPkgDir, unheadVuePkgDir, vueRouterPkgDir],\n },\n },\n appType: 'custom',\n logLevel: 'silent',\n optimizeDeps: {\n noDiscovery: true,\n },\n })\n\n return {\n async render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate> {\n let component: Component\n let configKey: InjectionKey<MaizzleConfig>\n let contextKey: InjectionKey<RenderContext>\n\n if (typeof input === 'string') {\n // String input goes through Vite — must use ssrLoadModule for injection keys\n // so they share the same module instance as the SFC\n const configModule = await server.ssrLoadModule(resolve(__dirname, '../composables/useConfig'))\n const contextModule = await server.ssrLoadModule(resolve(__dirname, '../composables/renderContext'))\n configKey = configModule.MaizzleConfigKey\n contextKey = contextModule.RenderContextKey\n\n if (input.includes('<template') || input.includes('<script')) {\n virtualSfcSource = input\n const mod = server.moduleGraph.getModuleById(VIRTUAL_SFC_ID)\n if (mod) server.moduleGraph.invalidateModule(mod)\n component = (await server.ssrLoadModule(VIRTUAL_SFC_ID)).default\n } else {\n component = (await server.ssrLoadModule(input)).default\n }\n } else {\n // Pre-compiled component — use directly imported keys\n component = input\n configKey = MaizzleConfigKey\n contextKey = RenderContextKey\n }\n\n const renderContext: RenderContext = {\n doctype: undefined,\n sfcConfig: undefined,\n sfcEventHandlers: [],\n }\n\n const head = createHead({ disableDefaults: true })\n const app = createSSRApp(component)\n app.use(head)\n app.provide(configKey, config)\n app.provide(contextKey, renderContext)\n\n const ssrContext: Record<string, any> = {}\n let html: string = await renderToString(app, ssrContext)\n\n const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)\n\n // Inject head entries into the rendered HTML\n if (htmlAttrs) {\n html = html.replace(/<html([^>]*)>/, `<html$1 ${htmlAttrs}>`)\n }\n if (headTags) {\n html = html.replace('</head>', `${headTags}\\n</head>`)\n }\n if (bodyAttrs) {\n html = html.replace(/<body([^>]*)>/, `<body$1 ${bodyAttrs}>`)\n }\n if (bodyTagsOpen) {\n html = html.replace(/<body([^>]*)>/, `<body$1>\\n${bodyTagsOpen}`)\n }\n if (bodyTags) {\n html = html.replace('</body>', `${bodyTags}\\n</body>`)\n }\n\n // Inject SSR teleport content into their target elements\n if (ssrContext.teleports) {\n const { parse: parseDom, serialize: serializeDom, walk } = await import('../utils/ast/index.ts')\n let dom = parseDom(html)\n\n for (const [rawTarget, content] of Object.entries(ssrContext.teleports) as [string, string][]) {\n if (!content) continue\n\n const prepend = rawTarget.endsWith(':start')\n const target = prepend ? rawTarget.slice(0, -6) : rawTarget\n const targetChildren = parseDom(content)\n\n walk(dom, (node) => {\n const el = node as import('domhandler').Element\n\n if (!el.name) return\n\n const matched\n = target === el.name\n || (target.startsWith('#') && el.attribs?.id === target.slice(1))\n || (target.startsWith('.') && el.attribs?.class?.split(/\\s+/).includes(target.slice(1)))\n\n if (matched) {\n for (const child of targetChildren) {\n child.parent = el as any\n }\n\n el.children = prepend\n ? [...targetChildren, ...(el.children || [])] as any\n : [...(el.children || []), ...targetChildren] as any\n }\n })\n }\n\n html = serializeDom(dom)\n }\n\n // Inject preview/preheader text from usePreviewText() composable\n if (renderContext.previewText) {\n const { text, fillerCount, shyCount } = renderContext.previewText\n const filler = '\\u2007\\u034F '.repeat(fillerCount)\n const shys = '\\u00AD '.repeat(shyCount)\n const previewHtml = `<div style=\"display:none\">${text}${filler}${shys}\\u00A0</div>`\n html = html.replace(/<body([^>]*)>/, `<body$1>${previewHtml}`)\n }\n\n return {\n html,\n doctype: renderContext.doctype,\n templateConfig: renderContext.sfcConfig ?? config,\n sfcEventHandlers: renderContext.sfcEventHandlers,\n plaintext: renderContext.plaintext,\n }\n },\n\n async invalidate(filePath: string): Promise<void> {\n const mod = await server.moduleGraph.getModuleByUrl(filePath)\n if (mod) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async invalidateAll(): Promise<void> {\n for (const mod of server.moduleGraph.idToModuleMap.values()) {\n server.moduleGraph.invalidateModule(mod)\n }\n },\n\n async close(): Promise<void> {\n await server.close()\n },\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.mts","names":[],"sources":["../src/serve.ts"],"mappings":";;;;UAwBiB,YAAA;EACf,MAAA,GAAS,OAAA,CAAQ,aAAA;;EAEjB,IAAA;EAH2B;EAK3B,MAAA;AAAA;;;;;;;;AAYF;;iBAAsB,KAAA,CAAM,OAAA,GAAS,YAAA,GAAiB,OAAA,CAAA,aAAA;AAAA,iBAqdtC,WAAA,CAAY,MAAA,EAAQ,aAAA,EAAe,WAAA"}
1
+ {"version":3,"file":"serve.d.mts","names":[],"sources":["../src/serve.ts"],"mappings":";;;;UAyBiB,YAAA;EACf,MAAA,GAAS,OAAA,CAAQ,aAAA;;EAEjB,IAAA;EAH2B;EAK3B,MAAA;AAAA;;;;;;;;AAYF;;iBAAsB,KAAA,CAAM,OAAA,GAAS,YAAA,GAAiB,OAAA,CAAA,aAAA;AAAA,iBAuiBtC,WAAA,CAAY,MAAA,EAAQ,aAAA,EAAe,WAAA"}
package/dist/serve.mjs CHANGED
@@ -4,6 +4,7 @@ import { createRenderer } from "./render/createRenderer.mjs";
4
4
  import { createPlaintext } from "./plaintext.mjs";
5
5
  import { serveCompatibility } from "./server/compatibility.mjs";
6
6
  import { serveLint } from "./server/linter.mjs";
7
+ import { sendEmail } from "./server/email.mjs";
7
8
  import { createRequire } from "node:module";
8
9
  import { readFileSync } from "node:fs";
9
10
  import { basename, dirname, resolve } from "node:path";
@@ -32,7 +33,7 @@ async function serve(options = {}) {
32
33
  const start = performance.now();
33
34
  let config = await resolveConfig(options.config);
34
35
  const port = config.server?.port ?? 3e3;
35
- const renderer = await createRenderer({
36
+ let renderer = await createRenderer({
36
37
  dts: true,
37
38
  markdown: config.markdown,
38
39
  root: config.root,
@@ -170,7 +171,16 @@ function maizzleDevPlugin(config, renderer, configInput) {
170
171
  }
171
172
  });
172
173
  server.watcher.on("change", async (file) => {
173
- if (watchPaths.some((p) => file.endsWith(p))) config = await resolveConfig(configInput);
174
+ if (watchPaths.some((p) => file.endsWith(p))) {
175
+ config = await resolveConfig(configInput);
176
+ await renderer.close();
177
+ renderer = await createRenderer({
178
+ dts: true,
179
+ markdown: config.markdown,
180
+ root: config.root,
181
+ componentDirs: [config.components?.source ?? []].flat()
182
+ });
183
+ }
174
184
  await renderer.invalidateAll();
175
185
  if (isTemplateFile(file) || watchPaths.some((p) => file.endsWith(p))) server.ws.send({
176
186
  type: "custom",
@@ -183,11 +193,13 @@ function maizzleDevPlugin(config, renderer, configInput) {
183
193
  if (url === "/__maizzle/templates") return serveTemplateList(config, res);
184
194
  if (url.startsWith("/__maizzle/render/")) return await serveRenderedTemplate(url, config, renderer, res);
185
195
  if (url.startsWith("/__maizzle/source/")) return await serveHighlightedSource(url, config, renderer, res);
186
- if (url.startsWith("/__maizzle/compatibility/")) return await serveCompatibility(url, config, res);
187
- if (url.startsWith("/__maizzle/lint/")) return await serveLint(url, config, res);
196
+ if (url === "/__maizzle/compatibility" && req.method === "POST") return await serveCompatibility(req, res);
197
+ if (url.startsWith("/__maizzle/lint/")) return serveLint(url, res);
188
198
  if (url.startsWith("/__maizzle/vue-source/")) return await serveVueSource(url, config, res);
189
199
  if (url.startsWith("/__maizzle/plaintext/")) return await servePlaintext(url, config, renderer, res);
190
200
  if (url.startsWith("/__maizzle/stats/")) return await serveStats(url, config, renderer, res);
201
+ if (url.startsWith("/__maizzle/email/") && req.method === "POST") return await serveEmailEndpoint(url, req, res, config, renderer);
202
+ if (url === "/__maizzle/email-config") return serveEmailConfig(config, res);
191
203
  next();
192
204
  });
193
205
  return () => {
@@ -392,6 +404,74 @@ async function serveStats(url, config, renderer, res) {
392
404
  res.end(JSON.stringify({ error: error.message }));
393
405
  }
394
406
  }
407
+ async function serveEmailEndpoint(url, req, res, config, renderer) {
408
+ const templateSlug = url.replace("/__maizzle/email/", "").replace(/\?.*$/, "");
409
+ const match = (await glob(config.content ?? ["emails/**/*.vue"])).find((t) => t.replace(/\.(vue|md)$/, "") === templateSlug);
410
+ if (!match) {
411
+ res.statusCode = 404;
412
+ res.end(JSON.stringify({
413
+ success: false,
414
+ message: "Template not found"
415
+ }));
416
+ return;
417
+ }
418
+ let body = "";
419
+ for await (const chunk of req) body += chunk;
420
+ let payload;
421
+ try {
422
+ payload = JSON.parse(body);
423
+ } catch {
424
+ res.statusCode = 400;
425
+ res.end(JSON.stringify({
426
+ success: false,
427
+ message: "Invalid JSON"
428
+ }));
429
+ return;
430
+ }
431
+ if (!payload.to?.length) {
432
+ res.statusCode = 400;
433
+ res.end(JSON.stringify({
434
+ success: false,
435
+ message: "Missing recipients"
436
+ }));
437
+ return;
438
+ }
439
+ try {
440
+ const absolutePath = resolve(match);
441
+ await renderer.invalidateAll();
442
+ const rendered = await renderer.render(absolutePath, config);
443
+ let html = rendered.html;
444
+ const templateConfig = rendered.templateConfig;
445
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
446
+ html = await runTransformers(html, templateConfig, absolutePath, doctype);
447
+ html = `${doctype}\n${html}`;
448
+ const text = createPlaintext(html);
449
+ const result = await sendEmail({
450
+ to: payload.to,
451
+ subject: payload.subject,
452
+ html,
453
+ text
454
+ }, config, templateConfig);
455
+ res.setHeader("Content-Type", "application/json");
456
+ res.end(JSON.stringify(result));
457
+ } catch (error) {
458
+ res.statusCode = 500;
459
+ res.end(JSON.stringify({
460
+ success: false,
461
+ message: error.message
462
+ }));
463
+ }
464
+ }
465
+ function serveEmailConfig(config, res) {
466
+ const emailConfig = config.server?.email;
467
+ res.setHeader("Content-Type", "application/json");
468
+ res.end(JSON.stringify({
469
+ to: emailConfig?.to ? Array.isArray(emailConfig.to) ? emailConfig.to : [emailConfig.to] : [],
470
+ from: emailConfig?.from ?? "",
471
+ subject: emailConfig?.subject ?? "",
472
+ hasTransport: !!emailConfig?.transport
473
+ }));
474
+ }
395
475
  function printBanner(server, startupTime) {
396
476
  const info = server.config.logger.info;
397
477
  const time = startupTime ?? server._maizzleStartupTime;