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

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 (61) hide show
  1. package/dist/build.mjs +3 -2
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/components/Body.vue +42 -0
  4. package/dist/components/Button.vue +65 -14
  5. package/dist/components/CodeBlock.vue +75 -0
  6. package/dist/components/CodeInline.vue +44 -0
  7. package/dist/components/Column.vue +61 -0
  8. package/dist/components/Container.vue +40 -0
  9. package/dist/components/Head.vue +8 -0
  10. package/dist/components/Html.vue +53 -0
  11. package/dist/components/Image.vue +70 -0
  12. package/dist/components/Overlap.vue +60 -0
  13. package/dist/components/Preview.vue +20 -0
  14. package/dist/components/Row.vue +80 -0
  15. package/dist/components/Spacer.vue +50 -7
  16. package/dist/composables/renderContext.d.mts +5 -0
  17. package/dist/composables/renderContext.d.mts.map +1 -1
  18. package/dist/composables/renderContext.mjs.map +1 -1
  19. package/dist/composables/usePreviewText.d.mts +24 -0
  20. package/dist/composables/usePreviewText.d.mts.map +1 -0
  21. package/dist/composables/usePreviewText.mjs +29 -0
  22. package/dist/composables/usePreviewText.mjs.map +1 -0
  23. package/dist/index.d.mts +3 -2
  24. package/dist/index.mjs +2 -1
  25. package/dist/render/createRenderer.d.mts +1 -0
  26. package/dist/render/createRenderer.d.mts.map +1 -1
  27. package/dist/render/createRenderer.mjs +60 -1
  28. package/dist/render/createRenderer.mjs.map +1 -1
  29. package/dist/render/index.mjs +3 -2
  30. package/dist/render/index.mjs.map +1 -1
  31. package/dist/serve.d.mts.map +1 -1
  32. package/dist/serve.mjs +31 -20
  33. package/dist/serve.mjs.map +1 -1
  34. package/dist/server/ui/pages/Preview.vue +11 -5
  35. package/dist/transformers/entities.d.mts.map +1 -1
  36. package/dist/transformers/entities.mjs +3 -0
  37. package/dist/transformers/entities.mjs.map +1 -1
  38. package/dist/transformers/filters/defaults.d.mts +6 -0
  39. package/dist/transformers/filters/defaults.d.mts.map +1 -0
  40. package/dist/transformers/filters/defaults.mjs +78 -0
  41. package/dist/transformers/filters/defaults.mjs.map +1 -0
  42. package/dist/transformers/filters/index.d.mts +22 -0
  43. package/dist/transformers/filters/index.d.mts.map +1 -0
  44. package/dist/transformers/filters/index.mjs +67 -0
  45. package/dist/transformers/filters/index.mjs.map +1 -0
  46. package/dist/transformers/index.d.mts +9 -8
  47. package/dist/transformers/index.d.mts.map +1 -1
  48. package/dist/transformers/index.mjs +15 -10
  49. package/dist/transformers/index.mjs.map +1 -1
  50. package/dist/transformers/tailwindcss.d.mts +6 -2
  51. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  52. package/dist/transformers/tailwindcss.mjs +49 -21
  53. package/dist/transformers/tailwindcss.mjs.map +1 -1
  54. package/dist/types/config.d.mts +15 -1
  55. package/dist/types/config.d.mts.map +1 -1
  56. package/dist/types/index.d.mts +2 -2
  57. package/dist/utils/ast/serializer.d.mts +3 -2
  58. package/dist/utils/ast/serializer.d.mts.map +1 -1
  59. package/dist/utils/ast/serializer.mjs +24 -0
  60. package/dist/utils/ast/serializer.mjs.map +1 -1
  61. package/package.json +1 -1
package/dist/serve.mjs CHANGED
@@ -151,20 +151,27 @@ function maizzleDevPlugin(config, renderer, configInput) {
151
151
  const userWatchPaths = config.server?.watch ?? [];
152
152
  const watchPaths = [...defaultWatchPaths, ...userWatchPaths];
153
153
  for (const watchPath of watchPaths) server.watcher.add(watchPath);
154
- server.watcher.on("add", (file) => {
155
- if (isTemplateFile(file)) server.ws.send({
156
- type: "custom",
157
- event: "maizzle:templates-changed"
158
- });
154
+ server.watcher.on("add", async (file) => {
155
+ if (isTemplateFile(file)) {
156
+ await renderer.invalidateAll();
157
+ server.ws.send({
158
+ type: "custom",
159
+ event: "maizzle:templates-changed"
160
+ });
161
+ }
159
162
  });
160
- server.watcher.on("unlink", (file) => {
161
- if (isTemplateFile(file)) server.ws.send({
162
- type: "custom",
163
- event: "maizzle:templates-changed"
164
- });
163
+ server.watcher.on("unlink", async (file) => {
164
+ if (isTemplateFile(file)) {
165
+ await renderer.invalidateAll();
166
+ server.ws.send({
167
+ type: "custom",
168
+ event: "maizzle:templates-changed"
169
+ });
170
+ }
165
171
  });
166
172
  server.watcher.on("change", async (file) => {
167
173
  if (watchPaths.some((p) => file.endsWith(p))) config = await resolveConfig(configInput);
174
+ await renderer.invalidateAll();
168
175
  if (isTemplateFile(file) || watchPaths.some((p) => file.endsWith(p))) server.ws.send({
169
176
  type: "custom",
170
177
  event: "maizzle:template-updated",
@@ -229,12 +236,13 @@ async function serveRenderedTemplate(url, config, renderer, res) {
229
236
  }
230
237
  try {
231
238
  const absolutePath = resolve(match);
232
- await renderer.invalidate(absolutePath);
239
+ await renderer.invalidateAll();
233
240
  const rendered = await renderer.render(absolutePath, config);
234
241
  let html = rendered.html;
235
242
  const templateConfig = rendered.templateConfig;
236
- html = await runTransformers(html, templateConfig, absolutePath);
237
- html = `${rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>"}\n${html}`;
243
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
244
+ html = await runTransformers(html, templateConfig, absolutePath, doctype);
245
+ html = `${doctype}\n${html}`;
238
246
  res.setHeader("Content-Type", "text/html");
239
247
  res.end(html);
240
248
  } catch (error) {
@@ -260,12 +268,13 @@ async function serveHighlightedSource(url, config, renderer, res) {
260
268
  }
261
269
  try {
262
270
  const absolutePath = resolve(match);
263
- await renderer.invalidate(absolutePath);
271
+ await renderer.invalidateAll();
264
272
  const rendered = await renderer.render(absolutePath, config);
265
273
  let html = rendered.html;
266
274
  const templateConfig = rendered.templateConfig;
267
- html = await runTransformers(html, templateConfig, absolutePath);
268
- html = `${rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>"}\n${html}`;
275
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
276
+ html = await runTransformers(html, templateConfig, absolutePath, doctype);
277
+ html = `${doctype}\n${html}`;
269
278
  const highlighted = (await getHighlighter()).codeToHtml(html, {
270
279
  lang: "html",
271
280
  theme: "laserwave",
@@ -315,11 +324,12 @@ async function servePlaintext(url, config, renderer, res) {
315
324
  }
316
325
  try {
317
326
  const absolutePath = resolve(match);
318
- await renderer.invalidate(absolutePath);
327
+ await renderer.invalidateAll();
319
328
  const rendered = await renderer.render(absolutePath, config);
320
329
  let html = rendered.html;
321
330
  const templateConfig = rendered.templateConfig;
322
- html = await runTransformers(html, templateConfig, absolutePath);
331
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
332
+ html = await runTransformers(html, templateConfig, absolutePath, doctype);
323
333
  const plaintext = createPlaintext(html);
324
334
  res.setHeader("Content-Type", "text/plain");
325
335
  res.end(plaintext);
@@ -359,11 +369,12 @@ async function serveStats(url, config, renderer, res) {
359
369
  }
360
370
  try {
361
371
  const absolutePath = resolve(match);
362
- await renderer.invalidate(absolutePath);
372
+ await renderer.invalidateAll();
363
373
  const rendered = await renderer.render(absolutePath, config);
364
374
  let html = rendered.html;
365
375
  const templateConfig = rendered.templateConfig;
366
- html = await runTransformers(html, templateConfig, absolutePath);
376
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
377
+ html = await runTransformers(html, templateConfig, absolutePath, doctype);
367
378
  const sizeBytes = Buffer.byteLength(html, "utf-8");
368
379
  const totalImages = (html.match(/<img\b[^>]*>/gi) || []).length + (html.match(/url\s*\([^)]+\)/gi) || []).length;
369
380
  const links = (html.match(/<a\b[^>]*href\s*=/gi) || []).length;
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../src/serve.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { dirname, resolve, basename } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { createRequire } from 'node:module'\nimport { createServer, createLogger, type ViteDevServer } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport tailwindcss from '@tailwindcss/vite'\nimport { glob } from 'tinyglobby'\nimport { createHighlighter, type Highlighter } from 'shiki'\nimport { createPlaintext } from './plaintext.ts'\nimport { resolveConfig } from './config/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer, type Renderer } from './render/createRenderer.ts'\nimport { serveCompatibility } from './server/compatibility.ts'\nimport { serveLint } from './server/linter.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst devUIDir = resolve(__dirname, 'server/ui')\n\nconst require = createRequire(import.meta.url)\nconst frameworkNodeModules = resolve(dirname(require.resolve('vue/package.json')), '..')\nconst vuePkgDir = dirname(require.resolve('vue/package.json'))\n\nexport interface ServeOptions {\n config?: Partial<MaizzleConfig> | string\n /** Expose the server on the network (e.g. --host) */\n host?: boolean | string\n /** When true, suppresses the banner/URL output (used by the Vite plugin, which prints its own) */\n silent?: boolean\n}\n\n/**\n * Start the Maizzle dev server.\n *\n * Creates two things:\n * 1. A Vite dev server for the dev UI (sidebar + preview, with Vue + Tailwind for the UI itself)\n * 2. A Renderer instance for SSR rendering email templates\n *\n * Template rendering goes through the Renderer, not the Vite dev server.\n */\nexport async function serve(options: ServeOptions = {}) {\n const start = performance.now()\n\n let config = await resolveConfig(options.config)\n const port = config.server?.port ?? 3000\n\n // Create a renderer for SSR rendering email templates (with dts for dev)\n const renderer = await createRenderer({ dts: true, markdown: config.markdown, root: config.root, componentDirs: [config.components?.source ?? []].flat() })\n\n const server = await createServer({\n configFile: false,\n plugins: [\n // Vue and Tailwind are only for the dev UI SPA, not for email templates\n vue(),\n tailwindcss(),\n maizzleDevPlugin(config, renderer, options.config),\n ],\n resolve: {\n dedupe: ['vue'],\n alias: [\n { find: '@', replacement: devUIDir },\n { find: 'vue', replacement: resolve(vuePkgDir, 'dist/vue.runtime.esm-bundler.js') },\n { find: 'vue-router', replacement: resolve(frameworkNodeModules, 'vue-router') },\n { find: 'reka-ui', replacement: resolve(frameworkNodeModules, 'reka-ui') },\n { find: '@vueuse/core', replacement: resolve(frameworkNodeModules, '@vueuse/core') },\n { find: '@vueuse/shared', replacement: resolve(frameworkNodeModules, '@vueuse/shared') },\n { find: 'lucide-vue-next', replacement: resolve(frameworkNodeModules, 'lucide-vue-next') },\n { find: 'class-variance-authority', replacement: resolve(frameworkNodeModules, 'class-variance-authority') },\n { find: 'clsx', replacement: resolve(frameworkNodeModules, 'clsx') },\n { find: 'tailwind-merge', replacement: resolve(frameworkNodeModules, 'tailwind-merge') },\n ],\n },\n cacheDir: resolve(devUIDir, '.vite'),\n optimizeDeps: {\n noDiscovery: true,\n include: [\n 'vue',\n 'vue-router',\n 'lucide-vue-next',\n '@vueuse/core',\n '@vueuse/shared',\n 'reka-ui',\n 'class-variance-authority',\n 'clsx',\n 'tailwind-merge',\n ],\n },\n server: {\n port,\n host: options.host,\n fs: {\n allow: [process.cwd(), config.root ?? process.cwd(), devUIDir, frameworkNodeModules],\n },\n },\n customLogger: customLogger(),\n })\n\n // Store renderer ref on server for cleanup\n const originalClose = server.close.bind(server)\n server.close = async () => {\n await renderer.close()\n return originalClose()\n }\n\n await server.listen()\n\n const startupTime = Math.round(performance.now() - start)\n\n if (!options.silent) {\n printBanner(server, startupTime)\n }\n\n // Expose startup time so the plugin can print it later\n ; (server as any)._maizzleStartupTime = startupTime\n\n return server\n}\n\n/**\n * Internal Vite plugin that adds Maizzle middleware and file watching to the dev UI server.\n */\nfunction maizzleDevPlugin(\n config: MaizzleConfig,\n renderer: Renderer,\n configInput: Partial<MaizzleConfig> | string | undefined,\n) {\n return {\n name: 'maizzle:dev',\n enforce: 'pre' as const,\n\n hotUpdate: {\n order: 'pre' as const,\n handler({ file }: { file: string }) {\n // Prevent Tailwind/Vue from triggering a full reload for email template files.\n // Maizzle handles these via custom HMR events in the watcher below.\n if (isTemplateFile(file)) {\n return []\n }\n },\n },\n\n configureServer(server: ViteDevServer) {\n // File watching\n const defaultWatchPaths = [\n 'maizzle.config.js',\n 'maizzle.config.ts',\n 'tailwind.config.js',\n 'tailwind.config.ts',\n ]\n\n const userWatchPaths = config.server?.watch ?? []\n const watchPaths = [...defaultWatchPaths, ...userWatchPaths]\n\n for (const watchPath of watchPaths) {\n server.watcher.add(watchPath)\n }\n\n server.watcher.on('add', (file) => {\n if (isTemplateFile(file)) {\n server.ws.send({ type: 'custom', event: 'maizzle:templates-changed' })\n }\n })\n\n server.watcher.on('unlink', (file) => {\n if (isTemplateFile(file)) {\n server.ws.send({ type: 'custom', event: 'maizzle:templates-changed' })\n }\n })\n\n server.watcher.on('change', async (file) => {\n if (watchPaths.some(p => file.endsWith(p))) {\n config = await resolveConfig(configInput)\n }\n\n if (\n isTemplateFile(file)\n || watchPaths.some(p => file.endsWith(p))\n ) {\n server.ws.send({ type: 'custom', event: 'maizzle:template-updated', data: { file } })\n }\n })\n\n // API middleware (before Vite's middleware)\n server.middlewares.use(async (req: any, res: any, next: any) => {\n const url = req.url || '/'\n\n if (url === '/__maizzle/templates') {\n return serveTemplateList(config, res)\n }\n\n if (url.startsWith('/__maizzle/render/')) {\n return await serveRenderedTemplate(url, config, renderer, res)\n }\n\n if (url.startsWith('/__maizzle/source/')) {\n return await serveHighlightedSource(url, config, renderer, res)\n }\n\n if (url.startsWith('/__maizzle/compatibility/')) {\n return await serveCompatibility(url, config, res)\n }\n\n if (url.startsWith('/__maizzle/lint/')) {\n return await serveLint(url, config, res)\n }\n\n if (url.startsWith('/__maizzle/vue-source/')) {\n return await serveVueSource(url, config, res)\n }\n\n if (url.startsWith('/__maizzle/plaintext/')) {\n return await servePlaintext(url, config, renderer, res)\n }\n\n if (url.startsWith('/__maizzle/stats/')) {\n return await serveStats(url, config, renderer, res)\n }\n\n next()\n })\n\n // Dev UI fallback (after Vite's middleware)\n return () => {\n server.middlewares.use(async (req: any, res: any, next: any) => {\n if (isNavigationRequest(req)) {\n return await serveDevUI(server, res, req.url || '/')\n }\n\n next()\n })\n }\n },\n }\n}\n\nfunction isTemplateFile(file: string): boolean {\n return (file.endsWith('.vue') || file.endsWith('.md')) && !file.includes('server/ui')\n}\n\nfunction isNavigationRequest(req: any): boolean {\n const accept = req.headers?.accept || ''\n return req.method === 'GET' && accept.includes('text/html')\n}\n\nasync function serveDevUI(server: ViteDevServer, res: any, url: string) {\n let indexHtml = readFileSync(resolve(devUIDir, 'index.html'), 'utf-8')\n\n indexHtml = indexHtml.replace('./main.ts', `/@fs/${resolve(devUIDir, 'main.ts')}`)\n indexHtml = indexHtml.replace('./favicon.svg', `/@fs/${resolve(devUIDir, 'favicon.svg')}`)\n\n const transformed = await server.transformIndexHtml(url, indexHtml)\n\n res.setHeader('Content-Type', 'text/html')\n res.end(transformed)\n}\n\nasync function serveTemplateList(config: MaizzleConfig, res: any) {\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n\n const data = templates.map(t => ({\n name: basename(t).replace(/\\.(vue|md)$/, ''),\n path: t,\n href: '/' + t.replace(/\\.(vue|md)$/, ''),\n }))\n\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify(data))\n}\n\n/**\n * SSR render a .vue template using the Renderer (not the dev UI server).\n */\nasync function serveRenderedTemplate(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/render/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const absolutePath = resolve(match)\n\n // Invalidate the module so changes are picked up\n await renderer.invalidate(absolutePath)\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n\n const templateConfig = rendered.templateConfig\n\n html = await runTransformers(html, templateConfig, absolutePath)\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n html = `${doctype}\\n${html}`\n\n res.setHeader('Content-Type', 'text/html')\n res.end(html)\n } catch (error: any) {\n res.statusCode = 500\n res.end(`<pre>${error.stack || error.message}</pre>`)\n }\n}\n\nlet highlighter: Highlighter | null = null\n\nasync function getHighlighter() {\n if (!highlighter) {\n highlighter = await createHighlighter({\n themes: ['laserwave'],\n langs: ['html', 'vue'],\n })\n }\n return highlighter\n}\n\nasync function serveHighlightedSource(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/source/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const absolutePath = resolve(match)\n\n await renderer.invalidate(absolutePath)\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n\n const templateConfig = rendered.templateConfig\n html = await runTransformers(html, templateConfig, absolutePath)\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n html = `${doctype}\\n${html}`\n\n const hl = await getHighlighter()\n const highlighted = hl.codeToHtml(html, {\n lang: 'html',\n theme: 'laserwave',\n transformers: [{\n line(node, line) {\n node.properties['data-line'] = line\n },\n }],\n })\n\n res.setHeader('Content-Type', 'text/html')\n res.end(highlighted)\n } catch (error: any) {\n res.statusCode = 500\n res.end(`<pre>${error.stack || error.message}</pre>`)\n }\n}\n\nasync function serveVueSource(url: string, config: MaizzleConfig, res: any) {\n const templateSlug = url.replace('/__maizzle/vue-source/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const source = readFileSync(resolve(match), 'utf-8')\n const lang = match.endsWith('.md') ? 'html' : 'vue'\n\n const hl = await getHighlighter()\n const highlighted = hl.codeToHtml(source, {\n lang,\n theme: 'laserwave',\n transformers: [{\n line(node, line) {\n node.properties['data-line'] = line\n },\n }],\n })\n\n res.setHeader('Content-Type', 'text/html')\n res.end(highlighted)\n } catch (error: any) {\n res.statusCode = 500\n res.end(`<pre>${error.stack || error.message}</pre>`)\n }\n}\n\nasync function servePlaintext(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/plaintext/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const absolutePath = resolve(match)\n await renderer.invalidate(absolutePath)\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n const templateConfig = rendered.templateConfig\n html = await runTransformers(html, templateConfig, absolutePath)\n\n const plaintext = createPlaintext(html)\n\n res.setHeader('Content-Type', 'text/plain')\n res.end(plaintext)\n } catch (error: any) {\n res.statusCode = 500\n res.end(error.message)\n }\n}\n\nfunction humanFileSize(bytes: number, si = false, dp = 2) {\n const threshold = si ? 1000 : 1024\n\n if (Math.abs(bytes) < threshold) {\n return bytes + ' B'\n }\n\n const units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n let u = -1\n const r = 10 ** dp\n\n do {\n bytes /= threshold\n ++u\n } while (Math.round(Math.abs(bytes) * r) / r >= threshold && u < units.length - 1)\n\n return bytes.toFixed(dp) + ' ' + units[u]\n}\n\nasync function serveStats(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/stats/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end(JSON.stringify({ error: 'Template not found' }))\n return\n }\n\n try {\n const absolutePath = resolve(match)\n await renderer.invalidate(absolutePath)\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n const templateConfig = rendered.templateConfig\n html = await runTransformers(html, templateConfig, absolutePath)\n\n const sizeBytes = Buffer.byteLength(html, 'utf-8')\n\n // Count images: <img> tags and CSS background images\n const imgTags = (html.match(/<img\\b[^>]*>/gi) || []).length\n const bgImages = (html.match(/url\\s*\\([^)]+\\)/gi) || []).length\n const totalImages = imgTags + bgImages\n\n // Count links\n const links = (html.match(/<a\\b[^>]*href\\s*=/gi) || []).length\n\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n size: {\n bytes: sizeBytes,\n formatted: humanFileSize(sizeBytes),\n },\n images: totalImages,\n links,\n }))\n } catch (error: any) {\n res.statusCode = 500\n res.end(JSON.stringify({ error: error.message }))\n }\n}\n\nexport function printBanner(server: ViteDevServer, startupTime?: number) {\n const info = server.config.logger.info\n const time = startupTime ?? (server as any)._maizzleStartupTime\n\n info('')\n info(` \\x1b[32m\\x1b[1mMAIZZLE\\x1b[0m\\x1b[32m v6.0.0\\x1b[0m \\x1b[2mready in\\x1b[0m \\x1b[1m${time}\\x1b[0m ms`)\n info('')\n server.printUrls()\n info('')\n}\n\nfunction customLogger() {\n const logger = createLogger('info')\n const warn = logger.warn\n\n logger.warn = (message, options) => {\n if (typeof message === 'string' && message.includes('<tr> cannot be child of <table>')) {\n return\n }\n\n warn(message, options)\n }\n\n return logger\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../src/serve.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { dirname, resolve, basename } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { createRequire } from 'node:module'\nimport { createServer, createLogger, type ViteDevServer } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport tailwindcss from '@tailwindcss/vite'\nimport { glob } from 'tinyglobby'\nimport { createHighlighter, type Highlighter } from 'shiki'\nimport { createPlaintext } from './plaintext.ts'\nimport { resolveConfig } from './config/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer, type Renderer } from './render/createRenderer.ts'\nimport { serveCompatibility } from './server/compatibility.ts'\nimport { serveLint } from './server/linter.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst devUIDir = resolve(__dirname, 'server/ui')\n\nconst require = createRequire(import.meta.url)\nconst frameworkNodeModules = resolve(dirname(require.resolve('vue/package.json')), '..')\nconst vuePkgDir = dirname(require.resolve('vue/package.json'))\n\nexport interface ServeOptions {\n config?: Partial<MaizzleConfig> | string\n /** Expose the server on the network (e.g. --host) */\n host?: boolean | string\n /** When true, suppresses the banner/URL output (used by the Vite plugin, which prints its own) */\n silent?: boolean\n}\n\n/**\n * Start the Maizzle dev server.\n *\n * Creates two things:\n * 1. A Vite dev server for the dev UI (sidebar + preview, with Vue + Tailwind for the UI itself)\n * 2. A Renderer instance for SSR rendering email templates\n *\n * Template rendering goes through the Renderer, not the Vite dev server.\n */\nexport async function serve(options: ServeOptions = {}) {\n const start = performance.now()\n\n let config = await resolveConfig(options.config)\n const port = config.server?.port ?? 3000\n\n // Create a renderer for SSR rendering email templates (with dts for dev)\n const renderer = await createRenderer({ dts: true, markdown: config.markdown, root: config.root, componentDirs: [config.components?.source ?? []].flat() })\n\n const server = await createServer({\n configFile: false,\n plugins: [\n // Vue and Tailwind are only for the dev UI SPA, not for email templates\n vue(),\n tailwindcss(),\n maizzleDevPlugin(config, renderer, options.config),\n ],\n resolve: {\n dedupe: ['vue'],\n alias: [\n { find: '@', replacement: devUIDir },\n { find: 'vue', replacement: resolve(vuePkgDir, 'dist/vue.runtime.esm-bundler.js') },\n { find: 'vue-router', replacement: resolve(frameworkNodeModules, 'vue-router') },\n { find: 'reka-ui', replacement: resolve(frameworkNodeModules, 'reka-ui') },\n { find: '@vueuse/core', replacement: resolve(frameworkNodeModules, '@vueuse/core') },\n { find: '@vueuse/shared', replacement: resolve(frameworkNodeModules, '@vueuse/shared') },\n { find: 'lucide-vue-next', replacement: resolve(frameworkNodeModules, 'lucide-vue-next') },\n { find: 'class-variance-authority', replacement: resolve(frameworkNodeModules, 'class-variance-authority') },\n { find: 'clsx', replacement: resolve(frameworkNodeModules, 'clsx') },\n { find: 'tailwind-merge', replacement: resolve(frameworkNodeModules, 'tailwind-merge') },\n ],\n },\n cacheDir: resolve(devUIDir, '.vite'),\n optimizeDeps: {\n noDiscovery: true,\n include: [\n 'vue',\n 'vue-router',\n 'lucide-vue-next',\n '@vueuse/core',\n '@vueuse/shared',\n 'reka-ui',\n 'class-variance-authority',\n 'clsx',\n 'tailwind-merge',\n ],\n },\n server: {\n port,\n host: options.host,\n fs: {\n allow: [process.cwd(), config.root ?? process.cwd(), devUIDir, frameworkNodeModules],\n },\n },\n customLogger: customLogger(),\n })\n\n // Store renderer ref on server for cleanup\n const originalClose = server.close.bind(server)\n server.close = async () => {\n await renderer.close()\n return originalClose()\n }\n\n await server.listen()\n\n const startupTime = Math.round(performance.now() - start)\n\n if (!options.silent) {\n printBanner(server, startupTime)\n }\n\n // Expose startup time so the plugin can print it later\n ; (server as any)._maizzleStartupTime = startupTime\n\n return server\n}\n\n/**\n * Internal Vite plugin that adds Maizzle middleware and file watching to the dev UI server.\n */\nfunction maizzleDevPlugin(\n config: MaizzleConfig,\n renderer: Renderer,\n configInput: Partial<MaizzleConfig> | string | undefined,\n) {\n return {\n name: 'maizzle:dev',\n enforce: 'pre' as const,\n\n hotUpdate: {\n order: 'pre' as const,\n handler({ file }: { file: string }) {\n // Prevent Tailwind/Vue from triggering a full reload for email template files.\n // Maizzle handles these via custom HMR events in the watcher below.\n if (isTemplateFile(file)) {\n return []\n }\n },\n },\n\n configureServer(server: ViteDevServer) {\n // File watching\n const defaultWatchPaths = [\n 'maizzle.config.js',\n 'maizzle.config.ts',\n 'tailwind.config.js',\n 'tailwind.config.ts',\n ]\n\n const userWatchPaths = config.server?.watch ?? []\n const watchPaths = [...defaultWatchPaths, ...userWatchPaths]\n\n for (const watchPath of watchPaths) {\n server.watcher.add(watchPath)\n }\n\n server.watcher.on('add', async (file) => {\n if (isTemplateFile(file)) {\n await renderer.invalidateAll()\n server.ws.send({ type: 'custom', event: 'maizzle:templates-changed' })\n }\n })\n\n server.watcher.on('unlink', async (file) => {\n if (isTemplateFile(file)) {\n await renderer.invalidateAll()\n server.ws.send({ type: 'custom', event: 'maizzle:templates-changed' })\n }\n })\n\n server.watcher.on('change', async (file) => {\n if (watchPaths.some(p => file.endsWith(p))) {\n config = await resolveConfig(configInput)\n }\n\n // Invalidate all renderer modules so component and config changes\n // are picked up on the next render (Tailwind recompiles with fresh content)\n await renderer.invalidateAll()\n\n if (\n isTemplateFile(file)\n || watchPaths.some(p => file.endsWith(p))\n ) {\n server.ws.send({ type: 'custom', event: 'maizzle:template-updated', data: { file } })\n }\n })\n\n // API middleware (before Vite's middleware)\n server.middlewares.use(async (req: any, res: any, next: any) => {\n const url = req.url || '/'\n\n if (url === '/__maizzle/templates') {\n return serveTemplateList(config, res)\n }\n\n if (url.startsWith('/__maizzle/render/')) {\n return await serveRenderedTemplate(url, config, renderer, res)\n }\n\n if (url.startsWith('/__maizzle/source/')) {\n return await serveHighlightedSource(url, config, renderer, res)\n }\n\n if (url.startsWith('/__maizzle/compatibility/')) {\n return await serveCompatibility(url, config, res)\n }\n\n if (url.startsWith('/__maizzle/lint/')) {\n return await serveLint(url, config, res)\n }\n\n if (url.startsWith('/__maizzle/vue-source/')) {\n return await serveVueSource(url, config, res)\n }\n\n if (url.startsWith('/__maizzle/plaintext/')) {\n return await servePlaintext(url, config, renderer, res)\n }\n\n if (url.startsWith('/__maizzle/stats/')) {\n return await serveStats(url, config, renderer, res)\n }\n\n next()\n })\n\n // Dev UI fallback (after Vite's middleware)\n return () => {\n server.middlewares.use(async (req: any, res: any, next: any) => {\n if (isNavigationRequest(req)) {\n return await serveDevUI(server, res, req.url || '/')\n }\n\n next()\n })\n }\n },\n }\n}\n\nfunction isTemplateFile(file: string): boolean {\n return (file.endsWith('.vue') || file.endsWith('.md')) && !file.includes('server/ui')\n}\n\nfunction isNavigationRequest(req: any): boolean {\n const accept = req.headers?.accept || ''\n return req.method === 'GET' && accept.includes('text/html')\n}\n\nasync function serveDevUI(server: ViteDevServer, res: any, url: string) {\n let indexHtml = readFileSync(resolve(devUIDir, 'index.html'), 'utf-8')\n\n indexHtml = indexHtml.replace('./main.ts', `/@fs/${resolve(devUIDir, 'main.ts')}`)\n indexHtml = indexHtml.replace('./favicon.svg', `/@fs/${resolve(devUIDir, 'favicon.svg')}`)\n\n const transformed = await server.transformIndexHtml(url, indexHtml)\n\n res.setHeader('Content-Type', 'text/html')\n res.end(transformed)\n}\n\nasync function serveTemplateList(config: MaizzleConfig, res: any) {\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n\n const data = templates.map(t => ({\n name: basename(t).replace(/\\.(vue|md)$/, ''),\n path: t,\n href: '/' + t.replace(/\\.(vue|md)$/, ''),\n }))\n\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify(data))\n}\n\n/**\n * SSR render a .vue template using the Renderer (not the dev UI server).\n */\nasync function serveRenderedTemplate(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/render/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const absolutePath = resolve(match)\n\n // Invalidate all modules so template + component changes are picked up\n await renderer.invalidateAll()\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n\n const templateConfig = rendered.templateConfig\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n html = `${doctype}\\n${html}`\n\n res.setHeader('Content-Type', 'text/html')\n res.end(html)\n } catch (error: any) {\n res.statusCode = 500\n res.end(`<pre>${error.stack || error.message}</pre>`)\n }\n}\n\nlet highlighter: Highlighter | null = null\n\nasync function getHighlighter() {\n if (!highlighter) {\n highlighter = await createHighlighter({\n themes: ['laserwave'],\n langs: ['html', 'vue'],\n })\n }\n return highlighter\n}\n\nasync function serveHighlightedSource(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/source/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const absolutePath = resolve(match)\n\n await renderer.invalidateAll()\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n\n const templateConfig = rendered.templateConfig\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n\n html = `${doctype}\\n${html}`\n\n const hl = await getHighlighter()\n const highlighted = hl.codeToHtml(html, {\n lang: 'html',\n theme: 'laserwave',\n transformers: [{\n line(node, line) {\n node.properties['data-line'] = line\n },\n }],\n })\n\n res.setHeader('Content-Type', 'text/html')\n res.end(highlighted)\n } catch (error: any) {\n res.statusCode = 500\n res.end(`<pre>${error.stack || error.message}</pre>`)\n }\n}\n\nasync function serveVueSource(url: string, config: MaizzleConfig, res: any) {\n const templateSlug = url.replace('/__maizzle/vue-source/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const source = readFileSync(resolve(match), 'utf-8')\n const lang = match.endsWith('.md') ? 'html' : 'vue'\n\n const hl = await getHighlighter()\n const highlighted = hl.codeToHtml(source, {\n lang,\n theme: 'laserwave',\n transformers: [{\n line(node, line) {\n node.properties['data-line'] = line\n },\n }],\n })\n\n res.setHeader('Content-Type', 'text/html')\n res.end(highlighted)\n } catch (error: any) {\n res.statusCode = 500\n res.end(`<pre>${error.stack || error.message}</pre>`)\n }\n}\n\nasync function servePlaintext(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/plaintext/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end('Template not found')\n return\n }\n\n try {\n const absolutePath = resolve(match)\n await renderer.invalidateAll()\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n const templateConfig = rendered.templateConfig\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n\n const plaintext = createPlaintext(html)\n\n res.setHeader('Content-Type', 'text/plain')\n res.end(plaintext)\n } catch (error: any) {\n res.statusCode = 500\n res.end(error.message)\n }\n}\n\nfunction humanFileSize(bytes: number, si = false, dp = 2) {\n const threshold = si ? 1000 : 1024\n\n if (Math.abs(bytes) < threshold) {\n return bytes + ' B'\n }\n\n const units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n let u = -1\n const r = 10 ** dp\n\n do {\n bytes /= threshold\n ++u\n } while (Math.round(Math.abs(bytes) * r) / r >= threshold && u < units.length - 1)\n\n return bytes.toFixed(dp) + ' ' + units[u]\n}\n\nasync function serveStats(url: string, config: MaizzleConfig, renderer: Renderer, res: any) {\n const templateSlug = url.replace('/__maizzle/stats/', '').replace(/\\?.*$/, '')\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const templates = await glob(contentPatterns)\n const match = templates.find(t => t.replace(/\\.(vue|md)$/, '') === templateSlug)\n\n if (!match) {\n res.statusCode = 404\n res.end(JSON.stringify({ error: 'Template not found' }))\n return\n }\n\n try {\n const absolutePath = resolve(match)\n await renderer.invalidateAll()\n\n const rendered = await renderer.render(absolutePath, config)\n let html = rendered.html\n const templateConfig = rendered.templateConfig\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n\n const sizeBytes = Buffer.byteLength(html, 'utf-8')\n\n // Count images: <img> tags and CSS background images\n const imgTags = (html.match(/<img\\b[^>]*>/gi) || []).length\n const bgImages = (html.match(/url\\s*\\([^)]+\\)/gi) || []).length\n const totalImages = imgTags + bgImages\n\n // Count links\n const links = (html.match(/<a\\b[^>]*href\\s*=/gi) || []).length\n\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n size: {\n bytes: sizeBytes,\n formatted: humanFileSize(sizeBytes),\n },\n images: totalImages,\n links,\n }))\n } catch (error: any) {\n res.statusCode = 500\n res.end(JSON.stringify({ error: error.message }))\n }\n}\n\nexport function printBanner(server: ViteDevServer, startupTime?: number) {\n const info = server.config.logger.info\n const time = startupTime ?? (server as any)._maizzleStartupTime\n\n info('')\n info(` \\x1b[32m\\x1b[1mMAIZZLE\\x1b[0m\\x1b[32m v6.0.0\\x1b[0m \\x1b[2mready in\\x1b[0m \\x1b[1m${time}\\x1b[0m ms`)\n info('')\n server.printUrls()\n info('')\n}\n\nfunction customLogger() {\n const logger = createLogger('info')\n const warn = logger.warn\n\n logger.warn = (message, options) => {\n if (typeof message === 'string' && message.includes('<tr> cannot be child of <table>')) {\n return\n }\n\n warn(message, options)\n }\n\n return logger\n}\n"],"mappings":"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"}
@@ -176,7 +176,11 @@ watch(() => route.params.template, () => {
176
176
  }, { immediate: true })
177
177
 
178
178
  watch(viewMode, (mode) => {
179
- if (mode === 'source' && !sourceHtml.value) fetchSource()
179
+ if (mode === 'source') {
180
+ if (sourceView.value === 'compiled' && !sourceHtml.value) fetchSource()
181
+ if (sourceView.value === 'vue' && !vueSourceHtml.value) fetchVueSource()
182
+ if (sourceView.value === 'plaintext' && !plaintextContent.value) fetchPlaintext()
183
+ }
180
184
  })
181
185
 
182
186
  watch(sourceView, (view) => {
@@ -191,11 +195,13 @@ if ((import.meta as any).hot) {
191
195
  fetchCompatibility()
192
196
  fetchLint()
193
197
  fetchStats()
194
- // Clear non-active source views so they re-fetch when switched to
195
- if (sourceView.value !== 'compiled') sourceHtml.value = ''
196
- if (sourceView.value !== 'vue') vueSourceHtml.value = ''
197
- if (sourceView.value !== 'plaintext') plaintextContent.value = ''
198
198
 
199
+ // Always clear all source views so they re-fetch when switched to
200
+ sourceHtml.value = ''
201
+ vueSourceHtml.value = ''
202
+ plaintextContent.value = ''
203
+
204
+ // Re-fetch the active source view immediately if currently visible
199
205
  if (viewMode.value === 'source') {
200
206
  if (sourceView.value === 'compiled') fetchSource()
201
207
  if (sourceView.value === 'vue') fetchVueSource()
@@ -1 +1 @@
1
- {"version":3,"file":"entities.d.mts","names":[],"sources":["../../src/transformers/entities.ts"],"mappings":";;;;iBA2BgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,cAAA,GAAwB,SAAA"}
1
+ {"version":3,"file":"entities.d.mts","names":[],"sources":["../../src/transformers/entities.ts"],"mappings":";;;;iBA8BgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,cAAA,GAAwB,SAAA"}
@@ -7,7 +7,10 @@ const DEFAULT_ENTITIES = {
7
7
  "‍": "&zwj;",
8
8
  "‌": "&zwnj;",
9
9
  "\xA0": "&nbsp;",
10
+ "­": "&shy;",
10
11
  "​": "&#8203;",
12
+ " ": "&#8199;",
13
+ "͏": "&#847;",
11
14
  " ": "&emsp;",
12
15
  "\u2028": "&LineSeparator;",
13
16
  "\u2029": "&ParagraphSeparator;",
@@ -1 +1 @@
1
- {"version":3,"file":"entities.mjs","names":["merge"],"sources":["../../src/transformers/entities.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport { walk } from '../utils/ast/index.ts'\nimport type { ChildNode } from 'domhandler'\nimport type { EntitiesConfig } from '../types/index.ts'\n\nconst DEFAULT_ENTITIES: Record<string, string> = {\n '\\u200D': '&zwj;',\n '\\u200C': '&zwnj;',\n '\\u00A0': '&nbsp;',\n '\\u200B': '&#8203;',\n '\\u2003': '&emsp;',\n '\\u2028': '&LineSeparator;',\n '\\u2029': '&ParagraphSeparator;',\n '\\u00B7': '&middot;',\n '\\u2013': '&ndash;',\n '\\u2014': '&mdash;',\n '\\u2018': '&lsquo;',\n '\\u2019': '&rsquo;',\n '\\u201C': '&ldquo;',\n '\\u201D': '&rdquo;',\n '\\u00AB': '&laquo;',\n '\\u00BB': '&raquo;',\n '\\u2022': '&bull;',\n '\\u2039': '&lsaquo;',\n '\\u203A': '&rsaquo;'\n}\n\nexport function entities(dom: ChildNode[], config: EntitiesConfig = true): ChildNode[] {\n if (!config) return dom\n\n const map = typeof config === 'object'\n ? merge(config as Record<string, string>, DEFAULT_ENTITIES)\n : DEFAULT_ENTITIES\n\n walk(dom, (node) => {\n if (node.type === 'text') {\n for (const [char, entity] of Object.entries(map)) {\n node.data = node.data.split(char).join(entity)\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;AAKA,MAAM,mBAA2C;CAC/C,KAAU;CACV,KAAU;CACV,QAAU;CACV,KAAU;CACV,KAAU;CACV,UAAU;CACV,UAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACX;AAED,SAAgB,SAAS,KAAkB,SAAyB,MAAmB;AACrF,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,MAAM,OAAO,WAAW,WAC1BA,KAAM,QAAkC,iBAAiB,GACzD;AAEJ,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,OAChB,MAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,IAAI,CAC9C,MAAK,OAAO,KAAK,KAAK,MAAM,KAAK,CAAC,KAAK,OAAO;GAGlD;AAEF,QAAO"}
1
+ {"version":3,"file":"entities.mjs","names":["merge"],"sources":["../../src/transformers/entities.ts"],"sourcesContent":["import { defu as merge } from 'defu'\nimport { walk } from '../utils/ast/index.ts'\nimport type { ChildNode } from 'domhandler'\nimport type { EntitiesConfig } from '../types/index.ts'\n\nconst DEFAULT_ENTITIES: Record<string, string> = {\n '\\u200D': '&zwj;',\n '\\u200C': '&zwnj;',\n '\\u00A0': '&nbsp;',\n '\\u00AD': '&shy;',\n '\\u200B': '&#8203;',\n '\\u2007': '&#8199;',\n '\\u034F': '&#847;',\n '\\u2003': '&emsp;',\n '\\u2028': '&LineSeparator;',\n '\\u2029': '&ParagraphSeparator;',\n '\\u00B7': '&middot;',\n '\\u2013': '&ndash;',\n '\\u2014': '&mdash;',\n '\\u2018': '&lsquo;',\n '\\u2019': '&rsquo;',\n '\\u201C': '&ldquo;',\n '\\u201D': '&rdquo;',\n '\\u00AB': '&laquo;',\n '\\u00BB': '&raquo;',\n '\\u2022': '&bull;',\n '\\u2039': '&lsaquo;',\n '\\u203A': '&rsaquo;'\n}\n\nexport function entities(dom: ChildNode[], config: EntitiesConfig = true): ChildNode[] {\n if (!config) return dom\n\n const map = typeof config === 'object'\n ? merge(config as Record<string, string>, DEFAULT_ENTITIES)\n : DEFAULT_ENTITIES\n\n walk(dom, (node) => {\n if (node.type === 'text') {\n for (const [char, entity] of Object.entries(map)) {\n node.data = node.data.split(char).join(entity)\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;AAKA,MAAM,mBAA2C;CAC/C,KAAU;CACV,KAAU;CACV,QAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,UAAU;CACV,UAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACV,KAAU;CACX;AAED,SAAgB,SAAS,KAAkB,SAAyB,MAAmB;AACrF,KAAI,CAAC,OAAQ,QAAO;CAEpB,MAAM,MAAM,OAAO,WAAW,WAC1BA,KAAM,QAAkC,iBAAiB,GACzD;AAEJ,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,OAChB,MAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,IAAI,CAC9C,MAAK,OAAO,KAAK,KAAK,MAAM,KAAK,CAAC,KAAK,OAAO;GAGlD;AAEF,QAAO"}
@@ -0,0 +1,6 @@
1
+ //#region src/transformers/filters/defaults.d.ts
2
+ type FilterFunction = (str: string, value: string) => string;
3
+ declare const defaults: Record<string, FilterFunction>;
4
+ //#endregion
5
+ export { FilterFunction, defaults };
6
+ //# sourceMappingURL=defaults.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.mts","names":[],"sources":["../../../src/transformers/filters/defaults.ts"],"mappings":";KAAY,cAAA,IAAkB,GAAA,UAAa,KAAA;AAAA,cAgB9B,QAAA,EAAU,MAAA,SAAe,cAAA"}
@@ -0,0 +1,78 @@
1
+ //#region src/transformers/filters/defaults.ts
2
+ const escapeMap = {
3
+ "\"": "&#34;",
4
+ "&": "&amp;",
5
+ "'": "&#39;",
6
+ "<": "&lt;",
7
+ ">": "&gt;"
8
+ };
9
+ const escapeRegex = /["&'<>]/g;
10
+ function escapeHtml(str) {
11
+ return str.replace(escapeRegex, (ch) => escapeMap[ch]);
12
+ }
13
+ const defaults = {
14
+ append: (str, value) => str + value,
15
+ prepend: (str, value) => value + str,
16
+ uppercase: (str) => str.toUpperCase(),
17
+ lowercase: (str) => str.toLowerCase(),
18
+ capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
19
+ ceil: (str) => String(Math.ceil(Number.parseFloat(str))),
20
+ floor: (str) => String(Math.floor(Number.parseFloat(str))),
21
+ round: (str) => String(Math.round(Number.parseFloat(str))),
22
+ escape: (str) => escapeHtml(str),
23
+ "escape-once": (str) => {
24
+ return escapeHtml(str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#34;/g, "\"").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&apos;/g, "'"));
25
+ },
26
+ lstrip: (str) => str.trimStart(),
27
+ rstrip: (str) => str.trimEnd(),
28
+ trim: (str) => str.trim(),
29
+ minus: (str, value) => String(Number.parseFloat(str) - Number.parseFloat(value)),
30
+ plus: (str, value) => String(Number.parseFloat(str) + Number.parseFloat(value)),
31
+ multiply: (str, value) => String(Number.parseFloat(str) * Number.parseFloat(value)),
32
+ times: (str, value) => String(Number.parseFloat(str) * Number.parseFloat(value)),
33
+ "divide-by": (str, value) => String(Number.parseFloat(str) / Number.parseFloat(value)),
34
+ divide: (str, value) => String(Number.parseFloat(str) / Number.parseFloat(value)),
35
+ modulo: (str, value) => String(Number.parseFloat(str) % Number.parseFloat(value)),
36
+ "newline-to-br": (str) => str.replace(/\n/g, "<br>"),
37
+ "strip-newlines": (str) => str.replace(/\n/g, ""),
38
+ remove: (str, value) => str.split(value).join(""),
39
+ "remove-first": (str, value) => {
40
+ const i = str.indexOf(value);
41
+ return i === -1 ? str : str.slice(0, i) + str.slice(i + value.length);
42
+ },
43
+ replace: (str, value) => {
44
+ const [search = "", replacement = ""] = value.split("|");
45
+ return str.split(search).join(replacement);
46
+ },
47
+ "replace-first": (str, value) => {
48
+ const [search = "", replacement = ""] = value.split("|");
49
+ const i = str.indexOf(search);
50
+ return i === -1 ? str : str.slice(0, i) + replacement + str.slice(i + search.length);
51
+ },
52
+ size: (str) => String(str.length),
53
+ slice: (str, value) => {
54
+ const args = value.split(",").map((s) => Number.parseInt(s.trim(), 10));
55
+ return str.slice(args[0], args[1]);
56
+ },
57
+ truncate: (str, value) => {
58
+ const commaIndex = value.indexOf(",");
59
+ const length = Number.parseInt(commaIndex === -1 ? value : value.slice(0, commaIndex), 10);
60
+ const ellipsis = commaIndex === -1 ? "..." : value.slice(commaIndex + 1);
61
+ if (str.length <= length) return str;
62
+ return str.slice(0, length) + ellipsis;
63
+ },
64
+ "truncate-words": (str, value) => {
65
+ const commaIndex = value.indexOf(",");
66
+ const count = Number.parseInt(commaIndex === -1 ? value : value.slice(0, commaIndex), 10);
67
+ const ellipsis = commaIndex === -1 ? "..." : value.slice(commaIndex + 1);
68
+ const words = str.split(/\s+/).filter(Boolean);
69
+ if (words.length <= count) return str;
70
+ return words.slice(0, count).join(" ") + ellipsis;
71
+ },
72
+ "url-decode": (str) => decodeURIComponent(str.replace(/\+/g, " ")),
73
+ "url-encode": (str) => encodeURIComponent(str)
74
+ };
75
+
76
+ //#endregion
77
+ export { defaults };
78
+ //# sourceMappingURL=defaults.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.mjs","names":[],"sources":["../../../src/transformers/filters/defaults.ts"],"sourcesContent":["export type FilterFunction = (str: string, value: string) => string\n\nconst escapeMap: Record<string, string> = {\n '\"': '&#34;',\n '&': '&amp;',\n \"'\": '&#39;',\n '<': '&lt;',\n '>': '&gt;',\n}\n\nconst escapeRegex = /[\"&'<>]/g\n\nfunction escapeHtml(str: string): string {\n return str.replace(escapeRegex, ch => escapeMap[ch])\n}\n\nexport const defaults: Record<string, FilterFunction> = {\n append: (str, value) => str + value,\n prepend: (str, value) => value + str,\n uppercase: str => str.toUpperCase(),\n lowercase: str => str.toLowerCase(),\n capitalize: str => str.charAt(0).toUpperCase() + str.slice(1),\n ceil: str => String(Math.ceil(Number.parseFloat(str))),\n floor: str => String(Math.floor(Number.parseFloat(str))),\n round: str => String(Math.round(Number.parseFloat(str))),\n escape: str => escapeHtml(str),\n 'escape-once': str => {\n const decoded = str\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&#34;/g, '\"')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n\n return escapeHtml(decoded)\n },\n lstrip: str => str.trimStart(),\n rstrip: str => str.trimEnd(),\n trim: str => str.trim(),\n minus: (str, value) => String(Number.parseFloat(str) - Number.parseFloat(value)),\n plus: (str, value) => String(Number.parseFloat(str) + Number.parseFloat(value)),\n multiply: (str, value) => String(Number.parseFloat(str) * Number.parseFloat(value)),\n times: (str, value) => String(Number.parseFloat(str) * Number.parseFloat(value)),\n 'divide-by': (str, value) => String(Number.parseFloat(str) / Number.parseFloat(value)),\n divide: (str, value) => String(Number.parseFloat(str) / Number.parseFloat(value)),\n modulo: (str, value) => String(Number.parseFloat(str) % Number.parseFloat(value)),\n 'newline-to-br': str => str.replace(/\\n/g, '<br>'),\n 'strip-newlines': str => str.replace(/\\n/g, ''),\n remove: (str, value) => str.split(value).join(''),\n 'remove-first': (str, value) => {\n const i = str.indexOf(value)\n return i === -1 ? str : str.slice(0, i) + str.slice(i + value.length)\n },\n replace: (str, value) => {\n const [search = '', replacement = ''] = value.split('|')\n return str.split(search).join(replacement)\n },\n 'replace-first': (str, value) => {\n const [search = '', replacement = ''] = value.split('|')\n const i = str.indexOf(search)\n return i === -1 ? str : str.slice(0, i) + replacement + str.slice(i + search.length)\n },\n size: str => String(str.length),\n slice: (str, value) => {\n const args = value.split(',').map(s => Number.parseInt(s.trim(), 10))\n return str.slice(args[0], args[1])\n },\n truncate: (str, value) => {\n const commaIndex = value.indexOf(',')\n const length = Number.parseInt(commaIndex === -1 ? value : value.slice(0, commaIndex), 10)\n const ellipsis = commaIndex === -1 ? '...' : value.slice(commaIndex + 1)\n\n if (str.length <= length) return str\n\n return str.slice(0, length) + ellipsis\n },\n 'truncate-words': (str, value) => {\n const commaIndex = value.indexOf(',')\n const count = Number.parseInt(commaIndex === -1 ? value : value.slice(0, commaIndex), 10)\n const ellipsis = commaIndex === -1 ? '...' : value.slice(commaIndex + 1)\n const words = str.split(/\\s+/).filter(Boolean)\n\n if (words.length <= count) return str\n\n return words.slice(0, count).join(' ') + ellipsis\n },\n 'url-decode': str => decodeURIComponent(str.replace(/\\+/g, ' ')),\n 'url-encode': str => encodeURIComponent(str),\n}\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { FilterFunction } from "./defaults.mjs";
2
+ import { ChildNode } from "domhandler";
3
+
4
+ //#region src/transformers/filters/index.d.ts
5
+ type FiltersConfig = false | Record<string, (str: string, value: string) => string>;
6
+ /**
7
+ * Filters transformer.
8
+ *
9
+ * Applies transformation functions to the content of elements that
10
+ * have matching filter attributes. Multiple filters on the same element
11
+ * are executed in the order the attributes are defined.
12
+ *
13
+ * Default filters include string manipulation (uppercase, lowercase, trim, etc.),
14
+ * math operations (plus, minus, multiply, etc.), and more.
15
+ *
16
+ * Custom filters can be added via config, and will be merged with defaults.
17
+ * Set config to `false` to disable all filters.
18
+ */
19
+ declare function filters(dom: ChildNode[], config?: FiltersConfig): ChildNode[];
20
+ //#endregion
21
+ export { type FilterFunction, FiltersConfig, filters };
22
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/transformers/filters/index.ts"],"mappings":";;;;KAMY,aAAA,WAAwB,MAAA,UAAgB,GAAA,UAAa,KAAA;;AAAjE;;;;;;;;;AA4BA;;;iBAAgB,OAAA,CAAQ,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,aAAA,GAAqB,SAAA"}
@@ -0,0 +1,67 @@
1
+ import { parse } from "../../utils/ast/parser.mjs";
2
+ import { serialize } from "../../utils/ast/serializer.mjs";
3
+ import "../../utils/ast/index.mjs";
4
+ import { defaults } from "./defaults.mjs";
5
+ import { Text } from "domhandler";
6
+
7
+ //#region src/transformers/filters/index.ts
8
+ /**
9
+ * Process children before parents so nested filter elements work correctly.
10
+ */
11
+ function walkBottomUp(nodes, callback) {
12
+ for (const node of [...nodes]) {
13
+ if ("children" in node && node.children?.length) walkBottomUp(node.children, callback);
14
+ callback(node);
15
+ }
16
+ }
17
+ /**
18
+ * Filters transformer.
19
+ *
20
+ * Applies transformation functions to the content of elements that
21
+ * have matching filter attributes. Multiple filters on the same element
22
+ * are executed in the order the attributes are defined.
23
+ *
24
+ * Default filters include string manipulation (uppercase, lowercase, trim, etc.),
25
+ * math operations (plus, minus, multiply, etc.), and more.
26
+ *
27
+ * Custom filters can be added via config, and will be merged with defaults.
28
+ * Set config to `false` to disable all filters.
29
+ */
30
+ function filters(dom, config = {}) {
31
+ if (config === false) return dom;
32
+ const allFilters = {
33
+ ...defaults,
34
+ ...config
35
+ };
36
+ const filterNames = new Set(Object.keys(allFilters));
37
+ walkBottomUp(dom, (node) => {
38
+ const el = node;
39
+ if (!el.attribs) return;
40
+ const matched = [];
41
+ for (const attr of Object.keys(el.attribs)) if (filterNames.has(attr)) matched.push({
42
+ name: attr,
43
+ value: el.attribs[attr]
44
+ });
45
+ if (matched.length === 0) return;
46
+ let content = serialize(el.children);
47
+ for (const { name, value } of matched) {
48
+ content = allFilters[name](content, value);
49
+ delete el.attribs[name];
50
+ }
51
+ if (content === "") el.children = [];
52
+ else if (/<[a-z/!]/i.test(content)) {
53
+ const newChildren = parse(content);
54
+ for (const child of newChildren) child.parent = el;
55
+ el.children = newChildren;
56
+ } else {
57
+ const textNode = new Text(content);
58
+ textNode.parent = el;
59
+ el.children = [textNode];
60
+ }
61
+ });
62
+ return dom;
63
+ }
64
+
65
+ //#endregion
66
+ export { filters };
67
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/transformers/filters/index.ts"],"sourcesContent":["import { Text } from 'domhandler'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize } from '../../utils/ast/index.ts'\nimport { defaults } from './defaults.ts'\n\nexport type { FilterFunction } from './defaults.ts'\nexport type FiltersConfig = false | Record<string, (str: string, value: string) => string>\n\n/**\n * Process children before parents so nested filter elements work correctly.\n */\nfunction walkBottomUp(nodes: ChildNode[], callback: (node: ChildNode) => void): void {\n for (const node of [...nodes]) {\n if ('children' in node && node.children?.length) {\n walkBottomUp(node.children as ChildNode[], callback)\n }\n\n callback(node)\n }\n}\n\n/**\n * Filters transformer.\n *\n * Applies transformation functions to the content of elements that\n * have matching filter attributes. Multiple filters on the same element\n * are executed in the order the attributes are defined.\n *\n * Default filters include string manipulation (uppercase, lowercase, trim, etc.),\n * math operations (plus, minus, multiply, etc.), and more.\n *\n * Custom filters can be added via config, and will be merged with defaults.\n * Set config to `false` to disable all filters.\n */\nexport function filters(dom: ChildNode[], config: FiltersConfig = {}): ChildNode[] {\n if (config === false) return dom\n\n const allFilters = { ...defaults, ...config }\n const filterNames = new Set(Object.keys(allFilters))\n\n walkBottomUp(dom, (node) => {\n const el = node as Element\n\n if (!el.attribs) return\n\n // Collect matching filter attributes in source order\n const matched: Array<{ name: string; value: string }> = []\n\n for (const attr of Object.keys(el.attribs)) {\n if (filterNames.has(attr)) {\n matched.push({ name: attr, value: el.attribs[attr] })\n }\n }\n\n if (matched.length === 0) return\n\n // Serialize children to get innerHTML\n let content = serialize(el.children as ChildNode[])\n\n // Apply each filter in attribute order\n for (const { name, value } of matched) {\n content = allFilters[name](content, value)\n delete el.attribs[name]\n }\n\n // Replace children with the filtered content\n if (content === '') {\n el.children = []\n } else if (/<[a-z/!]/i.test(content)) {\n // Result contains HTML elements — parse back to DOM\n const newChildren = parse(content)\n\n for (const child of newChildren) {\n child.parent = el as any\n }\n\n el.children = newChildren as ChildNode[]\n } else {\n // Text-only result — create a text node directly to preserve entity strings\n const textNode = new Text(content)\n textNode.parent = el as any\n el.children = [textNode]\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAS,aAAa,OAAoB,UAA2C;AACnF,MAAK,MAAM,QAAQ,CAAC,GAAG,MAAM,EAAE;AAC7B,MAAI,cAAc,QAAQ,KAAK,UAAU,OACvC,cAAa,KAAK,UAAyB,SAAS;AAGtD,WAAS,KAAK;;;;;;;;;;;;;;;;AAiBlB,SAAgB,QAAQ,KAAkB,SAAwB,EAAE,EAAe;AACjF,KAAI,WAAW,MAAO,QAAO;CAE7B,MAAM,aAAa;EAAE,GAAG;EAAU,GAAG;EAAQ;CAC7C,MAAM,cAAc,IAAI,IAAI,OAAO,KAAK,WAAW,CAAC;AAEpD,cAAa,MAAM,SAAS;EAC1B,MAAM,KAAK;AAEX,MAAI,CAAC,GAAG,QAAS;EAGjB,MAAM,UAAkD,EAAE;AAE1D,OAAK,MAAM,QAAQ,OAAO,KAAK,GAAG,QAAQ,CACxC,KAAI,YAAY,IAAI,KAAK,CACvB,SAAQ,KAAK;GAAE,MAAM;GAAM,OAAO,GAAG,QAAQ;GAAO,CAAC;AAIzD,MAAI,QAAQ,WAAW,EAAG;EAG1B,IAAI,UAAU,UAAU,GAAG,SAAwB;AAGnD,OAAK,MAAM,EAAE,MAAM,WAAW,SAAS;AACrC,aAAU,WAAW,MAAM,SAAS,MAAM;AAC1C,UAAO,GAAG,QAAQ;;AAIpB,MAAI,YAAY,GACd,IAAG,WAAW,EAAE;WACP,YAAY,KAAK,QAAQ,EAAE;GAEpC,MAAM,cAAc,MAAM,QAAQ;AAElC,QAAK,MAAM,SAAS,YAClB,OAAM,SAAS;AAGjB,MAAG,WAAW;SACT;GAEL,MAAM,WAAW,IAAI,KAAK,QAAQ;AAClC,YAAS,SAAS;AAClB,MAAG,WAAW,CAAC,SAAS;;GAE1B;AAEF,QAAO"}
@@ -20,16 +20,17 @@ import { MaizzleConfig } from "../types/config.mjs";
20
20
  * 5. Remove attributes
21
21
  * 6. Shorthand CSS
22
22
  * 7. Add attributes
23
- * 8. Base URL
24
- * 9. URL query
25
- * 10. Purge CSS (serializes/parses internally around email-comb)
26
- * 11. Entities
23
+ * 8. Filters
24
+ * 9. Base URL
25
+ * 10. URL query
26
+ * 11. Purge CSS (serializes/parses internally around email-comb)
27
+ * 12. Entities
27
28
  * + Vue-generated comments stripped here (on serialized string)
28
- * 12. Replace strings
29
- * 13. Prettify
30
- * 14. Minify
29
+ * 13. Replace strings
30
+ * 14. Prettify
31
+ * 15. Minify
31
32
  */
32
- declare function runTransformers(html: string, config: MaizzleConfig, filePath?: string): Promise<string>;
33
+ declare function runTransformers(html: string, config: MaizzleConfig, filePath?: string, doctype?: string): Promise<string>;
33
34
  //#endregion
34
35
  export { runTransformers };
35
36
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/transformers/index.ts"],"mappings":";;;;;AA8CA;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,eAAA,CACpB,IAAA,UACA,MAAA,EAAQ,aAAA,EACR,QAAA,YACC,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/transformers/index.ts"],"mappings":";;;;;AAgDA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,eAAA,CACpB,IAAA,UACA,MAAA,EAAQ,aAAA,EACR,QAAA,WACA,OAAA,YACC,OAAA"}
@@ -9,6 +9,7 @@ import { inlineCSS } from "./inlineCSS.mjs";
9
9
  import { removeAttributes } from "./removeAttributes.mjs";
10
10
  import { shorthandCSS } from "./shorthandCSS.mjs";
11
11
  import { addAttributes } from "./addAttributes.mjs";
12
+ import { filters } from "./filters/index.mjs";
12
13
  import { base } from "./base.mjs";
13
14
  import { entities } from "./entities.mjs";
14
15
  import { urlQuery } from "./urlQuery.mjs";
@@ -37,16 +38,17 @@ import { minify } from "./minify.mjs";
37
38
  * 5. Remove attributes
38
39
  * 6. Shorthand CSS
39
40
  * 7. Add attributes
40
- * 8. Base URL
41
- * 9. URL query
42
- * 10. Purge CSS (serializes/parses internally around email-comb)
43
- * 11. Entities
41
+ * 8. Filters
42
+ * 9. Base URL
43
+ * 10. URL query
44
+ * 11. Purge CSS (serializes/parses internally around email-comb)
45
+ * 12. Entities
44
46
  * + Vue-generated comments stripped here (on serialized string)
45
- * 12. Replace strings
46
- * 13. Prettify
47
- * 14. Minify
47
+ * 13. Replace strings
48
+ * 14. Prettify
49
+ * 15. Minify
48
50
  */
49
- async function runTransformers(html, config, filePath) {
51
+ async function runTransformers(html, config, filePath, doctype) {
50
52
  let dom = parse(html);
51
53
  dom = await inlineLink(dom, filePath);
52
54
  dom = await tailwindcss(dom, config, filePath);
@@ -56,15 +58,18 @@ async function runTransformers(html, config, filePath) {
56
58
  dom = removeAttributes(dom, config.html?.attributes);
57
59
  dom = shorthandCSS(dom, config.css);
58
60
  dom = addAttributes(dom, config.html?.attributes);
61
+ dom = filters(dom, config.filters);
59
62
  dom = base(dom, config.url);
60
63
  dom = urlQuery(dom, config.url);
61
64
  dom = purgeCSS(dom, config.css);
62
65
  dom = entities(dom, config.html?.decodeEntities);
63
- let result = serialize(dom);
64
- result = result.replaceAll("<!--[-->", "").replaceAll("<!--]-->", "");
66
+ const isXhtml = doctype ? /xhtml/i.test(doctype) : false;
67
+ let result = serialize(dom, { selfClosingTags: isXhtml });
68
+ result = result.replaceAll("<!--[-->", "").replaceAll("<!--]-->", "").replaceAll("<!--teleport start anchor-->", "").replaceAll("<!--teleport anchor-->", "").replaceAll("<!--teleport start-->", "").replaceAll("<!--teleport end-->", "");
65
69
  result = replaceStrings(result, config);
66
70
  result = await format(result, config);
67
71
  result = minify(result, config);
72
+ if (!isXhtml) result = result.replace(/ \/>/g, ">");
68
73
  return result;
69
74
  }
70
75
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/transformers/index.ts"],"sourcesContent":["import { parse, serialize } from '../utils/ast/index.ts'\nimport { inlineLink } from './inlineLink.ts'\nimport { tailwindcss } from './tailwindcss.ts'\nimport { safeClassNames } from './safeClassNames.ts'\nimport { attributeToStyle } from './attributeToStyle.ts'\nimport { inlineCSS } from './inlineCSS.ts'\nimport { removeAttributes } from './removeAttributes.ts'\nimport { shorthandCSS } from './shorthandCSS.ts'\nimport { addAttributes } from './addAttributes.ts'\nimport { base } from './base.ts'\nimport { entities } from './entities.ts'\nimport { urlQuery } from './urlQuery.ts'\nimport { purgeCSS } from './purgeCSS.ts'\nimport { replaceStrings } from './replaceStrings.ts'\nimport { format } from './format.ts'\nimport { minify } from './minify.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\n/**\n * Run all Maizzle transformers on the rendered HTML.\n *\n * The HTML is parsed into a DOM once at the start and passed through all\n * DOM-based transformers as a shared `ChildNode[]`. After all DOM transformers\n * complete, the DOM is serialized back to a string exactly once.\n *\n * String-only transformers (those that rely on external tools that require a\n * raw HTML string) then run on the serialized output.\n *\n * Transformers run in a specific order:\n * 0. Inline link stylesheets — replace `<link rel=\"stylesheet\">` with `<style>` tags\n * 1. Tailwind CSS — compile CSS, lower syntax, optimize (cleanup + merge media queries)\n * 2. Safe class names\n * 3. Attribute to style\n * 4. CSS inliner\n * 5. Remove attributes\n * 6. Shorthand CSS\n * 7. Add attributes\n * 8. Base URL\n * 9. URL query\n * 10. Purge CSS (serializes/parses internally around email-comb)\n * 11. Entities\n * + Vue-generated comments stripped here (on serialized string)\n * 12. Replace strings\n * 13. Prettify\n * 14. Minify\n */\nexport async function runTransformers(\n html: string,\n config: MaizzleConfig,\n filePath?: string,\n): Promise<string> {\n // Parse once — all DOM transformers share this array\n let dom = parse(html)\n\n // 0. Inline <link> stylesheets\n dom = await inlineLink(dom, filePath)\n\n // 1. Tailwind CSS — always runs first\n dom = await tailwindcss(dom, config, filePath)\n\n // 2. Safe class names\n dom = safeClassNames(dom, config.css)\n\n // 3. Attribute to style\n dom = attributeToStyle(dom, config.css)\n\n // 4. CSS inliner (serializes/parses internally around juice)\n dom = inlineCSS(dom, config.css)\n\n // 5. Remove attributes\n dom = removeAttributes(dom, config.html?.attributes)\n\n // 6. Shorthand CSS\n dom = shorthandCSS(dom, config.css)\n\n // 7. Add attributes\n dom = addAttributes(dom, config.html?.attributes)\n\n // 8. Base URL (serializes/parses internally for VML/MSO regex passes)\n dom = base(dom, config.url)\n\n // 9. URL query\n dom = urlQuery(dom, config.url)\n\n // 10. Purge CSS (serializes/parses internally around email-comb)\n dom = purgeCSS(dom, config.css)\n\n // 11. Entities\n dom = entities(dom, config.html?.decodeEntities)\n\n // Serialize once — remaining transformers operate on the HTML string\n let result = serialize(dom)\n\n // Remove Vue-generated comments after serializing\n result = result\n .replaceAll('<!--[-->', '')\n .replaceAll('<!--]-->', '')\n\n // 12. Replace strings\n result = replaceStrings(result, config)\n\n // 13. Format\n result = await format(result, config)\n\n // 14. Minify\n result = minify(result, config)\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,eAAsB,gBACpB,MACA,QACA,UACiB;CAEjB,IAAI,MAAM,MAAM,KAAK;AAGrB,OAAM,MAAM,WAAW,KAAK,SAAS;AAGrC,OAAM,MAAM,YAAY,KAAK,QAAQ,SAAS;AAG9C,OAAM,eAAe,KAAK,OAAO,IAAI;AAGrC,OAAM,iBAAiB,KAAK,OAAO,IAAI;AAGvC,OAAM,UAAU,KAAK,OAAO,IAAI;AAGhC,OAAM,iBAAiB,KAAK,OAAO,MAAM,WAAW;AAGpD,OAAM,aAAa,KAAK,OAAO,IAAI;AAGnC,OAAM,cAAc,KAAK,OAAO,MAAM,WAAW;AAGjD,OAAM,KAAK,KAAK,OAAO,IAAI;AAG3B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,MAAM,eAAe;CAGhD,IAAI,SAAS,UAAU,IAAI;AAG3B,UAAS,OACN,WAAW,YAAY,GAAG,CAC1B,WAAW,YAAY,GAAG;AAG7B,UAAS,eAAe,QAAQ,OAAO;AAGvC,UAAS,MAAM,OAAO,QAAQ,OAAO;AAGrC,UAAS,OAAO,QAAQ,OAAO;AAE/B,QAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/transformers/index.ts"],"sourcesContent":["import { parse, serialize } from '../utils/ast/index.ts'\nimport { inlineLink } from './inlineLink.ts'\nimport { tailwindcss } from './tailwindcss.ts'\nimport { safeClassNames } from './safeClassNames.ts'\nimport { attributeToStyle } from './attributeToStyle.ts'\nimport { inlineCSS } from './inlineCSS.ts'\nimport { removeAttributes } from './removeAttributes.ts'\nimport { shorthandCSS } from './shorthandCSS.ts'\nimport { addAttributes } from './addAttributes.ts'\nimport { filters } from './filters/index.ts'\nimport { base } from './base.ts'\nimport { entities } from './entities.ts'\nimport { urlQuery } from './urlQuery.ts'\nimport { purgeCSS } from './purgeCSS.ts'\nimport { replaceStrings } from './replaceStrings.ts'\nimport { format } from './format.ts'\nimport { minify } from './minify.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\n/**\n * Run all Maizzle transformers on the rendered HTML.\n *\n * The HTML is parsed into a DOM once at the start and passed through all\n * DOM-based transformers as a shared `ChildNode[]`. After all DOM transformers\n * complete, the DOM is serialized back to a string exactly once.\n *\n * String-only transformers (those that rely on external tools that require a\n * raw HTML string) then run on the serialized output.\n *\n * Transformers run in a specific order:\n * 0. Inline link stylesheets — replace `<link rel=\"stylesheet\">` with `<style>` tags\n * 1. Tailwind CSS — compile CSS, lower syntax, optimize (cleanup + merge media queries)\n * 2. Safe class names\n * 3. Attribute to style\n * 4. CSS inliner\n * 5. Remove attributes\n * 6. Shorthand CSS\n * 7. Add attributes\n * 8. Filters\n * 9. Base URL\n * 10. URL query\n * 11. Purge CSS (serializes/parses internally around email-comb)\n * 12. Entities\n * + Vue-generated comments stripped here (on serialized string)\n * 13. Replace strings\n * 14. Prettify\n * 15. Minify\n */\nexport async function runTransformers(\n html: string,\n config: MaizzleConfig,\n filePath?: string,\n doctype?: string,\n): Promise<string> {\n // Parse once — all DOM transformers share this array\n let dom = parse(html)\n\n // 0. Inline <link> stylesheets\n dom = await inlineLink(dom, filePath)\n\n // 1. Tailwind CSS — always runs first\n dom = await tailwindcss(dom, config, filePath)\n\n // 2. Safe class names\n dom = safeClassNames(dom, config.css)\n\n // 3. Attribute to style\n dom = attributeToStyle(dom, config.css)\n\n // 4. CSS inliner (serializes/parses internally around juice)\n dom = inlineCSS(dom, config.css)\n\n // 5. Remove attributes\n dom = removeAttributes(dom, config.html?.attributes)\n\n // 6. Shorthand CSS\n dom = shorthandCSS(dom, config.css)\n\n // 7. Add attributes\n dom = addAttributes(dom, config.html?.attributes)\n\n // 8. Filters\n dom = filters(dom, config.filters)\n\n // 9. Base URL (serializes/parses internally for VML/MSO regex passes)\n dom = base(dom, config.url)\n\n // 10. URL query\n dom = urlQuery(dom, config.url)\n\n // 11. Purge CSS (serializes/parses internally around email-comb)\n dom = purgeCSS(dom, config.css)\n\n // 12. Entities\n dom = entities(dom, config.html?.decodeEntities)\n\n // Serialize once — remaining transformers operate on the HTML string\n const isXhtml = doctype ? /xhtml/i.test(doctype) : false\n let result = serialize(dom, { selfClosingTags: isXhtml })\n\n // Remove Vue-generated comments after serializing\n result = result\n .replaceAll('<!--[-->', '')\n .replaceAll('<!--]-->', '')\n .replaceAll('<!--teleport start anchor-->', '')\n .replaceAll('<!--teleport anchor-->', '')\n .replaceAll('<!--teleport start-->', '')\n .replaceAll('<!--teleport end-->', '')\n\n // 13. Replace strings\n result = replaceStrings(result, config)\n\n // 14. Format\n result = await format(result, config)\n\n // 15. Minify\n result = minify(result, config)\n\n // Strip self-closing slashes for HTML5 doctypes\n if (!isXhtml) {\n result = result.replace(/ \\/>/g, '>')\n }\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,eAAsB,gBACpB,MACA,QACA,UACA,SACiB;CAEjB,IAAI,MAAM,MAAM,KAAK;AAGrB,OAAM,MAAM,WAAW,KAAK,SAAS;AAGrC,OAAM,MAAM,YAAY,KAAK,QAAQ,SAAS;AAG9C,OAAM,eAAe,KAAK,OAAO,IAAI;AAGrC,OAAM,iBAAiB,KAAK,OAAO,IAAI;AAGvC,OAAM,UAAU,KAAK,OAAO,IAAI;AAGhC,OAAM,iBAAiB,KAAK,OAAO,MAAM,WAAW;AAGpD,OAAM,aAAa,KAAK,OAAO,IAAI;AAGnC,OAAM,cAAc,KAAK,OAAO,MAAM,WAAW;AAGjD,OAAM,QAAQ,KAAK,OAAO,QAAQ;AAGlC,OAAM,KAAK,KAAK,OAAO,IAAI;AAG3B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,IAAI;AAG/B,OAAM,SAAS,KAAK,OAAO,MAAM,eAAe;CAGhD,MAAM,UAAU,UAAU,SAAS,KAAK,QAAQ,GAAG;CACnD,IAAI,SAAS,UAAU,KAAK,EAAE,iBAAiB,SAAS,CAAC;AAGzD,UAAS,OACN,WAAW,YAAY,GAAG,CAC1B,WAAW,YAAY,GAAG,CAC1B,WAAW,gCAAgC,GAAG,CAC9C,WAAW,0BAA0B,GAAG,CACxC,WAAW,yBAAyB,GAAG,CACvC,WAAW,uBAAuB,GAAG;AAGxC,UAAS,eAAe,QAAQ,OAAO;AAGvC,UAAS,MAAM,OAAO,QAAQ,OAAO;AAGrC,UAAS,OAAO,QAAQ,OAAO;AAG/B,KAAI,CAAC,QACH,UAAS,OAAO,QAAQ,SAAS,IAAI;AAGvC,QAAO"}