@maizzle/framework 6.0.0-rc.21 → 6.0.0-rc.23

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 (242) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +11 -0
  3. package/dist/build.js.map +1 -1
  4. package/dist/components/Body.vue +1 -1
  5. package/dist/components/CodeBlock.vue +1 -1
  6. package/dist/components/CodeInline.vue +72 -2
  7. package/dist/components/Column.vue +2 -1
  8. package/dist/components/Container.vue +1 -11
  9. package/dist/components/Heading.vue +1 -1
  10. package/dist/components/Img.vue +252 -7
  11. package/dist/components/Link.vue +1 -1
  12. package/dist/components/Preheader.vue +35 -5
  13. package/dist/components/Section.vue +9 -14
  14. package/dist/components/Tailwind.vue +4 -2
  15. package/dist/components/Text.vue +2 -2
  16. package/dist/components/Vml.vue +354 -0
  17. package/dist/components/utils.d.ts.map +1 -1
  18. package/dist/components/utils.js.map +1 -1
  19. package/dist/composables/defineConfig.d.ts +3 -4
  20. package/dist/composables/defineConfig.d.ts.map +1 -1
  21. package/dist/composables/defineConfig.js +3 -4
  22. package/dist/composables/defineConfig.js.map +1 -1
  23. package/dist/composables/renderContext.d.ts +0 -1
  24. package/dist/composables/renderContext.d.ts.map +1 -1
  25. package/dist/composables/renderContext.js.map +1 -1
  26. package/dist/composables/useBaseUrl.d.ts.map +1 -1
  27. package/dist/composables/useBaseUrl.js.map +1 -1
  28. package/dist/composables/useConfig.d.ts.map +1 -1
  29. package/dist/composables/useConfig.js.map +1 -1
  30. package/dist/composables/useCurrentTemplate.d.ts.map +1 -1
  31. package/dist/composables/useCurrentTemplate.js +10 -3
  32. package/dist/composables/useCurrentTemplate.js.map +1 -1
  33. package/dist/composables/useDoctype.d.ts.map +1 -1
  34. package/dist/composables/useDoctype.js.map +1 -1
  35. package/dist/composables/useEvent.js.map +1 -1
  36. package/dist/composables/useFont.d.ts.map +1 -1
  37. package/dist/composables/useFont.js.map +1 -1
  38. package/dist/composables/useOutlookFallback.d.ts.map +1 -1
  39. package/dist/composables/useOutlookFallback.js.map +1 -1
  40. package/dist/composables/usePlaintext.d.ts.map +1 -1
  41. package/dist/composables/usePlaintext.js.map +1 -1
  42. package/dist/composables/usePreheader.d.ts +6 -5
  43. package/dist/composables/usePreheader.d.ts.map +1 -1
  44. package/dist/composables/usePreheader.js +3 -3
  45. package/dist/composables/usePreheader.js.map +1 -1
  46. package/dist/composables/useTransformers.d.ts +1 -1
  47. package/dist/composables/useTransformers.d.ts.map +1 -1
  48. package/dist/composables/useTransformers.js +1 -1
  49. package/dist/composables/useTransformers.js.map +1 -1
  50. package/dist/composables/useUrlQuery.d.ts.map +1 -1
  51. package/dist/composables/useUrlQuery.js.map +1 -1
  52. package/dist/config/defaults.d.ts.map +1 -1
  53. package/dist/config/defaults.js.map +1 -1
  54. package/dist/config/index.js +12 -0
  55. package/dist/config/index.js.map +1 -1
  56. package/dist/events/index.d.ts +5 -0
  57. package/dist/events/index.d.ts.map +1 -1
  58. package/dist/events/index.js +5 -0
  59. package/dist/events/index.js.map +1 -1
  60. package/dist/index.d.ts +2 -2
  61. package/dist/index.js +2 -2
  62. package/dist/plaintext.d.ts.map +1 -1
  63. package/dist/plaintext.js.map +1 -1
  64. package/dist/plugin.js.map +1 -1
  65. package/dist/plugins/postcss/mergeMediaQueries.d.ts.map +1 -1
  66. package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -1
  67. package/dist/plugins/postcss/pruneVars.d.ts.map +1 -1
  68. package/dist/plugins/postcss/pruneVars.js.map +1 -1
  69. package/dist/plugins/postcss/quoteFontFamilies.d.ts.map +1 -1
  70. package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -1
  71. package/dist/plugins/postcss/removeDeclarations.d.ts.map +1 -1
  72. package/dist/plugins/postcss/removeDeclarations.js.map +1 -1
  73. package/dist/plugins/postcss/resolveMaizzleImports.d.ts.map +1 -1
  74. package/dist/plugins/postcss/resolveMaizzleImports.js.map +1 -1
  75. package/dist/plugins/postcss/resolveProps.d.ts.map +1 -1
  76. package/dist/plugins/postcss/resolveProps.js +14 -0
  77. package/dist/plugins/postcss/resolveProps.js.map +1 -1
  78. package/dist/plugins/postcss/tailwindCleanup.d.ts.map +1 -1
  79. package/dist/plugins/postcss/tailwindCleanup.js.map +1 -1
  80. package/dist/prepare.d.ts.map +1 -1
  81. package/dist/prepare.js.map +1 -1
  82. package/dist/render/active.d.ts.map +1 -1
  83. package/dist/render/active.js.map +1 -1
  84. package/dist/render/createRenderer.d.ts.map +1 -1
  85. package/dist/render/createRenderer.js +91 -3
  86. package/dist/render/createRenderer.js.map +1 -1
  87. package/dist/render/index.d.ts.map +1 -1
  88. package/dist/render/index.js +6 -0
  89. package/dist/render/index.js.map +1 -1
  90. package/dist/render/injectFonts.js.map +1 -1
  91. package/dist/render/plugins/codeBlockExtract.d.ts.map +1 -1
  92. package/dist/render/plugins/codeBlockExtract.js +4 -0
  93. package/dist/render/plugins/codeBlockExtract.js.map +1 -1
  94. package/dist/render/plugins/markdownExtract.d.ts.map +1 -1
  95. package/dist/render/plugins/markdownExtract.js.map +1 -1
  96. package/dist/render/plugins/rawExtract.d.ts.map +1 -1
  97. package/dist/render/plugins/rawExtract.js.map +1 -1
  98. package/dist/render/plugins/rowSourceLocation.d.ts.map +1 -1
  99. package/dist/render/plugins/rowSourceLocation.js.map +1 -1
  100. package/dist/serve.d.ts.map +1 -1
  101. package/dist/serve.js +48 -15
  102. package/dist/serve.js.map +1 -1
  103. package/dist/server/compatibility.d.ts.map +1 -1
  104. package/dist/server/compatibility.js +48 -0
  105. package/dist/server/compatibility.js.map +1 -1
  106. package/dist/server/email.js.map +1 -1
  107. package/dist/server/linter.js +6 -0
  108. package/dist/server/linter.js.map +1 -1
  109. package/dist/server/sfc-utils.d.ts.map +1 -1
  110. package/dist/server/sfc-utils.js.map +1 -1
  111. package/dist/server/ui/App.vue +17 -16
  112. package/dist/server/ui/components/Markdown.vue +17 -0
  113. package/dist/server/ui/components/SidebarClose.vue +1 -1
  114. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +1 -1
  115. package/dist/server/ui/components/ui/command/CommandInput.vue +2 -2
  116. package/dist/server/ui/components/ui/dialog/DialogContent.vue +1 -1
  117. package/dist/server/ui/components/ui/dialog/DialogScrollContent.vue +1 -1
  118. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +1 -1
  119. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  120. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +1 -1
  121. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  122. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
  123. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +1 -1
  124. package/dist/server/ui/lib/emulated-dark-mode.ts +25 -10
  125. package/dist/server/ui/pages/Home.vue +1 -1
  126. package/dist/server/ui/pages/Preview.vue +32 -18
  127. package/dist/tests/render/_helpers.js.map +1 -1
  128. package/dist/transformers/addAttributes.d.ts +18 -8
  129. package/dist/transformers/addAttributes.d.ts.map +1 -1
  130. package/dist/transformers/addAttributes.js +22 -8
  131. package/dist/transformers/addAttributes.js.map +1 -1
  132. package/dist/transformers/attributeToStyle.d.ts.map +1 -1
  133. package/dist/transformers/attributeToStyle.js.map +1 -1
  134. package/dist/transformers/base.d.ts.map +1 -1
  135. package/dist/transformers/base.js +4 -0
  136. package/dist/transformers/base.js.map +1 -1
  137. package/dist/transformers/columnWidth.d.ts.map +1 -1
  138. package/dist/transformers/columnWidth.js +136 -150
  139. package/dist/transformers/columnWidth.js.map +1 -1
  140. package/dist/transformers/entities.d.ts.map +1 -1
  141. package/dist/transformers/entities.js +1 -0
  142. package/dist/transformers/entities.js.map +1 -1
  143. package/dist/transformers/filters/defaults.d.ts.map +1 -1
  144. package/dist/transformers/filters/defaults.js.map +1 -1
  145. package/dist/transformers/filters/index.d.ts.map +1 -1
  146. package/dist/transformers/filters/index.js.map +1 -1
  147. package/dist/transformers/format.d.ts.map +1 -1
  148. package/dist/transformers/format.js.map +1 -1
  149. package/dist/transformers/index.d.ts.map +1 -1
  150. package/dist/transformers/index.js +33 -5
  151. package/dist/transformers/index.js.map +1 -1
  152. package/dist/transformers/inlineCss.d.ts.map +1 -1
  153. package/dist/transformers/inlineCss.js +27 -9
  154. package/dist/transformers/inlineCss.js.map +1 -1
  155. package/dist/transformers/inlineLink.d.ts.map +1 -1
  156. package/dist/transformers/inlineLink.js.map +1 -1
  157. package/dist/transformers/minify.d.ts.map +1 -1
  158. package/dist/transformers/minify.js.map +1 -1
  159. package/dist/transformers/minifyCodeInline.d.ts +29 -0
  160. package/dist/transformers/minifyCodeInline.d.ts.map +1 -0
  161. package/dist/transformers/minifyCodeInline.js +36 -0
  162. package/dist/transformers/minifyCodeInline.js.map +1 -0
  163. package/dist/transformers/msoPlaceholders.d.ts +10 -5
  164. package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
  165. package/dist/transformers/msoPlaceholders.js +38 -7
  166. package/dist/transformers/msoPlaceholders.js.map +1 -1
  167. package/dist/transformers/purgeCss.d.ts.map +1 -1
  168. package/dist/transformers/purgeCss.js +29 -3
  169. package/dist/transformers/purgeCss.js.map +1 -1
  170. package/dist/transformers/removeAttributes.d.ts.map +1 -1
  171. package/dist/transformers/removeAttributes.js.map +1 -1
  172. package/dist/transformers/replaceStrings.d.ts.map +1 -1
  173. package/dist/transformers/replaceStrings.js.map +1 -1
  174. package/dist/transformers/safeSelectors.d.ts +37 -0
  175. package/dist/transformers/safeSelectors.d.ts.map +1 -0
  176. package/dist/transformers/{safeClassNames.js → safeSelectors.js} +37 -6
  177. package/dist/transformers/safeSelectors.js.map +1 -0
  178. package/dist/transformers/shorthandCss.d.ts.map +1 -1
  179. package/dist/transformers/shorthandCss.js +38 -7
  180. package/dist/transformers/shorthandCss.js.map +1 -1
  181. package/dist/transformers/sixHex.d.ts.map +1 -1
  182. package/dist/transformers/sixHex.js.map +1 -1
  183. package/dist/transformers/tailwindComponent.js +9 -0
  184. package/dist/transformers/tailwindComponent.js.map +1 -1
  185. package/dist/transformers/tailwindcss.d.ts.map +1 -1
  186. package/dist/transformers/tailwindcss.js +22 -0
  187. package/dist/transformers/tailwindcss.js.map +1 -1
  188. package/dist/transformers/urlQuery.d.ts.map +1 -1
  189. package/dist/transformers/urlQuery.js.map +1 -1
  190. package/dist/types/config.d.ts +6 -10
  191. package/dist/types/config.d.ts.map +1 -1
  192. package/dist/types/index.d.ts +1 -1
  193. package/dist/utils/ast/parser.d.ts.map +1 -1
  194. package/dist/utils/ast/parser.js.map +1 -1
  195. package/dist/utils/ast/serializer.d.ts.map +1 -1
  196. package/dist/utils/ast/serializer.js +27 -17
  197. package/dist/utils/ast/serializer.js.map +1 -1
  198. package/dist/utils/ast/walker.d.ts.map +1 -1
  199. package/dist/utils/ast/walker.js.map +1 -1
  200. package/dist/utils/compileTailwindCss.d.ts.map +1 -1
  201. package/dist/utils/compileTailwindCss.js.map +1 -1
  202. package/dist/utils/componentSources.d.ts.map +1 -1
  203. package/dist/utils/componentSources.js.map +1 -1
  204. package/dist/utils/cssBox.d.ts +42 -0
  205. package/dist/utils/cssBox.d.ts.map +1 -0
  206. package/dist/utils/cssBox.js +156 -0
  207. package/dist/utils/cssBox.js.map +1 -0
  208. package/dist/utils/decodeStyleEntities.d.ts.map +1 -1
  209. package/dist/utils/decodeStyleEntities.js.map +1 -1
  210. package/dist/utils/detect.d.ts.map +1 -1
  211. package/dist/utils/detect.js.map +1 -1
  212. package/dist/utils/output-markers.d.ts.map +1 -1
  213. package/dist/utils/output-markers.js.map +1 -1
  214. package/dist/utils/url.d.ts.map +1 -1
  215. package/dist/utils/url.js.map +1 -1
  216. package/dist/utils/watchPaths.js.map +1 -1
  217. package/node_modules/@clack/core/CHANGELOG.md +6 -0
  218. package/node_modules/@clack/core/dist/index.d.mts +1 -1
  219. package/node_modules/@clack/core/dist/index.mjs +8 -8
  220. package/node_modules/@clack/core/dist/index.mjs.map +1 -1
  221. package/node_modules/@clack/core/package.json +1 -1
  222. package/node_modules/@clack/prompts/CHANGELOG.md +13 -0
  223. package/node_modules/@clack/prompts/README.md +2 -2
  224. package/node_modules/@clack/prompts/dist/index.d.mts +98 -0
  225. package/node_modules/@clack/prompts/dist/index.mjs +122 -121
  226. package/node_modules/@clack/prompts/dist/index.mjs.map +1 -1
  227. package/node_modules/@clack/prompts/package.json +2 -2
  228. package/node_modules/fast-wrap-ansi/lib/main.js +0 -1
  229. package/node_modules/fast-wrap-ansi/package.json +10 -10
  230. package/node_modules/maizzle/dist/commands/make/config.mjs +7 -6
  231. package/node_modules/maizzle/dist/commands/new.mjs +15 -84
  232. package/node_modules/maizzle/package.json +2 -2
  233. package/node_modules/tinyexec/README.md +8 -0
  234. package/node_modules/tinyexec/dist/main.d.mts +16 -1
  235. package/node_modules/tinyexec/dist/main.mjs +163 -457
  236. package/node_modules/tinyexec/package.json +12 -14
  237. package/package.json +3 -4
  238. package/dist/transformers/safeClassNames.d.ts +0 -22
  239. package/dist/transformers/safeClassNames.d.ts.map +0 -1
  240. package/dist/transformers/safeClassNames.js.map +0 -1
  241. package/node_modules/fast-wrap-ansi/lib/main.js.map +0 -1
  242. package/node_modules/tinyexec/dist/LICENSES.txt +0 -83
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAeiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,aAAA;AAAA;;;;;;;;;AAaV;;iBAAsB,KAAA,CAAM,WAAA,GAAc,OAAA,CAAQ,aAAA,aAA0B,OAAA,CAAQ,WAAA"}
1
+ {"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAeiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,aAAa;AAAA;;;;;;;;AAAA;AAavB;;iBAAsB,KAAA,CAAM,WAAA,GAAc,OAAA,CAAQ,aAAA,aAA0B,OAAA,CAAQ,WAAA"}
package/dist/build.js CHANGED
@@ -70,12 +70,23 @@ async function build(configInput) {
70
70
  template
71
71
  });
72
72
  const rendered = await renderer.render(absolutePath, config);
73
+ /**
74
+ * Register SFC event handlers collected during render so they take
75
+ * part in the post-render events (afterRender / afterTransform).
76
+ * They're cleared at the end of the iteration so they don't
77
+ * leak into the next template.
78
+ */
73
79
  for (const { name, handler } of rendered.sfcEventHandlers) events.on(name, handler);
74
80
  let html = await events.fireAfterRender({
75
81
  config,
76
82
  template,
77
83
  html: rendered.html
78
84
  });
85
+ /**
86
+ * Use the per-template merged config (from defineConfig() in the SFC) so
87
+ * that template-level overrides like css.safe: false are respected
88
+ * by transformers.
89
+ */
79
90
  const templateConfig = rendered.templateConfig;
80
91
  const doctype = rendered.doctype ?? templateConfig.doctype ?? "<!DOCTYPE html>";
81
92
  if (templateConfig.useTransformers !== false) html = await runTransformers(html, templateConfig, absolutePath, doctype, rendered.tailwindBlocks);
package/dist/build.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","names":["parsePath"],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join, parse as parsePath } 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 { stripForHtml, stripForPlaintext } from './utils/output-markers.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { _setCurrentTemplate } from './composables/useCurrentTemplate.ts'\nimport defu from 'defu'\nimport type { MaizzleConfig } from './types/index.ts'\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 *\n * Pass a `Partial<MaizzleConfig>` to override config inline, or a string\n * to load config from a specific file path. Omit to load `maizzle.config`\n * from the working directory.\n */\nexport async function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora({ text: 'Building templates...', spinner: 'circleHalves' }).start()\n\n const config = await resolveConfig(configInput)\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: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n const parsedPath = parsePath(absolutePath)\n const template = { source: readFileSync(absolutePath, 'utf-8'), path: parsedPath }\n\n _setCurrentTemplate(parsedPath)\n\n try {\n await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n // Register SFC event handlers collected during render so they participate\n // in the post-render events (afterRender / afterTransform). They're cleared\n // at the end of the iteration so they don't leak into the next template.\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\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, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, htmlOut)\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 globalCfg = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const stripOptions = defu(sfcPlaintext?.options, globalCfg.options)\n const plaintext = createPlaintext(stripForPlaintext(html), stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? globalCfg.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 (globalCfg.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), 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 } finally {\n _setCurrentTemplate(undefined)\n events.clearSfcHandlers()\n }\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.js","names":["parsePath"],"sources":["../src/build.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync } from 'node:fs'\nimport { resolve, dirname, basename, relative, join, parse as parsePath } 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 { stripForHtml, stripForPlaintext } from './utils/output-markers.ts'\nimport { normalizeComponentSources } from './utils/componentSources.ts'\nimport { _setCurrentTemplate } from './composables/useCurrentTemplate.ts'\nimport defu from 'defu'\nimport type { MaizzleConfig } from './types/index.ts'\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 *\n * Pass a `Partial<MaizzleConfig>` to override config inline, or a string\n * to load config from a specific file path. Omit to load `maizzle.config`\n * from the working directory.\n */\nexport async function build(configInput?: Partial<MaizzleConfig> | string): Promise<BuildResult> {\n const start = Date.now()\n const spinner = ora({ text: 'Building templates...', spinner: 'circleHalves' }).start()\n\n const config = await resolveConfig(configInput)\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: normalizeComponentSources(config.components?.source, process.cwd()), vite: config.vite })\n const outputFiles: string[] = []\n\n try {\n for (const templatePath of templateFiles) {\n const absolutePath = resolve(templatePath)\n const parsedPath = parsePath(absolutePath)\n const template = { source: readFileSync(absolutePath, 'utf-8'), path: parsedPath }\n\n _setCurrentTemplate(parsedPath)\n\n try {\n await events.fireBeforeRender({ config, template })\n\n const rendered = await renderer.render(absolutePath, config)\n\n /**\n * Register SFC event handlers collected during render so they take\n * part in the post-render events (afterRender / afterTransform).\n * They're cleared at the end of the iteration so they don't\n * leak into the next template.\n */\n for (const { name, handler } of rendered.sfcEventHandlers) {\n events.on(name, handler)\n }\n\n let html = await events.fireAfterRender({ config, template, html: rendered.html })\n\n /**\n * Use the per-template merged config (from defineConfig() in the SFC) so\n * that template-level overrides like css.safe: false are respected\n * by transformers.\n */\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, rendered.tailwindBlocks)\n }\n\n html = await events.fireAfterTransform({ config, template, html })\n html = `${doctype}\\n${html}`\n\n const htmlOut = stripForHtml(html)\n const outputFilePath = resolveOutputPath(templatePath, outputPath, outputExtension, contentBase)\n mkdirSync(dirname(outputFilePath), { recursive: true })\n writeFileSync(outputFilePath, htmlOut)\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 globalCfg = typeof globalPlaintext === 'object' ? globalPlaintext : {}\n const stripOptions = defu(sfcPlaintext?.options, globalCfg.options)\n const plaintext = createPlaintext(stripForPlaintext(html), stripOptions)\n const ptExtension = sfcPlaintext?.extension ?? globalCfg.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 (globalCfg.destination) {\n ptOutputPath = resolveOutputPath(templatePath, resolve(globalCfg.destination), 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 } finally {\n _setCurrentTemplate(undefined)\n events.clearSfcHandlers()\n }\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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"}
@@ -94,7 +94,7 @@ const render = () => {
94
94
 
95
95
  const parts = [
96
96
  `dir="${props.dir}"`,
97
- 'style="margin: 0; padding: 0; width: 100%; word-break: break-word;"',
97
+ 'style="margin: 0; padding: 0; width: 100%; height: 100%; word-break: break-word;"',
98
98
  ]
99
99
  if (outlookFallback) {
100
100
  parts.unshift(`xml:lang="${lang}"`)
@@ -61,7 +61,7 @@ export default {
61
61
  const baseStyles = `background-color:${bg};padding:16px;overflow:auto;white-space:pre;word-wrap:normal;word-break:normal;word-spacing:normal`
62
62
  const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
63
63
 
64
- const html = `<table class="w-full"><tr><td class="${props.tdClass}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
64
+ const html = `<table class="w-full"><tr><td class="${props.tdClass}" style="background-color:${bg}"><pre class="${classes}" style="${styles}"><code>${codeContent}</code></pre></td></tr></table>`
65
65
 
66
66
  return () => createStaticVNode(html, 1)
67
67
  }
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { createStaticVNode } from 'vue'
2
+ import { createStaticVNode, type PropType } from 'vue'
3
+ import { codeToHtml, getSingletonHighlighter, type BundledLanguage, type BundledTheme } from 'shiki'
3
4
 
4
5
  export default {
5
6
  inheritAttrs: false,
@@ -13,9 +14,27 @@ export default {
13
14
  code: {
14
15
  type: String,
15
16
  default: ''
17
+ },
18
+ /**
19
+ * Language for syntax highlighting. Only consulted when `theme` is set.
20
+ * @default 'html'
21
+ */
22
+ language: {
23
+ type: String as PropType<BundledLanguage>,
24
+ default: 'html'
25
+ },
26
+ /**
27
+ * Shiki theme to apply. When set, the inline code is syntax-highlighted
28
+ * with this theme and the cell uses the theme's background color.
29
+ * When unset, falls back to the plain gray-styled `<code>` (no Shiki
30
+ * pass, faster, and visually quieter in body copy).
31
+ */
32
+ theme: {
33
+ type: String as PropType<BundledTheme | undefined>,
34
+ default: undefined
16
35
  }
17
36
  },
18
- setup(props, { slots, attrs }) {
37
+ async setup(props, { slots, attrs }) {
19
38
  let source = props.code
20
39
 
21
40
  if (!source) {
@@ -32,6 +51,57 @@ export default {
32
51
  }
33
52
 
34
53
  const classes = attrs.class ? ` class="${attrs.class}"` : ''
54
+
55
+ if (props.theme) {
56
+ const highlighted = await codeToHtml(source, {
57
+ lang: props.language,
58
+ theme: props.theme,
59
+ })
60
+
61
+ const hl = await getSingletonHighlighter({ themes: [props.theme], langs: [] })
62
+ const bg = hl.getTheme(props.theme).bg
63
+
64
+ const codeContent = highlighted
65
+ .replace(/^<pre[^>]*><code>/, '')
66
+ .replace(/<\/code><\/pre>$/, '')
67
+
68
+ /**
69
+ * Replace shiki's structural `<`/`>` (the `<span>` tag delimiters)
70
+ * with private string markers `§MZLT§`/`§MZGT§`. Source-level
71
+ * entities like `&lt;` (representing a literal `<` in the user's
72
+ * code) are made of `&`, `l`, `t`, `;` — no real `<` character —
73
+ * so they pass through untouched.
74
+ *
75
+ * Why markers and not HTML entities? Both levels of escaping would
76
+ * end up as `&lt;` after a round-trip, and the decoder couldn't
77
+ * tell which to decode back to a real `<` (structural) vs leave
78
+ * as `&lt;` (content). Using non-entity markers makes the two
79
+ * levels distinguishable: only `§MZ*§` gets decoded.
80
+ *
81
+ * Two pipeline passes that would otherwise mangle the shiki HTML
82
+ * are defused by the markers:
83
+ * - `format` (oxfmt with `htmlWhitespaceSensitivity: 'ignore'`)
84
+ * sees the `<code>` body as plain text and won't reflow the
85
+ * chain of `<span>` tokens onto separate lines.
86
+ * - The HTML5 self-close strip (`( \/>)` regex at the end of
87
+ * the pipeline) won't match anything inside a shiki cell,
88
+ * so a highlighted Vue self-closing tag like `<MyTag />`
89
+ * keeps its ` />` instead of being silently shortened to `>`.
90
+ *
91
+ * `minifyCodeInline` swaps the markers back to real angle brackets
92
+ * at the very end of the pipeline, after both passes have run.
93
+ */
94
+ const escaped = codeContent
95
+ .replace(/</g, '§MZLT§')
96
+ .replace(/>/g, '§MZGT§')
97
+
98
+ const baseStyles = `background-color:${bg};border-radius:6px;padding:2px 6px;font-size:11px`
99
+ const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
100
+
101
+ const html = `<code${classes} style="${styles}" data-minify-inline>${escaped}</code>`
102
+ return () => createStaticVNode(html, 1)
103
+ }
104
+
35
105
  const baseStyles = 'white-space:normal;border-radius:6px;border:1px solid #d1d5db;background-color:#f3f4f6;padding:2px 6px;font-size:11px;color:inherit'
36
106
  const styles = [baseStyles, attrs.style].filter(Boolean).join(';')
37
107
 
@@ -71,13 +71,14 @@ const msoWidth = computed(() => {
71
71
  * `inline-table` during CSS inlining; routing both through twMerge lets
72
72
  * the user's utility cleanly replace ours instead of being dropped.
73
73
  */
74
- const baseClass = 'inline-block align-top text-base'
74
+ const baseClass = 'inline-block align-top text-[medium]'
75
75
  const mergedClass = computed(() => twMerge(baseClass, (attrs.class as string) ?? ''))
76
76
 
77
77
  const styles = computed(() => `min-width: ${minWidth.value};`)
78
78
 
79
79
  const tdStyle = computed(() => {
80
80
  const parts = [`width: ${msoWidth.value}`, 'vertical-align: top']
81
+ if (useMarker) parts.push(`__MAIZZLE_COLTDX_${colId}__`)
81
82
  if (props.msoStyle) parts.push(props.msoStyle)
82
83
  return parts.join('; ')
83
84
  })
@@ -26,15 +26,6 @@ const props = defineProps({
26
26
  type: [String, Number],
27
27
  default: null
28
28
  },
29
- /**
30
- * Override the Outlook (MSO) table width independently of the
31
- * div's width. Highest priority — wins over `width` and any
32
- * class-derived value.
33
- */
34
- msoWidth: {
35
- type: [String, Number],
36
- default: null
37
- },
38
29
  /**
39
30
  * Inline CSS applied only to the MSO `<td>` element.
40
31
  *
@@ -65,7 +56,7 @@ const outlookFallback = useOutlookFallback(props.outlookFallback)
65
56
 
66
57
  provide('containerWidth', computed(() => props.width))
67
58
 
68
- const useMarker = outlookFallback && props.width == null && props.msoWidth == null
59
+ const useMarker = outlookFallback && props.width == null
69
60
  const msoId = useMarker ? nextId('c') : null
70
61
  const tdId = outlookFallback ? nextId('ct') : null
71
62
 
@@ -84,7 +75,6 @@ const mergedClass = computed(() => {
84
75
  })
85
76
 
86
77
  const msoWidth = computed(() => {
87
- if (props.msoWidth != null) return normalizeToPixels(props.msoWidth)
88
78
  if (props.width != null) return normalizeToPixels(props.width)
89
79
  return `__MAIZZLE_MSOW_${msoId}__`
90
80
  })
@@ -22,7 +22,7 @@ const mergedClass = computed(() => twMerge('m-0', attrs.class as string))
22
22
  </script>
23
23
 
24
24
  <template>
25
- <component :is="tag" v-bind="$attrs" :class="mergedClass">
25
+ <component :is="tag" v-bind="{ ...$attrs, class: mergedClass }">
26
26
  <slot />
27
27
  </component>
28
28
  </template>
@@ -1,5 +1,16 @@
1
1
  <script setup lang="ts">
2
- import { computed, useAttrs } from 'vue'
2
+ import { computed, createStaticVNode, useAttrs, type PropType } from 'vue'
3
+ import { outlookFallbackProp } from './utils.ts'
4
+ import { useOutlookFallback } from '../composables/useOutlookFallback'
5
+
6
+ type AspectRatio = '1:1' | '4:3' | '3:2' | '16:9' | '21:9' | '2:1' | '3:4' | '9:16' | (string & {})
7
+ type BackgroundPosition =
8
+ | 'top' | 'right' | 'bottom' | 'left' | 'center'
9
+ | 'top left' | 'top right' | 'top center'
10
+ | 'bottom left' | 'bottom right' | 'bottom center'
11
+ | 'center left' | 'center right' | 'center center'
12
+ | (string & {})
13
+ type BackgroundSize = 'cover' | 'contain' | 'auto' | (string & {})
3
14
 
4
15
  defineOptions({ inheritAttrs: false })
5
16
 
@@ -30,9 +41,75 @@ const props = defineProps({
30
41
  motionSrc: {
31
42
  type: String,
32
43
  default: null
33
- }
44
+ },
45
+ /**
46
+ * Aspect ratio for cropped images.
47
+ *
48
+ * Accepts colon or slash form: `'16:9'`, `'16/9'`, `'4:3'`, `'1:1'`, etc.
49
+ *
50
+ * Alternatively, set a Tailwind aspect class on the component:
51
+ * `aspect-square`, `aspect-video`, `aspect-[16/9]`, `aspect-3/2`. The
52
+ * prop wins when both are provided.
53
+ *
54
+ * @example '16:9'
55
+ * @example '4:3'
56
+ * @example '1:1'
57
+ */
58
+ aspect: {
59
+ type: String as PropType<AspectRatio>,
60
+ default: ''
61
+ },
62
+ /**
63
+ * CSS `background-position` for the cropped image fill.
64
+ *
65
+ * @default 'center'
66
+ * @example 'top'
67
+ * @example 'top left'
68
+ * @example '20% 30%'
69
+ */
70
+ position: {
71
+ type: String as PropType<BackgroundPosition>,
72
+ default: 'center'
73
+ },
74
+ /**
75
+ * CSS `background-size` for the cropped image fill.
76
+ *
77
+ * @default 'cover'
78
+ * @example 'contain'
79
+ * @example 'auto'
80
+ */
81
+ size: {
82
+ type: String as PropType<BackgroundSize>,
83
+ default: 'cover'
84
+ },
85
+ /**
86
+ * Toggle Outlook (MSO) and VML fallback markup for this image.
87
+ *
88
+ * Inherits from an ancestor (e.g. a Layout calling
89
+ * `useOutlookFallback(false)`); an explicit value overrides. When
90
+ * `false`, the VML `<v:rect>` emitted in cropped mode (`aspect`)
91
+ * is skipped and the modern padding-hack div renders to all
92
+ * clients including Outlook (which will show an empty area).
93
+ *
94
+ * @default inherits — root default `true`
95
+ */
96
+ outlookFallback: outlookFallbackProp,
97
+ /**
98
+ * URL to navigate to when the image is clicked.
99
+ *
100
+ * Modern clients: output is wrapped in `<a href>`. In cropped mode the
101
+ * anchor is `display:block` so the whole padding-hack area is clickable.
102
+ * Outlook: emitted as the `href` attribute on the `<v:rect>` (a
103
+ * documented VML Shape attribute).
104
+ */
105
+ href: {
106
+ type: String,
107
+ default: ''
108
+ },
34
109
  })
35
110
 
111
+ const outlookFallback = useOutlookFallback(props.outlookFallback)
112
+
36
113
  function mimeFromExtension(src: string): string {
37
114
  const ext = src.split('.').pop()?.toLowerCase() ?? ''
38
115
 
@@ -51,20 +128,188 @@ function mimeFromExtension(src: string): string {
51
128
  return types[ext] ?? ''
52
129
  }
53
130
 
131
+ const ASPECT_KEYWORDS: Record<string, string> = {
132
+ 'aspect-square': '1/1',
133
+ 'aspect-video': '16/9',
134
+ }
135
+
136
+ function normalizeClass(value: unknown): string {
137
+ if (!value) return ''
138
+ if (typeof value === 'string') return value
139
+ if (Array.isArray(value)) return value.map(normalizeClass).filter(Boolean).join(' ')
140
+ if (typeof value === 'object') {
141
+ return Object.entries(value as Record<string, unknown>)
142
+ .filter(([, v]) => v)
143
+ .map(([k]) => k)
144
+ .join(' ')
145
+ }
146
+ return ''
147
+ }
148
+
149
+ /**
150
+ * Pull Tailwind `aspect-*` tokens out of the inherited class list. Returns
151
+ * both the derived ratio (first match wins) and the cleaned class string
152
+ * so the aspect token isn't duplicated on the wrapper.
153
+ */
154
+ const parsedClass = computed(() => {
155
+ const tokens = normalizeClass(attrs.class).split(/\s+/).filter(Boolean)
156
+ let ratio: string | null = null
157
+ const rest: string[] = []
158
+ for (const t of tokens) {
159
+ if (ASPECT_KEYWORDS[t]) {
160
+ if (!ratio) ratio = ASPECT_KEYWORDS[t]
161
+ continue
162
+ }
163
+ const m = t.match(/^aspect-(?:\[(\d+(?:\.\d+)?)[/:](\d+(?:\.\d+)?)\]|(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?))$/)
164
+ if (m) {
165
+ if (!ratio) ratio = `${m[1] ?? m[3]}/${m[2] ?? m[4]}`
166
+ continue
167
+ }
168
+ rest.push(t)
169
+ }
170
+ return { ratio, className: rest.join(' ') }
171
+ })
172
+
173
+ const resolvedAspect = computed(() => props.aspect || parsedClass.value.ratio || '')
174
+
175
+ const ratio = computed(() => {
176
+ if (!resolvedAspect.value) return null
177
+ const [w, h] = resolvedAspect.value.split(/[:/]/).map(Number)
178
+ if (!w || !h || !Number.isFinite(w) || !Number.isFinite(h)) return null
179
+ const pct = ((h / w) * 100).toFixed(4).replace(/\.?0+$/, '')
180
+ return { w, h, paddingBottom: `${pct}%` }
181
+ })
182
+
183
+ const isCropped = computed(() => ratio.value !== null)
184
+
54
185
  const motionType = computed(() => mimeFromExtension(props.motionSrc ?? ''))
55
186
 
56
187
  const imgWidth = computed(() => Number.parseInt(String(props.width), 10))
57
188
 
58
- const usePicture = computed(() => props.darkSrc || props.motionSrc)
189
+ const heightPx = computed(() =>
190
+ ratio.value && Number.isFinite(imgWidth.value)
191
+ ? Math.round((imgWidth.value * ratio.value.h) / ratio.value.w)
192
+ : null
193
+ )
194
+
195
+ const usePicture = computed(() => !isCropped.value && (props.darkSrc || props.motionSrc))
196
+
197
+ /**
198
+ * Escape characters that break Tailwind's `bg-[url('...')]` arbitrary value
199
+ * (the closing `']`, braces, spaces) and the `url()` wrapper itself (quotes,
200
+ * parens). Targeted replace so already-encoded URLs aren't double-encoded.
201
+ *
202
+ * Only used for the dark/motion variant classes — those have to be Tailwind
203
+ * arbitrary classes so they compile to `@media` rules. The base background
204
+ * image is set inline via `:style` to avoid the CSS pipeline rewriting it.
205
+ */
206
+ const escapeForClass = (url: string) => url
207
+ .replace(/'/g, '%27')
208
+ .replace(/\(/g, '%28')
209
+ .replace(/\)/g, '%29')
210
+ .replace(/ /g, '%20')
211
+ .replace(/\]/g, '%5D')
212
+ .replace(/\}/g, '%7D')
213
+
214
+ /** Escape a URL for safe use inside `url('...')` in an inline style. */
215
+ const escapeForCssUrl = (s: string) => s
216
+ .replace(/\\/g, '\\\\')
217
+ .replace(/'/g, "\\'")
218
+
219
+ const escapeAttr = (s: string) => s
220
+ .replace(/&/g, '&amp;')
221
+ .replace(/"/g, '&quot;')
222
+ .replace(/</g, '&lt;')
223
+ .replace(/>/g, '&gt;')
224
+
225
+ const vmlAspect = computed(() => {
226
+ if (props.size === 'cover') return 'atleast'
227
+ if (props.size === 'contain') return 'atmost'
228
+ return ''
229
+ })
230
+
231
+ const VmlRect = () => {
232
+ if (!isCropped.value || !heightPx.value || !Number.isFinite(imgWidth.value)) return null
233
+ const aspectAttr = vmlAspect.value ? ` aspect="${vmlAspect.value}"` : ''
234
+ const altAttr = props.alt ? ` alt="${escapeAttr(props.alt)}"` : ''
235
+ const hrefAttr = props.href ? ` href="${escapeAttr(props.href)}"` : ''
236
+ return createStaticVNode(
237
+ `<!--[if mso]><v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false"${hrefAttr}${altAttr} style="width:${imgWidth.value}px;height:${heightPx.value}px;"><v:fill type="frame" src="${escapeAttr(props.src)}"${aspectAttr} /></v:rect><![endif]-->`,
238
+ 1
239
+ )
240
+ }
241
+
242
+ const NotMsoBefore = () => createStaticVNode('<!--[if !mso]><!-->', 1)
243
+ const NotMsoAfter = () => createStaticVNode('<!--<![endif]-->', 1)
59
244
 
60
- const imgStyle = 'max-width: 100%; vertical-align: middle;'
245
+ const imgClass = 'max-w-full align-middle'
61
246
  </script>
62
247
 
63
248
  <template>
64
- <picture v-if="usePicture">
249
+ <template v-if="isCropped">
250
+ <VmlRect v-if="outlookFallback" />
251
+ <NotMsoBefore v-if="outlookFallback" />
252
+ <a v-if="href" :href="href" class="block no-underline">
253
+ <div
254
+ v-bind="{ ...attrs, class: undefined }"
255
+ role="img"
256
+ :aria-label="alt || undefined"
257
+ :class="['overflow-hidden table max-w-full', parsedClass.className]"
258
+ :style="`width: ${imgWidth}px;`"
259
+ >
260
+ <div
261
+ :class="[
262
+ 'table-cell w-full h-0 bg-no-repeat',
263
+ darkSrc ? `dark:bg-[url('${escapeForClass(darkSrc)}')]!` : '',
264
+ motionSrc ? `motion-safe:bg-[url('${escapeForClass(motionSrc)}')]!` : '',
265
+ ]"
266
+ :style="{
267
+ paddingBottom: ratio!.paddingBottom,
268
+ backgroundImage: `url('${escapeForCssUrl(src)}')`,
269
+ backgroundSize: size,
270
+ backgroundPosition: position,
271
+ }"
272
+ />
273
+ </div>
274
+ </a>
275
+ <div
276
+ v-else
277
+ v-bind="{ ...attrs, class: undefined }"
278
+ role="img"
279
+ :aria-label="alt || undefined"
280
+ :class="['overflow-hidden table max-w-full', parsedClass.className]"
281
+ :style="`width: ${imgWidth}px;`"
282
+ >
283
+ <div
284
+ :class="[
285
+ 'table-cell w-full h-0 bg-no-repeat',
286
+ darkSrc ? `dark:bg-[url('${escapeForClass(darkSrc)}')]!` : '',
287
+ motionSrc ? `motion-safe:bg-[url('${escapeForClass(motionSrc)}')]!` : '',
288
+ ]"
289
+ :style="{
290
+ paddingBottom: ratio!.paddingBottom,
291
+ backgroundImage: `url('${escapeForCssUrl(src)}')`,
292
+ backgroundSize: size,
293
+ backgroundPosition: position,
294
+ }"
295
+ />
296
+ </div>
297
+ <NotMsoAfter v-if="outlookFallback" />
298
+ </template>
299
+ <a v-else-if="href && usePicture" :href="href">
300
+ <picture>
301
+ <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
302
+ <source v-if="motionSrc" :srcset="motionSrc" :type="motionType || undefined" media="(prefers-reduced-motion: no-preference)">
303
+ <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
304
+ </picture>
305
+ </a>
306
+ <picture v-else-if="usePicture">
65
307
  <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
66
308
  <source v-if="motionSrc" :srcset="motionSrc" :type="motionType || undefined" media="(prefers-reduced-motion: no-preference)">
67
- <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :style="imgStyle">
309
+ <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
68
310
  </picture>
69
- <img v-else v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :style="imgStyle">
311
+ <a v-else-if="href" :href="href">
312
+ <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
313
+ </a>
314
+ <img v-else v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
70
315
  </template>
@@ -20,7 +20,7 @@ const mergedClass = computed(() => twMerge('no-underline', attrs.class as string
20
20
  </script>
21
21
 
22
22
  <template>
23
- <a :href="href" v-bind="$attrs" :class="mergedClass">
23
+ <a :href="href" v-bind="{ ...$attrs, class: mergedClass }">
24
24
  <slot />
25
25
  </a>
26
26
  </template>
@@ -1,15 +1,45 @@
1
1
  <script setup lang="ts">
2
- defineProps({
3
- /** Number of `&#8199;&#65279;&#847;` filler sequences to render after the preview text. */
2
+ import { useSlots, computed } from 'vue'
3
+
4
+ const props = defineProps({
5
+ /**
6
+ * Explicit number of filler sequences to render. When omitted, the count
7
+ * is auto-derived to fill our default 200-char inbox preview budget.
8
+ */
4
9
  spaces: {
5
10
  type: Number,
6
- default: 150
7
- }
11
+ default: undefined,
12
+ },
8
13
  })
14
+
15
+ const slots = useSlots()
16
+
17
+ function vnodesToText(nodes: unknown): string {
18
+ if (nodes == null || nodes === false || nodes === true) return ''
19
+ if (typeof nodes === 'string' || typeof nodes === 'number') return String(nodes)
20
+ if (Array.isArray(nodes)) return nodes.map(vnodesToText).join('')
21
+ if (typeof nodes === 'object' && 'children' in (nodes as Record<string, unknown>)) {
22
+ return vnodesToText((nodes as { children: unknown }).children)
23
+ }
24
+ return ''
25
+ }
26
+
27
+ /**
28
+ * Inbox preview budget. Pad with invisible fillers so the client
29
+ * doesn't pull body content into the snippet.
30
+ */
31
+ const PREVIEW_LENGTH = 200
32
+
33
+ const text = computed(() => vnodesToText(slots.default?.()))
34
+ const fillerCount = computed(() =>
35
+ props.spaces !== undefined
36
+ ? Math.max(0, props.spaces)
37
+ : Math.max(0, PREVIEW_LENGTH - text.value.length),
38
+ )
9
39
  </script>
10
40
 
11
41
  <template>
12
42
  <Teleport to="body:start">
13
- <div style="display: none"><slot /><template v-for="i in spaces" :key="i">&#8199;&#65279;&#847; </template>&nbsp;</div>
43
+ <div style="display: none">{{ text }}<template v-for="i in fillerCount" :key="i">&#8199;&#65279;&#847; </template>&nbsp;</div>
14
44
  </Teleport>
15
45
  </template>
@@ -63,6 +63,7 @@ const userHasWidth = computed(() => {
63
63
 
64
64
  const useMarker = outlookFallback && props.width == null && userHasWidth.value
65
65
  const msoId = useMarker ? nextId('s') : null
66
+ const tdId = outlookFallback ? nextId('st') : null
66
67
 
67
68
  const divStyle = computed(() => {
68
69
  const parts: string[] = []
@@ -76,13 +77,6 @@ const restAttrs = computed(() => {
76
77
  return rest
77
78
  })
78
79
 
79
- const tdStyles = computed(() => {
80
- const parts: string[] = []
81
- if (userStyle.value) parts.push(userStyle.value)
82
- if (props.msoStyle) parts.push(props.msoStyle)
83
- return parts.length ? parts.join('; ') : ''
84
- })
85
-
86
80
  const msoWidth = computed(() => {
87
81
  if (props.width != null) return normalizeToPixels(props.width)
88
82
  if (useMarker) return `__MAIZZLE_MSOW_${msoId}__`
@@ -95,13 +89,12 @@ const colWidthSource = computed(() => {
95
89
  return null
96
90
  })
97
91
 
98
- const MsoBefore = () => {
99
- const tdStyle = tdStyles.value ? ` style="${tdStyles.value}"` : ''
100
- return createStaticVNode(
101
- `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${msoWidth.value}"><tr><td${tdStyle}><![endif]-->`,
102
- 1
103
- )
104
- }
92
+ const tdMarker = tdId ? `__MAIZZLE_MSOTDSTYLE_${tdId}__` : ''
93
+
94
+ const MsoBefore = () => createStaticVNode(
95
+ `<!--[if mso]><table role="none" cellpadding="0" cellspacing="0" style="width: ${msoWidth.value}"><tr><td${tdMarker}><![endif]-->`,
96
+ 1
97
+ )
105
98
 
106
99
  const MsoAfter = () => createStaticVNode(
107
100
  '<!--[if mso]></td></tr></table><![endif]-->',
@@ -117,6 +110,8 @@ const MsoAfter = () => createStaticVNode(
117
110
  :data-maizzle-msow-id="msoId"
118
111
  :data-maizzle-msow-fallback="useMarker ? '100%' : null"
119
112
  :data-maizzle-cw="colWidthSource"
113
+ :data-maizzle-mso-td-id="tdId"
114
+ :data-maizzle-mso-style="tdId && props.msoStyle ? props.msoStyle : null"
120
115
  >
121
116
  <slot />
122
117
  </div>