@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.
- package/dist/components/Body.vue +105 -36
- package/dist/components/Button.vue +4 -1
- package/dist/components/CodeBlock.vue +11 -18
- package/dist/components/CodeInline.vue +6 -1
- package/dist/components/Column.vue +30 -5
- package/dist/components/Container.vue +10 -2
- package/dist/components/Divider.vue +28 -0
- package/dist/components/Head.vue +22 -0
- package/dist/components/Heading.vue +28 -0
- package/dist/components/Html.vue +98 -47
- package/dist/components/Layout.vue +93 -0
- package/dist/components/Link.vue +26 -0
- package/dist/components/Markdown.vue +83 -0
- package/dist/components/Outlook.vue +36 -0
- package/dist/components/Overlap.vue +25 -5
- package/dist/components/{Preview.vue → Preheader.vue} +1 -1
- package/dist/components/Row.vue +16 -5
- package/dist/components/Section.vue +83 -0
- package/dist/components/Text.vue +29 -0
- package/dist/components/Vml.vue +165 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
- package/dist/render/createRenderer.d.mts +2 -3
- package/dist/render/createRenderer.d.mts.map +1 -1
- package/dist/render/createRenderer.mjs +67 -4
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +84 -4
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +1 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +30 -16
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/email.d.mts +17 -0
- package/dist/server/email.d.mts.map +1 -0
- package/dist/server/email.mjs +41 -0
- package/dist/server/email.mjs.map +1 -0
- package/dist/server/linter.d.mts +1 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +60 -71
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/ui/App.vue +205 -69
- package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
- package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
- package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
- package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
- package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
- package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
- package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
- package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
- package/dist/server/ui/components/ui/toggle/index.ts +3 -3
- package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
- package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
- package/dist/server/ui/main.css +20 -20
- package/dist/server/ui/pages/Home.vue +12 -5
- package/dist/server/ui/pages/Preview.vue +495 -211
- package/dist/transformers/inlineCSS.d.mts +1 -14
- package/dist/transformers/inlineCSS.d.mts.map +1 -1
- package/dist/transformers/inlineCSS.mjs +25 -34
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/purgeCSS.d.mts.map +1 -1
- package/dist/transformers/purgeCSS.mjs +67 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.mjs +3 -7
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +47 -29
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/package.json +7 -3
- package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
- package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
- package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
- package/dist/server/ui/components/ui/resizable/index.ts +0 -3
package/dist/components/Vml.vue
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
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:
|
|
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 ? '
|
|
53
|
-
`stroke="${props.strokecolor ? '
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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?:
|
|
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":"
|
|
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
|
-
|
|
40
|
-
|
|
40
|
+
return `<${tag}${attrs} code="${(minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">")}" />`;
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">")}" />`;
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">")}" />`;
|
|
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:
|
|
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, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\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, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\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, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\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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"}
|
package/dist/serve.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.mts","names":[],"sources":["../src/serve.ts"],"mappings":";;;;
|
|
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
|
-
|
|
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)))
|
|
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
|
|
187
|
-
if (url.startsWith("/__maizzle/lint/")) return
|
|
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;
|