@maizzle/framework 6.0.0-rc.3 → 6.0.0-rc.5

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 (52) hide show
  1. package/dist/build.mjs +3 -2
  2. package/dist/build.mjs.map +1 -1
  3. package/dist/components/Button.vue +65 -14
  4. package/dist/components/CodeBlock.vue +75 -0
  5. package/dist/components/CodeInline.vue +44 -0
  6. package/dist/components/Preview.vue +20 -0
  7. package/dist/composables/renderContext.d.mts +5 -0
  8. package/dist/composables/renderContext.d.mts.map +1 -1
  9. package/dist/composables/renderContext.mjs.map +1 -1
  10. package/dist/composables/usePreviewText.d.mts +24 -0
  11. package/dist/composables/usePreviewText.d.mts.map +1 -0
  12. package/dist/composables/usePreviewText.mjs +29 -0
  13. package/dist/composables/usePreviewText.mjs.map +1 -0
  14. package/dist/index.d.mts +3 -2
  15. package/dist/index.mjs +2 -1
  16. package/dist/render/createRenderer.d.mts +1 -0
  17. package/dist/render/createRenderer.d.mts.map +1 -1
  18. package/dist/render/createRenderer.mjs +60 -1
  19. package/dist/render/createRenderer.mjs.map +1 -1
  20. package/dist/render/index.mjs +3 -2
  21. package/dist/render/index.mjs.map +1 -1
  22. package/dist/serve.d.mts.map +1 -1
  23. package/dist/serve.mjs +31 -20
  24. package/dist/serve.mjs.map +1 -1
  25. package/dist/server/ui/pages/Preview.vue +11 -5
  26. package/dist/transformers/entities.d.mts.map +1 -1
  27. package/dist/transformers/entities.mjs +3 -0
  28. package/dist/transformers/entities.mjs.map +1 -1
  29. package/dist/transformers/filters/defaults.d.mts +6 -0
  30. package/dist/transformers/filters/defaults.d.mts.map +1 -0
  31. package/dist/transformers/filters/defaults.mjs +78 -0
  32. package/dist/transformers/filters/defaults.mjs.map +1 -0
  33. package/dist/transformers/filters/index.d.mts +22 -0
  34. package/dist/transformers/filters/index.d.mts.map +1 -0
  35. package/dist/transformers/filters/index.mjs +67 -0
  36. package/dist/transformers/filters/index.mjs.map +1 -0
  37. package/dist/transformers/index.d.mts +9 -8
  38. package/dist/transformers/index.d.mts.map +1 -1
  39. package/dist/transformers/index.mjs +15 -10
  40. package/dist/transformers/index.mjs.map +1 -1
  41. package/dist/transformers/tailwindcss.d.mts +6 -2
  42. package/dist/transformers/tailwindcss.d.mts.map +1 -1
  43. package/dist/transformers/tailwindcss.mjs +49 -21
  44. package/dist/transformers/tailwindcss.mjs.map +1 -1
  45. package/dist/types/config.d.mts +373 -14
  46. package/dist/types/config.d.mts.map +1 -1
  47. package/dist/types/index.d.mts +2 -2
  48. package/dist/utils/ast/serializer.d.mts +3 -2
  49. package/dist/utils/ast/serializer.d.mts.map +1 -1
  50. package/dist/utils/ast/serializer.mjs +24 -0
  51. package/dist/utils/ast/serializer.mjs.map +1 -1
  52. package/package.json +1 -1
package/dist/build.mjs CHANGED
@@ -59,13 +59,14 @@ async function build(options = {}) {
59
59
  html: rendered.html
60
60
  });
61
61
  const templateConfig = rendered.templateConfig;
62
- if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath);
62
+ const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
63
+ if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath, doctype);
63
64
  html = await events.fireAfterTransform({
64
65
  config,
65
66
  template,
66
67
  html
67
68
  });
68
- html = `${rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>"}\n${html}`;
69
+ html = `${doctype}\n${html}`;
69
70
  const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase);
70
71
  mkdirSync(dirname(outputFilePath), { recursive: true });
71
72
  writeFileSync(outputFilePath, html);
@@ -1 +1 @@
1
- {"version":3,"file":"build.mjs","names":[],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { createPlaintext } from './plaintext.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nexport interface BuildOptions {\n config?: Partial<MaizzleConfig> | string\n}\n\nexport interface BuildResult {\n files: string[]\n config: MaizzleConfig\n}\n\n/**\n * Build all SFC email templates to HTML files.\n *\n * Creates a single Renderer instance, then loops through each template\n * calling render → transformers → write to disk.\n */\nexport async function build(options: BuildOptions = {}): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora('Building templates...').start()\n\n const config = await resolveConfig(options.config)\n\n const events = new EventManager()\n events.registerConfig(config)\n await events.fireBeforeCreate({ config })\n\n const outputPath = resolve(config.output?.path ?? 'dist')\n const outputExtension = config.output?.extension ?? 'html'\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const contentBase = computeContentBase(contentPatterns)\n const templateFiles = await glob(contentPatterns)\n\n if (templateFiles.length === 0) {\n spinner.succeed('No templates found')\n return { files: [], config }\n }\n\n // Clear the output directory before writing fresh output\n if (existsSync(outputPath)) {\n rmSync(outputPath, { recursive: true, force: true })\n }\n\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: [config.components?.source ?? []].flat() })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n let template = readFileSync(absolutePath, 'utf-8')\n\n template = await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n // Use the per-template merged config (from defineConfig() in the SFC) so that\n // template-level overrides like css.safe: false are respected by transformers.\n const templateConfig = rendered.templateConfig\n\n if (templateConfig.useTransformers !== false) {\n html = await runTransformers(html, templateConfig, absolutePath)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n html = `${doctype}\\n${html}`\n\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, html)\n outputFiles.push(outputFilePath)\n\n // Generate plaintext version if configured\n const globalPlaintext = templateConfig.plaintext\n const sfcPlaintext = rendered.plaintext\n\n if (globalPlaintext || sfcPlaintext) {\n const stripOptions = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const plaintext = createPlaintext(html, stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? 'txt'\n\n let ptOutputPath: string\n\n if (sfcPlaintext?.destination) {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`)\n } else if (typeof globalPlaintext === 'string') {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalPlaintext), ptExtension, contentBase)\n } else {\n ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase)\n }\n\n mkdirSync(dirname(ptOutputPath), { recursive: true })\n writeFileSync(ptOutputPath, plaintext)\n }\n\n // Register SFC event handlers that were collected during render\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n events.clearSfcHandlers()\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n\n const duration = ((Date.now() - start) / 1000).toFixed(2)\n const count = outputFiles.length\n spinner.stopAndPersist({\n symbol: '✅',\n text: `Built ${count} template${count !== 1 ? 's' : ''} in ${duration}s`,\n })\n\n return { files: outputFiles, config }\n}\n\n/**\n * Extract the static (non-glob) prefix from content patterns.\n *\n * For example, `['/abs/path/emails/**\\/*.vue']` → `'/abs/path/emails'`\n *\n * This is used to strip the content base from template paths\n * so the output preserves only the subdirectory structure.\n */\nfunction computeContentBase(patterns: string[]): string {\n // Use the first non-negated pattern\n const pattern = patterns.find(p => !p.startsWith('!')) ?? patterns[0]\n\n // Split on first glob character (* { ? [) and take the directory part\n const staticPart = pattern.split(/[*{?[]/)[0]\n\n // Ensure we have a clean directory path (not a partial segment)\n return resolve(staticPart.endsWith('/') ? staticPart : dirname(staticPart))\n}\n\nfunction resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n const absTemplate = resolve(templatePath)\n const rel = relative(contentBase, dirname(absTemplate))\n\n return join(outputDir, rel, `${name}.${extension}`)\n}\n\nasync function copyStatic(config: MaizzleConfig, outputPath: string): Promise<void> {\n const sources = config.static?.source ?? ['public/**/*.*']\n const destination = config.static?.destination ?? 'public'\n\n const files = await glob(sources)\n\n for (const file of files) {\n const destPath = join(outputPath, destination, relative(dirname(sources[0]).replace(/\\*.*$/, ''), file))\n const destDir = dirname(destPath)\n\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true })\n }\n\n cpSync(file, destPath)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build.mjs","names":[],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join } from 'node:path'\nimport { glob } from 'tinyglobby'\nimport ora from 'ora'\nimport { resolveConfig } from './config/index.ts'\nimport { EventManager } from './events/index.ts'\nimport { runTransformers } from './transformers/index.ts'\nimport { createRenderer } from './render/createRenderer.ts'\nimport { createPlaintext } from './plaintext.ts'\nimport type { MaizzleConfig } from './types/index.ts'\n\nexport interface BuildOptions {\n config?: Partial<MaizzleConfig> | string\n}\n\nexport interface BuildResult {\n files: string[]\n config: MaizzleConfig\n}\n\n/**\n * Build all SFC email templates to HTML files.\n *\n * Creates a single Renderer instance, then loops through each template\n * calling render → transformers → write to disk.\n */\nexport async function build(options: BuildOptions = {}): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora('Building templates...').start()\n\n const config = await resolveConfig(options.config)\n\n const events = new EventManager()\n events.registerConfig(config)\n await events.fireBeforeCreate({ config })\n\n const outputPath = resolve(config.output?.path ?? 'dist')\n const outputExtension = config.output?.extension ?? 'html'\n\n const contentPatterns = config.content ?? ['emails/**/*.vue']\n const contentBase = computeContentBase(contentPatterns)\n const templateFiles = await glob(contentPatterns)\n\n if (templateFiles.length === 0) {\n spinner.succeed('No templates found')\n return { files: [], config }\n }\n\n // Clear the output directory before writing fresh output\n if (existsSync(outputPath)) {\n rmSync(outputPath, { recursive: true, force: true })\n }\n\n const renderer = await createRenderer({ markdown: config.markdown, root: config.root, componentDirs: [config.components?.source ?? []].flat() })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n let template = readFileSync(absolutePath, 'utf-8')\n\n template = await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n // Use the per-template merged config (from defineConfig() in the SFC) so that\n // template-level overrides like css.safe: false are respected by transformers.\n const templateConfig = rendered.templateConfig\n\n const doctype = rendered.doctype ?? templateConfig.doctype ?? '<!DOCTYPE html>'\n\n if (templateConfig.useTransformers !== false) {\n html = await runTransformers(html, templateConfig, absolutePath, doctype)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n html = `${doctype}\\n${html}`\n\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, html)\n outputFiles.push(outputFilePath)\n\n // Generate plaintext version if configured\n const globalPlaintext = templateConfig.plaintext\n const sfcPlaintext = rendered.plaintext\n\n if (globalPlaintext || sfcPlaintext) {\n const stripOptions = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const plaintext = createPlaintext(html, stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? 'txt'\n\n let ptOutputPath: string\n\n if (sfcPlaintext?.destination) {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n ptOutputPath = join(resolve(sfcPlaintext.destination), `${name}.${ptExtension}`)\n } else if (typeof globalPlaintext === 'string') {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalPlaintext), ptExtension, contentBase)\n } else {\n ptOutputPath = resolveOutputPath(templatePath, outputPath, ptExtension, contentBase)\n }\n\n mkdirSync(dirname(ptOutputPath), { recursive: true })\n writeFileSync(ptOutputPath, plaintext)\n }\n\n // Register SFC event handlers that were collected during render\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n events.clearSfcHandlers()\n }\n\n await copyStatic(config, outputPath)\n await events.fireAfterBuild({ files: outputFiles, config })\n } finally {\n await renderer.close()\n }\n\n const duration = ((Date.now() - start) / 1000).toFixed(2)\n const count = outputFiles.length\n spinner.stopAndPersist({\n symbol: '✅',\n text: `Built ${count} template${count !== 1 ? 's' : ''} in ${duration}s`,\n })\n\n return { files: outputFiles, config }\n}\n\n/**\n * Extract the static (non-glob) prefix from content patterns.\n *\n * For example, `['/abs/path/emails/**\\/*.vue']` → `'/abs/path/emails'`\n *\n * This is used to strip the content base from template paths\n * so the output preserves only the subdirectory structure.\n */\nfunction computeContentBase(patterns: string[]): string {\n // Use the first non-negated pattern\n const pattern = patterns.find(p => !p.startsWith('!')) ?? patterns[0]\n\n // Split on first glob character (* { ? [) and take the directory part\n const staticPart = pattern.split(/[*{?[]/)[0]\n\n // Ensure we have a clean directory path (not a partial segment)\n return resolve(staticPart.endsWith('/') ? staticPart : dirname(staticPart))\n}\n\nfunction resolveOutputPath(templatePath: string, outputDir: string, extension: string, contentBase: string): string {\n const name = basename(templatePath).replace(/\\.(vue|md)$/, '')\n const absTemplate = resolve(templatePath)\n const rel = relative(contentBase, dirname(absTemplate))\n\n return join(outputDir, rel, `${name}.${extension}`)\n}\n\nasync function copyStatic(config: MaizzleConfig, outputPath: string): Promise<void> {\n const sources = config.static?.source ?? ['public/**/*.*']\n const destination = config.static?.destination ?? 'public'\n\n const files = await glob(sources)\n\n for (const file of files) {\n const destPath = join(outputPath, destination, relative(dirname(sources[0]).replace(/\\*.*$/, ''), file))\n const destDir = dirname(destPath)\n\n if (!existsSync(destDir)) {\n mkdirSync(destDir, { recursive: true })\n }\n\n cpSync(file, destPath)\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, useAttrs } from 'vue'
3
+ import type { PropType } from 'vue'
3
4
  import { twMerge } from 'tailwind-merge'
4
5
  import Outlook from './Outlook.vue'
5
6
 
@@ -10,42 +11,92 @@ const attrs = useAttrs()
10
11
  const props = defineProps({
11
12
  /** The URL the button links to. */
12
13
  href: String,
13
- type: {
14
- type: String,
15
- default: 'solid'
14
+ /**
15
+ * The button style variant.
16
+ * - `solid` — filled background (default)
17
+ * - `outline` — transparent background with a border
18
+ * - `ghost` — transparent background, no border
19
+ * - `link` — plain anchor with no button chrome
20
+ * @default 'solid'
21
+ */
22
+ variant: {
23
+ type: String as PropType<'solid' | 'outline' | 'ghost' | 'link'>,
24
+ default: 'solid' as const
16
25
  },
26
+ /**
27
+ * Horizontal alignment of the button wrapper.
28
+ * Accepts `'left'`, `'center'`, or `'right'`.
29
+ * @default null
30
+ */
17
31
  align: {
18
- type: String,
32
+ type: String as PropType<'left' | 'center' | 'right' | null>,
19
33
  default: null
20
34
  },
35
+ /**
36
+ * Background color for `solid` and `outline` variants.
37
+ * Also used as the text color for `outline` and `ghost` variants when `color` is not set.
38
+ * @default '#4338ca'
39
+ */
21
40
  bgColor: {
22
41
  type: String,
23
42
  default: '#4338ca'
24
43
  },
44
+ /**
45
+ * Explicit text color. When omitted, `solid` buttons use `#fffffe`
46
+ * and all other variants fall back to `bgColor`.
47
+ * @default null
48
+ */
25
49
  color: {
26
50
  type: String,
27
51
  default: null
28
52
  },
53
+ /**
54
+ * `mso-text-raise` value applied to the inner `<span>` elements.
55
+ * Controls vertical text alignment inside the button in old Outlook.
56
+ * @default '16px'
57
+ */
29
58
  msoPt: {
30
59
  type: String,
31
60
  default: '16px'
32
61
  },
62
+ /**
63
+ * `mso-text-raise` value applied to the spacer `<i>` element rendered for Outlook.
64
+ * Adjusts the bottom spacing that old Outlook uses to simulate padding.
65
+ * @default '31px'
66
+ */
33
67
  msoPb: {
34
68
  type: String,
35
69
  default: '31px'
36
70
  },
71
+ /**
72
+ * URL or path to an icon image displayed alongside the button label.
73
+ * @default null
74
+ */
37
75
  icon: {
38
76
  type: String,
39
77
  default: null
40
78
  },
79
+ /**
80
+ * Width of the icon image in pixels.
81
+ * @default 12
82
+ */
41
83
  iconWidth: {
42
84
  type: [String, Number],
43
85
  default: 12
44
86
  },
87
+ /**
88
+ * Side on which the icon is placed relative to the button label.
89
+ * Accepts `'left'` or `'right'`.
90
+ * @default 'right'
91
+ */
45
92
  iconPosition: {
46
- type: String,
47
- default: 'right'
93
+ type: String as PropType<'left' | 'right'>,
94
+ default: 'right' as const
48
95
  },
96
+ /**
97
+ * Additional CSS classes applied to the icon `<img>` element.
98
+ * @default ''
99
+ */
49
100
  iconClass: {
50
101
  type: String,
51
102
  default: ''
@@ -54,20 +105,20 @@ const props = defineProps({
54
105
 
55
106
  const parsedIconWidth = computed(() => parseInt(String(props.iconWidth), 10))
56
107
 
57
- const alignClass = computed(() => ({
108
+ const alignClass = computed(() => props.align ? ({
58
109
  left: 'text-left',
59
110
  center: 'text-center',
60
111
  right: 'text-right',
61
- })[props.align] || '')
112
+ })[props.align] || '' : '')
62
113
 
63
114
  const textColor = computed(() => {
64
115
  if (props.color) return props.color
65
116
 
66
- return props.type === 'solid' ? '#fffffe' : props.bgColor
117
+ return props.variant === 'solid' ? '#fffffe' : props.bgColor
67
118
  })
68
119
 
69
120
  const styles = computed(() => {
70
- if (props.type === 'link') {
121
+ if (props.variant === 'link') {
71
122
  return [
72
123
  'text-decoration: none;',
73
124
  `color: ${textColor.value};`,
@@ -84,12 +135,12 @@ const styles = computed(() => {
84
135
  `color: ${textColor.value};`,
85
136
  ]
86
137
 
87
- if (props.type === 'outline') {
138
+ if (props.variant === 'outline') {
88
139
  base.push(
89
140
  'background-color: transparent;',
90
141
  `border: 1px solid ${props.bgColor};`,
91
142
  )
92
- } else if (props.type === 'ghost') {
143
+ } else if (props.variant === 'ghost') {
93
144
  base.push('background-color: transparent;')
94
145
  } else {
95
146
  base.push(`background-color: ${props.bgColor};`)
@@ -98,10 +149,10 @@ const styles = computed(() => {
98
149
  return base.join('')
99
150
  })
100
151
 
101
- const isLink = computed(() => props.type === 'link')
152
+ const isLink = computed(() => props.variant === 'link')
102
153
 
103
154
  const defaultClasses = computed(() => {
104
- if (props.type === 'ghost') return 'hover:bg-indigo-50'
155
+ if (props.variant === 'ghost') return 'hover:bg-indigo-50'
105
156
  return ''
106
157
  })
107
158
 
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode } from 'vue'
3
+ import { codeToHtml, getSingletonHighlighter } from 'shiki'
4
+
5
+ export default {
6
+ props: {
7
+ /** The code to highlight. */
8
+ code: {
9
+ type: String,
10
+ default: ''
11
+ },
12
+ /** Base64-encoded code, set by the Vite transform for slot content. */
13
+ encodedCode: {
14
+ type: String,
15
+ default: ''
16
+ },
17
+ /** The language for syntax highlighting. @default 'html' */
18
+ lang: {
19
+ type: String,
20
+ default: 'html'
21
+ },
22
+ /** The shiki theme to use. @default 'github-light' */
23
+ theme: {
24
+ type: String,
25
+ default: 'github-light'
26
+ },
27
+ /** CSS class for the wrapping table cell. @default 'max-w-0 mso-padding-alt-6' */
28
+ tdClass: {
29
+ type: String,
30
+ default: 'max-w-0 mso-padding-alt-6'
31
+ }
32
+ },
33
+ inheritAttrs: false,
34
+ async setup(props, { slots, attrs }) {
35
+ // Prefer encodedCode (from Vite transform) → code prop → slot text
36
+ let source = props.encodedCode
37
+ ? Buffer.from(props.encodedCode, 'base64').toString('utf-8')
38
+ : props.code
39
+
40
+ if (!source) {
41
+ const slotContent = slots.default?.()
42
+ source = slotContent
43
+ ?.map((vnode: any) => (typeof vnode.children === 'string' ? vnode.children : ''))
44
+ .join('') ?? ''
45
+ }
46
+
47
+ source = source.trim()
48
+
49
+ if (!source) {
50
+ return () => createStaticVNode('', 0)
51
+ }
52
+
53
+ const highlighted = await codeToHtml(source, {
54
+ lang: props.lang,
55
+ theme: props.theme,
56
+ })
57
+
58
+ const hl = await getSingletonHighlighter({ themes: [props.theme], langs: [] })
59
+ const bg = hl.getTheme(props.theme).bg
60
+
61
+ // Shiki outputs <pre><code>...</code></pre>, extract the inner content
62
+ const codeContent = highlighted
63
+ .replace(/^<pre[^>]*><code>/, '')
64
+ .replace(/<\/code><\/pre>$/, '')
65
+
66
+ const classes = ['font-mono', attrs.class].filter(Boolean).join(' ')
67
+ const baseStyles = `background-color:${bg};padding:24px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
68
+ const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
69
+
70
+ const html = `<table class="w-full"><tr><td class="${props.tdClass}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
71
+
72
+ return () => createStaticVNode(html, 1)
73
+ }
74
+ }
75
+ </script>
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import { createStaticVNode } from 'vue'
3
+
4
+ export default {
5
+ inheritAttrs: false,
6
+ props: {
7
+ /** The inline code text. */
8
+ code: {
9
+ type: String,
10
+ default: ''
11
+ }
12
+ },
13
+ setup(props, { slots, attrs }) {
14
+ let source = props.code
15
+
16
+ if (!source) {
17
+ const slotContent = slots.default?.()
18
+ source = slotContent
19
+ ?.map((vnode: any) => (typeof vnode.children === 'string' ? vnode.children : ''))
20
+ .join('') ?? ''
21
+ }
22
+
23
+ source = source.trim()
24
+
25
+ if (!source) {
26
+ return () => createStaticVNode('', 0)
27
+ }
28
+
29
+ const classes = attrs.class ? ` class="${attrs.class}"` : ''
30
+ const baseStyles = 'white-space:normal;border-radius:6px;border:1px solid #d1d5db;background-color:#f3f4f6;padding:2px 6px;font-size:11px;color:inherit'
31
+ const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
32
+
33
+ const escaped = source
34
+ .replace(/&/g, '&amp;')
35
+ .replace(/</g, '&lt;')
36
+ .replace(/>/g, '&gt;')
37
+ .replace(/"/g, '&quot;')
38
+
39
+ const html = `<code${classes} style="${styles}">${escaped}</code>`
40
+
41
+ return () => createStaticVNode(html, 1)
42
+ }
43
+ }
44
+ </script>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ defineProps({
3
+ /** Number of `&#8199;&#847;` filler pairs to render. */
4
+ fillerCount: {
5
+ type: Number,
6
+ default: 150
7
+ },
8
+ /** Number of `&shy;` entities to render. */
9
+ shyCount: {
10
+ type: Number,
11
+ default: 150
12
+ }
13
+ })
14
+ </script>
15
+
16
+ <template>
17
+ <Teleport to="body:start">
18
+ <div style="display:none"><slot /><template v-for="i in fillerCount" :key="'f'+i">&#8199;&#847; </template><template v-for="i in shyCount" :key="'s'+i">&shy; </template>&nbsp;</div>
19
+ </Teleport>
20
+ </template>
@@ -6,6 +6,11 @@ import { InjectionKey } from "vue";
6
6
  //#region src/composables/renderContext.d.ts
7
7
  interface RenderContext {
8
8
  doctype?: string;
9
+ previewText?: {
10
+ text: string;
11
+ fillerCount: number;
12
+ shyCount: number;
13
+ };
9
14
  sfcConfig?: MaizzleConfig;
10
15
  sfcEventHandlers: Array<{
11
16
  name: EventName;
@@ -1 +1 @@
1
- {"version":3,"file":"renderContext.d.mts","names":[],"sources":["../../src/composables/renderContext.ts"],"mappings":";;;;;;UAKiB,aAAA;EACf,OAAA;EACA,SAAA,GAAY,aAAA;EACZ,gBAAA,EAAkB,KAAA;IAAQ,IAAA,EAAM,SAAA;IAAW,OAAA,EAAS,QAAA,CAAS,SAAA;EAAA;EAC7D,SAAA,GAAY,mBAAA;AAAA;AAAA,cAGD,gBAAA,EAAkB,YAAA,CAAa,aAAA"}
1
+ {"version":3,"file":"renderContext.d.mts","names":[],"sources":["../../src/composables/renderContext.ts"],"mappings":";;;;;;UAKiB,aAAA;EACf,OAAA;EACA,WAAA;IAAgB,IAAA;IAAc,WAAA;IAAqB,QAAA;EAAA;EACnD,SAAA,GAAY,aAAA;EACZ,gBAAA,EAAkB,KAAA;IAAQ,IAAA,EAAM,SAAA;IAAW,OAAA,EAAS,QAAA,CAAS,SAAA;EAAA;EAC7D,SAAA,GAAY,mBAAA;AAAA;AAAA,cAGD,gBAAA,EAAkB,YAAA,CAAa,aAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderContext.mjs","names":[],"sources":["../../src/composables/renderContext.ts"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { EventName, EventMap } from '../events/index.ts'\nimport type { UsePlaintextOptions } from './usePlaintext.ts'\n\nexport interface RenderContext {\n doctype?: string\n sfcConfig?: MaizzleConfig\n sfcEventHandlers: Array<{ name: EventName; handler: EventMap[EventName] }>\n plaintext?: UsePlaintextOptions\n}\n\nexport const RenderContextKey: InjectionKey<RenderContext> = Symbol('RenderContext')\n"],"mappings":";AAYA,MAAa,mBAAgD,OAAO,gBAAgB"}
1
+ {"version":3,"file":"renderContext.mjs","names":[],"sources":["../../src/composables/renderContext.ts"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport type { MaizzleConfig } from '../types/index.ts'\nimport type { EventName, EventMap } from '../events/index.ts'\nimport type { UsePlaintextOptions } from './usePlaintext.ts'\n\nexport interface RenderContext {\n doctype?: string\n previewText?: { text: string; fillerCount: number; shyCount: number }\n sfcConfig?: MaizzleConfig\n sfcEventHandlers: Array<{ name: EventName; handler: EventMap[EventName] }>\n plaintext?: UsePlaintextOptions\n}\n\nexport const RenderContextKey: InjectionKey<RenderContext> = Symbol('RenderContext')\n"],"mappings":";AAaA,MAAa,mBAAgD,OAAO,gBAAgB"}
@@ -0,0 +1,24 @@
1
+ //#region src/composables/usePreviewText.d.ts
2
+ interface UsePreviewTextOptions {
3
+ /** Number of &#8199;&#847; filler pairs to render. @default 150 */
4
+ fillerCount?: number;
5
+ /** Number of &shy; entities to render. @default 150 */
6
+ shyCount?: number;
7
+ }
8
+ /**
9
+ * Set the preview/preheader text for the current email template.
10
+ *
11
+ * Injects a hidden `<div>` at the start of `<body>` with the preview text
12
+ * followed by filler characters that prevent email clients from pulling
13
+ * in body content after the preheader.
14
+ *
15
+ * Usage in SFC <script setup>:
16
+ * ```ts
17
+ * usePreviewText('Thanks for signing up!')
18
+ * usePreviewText('Welcome!', { fillerCount: 200, shyCount: 200 })
19
+ * ```
20
+ */
21
+ declare function usePreviewText(text: string, options?: UsePreviewTextOptions): void;
22
+ //#endregion
23
+ export { UsePreviewTextOptions, usePreviewText };
24
+ //# sourceMappingURL=usePreviewText.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePreviewText.d.mts","names":[],"sources":["../../src/composables/usePreviewText.ts"],"mappings":";UAGiB,qBAAA;EAAA;EAEf,WAAA;;EAEA,QAAA;AAAA;AAgBF;;;;;;;;;;;;;AAAA,iBAAgB,cAAA,CAAe,IAAA,UAAc,OAAA,GAAU,qBAAA"}
@@ -0,0 +1,29 @@
1
+ import { RenderContextKey } from "./renderContext.mjs";
2
+ import { inject } from "vue";
3
+
4
+ //#region src/composables/usePreviewText.ts
5
+ /**
6
+ * Set the preview/preheader text for the current email template.
7
+ *
8
+ * Injects a hidden `<div>` at the start of `<body>` with the preview text
9
+ * followed by filler characters that prevent email clients from pulling
10
+ * in body content after the preheader.
11
+ *
12
+ * Usage in SFC <script setup>:
13
+ * ```ts
14
+ * usePreviewText('Thanks for signing up!')
15
+ * usePreviewText('Welcome!', { fillerCount: 200, shyCount: 200 })
16
+ * ```
17
+ */
18
+ function usePreviewText(text, options) {
19
+ const ctx = inject(RenderContextKey);
20
+ if (ctx) ctx.previewText = {
21
+ text,
22
+ fillerCount: options?.fillerCount ?? 150,
23
+ shyCount: options?.shyCount ?? 150
24
+ };
25
+ }
26
+
27
+ //#endregion
28
+ export { usePreviewText };
29
+ //# sourceMappingURL=usePreviewText.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePreviewText.mjs","names":[],"sources":["../../src/composables/usePreviewText.ts"],"sourcesContent":["import { inject } from 'vue'\nimport { RenderContextKey } from './renderContext.ts'\n\nexport interface UsePreviewTextOptions {\n /** Number of &#8199;&#847; filler pairs to render. @default 150 */\n fillerCount?: number\n /** Number of &shy; entities to render. @default 150 */\n shyCount?: number\n}\n\n/**\n * Set the preview/preheader text for the current email template.\n *\n * Injects a hidden `<div>` at the start of `<body>` with the preview text\n * followed by filler characters that prevent email clients from pulling\n * in body content after the preheader.\n *\n * Usage in SFC <script setup>:\n * ```ts\n * usePreviewText('Thanks for signing up!')\n * usePreviewText('Welcome!', { fillerCount: 200, shyCount: 200 })\n * ```\n */\nexport function usePreviewText(text: string, options?: UsePreviewTextOptions): void {\n const ctx = inject(RenderContextKey)\n if (ctx) {\n ctx.previewText = {\n text,\n fillerCount: options?.fillerCount ?? 150,\n shyCount: options?.shyCount ?? 150,\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAuBA,SAAgB,eAAe,MAAc,SAAuC;CAClF,MAAM,MAAM,OAAO,iBAAiB;AACpC,KAAI,IACF,KAAI,cAAc;EAChB;EACA,aAAa,SAAS,eAAe;EACrC,UAAU,SAAS,YAAY;EAChC"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AttributesConfig, CssConfig, EntitiesConfig, HtmlConfig, MaizzleConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./types/config.mjs";
1
+ import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./types/config.mjs";
2
2
  import { build } from "./build.mjs";
3
3
  import { defineConfig } from "./composables/defineConfig.mjs";
4
4
  import { usePlaintext } from "./composables/usePlaintext.mjs";
@@ -22,8 +22,9 @@ import { shorthandCSS } from "./transformers/shorthandCSS.mjs";
22
22
  import { removeAttributes } from "./transformers/removeAttributes.mjs";
23
23
  import { addAttributes } from "./transformers/addAttributes.mjs";
24
24
  import { purgeCSS } from "./transformers/purgeCSS.mjs";
25
+ import { filters } from "./transformers/filters/index.mjs";
25
26
  import { replaceStrings } from "./transformers/replaceStrings.mjs";
26
27
  import { format } from "./transformers/format.mjs";
27
28
  import { minify } from "./transformers/minify.mjs";
28
29
  import { useHead } from "@unhead/vue";
29
- export { type AttributesConfig, type CreateRendererOptions, type CssConfig, type EntitiesConfig, type HtmlConfig, type MaizzleConfig, type RenderOptions, type RenderResult, type RenderedTemplate, type Renderer, type UrlConfig, type UrlQuery, type UrlQueryOptions, addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, urlQuery, useConfig, useDoctype, useEvent, useHead, usePlaintext };
30
+ export { type AttributesConfig, type CreateRendererOptions, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type RenderOptions, type RenderResult, type RenderedTemplate, type Renderer, type UrlConfig, type UrlQuery, type UrlQueryOptions, addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, filters, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, urlQuery, useConfig, useDoctype, useEvent, useHead, usePlaintext };
package/dist/index.mjs CHANGED
@@ -8,6 +8,7 @@ import { inlineCSS } from "./transformers/inlineCSS.mjs";
8
8
  import { removeAttributes } from "./transformers/removeAttributes.mjs";
9
9
  import { shorthandCSS } from "./transformers/shorthandCSS.mjs";
10
10
  import { addAttributes } from "./transformers/addAttributes.mjs";
11
+ import { filters } from "./transformers/filters/index.mjs";
11
12
  import { base } from "./transformers/base.mjs";
12
13
  import { entities } from "./transformers/entities.mjs";
13
14
  import { urlQuery } from "./transformers/urlQuery.mjs";
@@ -26,4 +27,4 @@ import { useEvent } from "./composables/useEvent.mjs";
26
27
  import { usePlaintext } from "./composables/usePlaintext.mjs";
27
28
  import { useHead } from "@unhead/vue";
28
29
 
29
- export { addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, urlQuery, useConfig, useDoctype, useEvent, useHead, usePlaintext };
30
+ export { addAttributes, attributeToStyle, base, build, createPlaintext, createRenderer, defineConfig, entities, filters, format, inlineCSS, inlineLink, maizzle, minify, removeAttributes, purgeCSS as removeUnusedCSS, render, replaceStrings, resolveConfig, safeClassNames, serve, shorthandCSS, urlQuery, useConfig, useDoctype, useEvent, useHead, usePlaintext };
@@ -14,6 +14,7 @@ interface RenderedTemplate {
14
14
  interface Renderer {
15
15
  render(input: string | Component, config: MaizzleConfig): Promise<RenderedTemplate>;
16
16
  invalidate(filePath: string): Promise<void>;
17
+ invalidateAll(): Promise<void>;
17
18
  close(): Promise<void>;
18
19
  }
19
20
  interface CreateRendererOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"createRenderer.d.mts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;;UA2BiB,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,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAXf;EAaA,GAAA;EAZA;EAcA,QAAA,GAAW,OAAA;EAbX;EAeA,IAAA;EAfyB;EAiBzB,aAAA;AAAA;;;;;;;iBASoB,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
1
+ {"version":3,"file":"createRenderer.d.mts","names":[],"sources":["../../src/render/createRenderer.ts"],"mappings":";;;;;;UA2EiB,gBAAA;EACf,IAAA;EACA,OAAA;EACA,cAAA,EAAgB,aAAA;EAChB,gBAAA,EAAkB,aAAA;EAClB,SAAA,GAAY,aAAA;AAAA;AAAA,UAGG,QAAA;EACf,MAAA,CAAO,KAAA,WAAgB,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA,CAAQ,gBAAA;EAClE,UAAA,CAAW,QAAA,WAAmB,OAAA;EAC9B,aAAA,IAAiB,OAAA;EACjB,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,qBAAA;EAZC;EAchB,GAAA;EAbkB;EAelB,QAAA,GAAW,OAAA;EAdC;EAgBZ,IAAA;EAhByB;EAkBzB,aAAA;AAAA;;;;;;;iBASoB,cAAA,CACpB,OAAA,GAAS,qBAAA,GACR,OAAA,CAAQ,QAAA"}
@@ -16,6 +16,36 @@ import { createHead, renderSSRHead } from "@unhead/vue/server";
16
16
 
17
17
  //#region src/render/createRenderer.ts
18
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ /**
20
+ * Vite plugin that extracts raw slot content from <CodeBlock> tags
21
+ * and passes it as a :code prop before Vue compiles the template.
22
+ *
23
+ * This lets users write HTML naturally inside CodeBlock slots without
24
+ * Vue attempting to compile it as template syntax.
25
+ */
26
+ function codeBlockExtract() {
27
+ const re = /<(CodeBlock|code-block)((?:\s[^>]*?)?)>([\s\S]*?)<\/\1>/g;
28
+ return {
29
+ name: "maizzle:code-block-extract",
30
+ enforce: "pre",
31
+ transform(code, id) {
32
+ if (!id.endsWith(".vue") && !id.endsWith(".md")) return;
33
+ if (!code.includes("CodeBlock") && !code.includes("code-block")) return;
34
+ const transformed = code.replace(re, (_match, tag, attrs, content) => {
35
+ if (/(?:^|\s):code\b/.test(attrs) || /v-bind:code\b/.test(attrs)) return _match;
36
+ const stripped = content.replace(/^\n+/, "").replace(/\s+$/, "");
37
+ if (!stripped) return _match;
38
+ const minIndent = stripped.match(/^[ \t]*(?=\S)/gm)?.reduce((min, ws) => Math.min(min, ws.length), Infinity) ?? 0;
39
+ const dedented = minIndent > 0 ? stripped.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "") : stripped;
40
+ return `<${tag}${attrs} encoded-code="${Buffer.from(dedented).toString("base64")}" />`;
41
+ });
42
+ if (transformed !== code) return {
43
+ code: transformed,
44
+ map: null
45
+ };
46
+ }
47
+ };
48
+ }
19
49
  const vuePkgDir = dirname(fileURLToPath(import.meta.resolve("vue/package.json")));
20
50
  const vueServerRendererPkgDir = dirname(fileURLToPath(import.meta.resolve("@vue/server-renderer/package.json")));
21
51
  const unheadVuePkgDir = resolve(dirname(fileURLToPath(import.meta.resolve("@unhead/vue"))), "..");
@@ -34,6 +64,7 @@ async function createRenderer(options = {}) {
34
64
  const server = await createServer({
35
65
  configFile: false,
36
66
  plugins: [
67
+ codeBlockExtract(),
37
68
  {
38
69
  name: "maizzle:virtual-sfc",
39
70
  resolveId(id) {
@@ -127,13 +158,38 @@ async function createRenderer(options = {}) {
127
158
  app.use(head);
128
159
  app.provide(configKey, config);
129
160
  app.provide(contextKey, renderContext);
130
- let html = await renderToString(app);
161
+ const ssrContext = {};
162
+ let html = await renderToString(app, ssrContext);
131
163
  const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head);
132
164
  if (htmlAttrs) html = html.replace(/<html([^>]*)>/, `<html$1 ${htmlAttrs}>`);
133
165
  if (headTags) html = html.replace("</head>", `${headTags}\n</head>`);
134
166
  if (bodyAttrs) html = html.replace(/<body([^>]*)>/, `<body$1 ${bodyAttrs}>`);
135
167
  if (bodyTagsOpen) html = html.replace(/<body([^>]*)>/, `<body$1>\n${bodyTagsOpen}`);
136
168
  if (bodyTags) html = html.replace("</body>", `${bodyTags}\n</body>`);
169
+ if (ssrContext.teleports) {
170
+ const { parse: parseDom, serialize: serializeDom, walk } = await import("../utils/ast/index.mjs");
171
+ let dom = parseDom(html);
172
+ for (const [rawTarget, content] of Object.entries(ssrContext.teleports)) {
173
+ if (!content) continue;
174
+ const prepend = rawTarget.endsWith(":start");
175
+ const target = prepend ? rawTarget.slice(0, -6) : rawTarget;
176
+ const targetChildren = parseDom(content);
177
+ walk(dom, (node) => {
178
+ const el = node;
179
+ if (!el.name) return;
180
+ if (target === el.name || target.startsWith("#") && el.attribs?.id === target.slice(1) || target.startsWith(".") && el.attribs?.class?.split(/\s+/).includes(target.slice(1))) {
181
+ for (const child of targetChildren) child.parent = el;
182
+ el.children = prepend ? [...targetChildren, ...el.children || []] : [...el.children || [], ...targetChildren];
183
+ }
184
+ });
185
+ }
186
+ html = serializeDom(dom);
187
+ }
188
+ if (renderContext.previewText) {
189
+ const { text, fillerCount, shyCount } = renderContext.previewText;
190
+ const previewHtml = `<div style="display:none">${text}${" ͏ ".repeat(fillerCount)}${"­ ".repeat(shyCount)}\u00A0</div>`;
191
+ html = html.replace(/<body([^>]*)>/, `<body$1>${previewHtml}`);
192
+ }
137
193
  return {
138
194
  html,
139
195
  doctype: renderContext.doctype,
@@ -146,6 +202,9 @@ async function createRenderer(options = {}) {
146
202
  const mod = await server.moduleGraph.getModuleByUrl(filePath);
147
203
  if (mod) server.moduleGraph.invalidateModule(mod);
148
204
  },
205
+ async invalidateAll() {
206
+ for (const mod of server.moduleGraph.idToModuleMap.values()) server.moduleGraph.invalidateModule(mod);
207
+ },
149
208
  async close() {
150
209
  await server.close();
151
210
  }