@maizzle/framework 6.0.0-rc.24 → 6.0.0-rc.26

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 (143) hide show
  1. package/dist/build.d.ts +19 -1
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/build.js +139 -102
  4. package/dist/build.js.map +1 -1
  5. package/dist/components/Body.vue +12 -0
  6. package/dist/components/Button.vue +16 -29
  7. package/dist/components/CodeBlock.vue +5 -4
  8. package/dist/components/CodeInline.vue +9 -8
  9. package/dist/components/Column.vue +17 -4
  10. package/dist/components/Container.vue +7 -11
  11. package/dist/components/Hr.vue +1 -1
  12. package/dist/components/Img.vue +39 -22
  13. package/dist/components/Layout.vue +1 -1
  14. package/dist/components/Markdown.vue +9 -14
  15. package/dist/components/QrCode.vue +2 -2
  16. package/dist/components/Section.vue +9 -6
  17. package/dist/components/Text.vue +2 -2
  18. package/dist/components/utils.d.ts +25 -1
  19. package/dist/components/utils.d.ts.map +1 -1
  20. package/dist/components/utils.js +29 -1
  21. package/dist/components/utils.js.map +1 -1
  22. package/dist/components/utils.ts +46 -0
  23. package/dist/composables/useConfig.d.ts.map +1 -1
  24. package/dist/composables/useCurrentTemplate.d.ts.map +1 -1
  25. package/dist/composables/useEvent.d.ts.map +1 -1
  26. package/dist/composables/useFont.js.map +1 -1
  27. package/dist/config/index.js +1 -1
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/events/index.d.ts.map +1 -1
  30. package/dist/events/index.js.map +1 -1
  31. package/dist/index.js +2 -2
  32. package/dist/plaintext.js.map +1 -1
  33. package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -1
  34. package/dist/plugins/postcss/pruneVars.js.map +1 -1
  35. package/dist/plugins/postcss/quoteFontFamilies.d.ts.map +1 -1
  36. package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -1
  37. package/dist/plugins/postcss/removeDeclarations.js.map +1 -1
  38. package/dist/plugins/postcss/resolveProps.d.ts.map +1 -1
  39. package/dist/plugins/postcss/resolveProps.js +0 -3
  40. package/dist/plugins/postcss/resolveProps.js.map +1 -1
  41. package/dist/plugins/postcss/tailwindCleanup.js.map +1 -1
  42. package/dist/prepare.js +1 -1
  43. package/dist/prepare.js.map +1 -1
  44. package/dist/render/active.d.ts.map +1 -1
  45. package/dist/render/buildTemplate.d.ts +49 -0
  46. package/dist/render/buildTemplate.d.ts.map +1 -0
  47. package/dist/render/buildTemplate.js +139 -0
  48. package/dist/render/buildTemplate.js.map +1 -0
  49. package/dist/render/createRenderer.d.ts +3 -1
  50. package/dist/render/createRenderer.d.ts.map +1 -1
  51. package/dist/render/createRenderer.js +43 -10
  52. package/dist/render/createRenderer.js.map +1 -1
  53. package/dist/render/index.js +1 -1
  54. package/dist/render/index.js.map +1 -1
  55. package/dist/render/injectFonts.js.map +1 -1
  56. package/dist/render/parallel/buildWorker.d.ts +31 -0
  57. package/dist/render/parallel/buildWorker.d.ts.map +1 -0
  58. package/dist/render/parallel/buildWorker.js +66 -0
  59. package/dist/render/parallel/buildWorker.js.map +1 -0
  60. package/dist/render/parallel/worker.mjs +28 -0
  61. package/dist/render/plugins/codeBlockExtract.d.ts.map +1 -1
  62. package/dist/render/plugins/codeBlockExtract.js.map +1 -1
  63. package/dist/render/plugins/markdownExtract.d.ts.map +1 -1
  64. package/dist/render/plugins/markdownExtract.js.map +1 -1
  65. package/dist/render/plugins/rawExtract.d.ts.map +1 -1
  66. package/dist/render/plugins/rawExtract.js.map +1 -1
  67. package/dist/render/plugins/rowSourceLocation.d.ts.map +1 -1
  68. package/dist/render/plugins/rowSourceLocation.js.map +1 -1
  69. package/dist/serve.d.ts.map +1 -1
  70. package/dist/serve.js +73 -53
  71. package/dist/serve.js.map +1 -1
  72. package/dist/server/compatibility.d.ts.map +1 -1
  73. package/dist/server/compatibility.js.map +1 -1
  74. package/dist/server/linter.js.map +1 -1
  75. package/dist/server/sfc-utils.js +1 -1
  76. package/dist/server/sfc-utils.js.map +1 -1
  77. package/dist/server/ui/pages/Preview.vue +34 -11
  78. package/dist/server/ui/vite-env.d.ts +1 -0
  79. package/dist/tests/render/_helpers.d.ts.map +1 -1
  80. package/dist/tests/render/_helpers.js.map +1 -1
  81. package/dist/transformers/addAttributes.js +2 -3
  82. package/dist/transformers/addAttributes.js.map +1 -1
  83. package/dist/transformers/base.d.ts +1 -1
  84. package/dist/transformers/base.d.ts.map +1 -1
  85. package/dist/transformers/base.js +5 -10
  86. package/dist/transformers/base.js.map +1 -1
  87. package/dist/transformers/columnWidth.d.ts.map +1 -1
  88. package/dist/transformers/columnWidth.js +2 -7
  89. package/dist/transformers/columnWidth.js.map +1 -1
  90. package/dist/transformers/entities.js.map +1 -1
  91. package/dist/transformers/filters/defaults.js.map +1 -1
  92. package/dist/transformers/filters/index.js.map +1 -1
  93. package/dist/transformers/format.js.map +1 -1
  94. package/dist/transformers/imgWidth.d.ts +20 -0
  95. package/dist/transformers/imgWidth.d.ts.map +1 -0
  96. package/dist/transformers/imgWidth.js +76 -0
  97. package/dist/transformers/imgWidth.js.map +1 -0
  98. package/dist/transformers/index.d.ts.map +1 -1
  99. package/dist/transformers/index.js +2 -0
  100. package/dist/transformers/index.js.map +1 -1
  101. package/dist/transformers/inlineCss.d.ts +3 -2
  102. package/dist/transformers/inlineCss.d.ts.map +1 -1
  103. package/dist/transformers/inlineCss.js.map +1 -1
  104. package/dist/transformers/inlineLink.js +1 -1
  105. package/dist/transformers/inlineLink.js.map +1 -1
  106. package/dist/transformers/minify.js.map +1 -1
  107. package/dist/transformers/minifyCodeInline.js.map +1 -1
  108. package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
  109. package/dist/transformers/msoPlaceholders.js +2 -7
  110. package/dist/transformers/msoPlaceholders.js.map +1 -1
  111. package/dist/transformers/purgeCss.js.map +1 -1
  112. package/dist/transformers/replaceStrings.js.map +1 -1
  113. package/dist/transformers/safeSelectors.js.map +1 -1
  114. package/dist/transformers/shorthandCss.js.map +1 -1
  115. package/dist/transformers/tailwindComponent.js.map +1 -1
  116. package/dist/transformers/tailwindcss.js +1 -1
  117. package/dist/transformers/tailwindcss.js.map +1 -1
  118. package/dist/transformers/urlQuery.js.map +1 -1
  119. package/dist/types/config.d.ts +26 -4
  120. package/dist/types/config.d.ts.map +1 -1
  121. package/dist/utils/ast/serializer.js.map +1 -1
  122. package/dist/utils/compileTailwindCss.js.map +1 -1
  123. package/dist/utils/componentSources.js.map +1 -1
  124. package/dist/utils/cssBox.d.ts.map +1 -1
  125. package/dist/utils/cssBox.js +2 -7
  126. package/dist/utils/cssBox.js.map +1 -1
  127. package/dist/utils/decodeStyleEntities.js.map +1 -1
  128. package/dist/utils/url.js.map +1 -1
  129. package/dist/utils/watchPaths.js.map +1 -1
  130. package/node_modules/@clack/core/CHANGELOG.md +30 -0
  131. package/node_modules/@clack/core/dist/index.d.mts +109 -3
  132. package/node_modules/@clack/core/dist/index.mjs +972 -17
  133. package/node_modules/@clack/core/package.json +2 -1
  134. package/node_modules/@clack/prompts/CHANGELOG.md +42 -0
  135. package/node_modules/@clack/prompts/README.md +30 -9
  136. package/node_modules/@clack/prompts/dist/index.d.mts +458 -27
  137. package/node_modules/@clack/prompts/dist/index.mjs +1378 -141
  138. package/node_modules/@clack/prompts/package.json +2 -2
  139. package/node_modules/tinyexec/package.json +4 -4
  140. package/package.json +13 -11
  141. package/dist/components/Overlap.vue +0 -156
  142. package/node_modules/@clack/core/dist/index.mjs.map +0 -1
  143. package/node_modules/@clack/prompts/dist/index.mjs.map +0 -1
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, createStaticVNode, useAttrs, type PropType } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
3
4
  import { outlookFallbackProp } from './utils.ts'
4
5
  import { useOutlookFallback } from '../composables/useOutlookFallback'
5
6
 
@@ -32,15 +33,22 @@ const props = defineProps({
32
33
  type: String,
33
34
  default: null
34
35
  },
35
- /** The width of the image, rendered without units. */
36
+ /**
37
+ * The width of the image, rendered without units.
38
+ *
39
+ * Optional: when omitted, the width is auto-derived post-render from
40
+ * the nearest sized ancestor (Container/Section/Column or any element
41
+ * with a pixel width). Falls back to fluid when no pixel width is
42
+ * resolvable. The `aspect` crop mode still requires an explicit width.
43
+ */
36
44
  width: {
37
45
  type: [String, Number],
38
- required: true
46
+ default: undefined
39
47
  },
40
48
  /** Animated image source, shown when user has no reduced motion preference. */
41
49
  motionSrc: {
42
50
  type: String,
43
- default: null
51
+ default: ''
44
52
  },
45
53
  /**
46
54
  * Aspect ratio for cropped images.
@@ -111,7 +119,7 @@ const props = defineProps({
111
119
  const outlookFallback = useOutlookFallback(props.outlookFallback)
112
120
 
113
121
  function mimeFromExtension(src: string): string {
114
- const ext = src.split('.').pop()?.toLowerCase() ?? ''
122
+ const ext = src.slice(src.lastIndexOf('.') + 1).toLowerCase()
115
123
 
116
124
  const types: Record<string, string> = {
117
125
  apng: 'image/apng',
@@ -133,17 +141,12 @@ const ASPECT_KEYWORDS: Record<string, string> = {
133
141
  'aspect-video': '16/9',
134
142
  }
135
143
 
144
+ /**
145
+ * Vue normalizes a component's `class` attr to a string before it
146
+ * reaches `attrs`, so only the string/empty cases can occur here.
147
+ */
136
148
  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 ''
149
+ return typeof value === 'string' ? value : ''
147
150
  }
148
151
 
149
152
  /**
@@ -182,10 +185,18 @@ const ratio = computed(() => {
182
185
 
183
186
  const isCropped = computed(() => ratio.value !== null)
184
187
 
185
- const motionType = computed(() => mimeFromExtension(props.motionSrc ?? ''))
188
+ const motionType = computed(() => mimeFromExtension(props.motionSrc))
186
189
 
187
190
  const imgWidth = computed(() => Number.parseInt(String(props.width), 10))
188
191
 
192
+ /**
193
+ * Whether an explicit, usable pixel width was supplied. When false, the
194
+ * non-cropped `<img>` is emitted without a width attribute plus a
195
+ * `data-maizzle-img-width` marker the `imgWidth` transformer reads to
196
+ * backfill the width from the nearest sized ancestor.
197
+ */
198
+ const hasWidth = computed(() => props.width != null && props.width !== '' && Number.isFinite(imgWidth.value))
199
+
189
200
  const heightPx = computed(() =>
190
201
  ratio.value && Number.isFinite(imgWidth.value)
191
202
  ? Math.round((imgWidth.value * ratio.value.h) / ratio.value.w)
@@ -243,6 +254,13 @@ const NotMsoBefore = () => createStaticVNode('<!--[if !mso]><!-->', 1)
243
254
  const NotMsoAfter = () => createStaticVNode('<!--<![endif]-->', 1)
244
255
 
245
256
  const imgClass = 'max-w-full align-middle'
257
+
258
+ const cropClass = computed(() =>
259
+ twMerge(
260
+ `overflow-hidden table max-w-full${hasWidth.value ? ` w-[${imgWidth.value}px]` : ''}`,
261
+ parsedClass.value.className,
262
+ )
263
+ )
246
264
  </script>
247
265
 
248
266
  <template>
@@ -254,8 +272,7 @@ const imgClass = 'max-w-full align-middle'
254
272
  v-bind="{ ...attrs, class: undefined }"
255
273
  role="img"
256
274
  :aria-label="alt || undefined"
257
- :class="['overflow-hidden table max-w-full', parsedClass.className]"
258
- :style="`width: ${imgWidth}px;`"
275
+ :class="cropClass"
259
276
  >
260
277
  <div
261
278
  :class="[
@@ -278,7 +295,7 @@ const imgClass = 'max-w-full align-middle'
278
295
  role="img"
279
296
  :aria-label="alt || undefined"
280
297
  :class="['overflow-hidden table max-w-full', parsedClass.className]"
281
- :style="`width: ${imgWidth}px;`"
298
+ :style="hasWidth ? `width: ${imgWidth}px;` : undefined"
282
299
  >
283
300
  <div
284
301
  :class="[
@@ -300,16 +317,16 @@ const imgClass = 'max-w-full align-middle'
300
317
  <picture>
301
318
  <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
302
319
  <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">
320
+ <img v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
304
321
  </picture>
305
322
  </a>
306
323
  <picture v-else-if="usePicture">
307
324
  <source v-if="darkSrc" :srcset="darkSrc" media="(prefers-color-scheme: dark)">
308
325
  <source v-if="motionSrc" :srcset="motionSrc" :type="motionType || undefined" media="(prefers-reduced-motion: no-preference)">
309
- <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
326
+ <img v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
310
327
  </picture>
311
328
  <a v-else-if="href" :href="href">
312
- <img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
329
+ <img v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
313
330
  </a>
314
- <img v-else v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
331
+ <img v-else v-bind="attrs" :src="src" :alt="alt" :width="hasWidth ? imgWidth : undefined" :data-maizzle-img-width="hasWidth ? undefined : ''" :class="imgClass" data-juice-duplicates="false">
315
332
  </template>
@@ -116,7 +116,7 @@ const htmlXmlns = computed(() => outlookFallback ? {
116
116
  <MsoHead v-if="outlookFallback" />
117
117
  <link rel="preconnect" href="https://fonts.googleapis.com">
118
118
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
119
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet" media="screen">
119
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" media="screen">
120
120
  <style>
121
121
  @import "@maizzle/tailwindcss";
122
122
 
@@ -3,7 +3,8 @@ import { createStaticVNode, inject, type PropType } from 'vue'
3
3
  import { createMarkdownExit, type MarkdownExitOptions } from 'markdown-exit'
4
4
  import { codeToHtml, type BundledTheme } from 'shiki'
5
5
  import { defu } from 'defu'
6
- import { MaizzleConfigKey } from '../composables/useConfig.ts'
6
+ import { MaizzleConfigKey } from '../composables/useConfig'
7
+ import { shikiToCodeBlock } from './utils'
7
8
 
8
9
  export default {
9
10
  props: {
@@ -62,9 +63,9 @@ export default {
62
63
  * still works standalone, when no config is provided.
63
64
  */
64
65
  const mdConfig = inject(MaizzleConfigKey, undefined)?.markdown ?? {}
65
- const markdownOptions = mdConfig.markdownOptions ?? mdConfig.markdownItOptions
66
- const markdownUses = mdConfig.markdownUses ?? mdConfig.markdownItUses
67
- const markdownSetup = mdConfig.markdownSetup ?? mdConfig.markdownItSetup
66
+ const markdownOptions = mdConfig.markdownOptions
67
+ const markdownUses = mdConfig.markdownUses
68
+ const markdownSetup = mdConfig.markdownSetup
68
69
  const theme = props.shikiTheme ?? mdConfig.shikiTheme ?? 'github-dark-high-contrast'
69
70
 
70
71
  const md = createMarkdownExit(defu(
@@ -89,23 +90,17 @@ export default {
89
90
  * code-block wrapping below composes over whatever they emit.
90
91
  */
91
92
  for (const use of markdownUses ?? []) {
92
- if (Array.isArray(use)) md.use(...use)
93
+ if (Array.isArray(use)) md.use(use[0], ...use.slice(1))
93
94
  else md.use(use)
94
95
  }
95
96
  await markdownSetup?.(md)
96
97
 
97
- const wrapPre = (html: string) =>
98
- `<table class="w-full"><tr><td class="max-w-0 mso-padding-alt-4">${html}</td></tr></table>\n`
99
-
100
98
  const defaultFence = md.renderer.rules.fence!
101
- md.renderer.rules.fence = (...args) => {
102
- const result = defaultFence(...args)
103
- if (typeof result === 'string') return wrapPre(result)
104
- return result.then(wrapPre)
105
- }
99
+ md.renderer.rules.fence = (...args) =>
100
+ Promise.resolve(defaultFence(...args)).then(shikiToCodeBlock)
106
101
 
107
102
  const defaultCodeBlock = md.renderer.rules.code_block!
108
- md.renderer.rules.code_block = (...args) => wrapPre(defaultCodeBlock(...args) as string)
103
+ md.renderer.rules.code_block = (...args) => shikiToCodeBlock(defaultCodeBlock(...args) as string)
109
104
 
110
105
  let html = await md.renderAsync(source)
111
106
 
@@ -21,7 +21,7 @@ const escapeAttr = (v: string) =>
21
21
  * fall through to the caller's default.
22
22
  */
23
23
  function tokenToPx(token: string): number {
24
- const seg = token.split(':').at(-1) ?? ''
24
+ const seg = token.slice(token.lastIndexOf(':') + 1)
25
25
  const m = seg.match(/^(?:size|w|h)-(.+)$/)
26
26
  if (!m) return 0
27
27
  const v = m[1]
@@ -45,7 +45,7 @@ function partition(cls: string): { neutral: string[]; sizing: string[] } {
45
45
  const neutral: string[] = []
46
46
  const sizing: string[] = []
47
47
  for (const t of cls.split(/\s+/).filter(Boolean)) {
48
- const last = t.split(':').at(-1) ?? ''
48
+ const last = t.slice(t.lastIndexOf(':') + 1)
49
49
  if (/^(?:size|w|h|min-w|min-h|max-w|max-h)-/.test(last)) sizing.push(t)
50
50
  else neutral.push(t)
51
51
  }
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, createStaticVNode, useAttrs } from 'vue'
3
+ import { twMerge } from 'tailwind-merge'
3
4
  import { hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp } from './utils.ts'
4
5
  import { useOutlookFallback } from '../composables/useOutlookFallback'
5
6
 
@@ -65,15 +66,16 @@ const useMarker = outlookFallback && props.width == null && userHasWidth.value
65
66
  const msoId = useMarker ? nextId('s') : null
66
67
  const tdId = outlookFallback ? nextId('st') : null
67
68
 
68
- const divStyle = computed(() => {
69
- const parts: string[] = []
70
- if (props.width != null) parts.push(`max-width: ${normalizeToPixels(props.width)}`)
71
- if (userStyle.value) parts.push(userStyle.value)
72
- return parts.length ? parts.join('; ') : undefined
69
+ const mergedClass = computed(() => {
70
+ const userClass = (attrs.class as string) ?? ''
71
+ if (props.width == null) return userClass || undefined
72
+ return twMerge(`max-w-[${normalizeToPixels(props.width)}]`, userClass)
73
73
  })
74
74
 
75
+ const divStyle = computed(() => userStyle.value || undefined)
76
+
75
77
  const restAttrs = computed(() => {
76
- const { style: _, ...rest } = attrs
78
+ const { style: _, class: __, ...rest } = attrs
77
79
  return rest
78
80
  })
79
81
 
@@ -106,6 +108,7 @@ const MsoAfter = () => createStaticVNode(
106
108
  <MsoBefore v-if="outlookFallback" />
107
109
  <div
108
110
  v-bind="restAttrs"
111
+ :class="mergedClass"
109
112
  :style="divStyle"
110
113
  :data-maizzle-msow-id="msoId"
111
114
  :data-maizzle-msow-fallback="useMarker ? '100%' : null"
@@ -18,8 +18,8 @@ const props = defineProps({
18
18
 
19
19
  const attrs = useAttrs()
20
20
 
21
- const defaultClass = computed(() => props.as === 'span' ? 'text-base' : 'mt-4 text-base')
22
- const mergedClass = computed(() => twMerge(defaultClass.value, attrs.class as string))
21
+ const defaultClass = computed(() => props.as === 'span' ? '' : 'mt-4 text-base')
22
+ const mergedClass = computed(() => twMerge(defaultClass.value, attrs.class as string) || undefined)
23
23
  </script>
24
24
 
25
25
  <template>
@@ -23,6 +23,30 @@ declare const outlookFallbackProp: {
23
23
  readonly type: BooleanConstructor;
24
24
  readonly default: null;
25
25
  };
26
+ /**
27
+ * Default utility classes for a code-block `<pre>`. `whitespace-pre!` is
28
+ * forced important so Gmail's stylesheet can't reset it to `normal`, and
29
+ * `mb-0` strips the browser's default `<pre>` bottom margin.
30
+ */
31
+ declare function codeBlockPreClass(bg: string): string;
32
+ /**
33
+ * Build the email-safe table wrapper around highlighted code. Shared by the
34
+ * `<CodeBlock>` component and the Markdown fenced/indented code-block
35
+ * rules so both render identical markup: a full-width table whose
36
+ * cell carries the theme background, wrapping a `<pre>` styled
37
+ * with utility classes (not Shiki's raw inline styles).
38
+ */
39
+ declare function buildCodeBlock(codeContent: string, bg: string, options?: {
40
+ preClass?: string;
41
+ tdClass?: string;
42
+ styleAttr?: string;
43
+ }): string;
44
+ /**
45
+ * Re-wrap a Shiki (or plain markdown-it) `<pre><code>` block as a CodeBlock,
46
+ * pulling the inner code and the theme background out of the highlighted
47
+ * HTML. Falls back to a white background for unhighlighted blocks.
48
+ */
49
+ declare function shikiToCodeBlock(highlighted: string): string;
26
50
  //#endregion
27
- export { hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp };
51
+ export { buildCodeBlock, codeBlockPreClass, hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp, shikiToCodeBlock };
28
52
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","names":[],"sources":["../../src/components/utils.ts"],"mappings":";iBAAgB,iBAAA,CAAkB,KAAsB;AAAxD;;;;AAAwD;AAiBxD;;;AAjBA,iBAiBgB,MAAA,CAAO,MAAc;AAAA,iBAKrB,eAAA,CAAgB,QAAgB;AAAA,iBAQhC,eAAA,CAAgB,QAAgB;AAAA,iBAIhC,gBAAA,CAAiB,QAAgB;AAAA,iBAQjC,gBAAA,CAAiB,QAAgB;;AApBD;AAQhD;;;;cAsBa,mBAAA;EAAA,eAGH,kBAAA;EAAA"}
1
+ {"version":3,"file":"utils.d.ts","names":[],"sources":["../../src/components/utils.ts"],"mappings":";iBAAgB,iBAAA,CAAkB,KAAsB;AAAxD;;;;AAAwD;AAiBxD;;;AAjBA,iBAiBgB,MAAA,CAAO,MAAc;AAAA,iBAKrB,eAAA,CAAgB,QAAgB;AAAA,iBAQhC,eAAA,CAAgB,QAAgB;AAAA,iBAIhC,gBAAA,CAAiB,QAAgB;AAAA,iBAQjC,gBAAA,CAAiB,QAAgB;;AApBD;AAQhD;;;;cAsBa,mBAAA;EAAA,eAGH,kBAAA;EAAA;;;AArBuC;AAQjD;;;iBAoBgB,iBAAA,CAAkB,EAAU;AApBK;AAUjD;;;;;;AAViD,iBA+BjC,cAAA,CACd,WAAA,UACA,EAAA,UACA,OAAA;EAAW,QAAA;EAAmB,OAAA;EAAkB,SAAA;AAAA;;;AAdN;AAW5C;;iBAoBgB,gBAAA,CAAiB,WAAmB"}
@@ -44,7 +44,35 @@ const outlookFallbackProp = {
44
44
  type: Boolean,
45
45
  default: null
46
46
  };
47
+ /**
48
+ * Default utility classes for a code-block `<pre>`. `whitespace-pre!` is
49
+ * forced important so Gmail's stylesheet can't reset it to `normal`, and
50
+ * `mb-0` strips the browser's default `<pre>` bottom margin.
51
+ */
52
+ function codeBlockPreClass(bg) {
53
+ return `font-mono bg-[${bg}] p-4 mb-0 overflow-auto whitespace-pre! [word-wrap:normal] [word-break:normal] [word-spacing:normal]`;
54
+ }
55
+ /**
56
+ * Build the email-safe table wrapper around highlighted code. Shared by the
57
+ * `<CodeBlock>` component and the Markdown fenced/indented code-block
58
+ * rules so both render identical markup: a full-width table whose
59
+ * cell carries the theme background, wrapping a `<pre>` styled
60
+ * with utility classes (not Shiki's raw inline styles).
61
+ */
62
+ function buildCodeBlock(codeContent, bg, options = {}) {
63
+ const preClass = options.preClass ?? codeBlockPreClass(bg);
64
+ return `<table class="w-full"><tr><td class="${options.tdClass ?? `bg-[${bg}] max-w-0 mso-padding-alt-4`}"><pre class="${preClass}"${options.styleAttr ?? ""} data-juice-important><code>${codeContent}</code></pre></td></tr></table>`;
65
+ }
66
+ /**
67
+ * Re-wrap a Shiki (or plain markdown-it) `<pre><code>` block as a CodeBlock,
68
+ * pulling the inner code and the theme background out of the highlighted
69
+ * HTML. Falls back to a white background for unhighlighted blocks.
70
+ */
71
+ function shikiToCodeBlock(highlighted) {
72
+ const bg = highlighted.match(/background-color:\s*(#[0-9a-fA-F]+)/)?.[1] ?? "#fff";
73
+ return buildCodeBlock(highlighted.trim().replace(/^<pre[^>]*><code[^>]*>/, "").replace(/<\/code><\/pre>$/, ""), bg);
74
+ }
47
75
  //#endregion
48
- export { hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp };
76
+ export { buildCodeBlock, codeBlockPreClass, hasHeightInStyle, hasHeightUtility, hasWidthInStyle, hasWidthUtility, nextId, normalizeToPixels, outlookFallbackProp, shikiToCodeBlock };
49
77
 
50
78
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../src/components/utils.ts"],"sourcesContent":["export function normalizeToPixels(value: string | number): string {\n if (typeof value === 'number' || Number.isFinite(Number(value))) {\n return `${value}px`\n }\n return value\n}\n\nconst counters: Record<string, number> = {}\n\n/**\n * Module-scoped sequential ID generator. Used by components to mint\n * unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.\n *\n * Must live here (not inside `<script setup>`) because Vue compiles\n * `<script setup>` into the component's `setup()` function — any\n * `let counter = 0` there resets per instance, causing id collisions.\n */\nexport function nextId(prefix: string): string {\n counters[prefix] = (counters[prefix] ?? 0) + 1\n return `${prefix}${counters[prefix]}`\n}\n\nexport function hasWidthUtility(classStr: string): boolean {\n return classStr.split(/\\s+/).some((c) => {\n const utility = c.split(':').pop() ?? ''\n const clean = utility.replace(/^!/, '')\n return /^(w-|max-w-|min-w-)/.test(clean)\n })\n}\n\nexport function hasWidthInStyle(styleStr: string): boolean {\n return /(?:^|;\\s*)(?:max-width|width)\\s*:/i.test(styleStr)\n}\n\nexport function hasHeightUtility(classStr: string): boolean {\n return classStr.split(/\\s+/).some((c) => {\n const utility = c.split(':').pop() ?? ''\n const clean = utility.replace(/^!/, '')\n return /^(h-|max-h-|min-h-)/.test(clean)\n })\n}\n\nexport function hasHeightInStyle(styleStr: string): boolean {\n return /(?:^|;\\s*)(?:max-height|height)\\s*:/i.test(styleStr)\n}\n\n/**\n * Shared prop for components that emit MSO/VML fallback markup. The\n * `null` default acts as the \"unset\" sentinel — `useOutlookFallback`\n * treats `null` as inherit-from-ancestor (root default `true`),\n * letting users override per-component without losing inheritance.\n */\nexport const outlookFallbackProp = {\n type: Boolean,\n default: null,\n} as const\n\n"],"mappings":";AAAA,SAAgB,kBAAkB,OAAgC;CAChE,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,OAAO,KAAK,CAAC,GAC5D,OAAO,GAAG,MAAM;CAElB,OAAO;AACT;AAEA,MAAM,WAAmC,CAAC;;;;;;;;;AAU1C,SAAgB,OAAO,QAAwB;CAC7C,SAAS,WAAW,SAAS,WAAW,KAAK;CAC7C,OAAO,GAAG,SAAS,SAAS;AAC9B;AAEA,SAAgB,gBAAgB,UAA2B;CACzD,OAAO,SAAS,MAAM,KAAK,EAAE,MAAM,MAAM;EAEvC,MAAM,SADU,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,IAChB,QAAQ,MAAM,EAAE;EACtC,OAAO,sBAAsB,KAAK,KAAK;CACzC,CAAC;AACH;AAEA,SAAgB,gBAAgB,UAA2B;CACzD,OAAO,qCAAqC,KAAK,QAAQ;AAC3D;AAEA,SAAgB,iBAAiB,UAA2B;CAC1D,OAAO,SAAS,MAAM,KAAK,EAAE,MAAM,MAAM;EAEvC,MAAM,SADU,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,IAChB,QAAQ,MAAM,EAAE;EACtC,OAAO,sBAAsB,KAAK,KAAK;CACzC,CAAC;AACH;AAEA,SAAgB,iBAAiB,UAA2B;CAC1D,OAAO,uCAAuC,KAAK,QAAQ;AAC7D;;;;;;;AAQA,MAAa,sBAAsB;CACjC,MAAM;CACN,SAAS;AACX"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../src/components/utils.ts"],"sourcesContent":["export function normalizeToPixels(value: string | number): string {\n if (typeof value === 'number' || Number.isFinite(Number(value))) {\n return `${value}px`\n }\n return value\n}\n\nconst counters: Record<string, number> = {}\n\n/**\n * Module-scoped sequential ID generator. Used by components to mint\n * unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.\n *\n * Must live here (not inside `<script setup>`) because Vue compiles\n * `<script setup>` into the component's `setup()` function — any\n * `let counter = 0` there resets per instance, causing id collisions.\n */\nexport function nextId(prefix: string): string {\n counters[prefix] = (counters[prefix] ?? 0) + 1\n return `${prefix}${counters[prefix]}`\n}\n\nexport function hasWidthUtility(classStr: string): boolean {\n return classStr.split(/\\s+/).some((c) => {\n const utility = c.split(':').pop() ?? ''\n const clean = utility.replace(/^!/, '')\n return /^(w-|max-w-|min-w-)/.test(clean)\n })\n}\n\nexport function hasWidthInStyle(styleStr: string): boolean {\n return /(?:^|;\\s*)(?:max-width|width)\\s*:/i.test(styleStr)\n}\n\nexport function hasHeightUtility(classStr: string): boolean {\n return classStr.split(/\\s+/).some((c) => {\n const utility = c.split(':').pop() ?? ''\n const clean = utility.replace(/^!/, '')\n return /^(h-|max-h-|min-h-)/.test(clean)\n })\n}\n\nexport function hasHeightInStyle(styleStr: string): boolean {\n return /(?:^|;\\s*)(?:max-height|height)\\s*:/i.test(styleStr)\n}\n\n/**\n * Shared prop for components that emit MSO/VML fallback markup. The\n * `null` default acts as the \"unset\" sentinel — `useOutlookFallback`\n * treats `null` as inherit-from-ancestor (root default `true`),\n * letting users override per-component without losing inheritance.\n */\nexport const outlookFallbackProp = {\n type: Boolean,\n default: null,\n} as const\n\n/**\n * Default utility classes for a code-block `<pre>`. `whitespace-pre!` is\n * forced important so Gmail's stylesheet can't reset it to `normal`, and\n * `mb-0` strips the browser's default `<pre>` bottom margin.\n */\nexport function codeBlockPreClass(bg: string): string {\n return `font-mono bg-[${bg}] p-4 mb-0 overflow-auto whitespace-pre! [word-wrap:normal] [word-break:normal] [word-spacing:normal]`\n}\n\n/**\n * Build the email-safe table wrapper around highlighted code. Shared by the\n * `<CodeBlock>` component and the Markdown fenced/indented code-block\n * rules so both render identical markup: a full-width table whose\n * cell carries the theme background, wrapping a `<pre>` styled\n * with utility classes (not Shiki's raw inline styles).\n */\nexport function buildCodeBlock(\n codeContent: string,\n bg: string,\n options: { preClass?: string; tdClass?: string; styleAttr?: string } = {},\n): string {\n const preClass = options.preClass ?? codeBlockPreClass(bg)\n const tdClass = options.tdClass ?? `bg-[${bg}] max-w-0 mso-padding-alt-4`\n const styleAttr = options.styleAttr ?? ''\n\n // `data-juice-important` tells the CSS inliner to keep `!important` on this\n // element's inlined declarations (e.g. `white-space: pre !important`), which\n // it strips by default. Juice removes the attribute from the output.\n return `<table class=\"w-full\"><tr><td class=\"${tdClass}\"><pre class=\"${preClass}\"${styleAttr} data-juice-important><code>${codeContent}</code></pre></td></tr></table>`\n}\n\n/**\n * Re-wrap a Shiki (or plain markdown-it) `<pre><code>` block as a CodeBlock,\n * pulling the inner code and the theme background out of the highlighted\n * HTML. Falls back to a white background for unhighlighted blocks.\n */\nexport function shikiToCodeBlock(highlighted: string): string {\n const bg = highlighted.match(/background-color:\\s*(#[0-9a-fA-F]+)/)?.[1] ?? '#fff'\n const codeContent = highlighted\n .trim()\n .replace(/^<pre[^>]*><code[^>]*>/, '')\n .replace(/<\\/code><\\/pre>$/, '')\n\n return buildCodeBlock(codeContent, bg)\n}\n\n"],"mappings":";AAAA,SAAgB,kBAAkB,OAAgC;CAChE,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,OAAO,KAAK,CAAC,GAC5D,OAAO,GAAG,MAAM;CAElB,OAAO;AACT;AAEA,MAAM,WAAmC,CAAC;;;;;;;;;AAU1C,SAAgB,OAAO,QAAwB;CAC7C,SAAS,WAAW,SAAS,WAAW,KAAK;CAC7C,OAAO,GAAG,SAAS,SAAS;AAC9B;AAEA,SAAgB,gBAAgB,UAA2B;CACzD,OAAO,SAAS,MAAM,KAAK,CAAC,CAAC,MAAM,MAAM;EAEvC,MAAM,SADU,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,GAAA,CAChB,QAAQ,MAAM,EAAE;EACtC,OAAO,sBAAsB,KAAK,KAAK;CACzC,CAAC;AACH;AAEA,SAAgB,gBAAgB,UAA2B;CACzD,OAAO,qCAAqC,KAAK,QAAQ;AAC3D;AAEA,SAAgB,iBAAiB,UAA2B;CAC1D,OAAO,SAAS,MAAM,KAAK,CAAC,CAAC,MAAM,MAAM;EAEvC,MAAM,SADU,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,GAAA,CAChB,QAAQ,MAAM,EAAE;EACtC,OAAO,sBAAsB,KAAK,KAAK;CACzC,CAAC;AACH;AAEA,SAAgB,iBAAiB,UAA2B;CAC1D,OAAO,uCAAuC,KAAK,QAAQ;AAC7D;;;;;;;AAQA,MAAa,sBAAsB;CACjC,MAAM;CACN,SAAS;AACX;;;;;;AAOA,SAAgB,kBAAkB,IAAoB;CACpD,OAAO,iBAAiB,GAAG;AAC7B;;;;;;;;AASA,SAAgB,eACd,aACA,IACA,UAAuE,CAAC,GAChE;CACR,MAAM,WAAW,QAAQ,YAAY,kBAAkB,EAAE;CAOzD,OAAO,wCANS,QAAQ,WAAW,OAAO,GAAG,6BAMU,gBAAgB,SAAS,GAL9D,QAAQ,aAAa,GAKsD,8BAA8B,YAAY;AACzI;;;;;;AAOA,SAAgB,iBAAiB,aAA6B;CAC5D,MAAM,KAAK,YAAY,MAAM,qCAAqC,CAAC,GAAG,MAAM;CAM5E,OAAO,eALa,YACjB,KAAK,CAAC,CACN,QAAQ,0BAA0B,EAAE,CAAC,CACrC,QAAQ,oBAAoB,EAEC,GAAG,EAAE;AACvC"}
@@ -55,3 +55,49 @@ export const outlookFallbackProp = {
55
55
  default: null,
56
56
  } as const
57
57
 
58
+ /**
59
+ * Default utility classes for a code-block `<pre>`. `whitespace-pre!` is
60
+ * forced important so Gmail's stylesheet can't reset it to `normal`, and
61
+ * `mb-0` strips the browser's default `<pre>` bottom margin.
62
+ */
63
+ export function codeBlockPreClass(bg: string): string {
64
+ return `font-mono bg-[${bg}] p-4 mb-0 overflow-auto whitespace-pre! [word-wrap:normal] [word-break:normal] [word-spacing:normal]`
65
+ }
66
+
67
+ /**
68
+ * Build the email-safe table wrapper around highlighted code. Shared by the
69
+ * `<CodeBlock>` component and the Markdown fenced/indented code-block
70
+ * rules so both render identical markup: a full-width table whose
71
+ * cell carries the theme background, wrapping a `<pre>` styled
72
+ * with utility classes (not Shiki's raw inline styles).
73
+ */
74
+ export function buildCodeBlock(
75
+ codeContent: string,
76
+ bg: string,
77
+ options: { preClass?: string; tdClass?: string; styleAttr?: string } = {},
78
+ ): string {
79
+ const preClass = options.preClass ?? codeBlockPreClass(bg)
80
+ const tdClass = options.tdClass ?? `bg-[${bg}] max-w-0 mso-padding-alt-4`
81
+ const styleAttr = options.styleAttr ?? ''
82
+
83
+ // `data-juice-important` tells the CSS inliner to keep `!important` on this
84
+ // element's inlined declarations (e.g. `white-space: pre !important`), which
85
+ // it strips by default. Juice removes the attribute from the output.
86
+ return `<table class="w-full"><tr><td class="${tdClass}"><pre class="${preClass}"${styleAttr} data-juice-important><code>${codeContent}</code></pre></td></tr></table>`
87
+ }
88
+
89
+ /**
90
+ * Re-wrap a Shiki (or plain markdown-it) `<pre><code>` block as a CodeBlock,
91
+ * pulling the inner code and the theme background out of the highlighted
92
+ * HTML. Falls back to a white background for unhighlighted blocks.
93
+ */
94
+ export function shikiToCodeBlock(highlighted: string): string {
95
+ const bg = highlighted.match(/background-color:\s*(#[0-9a-fA-F]+)/)?.[1] ?? '#fff'
96
+ const codeContent = highlighted
97
+ .trim()
98
+ .replace(/^<pre[^>]*><code[^>]*>/, '')
99
+ .replace(/<\/code><\/pre>$/, '')
100
+
101
+ return buildCodeBlock(codeContent, bg)
102
+ }
103
+
@@ -1 +1 @@
1
- {"version":3,"file":"useConfig.d.ts","names":[],"sources":["../../src/composables/useConfig.ts"],"mappings":";;;;;;;AAWA;;;;cAAa,gBAAA,EAAkB,YAAY,CAAC,aAAA;AAAA,iBAE5B,SAAA,CAAA,GAAa,aAAa"}
1
+ {"version":3,"file":"useConfig.d.ts","names":[],"sources":["../../src/composables/useConfig.ts"],"mappings":";;;;;;;AAWA;;;;cAAa,gBAAA,EAAkB,YAAY,CAAC,aAAA;AAAA,iBAE5B,SAAA,IAAa,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCurrentTemplate.d.ts","names":[],"sources":["../../src/composables/useCurrentTemplate.ts"],"mappings":";;;;;AAmBA;;iBAAgB,mBAAA,CAAoB,MAA8B,EAAtB,UAAU;;AAAY;AAuBlE;;;;AAAgD;;;;;;;;;;;;;iBAAhC,kBAAA,CAAA,GAAsB,UAAU"}
1
+ {"version":3,"file":"useCurrentTemplate.d.ts","names":[],"sources":["../../src/composables/useCurrentTemplate.ts"],"mappings":";;;;;AAmBA;;iBAAgB,mBAAA,CAAoB,MAA8B,EAAtB,UAAU;;AAAY;AAuBlE;;;;AAAgD;;;;;;;;;;;;;iBAAhC,kBAAA,IAAsB,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"useEvent.d.ts","names":[],"sources":["../../src/composables/useEvent.ts"],"mappings":";;;;;AAcA;;;;;;;;iBAAgB,QAAA,WAAmB,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,QAAA,CAAS,CAAA"}
1
+ {"version":3,"file":"useEvent.d.ts","names":[],"sources":["../../src/composables/useEvent.ts"],"mappings":";;;;;AAcA;;;;;;;;iBAAgB,QAAA,WAAmB,SAAA,EAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,QAAA,CAAS,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"useFont.js","names":[],"sources":["../../src/composables/useFont.ts"],"sourcesContent":["import { inject } from 'vue'\nimport { RenderContextKey } from './renderContext.ts'\n\ntype FontCategory = 'sans' | 'serif' | 'mono' | 'display' | 'handwriting'\n\nconst FAMILY_CATEGORIES: Record<string, FontCategory> = {\n // Sans-serif\n 'Roboto': 'sans',\n 'Open Sans': 'sans',\n 'Inter': 'sans',\n 'Lato': 'sans',\n 'Montserrat': 'sans',\n // Serif\n 'Merriweather': 'serif',\n 'Playfair Display': 'serif',\n 'Lora': 'serif',\n 'PT Serif': 'serif',\n 'Noto Serif': 'serif',\n // Display\n 'Oswald': 'display',\n 'Bebas Neue': 'display',\n 'Anton': 'display',\n 'Lobster': 'display',\n 'Pacifico': 'display',\n // Handwriting\n 'Dancing Script': 'handwriting',\n 'Caveat': 'handwriting',\n 'Shadows Into Light': 'handwriting',\n 'Satisfy': 'handwriting',\n 'Great Vibes': 'handwriting',\n // Monospace\n 'Roboto Mono': 'mono',\n 'Source Code Pro': 'mono',\n 'JetBrains Mono': 'mono',\n 'Fira Code': 'mono',\n 'Inconsolata': 'mono',\n}\n\nconst DEFAULT_FALLBACKS: Record<FontCategory, string> = {\n sans: 'ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif',\n serif: 'ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif',\n mono: 'ui-monospace, Menlo, Consolas, monospace',\n display: 'Impact, \"Arial Black\", system-ui, sans-serif',\n handwriting: '\"Segoe Script\", \"Brush Script MT\", cursive',\n}\n\nexport type FontProvider = 'google' | 'bunny'\n\nexport interface UseFontOptions {\n /**\n * A single font family name, e.g. `\"Roboto\"` or `\"Open Sans\"`.\n *\n * For fallback fonts, use the `fallback` option instead of a\n * comma-separated list here.\n */\n family: string\n /** CSS fallback list appended to the `font-family` declaration. */\n fallback?: string\n /**\n * Font provider used to build the stylesheet URL when `url` is omitted.\n * Bunny Fonts is a drop-in, privacy-friendly Google Fonts mirror.\n * @default 'google'\n */\n provider?: FontProvider\n /**\n * Stylesheet URL. When provided, used as-is for the `<link href>`.\n * When omitted, a URL is built from `provider`, `family`, `weights`,\n * `display` and `styles`.\n */\n url?: string\n /** Font weights to load. Ignored when `url` is provided. */\n weights?: number[]\n /** `font-display` value. Ignored when `url` is provided. */\n display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional'\n /** Font styles to load. Ignored when `url` is provided. */\n styles?: Array<'normal' | 'italic'>\n}\n\nconst PROVIDER_BASE_URL: Record<FontProvider, string> = {\n google: 'https://fonts.googleapis.com/css2',\n bunny: 'https://fonts.bunny.net/css2',\n}\n\nfunction slugify(family: string): string {\n return family\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n}\n\nfunction buildProviderUrl(opts: Required<Omit<UseFontOptions, 'url' | 'fallback'>>): string {\n const familyParam = opts.family.trim().replace(/\\s+/g, '+')\n const weights = [...opts.weights].sort((a, b) => a - b)\n const hasItalic = opts.styles.includes('italic')\n const hasNormal = opts.styles.includes('normal')\n\n const axis = hasItalic\n ? `:ital,wght@${weights.flatMap(w => [\n ...(hasNormal ? [`0,${w}`] : []),\n `1,${w}`,\n ]).join(';')}`\n : `:wght@${weights.join(';')}`\n\n return `${PROVIDER_BASE_URL[opts.provider]}?family=${familyParam}${axis}&display=${opts.display}`\n}\n\n/**\n * Register a font for the current email template.\n *\n * Builds a Google Fonts stylesheet URL from `family`/`weights`/`display`/`styles`\n * (or uses `url` as-is). The renderer injects a `<link>` tag into `<head>`\n * and merges `--font-{slug}` declarations into the template's existing\n * `@import \"tailwindcss\"` style block so a `font-{slug}` utility class\n * is generated. If no Tailwind import is found, falls back to a `:root`\n * declaration so the CSS variable is still available.\n *\n * Usage in SFC <script setup>:\n * ```ts\n * useFont({ family: 'Roboto', fallback: 'Verdana, sans-serif', weights: [400, 600] })\n * ```\n */\nexport function useFont(options: UseFontOptions): void {\n const ctx = inject(RenderContextKey)\n if (!ctx) return\n\n ctx.fonts = ctx.fonts ?? []\n if (ctx.fonts.some(f => f.family === options.family)) return\n\n const url = options.url ?? buildProviderUrl({\n family: options.family,\n provider: options.provider ?? 'google',\n weights: options.weights ?? [400],\n display: options.display ?? 'swap',\n styles: options.styles ?? ['normal'],\n })\n\n const fallback = options.fallback\n ?? DEFAULT_FALLBACKS[FAMILY_CATEGORIES[options.family] ?? 'sans']\n const quoted = /\\s/.test(options.family) ? `\"${options.family}\"` : options.family\n const declaration = `${quoted}, ${fallback}`\n\n ctx.fonts.push({\n family: options.family,\n slug: slugify(options.family),\n declaration,\n url,\n })\n}\n"],"mappings":";;;AAKA,MAAM,oBAAkD;CAEtD,UAAU;CACV,aAAa;CACb,SAAS;CACT,QAAQ;CACR,cAAc;CAEd,gBAAgB;CAChB,oBAAoB;CACpB,QAAQ;CACR,YAAY;CACZ,cAAc;CAEd,UAAU;CACV,cAAc;CACd,SAAS;CACT,WAAW;CACX,YAAY;CAEZ,kBAAkB;CAClB,UAAU;CACV,sBAAsB;CACtB,WAAW;CACX,eAAe;CAEf,eAAe;CACf,mBAAmB;CACnB,kBAAkB;CAClB,aAAa;CACb,eAAe;AACjB;AAEA,MAAM,oBAAkD;CACtD,MAAM;CACN,OAAO;CACP,MAAM;CACN,SAAS;CACT,aAAa;AACf;AAkCA,MAAM,oBAAkD;CACtD,QAAQ;CACR,OAAO;AACT;AAEA,SAAS,QAAQ,QAAwB;CACvC,OAAO,OACJ,KAAK,EACL,YAAY,EACZ,QAAQ,QAAQ,GAAG,EACnB,QAAQ,eAAe,EAAE;AAC9B;AAEA,SAAS,iBAAiB,MAAkE;CAC1F,MAAM,cAAc,KAAK,OAAO,KAAK,EAAE,QAAQ,QAAQ,GAAG;CAC1D,MAAM,UAAU,CAAC,GAAG,KAAK,OAAO,EAAE,MAAM,GAAG,MAAM,IAAI,CAAC;CACtD,MAAM,YAAY,KAAK,OAAO,SAAS,QAAQ;CAC/C,MAAM,YAAY,KAAK,OAAO,SAAS,QAAQ;CAE/C,MAAM,OAAO,YACT,cAAc,QAAQ,SAAQ,MAAK,CACjC,GAAI,YAAY,CAAC,KAAK,GAAG,IAAI,CAAC,GAC9B,KAAK,GACP,CAAC,EAAE,KAAK,GAAG,MACX,SAAS,QAAQ,KAAK,GAAG;CAE7B,OAAO,GAAG,kBAAkB,KAAK,UAAU,UAAU,cAAc,KAAK,WAAW,KAAK;AAC1F;;;;;;;;;;;;;;;;AAiBA,SAAgB,QAAQ,SAA+B;CACrD,MAAM,MAAM,OAAO,gBAAgB;CACnC,IAAI,CAAC,KAAK;CAEV,IAAI,QAAQ,IAAI,SAAS,CAAC;CAC1B,IAAI,IAAI,MAAM,MAAK,MAAK,EAAE,WAAW,QAAQ,MAAM,GAAG;CAEtD,MAAM,MAAM,QAAQ,OAAO,iBAAiB;EAC1C,QAAQ,QAAQ;EAChB,UAAU,QAAQ,YAAY;EAC9B,SAAS,QAAQ,WAAW,CAAC,GAAG;EAChC,SAAS,QAAQ,WAAW;EAC5B,QAAQ,QAAQ,UAAU,CAAC,QAAQ;CACrC,CAAC;CAED,MAAM,WAAW,QAAQ,YACpB,kBAAkB,kBAAkB,QAAQ,WAAW;CAE5D,MAAM,cAAc,GADL,KAAK,KAAK,QAAQ,MAAM,IAAI,IAAI,QAAQ,OAAO,KAAK,QAAQ,OAC7C,IAAI;CAElC,IAAI,MAAM,KAAK;EACb,QAAQ,QAAQ;EAChB,MAAM,QAAQ,QAAQ,MAAM;EAC5B;EACA;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"useFont.js","names":[],"sources":["../../src/composables/useFont.ts"],"sourcesContent":["import { inject } from 'vue'\nimport { RenderContextKey } from './renderContext.ts'\n\ntype FontCategory = 'sans' | 'serif' | 'mono' | 'display' | 'handwriting'\n\nconst FAMILY_CATEGORIES: Record<string, FontCategory> = {\n // Sans-serif\n 'Roboto': 'sans',\n 'Open Sans': 'sans',\n 'Inter': 'sans',\n 'Lato': 'sans',\n 'Montserrat': 'sans',\n // Serif\n 'Merriweather': 'serif',\n 'Playfair Display': 'serif',\n 'Lora': 'serif',\n 'PT Serif': 'serif',\n 'Noto Serif': 'serif',\n // Display\n 'Oswald': 'display',\n 'Bebas Neue': 'display',\n 'Anton': 'display',\n 'Lobster': 'display',\n 'Pacifico': 'display',\n // Handwriting\n 'Dancing Script': 'handwriting',\n 'Caveat': 'handwriting',\n 'Shadows Into Light': 'handwriting',\n 'Satisfy': 'handwriting',\n 'Great Vibes': 'handwriting',\n // Monospace\n 'Roboto Mono': 'mono',\n 'Source Code Pro': 'mono',\n 'JetBrains Mono': 'mono',\n 'Fira Code': 'mono',\n 'Inconsolata': 'mono',\n}\n\nconst DEFAULT_FALLBACKS: Record<FontCategory, string> = {\n sans: 'ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif',\n serif: 'ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif',\n mono: 'ui-monospace, Menlo, Consolas, monospace',\n display: 'Impact, \"Arial Black\", system-ui, sans-serif',\n handwriting: '\"Segoe Script\", \"Brush Script MT\", cursive',\n}\n\nexport type FontProvider = 'google' | 'bunny'\n\nexport interface UseFontOptions {\n /**\n * A single font family name, e.g. `\"Roboto\"` or `\"Open Sans\"`.\n *\n * For fallback fonts, use the `fallback` option instead of a\n * comma-separated list here.\n */\n family: string\n /** CSS fallback list appended to the `font-family` declaration. */\n fallback?: string\n /**\n * Font provider used to build the stylesheet URL when `url` is omitted.\n * Bunny Fonts is a drop-in, privacy-friendly Google Fonts mirror.\n * @default 'google'\n */\n provider?: FontProvider\n /**\n * Stylesheet URL. When provided, used as-is for the `<link href>`.\n * When omitted, a URL is built from `provider`, `family`, `weights`,\n * `display` and `styles`.\n */\n url?: string\n /** Font weights to load. Ignored when `url` is provided. */\n weights?: number[]\n /** `font-display` value. Ignored when `url` is provided. */\n display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional'\n /** Font styles to load. Ignored when `url` is provided. */\n styles?: Array<'normal' | 'italic'>\n}\n\nconst PROVIDER_BASE_URL: Record<FontProvider, string> = {\n google: 'https://fonts.googleapis.com/css2',\n bunny: 'https://fonts.bunny.net/css2',\n}\n\nfunction slugify(family: string): string {\n return family\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n}\n\nfunction buildProviderUrl(opts: Required<Omit<UseFontOptions, 'url' | 'fallback'>>): string {\n const familyParam = opts.family.trim().replace(/\\s+/g, '+')\n const weights = [...opts.weights].sort((a, b) => a - b)\n const hasItalic = opts.styles.includes('italic')\n const hasNormal = opts.styles.includes('normal')\n\n const axis = hasItalic\n ? `:ital,wght@${weights.flatMap(w => [\n ...(hasNormal ? [`0,${w}`] : []),\n `1,${w}`,\n ]).join(';')}`\n : `:wght@${weights.join(';')}`\n\n return `${PROVIDER_BASE_URL[opts.provider]}?family=${familyParam}${axis}&display=${opts.display}`\n}\n\n/**\n * Register a font for the current email template.\n *\n * Builds a Google Fonts stylesheet URL from `family`/`weights`/`display`/`styles`\n * (or uses `url` as-is). The renderer injects a `<link>` tag into `<head>`\n * and merges `--font-{slug}` declarations into the template's existing\n * `@import \"tailwindcss\"` style block so a `font-{slug}` utility class\n * is generated. If no Tailwind import is found, falls back to a `:root`\n * declaration so the CSS variable is still available.\n *\n * Usage in SFC <script setup>:\n * ```ts\n * useFont({ family: 'Roboto', fallback: 'Verdana, sans-serif', weights: [400, 600] })\n * ```\n */\nexport function useFont(options: UseFontOptions): void {\n const ctx = inject(RenderContextKey)\n if (!ctx) return\n\n ctx.fonts = ctx.fonts ?? []\n if (ctx.fonts.some(f => f.family === options.family)) return\n\n const url = options.url ?? buildProviderUrl({\n family: options.family,\n provider: options.provider ?? 'google',\n weights: options.weights ?? [400],\n display: options.display ?? 'swap',\n styles: options.styles ?? ['normal'],\n })\n\n const fallback = options.fallback\n ?? DEFAULT_FALLBACKS[FAMILY_CATEGORIES[options.family] ?? 'sans']\n const quoted = /\\s/.test(options.family) ? `\"${options.family}\"` : options.family\n const declaration = `${quoted}, ${fallback}`\n\n ctx.fonts.push({\n family: options.family,\n slug: slugify(options.family),\n declaration,\n url,\n })\n}\n"],"mappings":";;;AAKA,MAAM,oBAAkD;CAEtD,UAAU;CACV,aAAa;CACb,SAAS;CACT,QAAQ;CACR,cAAc;CAEd,gBAAgB;CAChB,oBAAoB;CACpB,QAAQ;CACR,YAAY;CACZ,cAAc;CAEd,UAAU;CACV,cAAc;CACd,SAAS;CACT,WAAW;CACX,YAAY;CAEZ,kBAAkB;CAClB,UAAU;CACV,sBAAsB;CACtB,WAAW;CACX,eAAe;CAEf,eAAe;CACf,mBAAmB;CACnB,kBAAkB;CAClB,aAAa;CACb,eAAe;AACjB;AAEA,MAAM,oBAAkD;CACtD,MAAM;CACN,OAAO;CACP,MAAM;CACN,SAAS;CACT,aAAa;AACf;AAkCA,MAAM,oBAAkD;CACtD,QAAQ;CACR,OAAO;AACT;AAEA,SAAS,QAAQ,QAAwB;CACvC,OAAO,OACJ,KAAK,CAAC,CACN,YAAY,CAAC,CACb,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,eAAe,EAAE;AAC9B;AAEA,SAAS,iBAAiB,MAAkE;CAC1F,MAAM,cAAc,KAAK,OAAO,KAAK,CAAC,CAAC,QAAQ,QAAQ,GAAG;CAC1D,MAAM,UAAU,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;CACtD,MAAM,YAAY,KAAK,OAAO,SAAS,QAAQ;CAC/C,MAAM,YAAY,KAAK,OAAO,SAAS,QAAQ;CAE/C,MAAM,OAAO,YACT,cAAc,QAAQ,SAAQ,MAAK,CACjC,GAAI,YAAY,CAAC,KAAK,GAAG,IAAI,CAAC,GAC9B,KAAK,GACP,CAAC,CAAC,CAAC,KAAK,GAAG,MACX,SAAS,QAAQ,KAAK,GAAG;CAE7B,OAAO,GAAG,kBAAkB,KAAK,UAAU,UAAU,cAAc,KAAK,WAAW,KAAK;AAC1F;;;;;;;;;;;;;;;;AAiBA,SAAgB,QAAQ,SAA+B;CACrD,MAAM,MAAM,OAAO,gBAAgB;CACnC,IAAI,CAAC,KAAK;CAEV,IAAI,QAAQ,IAAI,SAAS,CAAC;CAC1B,IAAI,IAAI,MAAM,MAAK,MAAK,EAAE,WAAW,QAAQ,MAAM,GAAG;CAEtD,MAAM,MAAM,QAAQ,OAAO,iBAAiB;EAC1C,QAAQ,QAAQ;EAChB,UAAU,QAAQ,YAAY;EAC9B,SAAS,QAAQ,WAAW,CAAC,GAAG;EAChC,SAAS,QAAQ,WAAW;EAC5B,QAAQ,QAAQ,UAAU,CAAC,QAAQ;CACrC,CAAC;CAED,MAAM,WAAW,QAAQ,YACpB,kBAAkB,kBAAkB,QAAQ,WAAW;CAE5D,MAAM,cAAc,GADL,KAAK,KAAK,QAAQ,MAAM,IAAI,IAAI,QAAQ,OAAO,KAAK,QAAQ,OAC7C,IAAI;CAElC,IAAI,MAAM,KAAK;EACb,QAAQ,QAAQ;EAChB,MAAM,QAAQ,QAAQ,MAAM;EAC5B;EACA;CACF,CAAC;AACH"}
@@ -2,8 +2,8 @@ import { defaults } from "./defaults.js";
2
2
  import { defineConfig } from "../composables/defineConfig.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
- import { createJiti } from "jiti";
6
5
  import { fileURLToPath } from "node:url";
6
+ import { createJiti } from "jiti";
7
7
  import { createDefu } from "defu";
8
8
  //#region src/config/index.ts
9
9
  const merge = createDefu((obj, key, value) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/config/index.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { createJiti } from 'jiti'\nimport { fileURLToPath } from 'node:url'\nimport { createDefu } from 'defu'\n\n// defu that replaces arrays: if user provides content: ['x'], it replaces the default, not appends\nconst merge = createDefu((obj, key, value) => {\n if (Array.isArray(obj[key])) {\n obj[key] = value\n return true\n }\n})\nimport { defaults } from './defaults.ts'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nexport { defineConfig } from '../composables/defineConfig.ts'\nexport { defaults } from './defaults.ts'\n\nconst CONFIG_FILES = [\n 'maizzle.config.ts',\n 'maizzle.config.js',\n]\n\n/**\n * Resolve the Maizzle config.\n *\n * Always loads from the config file on disk (maizzle.config.{ts,js}),\n * then merges the programmatic config on top, then fills in defaults.\n */\nexport async function resolveConfig(\n config?: Partial<MaizzleConfig> | string,\n cwd: string = process.cwd(),\n): Promise<MaizzleConfig> {\n // If a string path was provided, load that specific file\n const fileConfig = await loadConfig(\n typeof config === 'string' ? config : undefined,\n cwd,\n )\n\n // Programmatic config (object) overrides file config, which overrides defaults\n const programmaticConfig = typeof config === 'object' && config !== null ? config : {}\n\n const merged = merge(programmaticConfig, fileConfig, defaults) as MaizzleConfig\n\n // Check if root was explicitly provided before resolving\n const hasExplicitRoot = !!(programmaticConfig.root ?? fileConfig.root)\n\n // Resolve root to an absolute path (defaults to cwd)\n const root = resolve(cwd, merged.root ?? '.')\n merged.root = root\n\n // Resolve content patterns relative to root\n if (merged.content) {\n merged.content = merged.content.map(p => {\n // Skip already-absolute or negated patterns\n if (p.startsWith('/') || p.startsWith('!')) return p\n return resolve(root, p).replace(/\\\\/g, '/')\n })\n }\n\n // Resolve static source patterns relative to root\n if (merged.static?.source) {\n merged.static.source = merged.static.source.map(p => {\n if (p.startsWith('/') || p.startsWith('!')) return p\n return resolve(root, p).replace(/\\\\/g, '/')\n })\n }\n\n /**\n * Resolve components.source paths relative to cwd (not root), since extra\n * component dirs often live outside the root directory. String\n * entries → resolve in-place. Object entries → resolve\n * `path`, preserve `prefix`/`pathPrefix`.\n */\n if (merged.components?.source) {\n const dirs = Array.isArray(merged.components.source)\n ? merged.components.source\n : [merged.components.source]\n\n merged.components.source = dirs.map(entry => {\n if (typeof entry === 'string') {\n return entry.startsWith('/') ? entry : resolve(cwd, entry)\n }\n return {\n ...entry,\n path: entry.path.startsWith('/') ? entry.path : resolve(cwd, entry.path),\n }\n })\n }\n\n /**\n * Default css.base to root when root is explicitly set, so Tailwind resolves\n * @source from the right directory. When root is not set, leave\n * css.base undefined so Tailwind uses its own default (the\n * template file's directory).\n */\n if (hasExplicitRoot && !merged.css?.base) {\n if (!merged.css) merged.css = {}\n merged.css.base = root\n }\n\n return merged\n}\n\nasync function loadConfig(\n configPath?: string,\n cwd: string = process.cwd(),\n): Promise<MaizzleConfig> {\n const jiti = createJiti(fileURLToPath(import.meta.url), { moduleCache: false })\n\n // If an explicit path was provided, use it directly\n if (configPath) {\n const absolutePath = resolve(cwd, configPath)\n\n if (!existsSync(absolutePath)) {\n throw new Error(`Config file not found: ${absolutePath}`)\n }\n\n const mod = await jiti.import(absolutePath) as any\n return mod.default ?? mod\n }\n\n // Otherwise scan cwd for known config file names\n for (const filename of CONFIG_FILES) {\n const filepath = resolve(cwd, filename)\n\n if (existsSync(filepath)) {\n const mod = await jiti.import(filepath) as any\n return mod.default ?? mod\n }\n }\n\n // No config file found, return empty (defaults will be applied by resolveConfig)\n return {}\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,QAAQ,YAAY,KAAK,KAAK,UAAU;CAC5C,IAAI,MAAM,QAAQ,IAAI,IAAI,GAAG;EAC3B,IAAI,OAAO;EACX,OAAO;CACT;AACF,CAAC;AAOD,MAAM,eAAe,CACnB,qBACA,mBACF;;;;;;;AAQA,eAAsB,cACpB,QACA,MAAc,QAAQ,IAAI,GACF;CAExB,MAAM,aAAa,MAAM,WACvB,OAAO,WAAW,WAAW,SAAS,KAAA,GACtC,GACF;CAGA,MAAM,qBAAqB,OAAO,WAAW,YAAY,WAAW,OAAO,SAAS,CAAC;CAErF,MAAM,SAAS,MAAM,oBAAoB,YAAY,QAAQ;CAG7D,MAAM,kBAAkB,CAAC,EAAE,mBAAmB,QAAQ,WAAW;CAGjE,MAAM,OAAO,QAAQ,KAAK,OAAO,QAAQ,GAAG;CAC5C,OAAO,OAAO;CAGd,IAAI,OAAO,SACT,OAAO,UAAU,OAAO,QAAQ,KAAI,MAAK;EAEvC,IAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,GAAG,OAAO;EACnD,OAAO,QAAQ,MAAM,CAAC,EAAE,QAAQ,OAAO,GAAG;CAC5C,CAAC;CAIH,IAAI,OAAO,QAAQ,QACjB,OAAO,OAAO,SAAS,OAAO,OAAO,OAAO,KAAI,MAAK;EACnD,IAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,GAAG,OAAO;EACnD,OAAO,QAAQ,MAAM,CAAC,EAAE,QAAQ,OAAO,GAAG;CAC5C,CAAC;;;;;;;CASH,IAAI,OAAO,YAAY,QAAQ;EAC7B,MAAM,OAAO,MAAM,QAAQ,OAAO,WAAW,MAAM,IAC/C,OAAO,WAAW,SAClB,CAAC,OAAO,WAAW,MAAM;EAE7B,OAAO,WAAW,SAAS,KAAK,KAAI,UAAS;GAC3C,IAAI,OAAO,UAAU,UACnB,OAAO,MAAM,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;GAE3D,OAAO;IACL,GAAG;IACH,MAAM,MAAM,KAAK,WAAW,GAAG,IAAI,MAAM,OAAO,QAAQ,KAAK,MAAM,IAAI;GACzE;EACF,CAAC;CACH;;;;;;;CAQA,IAAI,mBAAmB,CAAC,OAAO,KAAK,MAAM;EACxC,IAAI,CAAC,OAAO,KAAK,OAAO,MAAM,CAAC;EAC/B,OAAO,IAAI,OAAO;CACpB;CAEA,OAAO;AACT;AAEA,eAAe,WACb,YACA,MAAc,QAAQ,IAAI,GACF;CACxB,MAAM,OAAO,WAAW,cAAc,OAAO,KAAK,GAAG,GAAG,EAAE,aAAa,MAAM,CAAC;CAG9E,IAAI,YAAY;EACd,MAAM,eAAe,QAAQ,KAAK,UAAU;EAE5C,IAAI,CAAC,WAAW,YAAY,GAC1B,MAAM,IAAI,MAAM,0BAA0B,cAAc;EAG1D,MAAM,MAAM,MAAM,KAAK,OAAO,YAAY;EAC1C,OAAO,IAAI,WAAW;CACxB;CAGA,KAAK,MAAM,YAAY,cAAc;EACnC,MAAM,WAAW,QAAQ,KAAK,QAAQ;EAEtC,IAAI,WAAW,QAAQ,GAAG;GACxB,MAAM,MAAM,MAAM,KAAK,OAAO,QAAQ;GACtC,OAAO,IAAI,WAAW;EACxB;CACF;CAGA,OAAO,CAAC;AACV"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/config/index.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { createJiti } from 'jiti'\nimport { fileURLToPath } from 'node:url'\nimport { createDefu } from 'defu'\n\n// defu that replaces arrays: if user provides content: ['x'], it replaces the default, not appends\nconst merge = createDefu((obj, key, value) => {\n if (Array.isArray(obj[key])) {\n obj[key] = value\n return true\n }\n})\nimport { defaults } from './defaults.ts'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nexport { defineConfig } from '../composables/defineConfig.ts'\nexport { defaults } from './defaults.ts'\n\nconst CONFIG_FILES = [\n 'maizzle.config.ts',\n 'maizzle.config.js',\n]\n\n/**\n * Resolve the Maizzle config.\n *\n * Always loads from the config file on disk (maizzle.config.{ts,js}),\n * then merges the programmatic config on top, then fills in defaults.\n */\nexport async function resolveConfig(\n config?: Partial<MaizzleConfig> | string,\n cwd: string = process.cwd(),\n): Promise<MaizzleConfig> {\n // If a string path was provided, load that specific file\n const fileConfig = await loadConfig(\n typeof config === 'string' ? config : undefined,\n cwd,\n )\n\n // Programmatic config (object) overrides file config, which overrides defaults\n const programmaticConfig = typeof config === 'object' && config !== null ? config : {}\n\n const merged = merge(programmaticConfig, fileConfig, defaults) as MaizzleConfig\n\n // Check if root was explicitly provided before resolving\n const hasExplicitRoot = !!(programmaticConfig.root ?? fileConfig.root)\n\n // Resolve root to an absolute path (defaults to cwd)\n const root = resolve(cwd, merged.root ?? '.')\n merged.root = root\n\n // Resolve content patterns relative to root\n if (merged.content) {\n merged.content = merged.content.map(p => {\n // Skip already-absolute or negated patterns\n if (p.startsWith('/') || p.startsWith('!')) return p\n return resolve(root, p).replace(/\\\\/g, '/')\n })\n }\n\n // Resolve static source patterns relative to root\n if (merged.static?.source) {\n merged.static.source = merged.static.source.map(p => {\n if (p.startsWith('/') || p.startsWith('!')) return p\n return resolve(root, p).replace(/\\\\/g, '/')\n })\n }\n\n /**\n * Resolve components.source paths relative to cwd (not root), since extra\n * component dirs often live outside the root directory. String\n * entries → resolve in-place. Object entries → resolve\n * `path`, preserve `prefix`/`pathPrefix`.\n */\n if (merged.components?.source) {\n const dirs = Array.isArray(merged.components.source)\n ? merged.components.source\n : [merged.components.source]\n\n merged.components.source = dirs.map(entry => {\n if (typeof entry === 'string') {\n return entry.startsWith('/') ? entry : resolve(cwd, entry)\n }\n return {\n ...entry,\n path: entry.path.startsWith('/') ? entry.path : resolve(cwd, entry.path),\n }\n })\n }\n\n /**\n * Default css.base to root when root is explicitly set, so Tailwind resolves\n * @source from the right directory. When root is not set, leave\n * css.base undefined so Tailwind uses its own default (the\n * template file's directory).\n */\n if (hasExplicitRoot && !merged.css?.base) {\n if (!merged.css) merged.css = {}\n merged.css.base = root\n }\n\n return merged\n}\n\nasync function loadConfig(\n configPath?: string,\n cwd: string = process.cwd(),\n): Promise<MaizzleConfig> {\n const jiti = createJiti(fileURLToPath(import.meta.url), { moduleCache: false })\n\n // If an explicit path was provided, use it directly\n if (configPath) {\n const absolutePath = resolve(cwd, configPath)\n\n if (!existsSync(absolutePath)) {\n throw new Error(`Config file not found: ${absolutePath}`)\n }\n\n const mod = await jiti.import(absolutePath) as any\n return mod.default ?? mod\n }\n\n // Otherwise scan cwd for known config file names\n for (const filename of CONFIG_FILES) {\n const filepath = resolve(cwd, filename)\n\n if (existsSync(filepath)) {\n const mod = await jiti.import(filepath) as any\n return mod.default ?? mod\n }\n }\n\n // No config file found, return empty (defaults will be applied by resolveConfig)\n return {}\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,QAAQ,YAAY,KAAK,KAAK,UAAU;CAC5C,IAAI,MAAM,QAAQ,IAAI,IAAI,GAAG;EAC3B,IAAI,OAAO;EACX,OAAO;CACT;AACF,CAAC;AAOD,MAAM,eAAe,CACnB,qBACA,mBACF;;;;;;;AAQA,eAAsB,cACpB,QACA,MAAc,QAAQ,IAAI,GACF;CAExB,MAAM,aAAa,MAAM,WACvB,OAAO,WAAW,WAAW,SAAS,KAAA,GACtC,GACF;CAGA,MAAM,qBAAqB,OAAO,WAAW,YAAY,WAAW,OAAO,SAAS,CAAC;CAErF,MAAM,SAAS,MAAM,oBAAoB,YAAY,QAAQ;CAG7D,MAAM,kBAAkB,CAAC,EAAE,mBAAmB,QAAQ,WAAW;CAGjE,MAAM,OAAO,QAAQ,KAAK,OAAO,QAAQ,GAAG;CAC5C,OAAO,OAAO;CAGd,IAAI,OAAO,SACT,OAAO,UAAU,OAAO,QAAQ,KAAI,MAAK;EAEvC,IAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,GAAG,OAAO;EACnD,OAAO,QAAQ,MAAM,CAAC,CAAC,CAAC,QAAQ,OAAO,GAAG;CAC5C,CAAC;CAIH,IAAI,OAAO,QAAQ,QACjB,OAAO,OAAO,SAAS,OAAO,OAAO,OAAO,KAAI,MAAK;EACnD,IAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,GAAG,OAAO;EACnD,OAAO,QAAQ,MAAM,CAAC,CAAC,CAAC,QAAQ,OAAO,GAAG;CAC5C,CAAC;;;;;;;CASH,IAAI,OAAO,YAAY,QAAQ;EAC7B,MAAM,OAAO,MAAM,QAAQ,OAAO,WAAW,MAAM,IAC/C,OAAO,WAAW,SAClB,CAAC,OAAO,WAAW,MAAM;EAE7B,OAAO,WAAW,SAAS,KAAK,KAAI,UAAS;GAC3C,IAAI,OAAO,UAAU,UACnB,OAAO,MAAM,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;GAE3D,OAAO;IACL,GAAG;IACH,MAAM,MAAM,KAAK,WAAW,GAAG,IAAI,MAAM,OAAO,QAAQ,KAAK,MAAM,IAAI;GACzE;EACF,CAAC;CACH;;;;;;;CAQA,IAAI,mBAAmB,CAAC,OAAO,KAAK,MAAM;EACxC,IAAI,CAAC,OAAO,KAAK,OAAO,MAAM,CAAC;EAC/B,OAAO,IAAI,OAAO;CACpB;CAEA,OAAO;AACT;AAEA,eAAe,WACb,YACA,MAAc,QAAQ,IAAI,GACF;CACxB,MAAM,OAAO,WAAW,cAAc,OAAO,KAAK,GAAG,GAAG,EAAE,aAAa,MAAM,CAAC;CAG9E,IAAI,YAAY;EACd,MAAM,eAAe,QAAQ,KAAK,UAAU;EAE5C,IAAI,CAAC,WAAW,YAAY,GAC1B,MAAM,IAAI,MAAM,0BAA0B,cAAc;EAG1D,MAAM,MAAM,MAAM,KAAK,OAAO,YAAY;EAC1C,OAAO,IAAI,WAAW;CACxB;CAGA,KAAK,MAAM,YAAY,cAAc;EACnC,MAAM,WAAW,QAAQ,KAAK,QAAQ;EAEtC,IAAI,WAAW,QAAQ,GAAG;GACxB,MAAM,MAAM,MAAM,KAAK,OAAO,QAAQ;GACtC,OAAO,IAAI,WAAW;EACxB;CACF;CAGA,OAAO,CAAC;AACV"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/events/index.ts"],"mappings":";;;;KAGY,SAAA;;;AAAZ;;;UAOiB,YAAA;EACf,MAAA;EACA,IAAA,EAAM,UAAU;AAAA;AAAA,UAGD,QAAA;EACf,YAAA,GAAe,MAAA;IAAU,MAAA,EAAQ,aAAA;EAAA,aAA2B,OAAA;EAC5D,YAAA,GAAe,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;EAAA,sBAAmC,OAAA;EAC7F,WAAA,GAAc,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,sBAAmC,OAAA;EAC1G,cAAA,GAAiB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,sBAAmC,OAAA;EAC7G,UAAA,GAAa,MAAA;IAAU,KAAA;IAAiB,MAAA,EAAQ,aAAA;EAAA,aAA2B,OAAA;AAAA;;;;;;;;cAUhE,YAAA;EAAA,QACH,QAAA;EAdyB;;;;;EAAA,QAoBzB,kBAAA;EAnBgB;;;EAwBxB,cAAA,CAAe,MAAA,EAAQ,aAAA;EAxBgD;;;EAuCvE,EAAA,WAAa,SAAA,CAAA,CAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,QAAA,CAAS,CAAA;EAtCxB;;;EAiDrB,gBAAA,CAAiB,MAAA;IAAU,MAAA,EAAQ,aAAA;EAAA,IAAe,OAAA;EAjDqD;;;;EA6DvG,gBAAA,CAAiB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;EAAA,IAAiB,OAAA;EA5DD;AAUpF;;EAmEQ,eAAA,CAAgB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,IAAiB,OAAA;EAxC7C;;;EA2D7C,kBAAA,CAAmB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,IAAiB,OAAA;EAnBH;;;EAsC1F,cAAA,CAAe,MAAA;IAAU,KAAA;IAAiB,MAAA,EAAQ,aAAA;EAAA,IAAe,OAAA;EAgBrC;;;;;;;;EAAlC,gBAAA,CAAiB,MAAA,GAAQ,SAAA;EA9FZ;;;EA4Gb,KAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/events/index.ts"],"mappings":";;;;KAGY,SAAA;;;AAAZ;;;UAOiB,YAAA;EACf,MAAA;EACA,IAAA,EAAM,UAAU;AAAA;AAAA,UAGD,QAAA;EACf,YAAA,GAAe,MAAA;IAAU,MAAA,EAAQ,aAAA;EAAA,aAA2B,OAAA;EAC5D,YAAA,GAAe,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;EAAA,sBAAmC,OAAA;EAC7F,WAAA,GAAc,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,sBAAmC,OAAA;EAC1G,cAAA,GAAiB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,sBAAmC,OAAA;EAC7G,UAAA,GAAa,MAAA;IAAU,KAAA;IAAiB,MAAA,EAAQ,aAAA;EAAA,aAA2B,OAAA;AAAA;;;;;;;;cAUhE,YAAA;EAAA,QACH,QAAA;EAdyB;;;;;EAAA,QAoBzB,kBAAA;EAnBgB;;;EAwBxB,cAAA,CAAe,MAAA,EAAQ,aAAA;EAxBgD;;;EAuCvE,EAAA,WAAa,SAAA,EAAW,IAAA,EAAM,CAAA,EAAG,OAAA,EAAS,QAAA,CAAS,CAAA;EAtCxB;;;EAiDrB,gBAAA,CAAiB,MAAA;IAAU,MAAA,EAAQ,aAAA;EAAA,IAAe,OAAA;EAjDqD;;;;EA6DvG,gBAAA,CAAiB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;EAAA,IAAiB,OAAA;EA5DD;AAUpF;;EAmEQ,eAAA,CAAgB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,IAAiB,OAAA;EAxC7C;;;EA2D7C,kBAAA,CAAmB,MAAA;IAAU,MAAA,EAAQ,aAAA;IAAe,QAAA,EAAU,YAAA;IAAc,IAAA;EAAA,IAAiB,OAAA;EAnBH;;;EAsC1F,cAAA,CAAe,MAAA;IAAU,KAAA;IAAiB,MAAA,EAAQ,aAAA;EAAA,IAAe,OAAA;EAgBrC;;;;;;;;EAAlC,gBAAA,CAAiB,MAAA,GAAQ,SAAA;EA9FZ;;;EA4Gb,KAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/events/index.ts"],"sourcesContent":["import type { ParsedPath } from 'node:path'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nexport type EventName = 'beforeCreate' | 'beforeRender' | 'afterRender' | 'afterTransform' | 'afterBuild'\n\n/**\n * Info about the template currently being processed, passed to per-template\n * event handlers. `source` is the SFC file contents; `path` is the result of\n * `path.parse(absolutePath)` — `{ root, dir, base, ext, name }`.\n */\nexport interface TemplateInfo {\n source: string\n path: ParsedPath\n}\n\nexport interface EventMap {\n beforeCreate: (params: { config: MaizzleConfig }) => void | Promise<void>\n beforeRender: (params: { config: MaizzleConfig; template: TemplateInfo }) => string | void | Promise<string | void>\n afterRender: (params: { config: MaizzleConfig; template: TemplateInfo; html: string }) => string | void | Promise<string | void>\n afterTransform: (params: { config: MaizzleConfig; template: TemplateInfo; html: string }) => string | void | Promise<string | void>\n afterBuild: (params: { files: string[]; config: MaizzleConfig }) => void | Promise<void>\n}\n\n/**\n * Central event manager that collects handlers from config and useEvent() calls.\n *\n * Handlers are run in order: config handler first, then SFC handlers in registration order.\n * For events that return a value (beforeRender, afterRender, afterTransform),\n * the returned value replaces the corresponding input for the next handler.\n */\nexport class EventManager {\n private handlers = new Map<EventName, EventMap[EventName][]>()\n /**\n * Snapshot of config-handler counts per event, captured at registerConfig().\n * clearSfcHandlers() truncates each list back to this count, dropping\n * any SFC-registered handlers that were appended after.\n */\n private configHandlerCount = new Map<EventName, number>()\n\n /**\n * Register handlers from the Maizzle config.\n */\n registerConfig(config: MaizzleConfig) {\n const eventNames: EventName[] = ['beforeCreate', 'beforeRender', 'afterRender', 'afterTransform', 'afterBuild']\n\n for (const name of eventNames) {\n const handler = config[name]\n if (typeof handler === 'function') {\n this.on(name, handler as EventMap[typeof name])\n }\n this.configHandlerCount.set(name, this.handlers.get(name)?.length ?? 0)\n }\n }\n\n /**\n * Register a handler for an event (used by useEvent composable).\n */\n on<K extends EventName>(name: K, handler: EventMap[K]) {\n if (!this.handlers.has(name)) {\n this.handlers.set(name, [])\n }\n\n this.handlers.get(name)!.push(handler)\n }\n\n /**\n * Fire beforeCreate — runs all handlers, config is mutated in place.\n */\n async fireBeforeCreate(params: { config: MaizzleConfig }) {\n const handlers = this.handlers.get('beforeCreate') ?? []\n\n for (const handler of handlers) {\n await (handler as EventMap['beforeCreate'])(params)\n }\n }\n\n /**\n * Fire beforeRender — if a handler returns a string, it replaces\n * `template.source` for subsequent handlers and the renderer.\n */\n async fireBeforeRender(params: { config: MaizzleConfig; template: TemplateInfo }): Promise<string> {\n const handlers = this.handlers.get('beforeRender') ?? []\n\n for (const handler of handlers) {\n const result = await (handler as EventMap['beforeRender'])(params)\n\n if (typeof result === 'string') {\n params.template.source = result\n }\n }\n\n return params.template.source\n }\n\n /**\n * Fire afterRender — if a handler returns a string, it replaces `html`.\n */\n async fireAfterRender(params: { config: MaizzleConfig; template: TemplateInfo; html: string }): Promise<string> {\n const handlers = this.handlers.get('afterRender') ?? []\n\n let { html } = params\n\n for (const handler of handlers) {\n const result = await (handler as EventMap['afterRender'])({ config: params.config, template: params.template, html })\n\n if (typeof result === 'string') {\n html = result\n }\n }\n\n return html\n }\n\n /**\n * Fire afterTransform — if a handler returns a string, it replaces `html`.\n */\n async fireAfterTransform(params: { config: MaizzleConfig; template: TemplateInfo; html: string }): Promise<string> {\n const handlers = this.handlers.get('afterTransform') ?? []\n\n let { html } = params\n\n for (const handler of handlers) {\n const result = await (handler as EventMap['afterTransform'])({ config: params.config, template: params.template, html })\n\n if (typeof result === 'string') {\n html = result\n }\n }\n\n return html\n }\n\n /**\n * Fire afterBuild — runs all handlers with the file list.\n */\n async fireAfterBuild(params: { files: string[]; config: MaizzleConfig }) {\n const handlers = this.handlers.get('afterBuild') ?? []\n\n for (const handler of handlers) {\n await (handler as EventMap['afterBuild'])(params)\n }\n }\n\n /**\n * Drop SFC-registered handlers, keep config-registered ones.\n *\n * Per default, only clears events whose scope is per-template\n * (`beforeRender`, `afterRender`, `afterTransform`). Build-scoped events\n * (`afterBuild`) accumulate across all templates and fire once at end of\n * build. Pass an explicit list to override.\n */\n clearSfcHandlers(events: EventName[] = ['beforeRender', 'afterRender', 'afterTransform']) {\n for (const name of events) {\n const handlers = this.handlers.get(name)\n if (!handlers) continue\n const keep = this.configHandlerCount.get(name) ?? 0\n if (handlers.length > keep) {\n this.handlers.set(name, handlers.slice(0, keep))\n }\n }\n }\n\n /**\n * Clear all handlers entirely.\n */\n clear() {\n this.handlers.clear()\n }\n}\n"],"mappings":";;;;;;;;AA8BA,IAAa,eAAb,MAA0B;CACxB,2BAAmB,IAAI,IAAsC;;;;;;CAM7D,qCAA6B,IAAI,IAAuB;;;;CAKxD,eAAe,QAAuB;EAGpC,KAAK,MAAM,QAAQ;GAFc;GAAgB;GAAgB;GAAe;GAAkB;EAEtE,GAAG;GAC7B,MAAM,UAAU,OAAO;GACvB,IAAI,OAAO,YAAY,YACrB,KAAK,GAAG,MAAM,OAAgC;GAEhD,KAAK,mBAAmB,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,GAAG,UAAU,CAAC;EACxE;CACF;;;;CAKA,GAAwB,MAAS,SAAsB;EACrD,IAAI,CAAC,KAAK,SAAS,IAAI,IAAI,GACzB,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC;EAG5B,KAAK,SAAS,IAAI,IAAI,EAAG,KAAK,OAAO;CACvC;;;;CAKA,MAAM,iBAAiB,QAAmC;EACxD,MAAM,WAAW,KAAK,SAAS,IAAI,cAAc,KAAK,CAAC;EAEvD,KAAK,MAAM,WAAW,UACpB,MAAO,QAAqC,MAAM;CAEtD;;;;;CAMA,MAAM,iBAAiB,QAA4E;EACjG,MAAM,WAAW,KAAK,SAAS,IAAI,cAAc,KAAK,CAAC;EAEvD,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,SAAS,MAAO,QAAqC,MAAM;GAEjE,IAAI,OAAO,WAAW,UACpB,OAAO,SAAS,SAAS;EAE7B;EAEA,OAAO,OAAO,SAAS;CACzB;;;;CAKA,MAAM,gBAAgB,QAA0F;EAC9G,MAAM,WAAW,KAAK,SAAS,IAAI,aAAa,KAAK,CAAC;EAEtD,IAAI,EAAE,SAAS;EAEf,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,SAAS,MAAO,QAAoC;IAAE,QAAQ,OAAO;IAAQ,UAAU,OAAO;IAAU;GAAK,CAAC;GAEpH,IAAI,OAAO,WAAW,UACpB,OAAO;EAEX;EAEA,OAAO;CACT;;;;CAKA,MAAM,mBAAmB,QAA0F;EACjH,MAAM,WAAW,KAAK,SAAS,IAAI,gBAAgB,KAAK,CAAC;EAEzD,IAAI,EAAE,SAAS;EAEf,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,SAAS,MAAO,QAAuC;IAAE,QAAQ,OAAO;IAAQ,UAAU,OAAO;IAAU;GAAK,CAAC;GAEvH,IAAI,OAAO,WAAW,UACpB,OAAO;EAEX;EAEA,OAAO;CACT;;;;CAKA,MAAM,eAAe,QAAoD;EACvE,MAAM,WAAW,KAAK,SAAS,IAAI,YAAY,KAAK,CAAC;EAErD,KAAK,MAAM,WAAW,UACpB,MAAO,QAAmC,MAAM;CAEpD;;;;;;;;;CAUA,iBAAiB,SAAsB;EAAC;EAAgB;EAAe;CAAgB,GAAG;EACxF,KAAK,MAAM,QAAQ,QAAQ;GACzB,MAAM,WAAW,KAAK,SAAS,IAAI,IAAI;GACvC,IAAI,CAAC,UAAU;GACf,MAAM,OAAO,KAAK,mBAAmB,IAAI,IAAI,KAAK;GAClD,IAAI,SAAS,SAAS,MACpB,KAAK,SAAS,IAAI,MAAM,SAAS,MAAM,GAAG,IAAI,CAAC;EAEnD;CACF;;;;CAKA,QAAQ;EACN,KAAK,SAAS,MAAM;CACtB;AACF"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/events/index.ts"],"sourcesContent":["import type { ParsedPath } from 'node:path'\nimport type { MaizzleConfig } from '../types/index.ts'\n\nexport type EventName = 'beforeCreate' | 'beforeRender' | 'afterRender' | 'afterTransform' | 'afterBuild'\n\n/**\n * Info about the template currently being processed, passed to per-template\n * event handlers. `source` is the SFC file contents; `path` is the result of\n * `path.parse(absolutePath)` — `{ root, dir, base, ext, name }`.\n */\nexport interface TemplateInfo {\n source: string\n path: ParsedPath\n}\n\nexport interface EventMap {\n beforeCreate: (params: { config: MaizzleConfig }) => void | Promise<void>\n beforeRender: (params: { config: MaizzleConfig; template: TemplateInfo }) => string | void | Promise<string | void>\n afterRender: (params: { config: MaizzleConfig; template: TemplateInfo; html: string }) => string | void | Promise<string | void>\n afterTransform: (params: { config: MaizzleConfig; template: TemplateInfo; html: string }) => string | void | Promise<string | void>\n afterBuild: (params: { files: string[]; config: MaizzleConfig }) => void | Promise<void>\n}\n\n/**\n * Central event manager that collects handlers from config and useEvent() calls.\n *\n * Handlers are run in order: config handler first, then SFC handlers in registration order.\n * For events that return a value (beforeRender, afterRender, afterTransform),\n * the returned value replaces the corresponding input for the next handler.\n */\nexport class EventManager {\n private handlers = new Map<EventName, EventMap[EventName][]>()\n /**\n * Snapshot of config-handler counts per event, captured at registerConfig().\n * clearSfcHandlers() truncates each list back to this count, dropping\n * any SFC-registered handlers that were appended after.\n */\n private configHandlerCount = new Map<EventName, number>()\n\n /**\n * Register handlers from the Maizzle config.\n */\n registerConfig(config: MaizzleConfig) {\n const eventNames: EventName[] = ['beforeCreate', 'beforeRender', 'afterRender', 'afterTransform', 'afterBuild']\n\n for (const name of eventNames) {\n const handler = config[name]\n if (typeof handler === 'function') {\n this.on(name, handler as EventMap[typeof name])\n }\n this.configHandlerCount.set(name, this.handlers.get(name)?.length ?? 0)\n }\n }\n\n /**\n * Register a handler for an event (used by useEvent composable).\n */\n on<K extends EventName>(name: K, handler: EventMap[K]) {\n if (!this.handlers.has(name)) {\n this.handlers.set(name, [])\n }\n\n this.handlers.get(name)!.push(handler)\n }\n\n /**\n * Fire beforeCreate — runs all handlers, config is mutated in place.\n */\n async fireBeforeCreate(params: { config: MaizzleConfig }) {\n const handlers = this.handlers.get('beforeCreate') ?? []\n\n for (const handler of handlers) {\n await (handler as EventMap['beforeCreate'])(params)\n }\n }\n\n /**\n * Fire beforeRender — if a handler returns a string, it replaces\n * `template.source` for subsequent handlers and the renderer.\n */\n async fireBeforeRender(params: { config: MaizzleConfig; template: TemplateInfo }): Promise<string> {\n const handlers = this.handlers.get('beforeRender') ?? []\n\n for (const handler of handlers) {\n const result = await (handler as EventMap['beforeRender'])(params)\n\n if (typeof result === 'string') {\n params.template.source = result\n }\n }\n\n return params.template.source\n }\n\n /**\n * Fire afterRender — if a handler returns a string, it replaces `html`.\n */\n async fireAfterRender(params: { config: MaizzleConfig; template: TemplateInfo; html: string }): Promise<string> {\n const handlers = this.handlers.get('afterRender') ?? []\n\n let { html } = params\n\n for (const handler of handlers) {\n const result = await (handler as EventMap['afterRender'])({ config: params.config, template: params.template, html })\n\n if (typeof result === 'string') {\n html = result\n }\n }\n\n return html\n }\n\n /**\n * Fire afterTransform — if a handler returns a string, it replaces `html`.\n */\n async fireAfterTransform(params: { config: MaizzleConfig; template: TemplateInfo; html: string }): Promise<string> {\n const handlers = this.handlers.get('afterTransform') ?? []\n\n let { html } = params\n\n for (const handler of handlers) {\n const result = await (handler as EventMap['afterTransform'])({ config: params.config, template: params.template, html })\n\n if (typeof result === 'string') {\n html = result\n }\n }\n\n return html\n }\n\n /**\n * Fire afterBuild — runs all handlers with the file list.\n */\n async fireAfterBuild(params: { files: string[]; config: MaizzleConfig }) {\n const handlers = this.handlers.get('afterBuild') ?? []\n\n for (const handler of handlers) {\n await (handler as EventMap['afterBuild'])(params)\n }\n }\n\n /**\n * Drop SFC-registered handlers, keep config-registered ones.\n *\n * Per default, only clears events whose scope is per-template\n * (`beforeRender`, `afterRender`, `afterTransform`). Build-scoped events\n * (`afterBuild`) accumulate across all templates and fire once at end of\n * build. Pass an explicit list to override.\n */\n clearSfcHandlers(events: EventName[] = ['beforeRender', 'afterRender', 'afterTransform']) {\n for (const name of events) {\n const handlers = this.handlers.get(name)\n if (!handlers) continue\n const keep = this.configHandlerCount.get(name) ?? 0\n if (handlers.length > keep) {\n this.handlers.set(name, handlers.slice(0, keep))\n }\n }\n }\n\n /**\n * Clear all handlers entirely.\n */\n clear() {\n this.handlers.clear()\n }\n}\n"],"mappings":";;;;;;;;AA8BA,IAAa,eAAb,MAA0B;CACxB,2BAAmB,IAAI,IAAsC;;;;;;CAM7D,qCAA6B,IAAI,IAAuB;;;;CAKxD,eAAe,QAAuB;EAGpC,KAAK,MAAM,QAAQ;GAFc;GAAgB;GAAgB;GAAe;GAAkB;EAEtE,GAAG;GAC7B,MAAM,UAAU,OAAO;GACvB,IAAI,OAAO,YAAY,YACrB,KAAK,GAAG,MAAM,OAAgC;GAEhD,KAAK,mBAAmB,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,EAAE,UAAU,CAAC;EACxE;CACF;;;;CAKA,GAAwB,MAAS,SAAsB;EACrD,IAAI,CAAC,KAAK,SAAS,IAAI,IAAI,GACzB,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC;EAG5B,KAAK,SAAS,IAAI,IAAI,CAAC,CAAE,KAAK,OAAO;CACvC;;;;CAKA,MAAM,iBAAiB,QAAmC;EACxD,MAAM,WAAW,KAAK,SAAS,IAAI,cAAc,KAAK,CAAC;EAEvD,KAAK,MAAM,WAAW,UACpB,MAAO,QAAqC,MAAM;CAEtD;;;;;CAMA,MAAM,iBAAiB,QAA4E;EACjG,MAAM,WAAW,KAAK,SAAS,IAAI,cAAc,KAAK,CAAC;EAEvD,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,SAAS,MAAO,QAAqC,MAAM;GAEjE,IAAI,OAAO,WAAW,UACpB,OAAO,SAAS,SAAS;EAE7B;EAEA,OAAO,OAAO,SAAS;CACzB;;;;CAKA,MAAM,gBAAgB,QAA0F;EAC9G,MAAM,WAAW,KAAK,SAAS,IAAI,aAAa,KAAK,CAAC;EAEtD,IAAI,EAAE,SAAS;EAEf,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,SAAS,MAAO,QAAoC;IAAE,QAAQ,OAAO;IAAQ,UAAU,OAAO;IAAU;GAAK,CAAC;GAEpH,IAAI,OAAO,WAAW,UACpB,OAAO;EAEX;EAEA,OAAO;CACT;;;;CAKA,MAAM,mBAAmB,QAA0F;EACjH,MAAM,WAAW,KAAK,SAAS,IAAI,gBAAgB,KAAK,CAAC;EAEzD,IAAI,EAAE,SAAS;EAEf,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,SAAS,MAAO,QAAuC;IAAE,QAAQ,OAAO;IAAQ,UAAU,OAAO;IAAU;GAAK,CAAC;GAEvH,IAAI,OAAO,WAAW,UACpB,OAAO;EAEX;EAEA,OAAO;CACT;;;;CAKA,MAAM,eAAe,QAAoD;EACvE,MAAM,WAAW,KAAK,SAAS,IAAI,YAAY,KAAK,CAAC;EAErD,KAAK,MAAM,WAAW,UACpB,MAAO,QAAmC,MAAM;CAEpD;;;;;;;;;CAUA,iBAAiB,SAAsB;EAAC;EAAgB;EAAe;CAAgB,GAAG;EACxF,KAAK,MAAM,QAAQ,QAAQ;GACzB,MAAM,WAAW,KAAK,SAAS,IAAI,IAAI;GACvC,IAAI,CAAC,UAAU;GACf,MAAM,OAAO,KAAK,mBAAmB,IAAI,IAAI,KAAK;GAClD,IAAI,SAAS,SAAS,MACpB,KAAK,SAAS,IAAI,MAAM,SAAS,MAAM,GAAG,IAAI,CAAC;EAEnD;CACF;;;;CAKA,QAAQ;EACN,KAAK,SAAS,MAAM;CACtB;AACF"}
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { useConfig } from "./composables/useConfig.js";
2
2
  import { defineConfig } from "./composables/defineConfig.js";
3
3
  import { resolveConfig } from "./config/index.js";
4
+ import { normalizeComponentSources } from "./utils/componentSources.js";
5
+ import { createRenderer } from "./render/createRenderer.js";
4
6
  import { inlineLink } from "./transformers/inlineLink.js";
5
7
  import { safeSelectors } from "./transformers/safeSelectors.js";
6
8
  import { attributeToStyle } from "./transformers/attributeToStyle.js";
@@ -17,8 +19,6 @@ import { purgeCss } from "./transformers/purgeCss.js";
17
19
  import { replaceStrings } from "./transformers/replaceStrings.js";
18
20
  import { format } from "./transformers/format.js";
19
21
  import { minify } from "./transformers/minify.js";
20
- import { normalizeComponentSources } from "./utils/componentSources.js";
21
- import { createRenderer } from "./render/createRenderer.js";
22
22
  import { createPlaintext } from "./plaintext.js";
23
23
  import { useCurrentTemplate } from "./composables/useCurrentTemplate.js";
24
24
  import { build } from "./build.js";
@@ -1 +1 @@
1
- {"version":3,"file":"plaintext.js","names":[],"sources":["../src/plaintext.ts"],"sourcesContent":["import { stripHtml } from 'string-strip-html'\nimport defu from 'defu'\n\nconst defaults = {\n dumpLinkHrefsNearby: {\n enabled: true,\n putOnNewLine: true,\n },\n}\n\nexport function createPlaintext(html: string, options?: Record<string, unknown>): string {\n return stripHtml(html, defu(options, defaults)).result\n}\n"],"mappings":";;;AAGA,MAAM,WAAW,EACf,qBAAqB;CACnB,SAAS;CACT,cAAc;AAChB,EACF;AAEA,SAAgB,gBAAgB,MAAc,SAA2C;CACvF,OAAO,UAAU,MAAM,KAAK,SAAS,QAAQ,CAAC,EAAE;AAClD"}
1
+ {"version":3,"file":"plaintext.js","names":[],"sources":["../src/plaintext.ts"],"sourcesContent":["import { stripHtml } from 'string-strip-html'\nimport defu from 'defu'\n\nconst defaults = {\n dumpLinkHrefsNearby: {\n enabled: true,\n putOnNewLine: true,\n },\n}\n\nexport function createPlaintext(html: string, options?: Record<string, unknown>): string {\n return stripHtml(html, defu(options, defaults)).result\n}\n"],"mappings":";;;AAGA,MAAM,WAAW,EACf,qBAAqB;CACnB,SAAS;CACT,cAAc;AAChB,EACF;AAEA,SAAgB,gBAAgB,MAAc,SAA2C;CACvF,OAAO,UAAU,MAAM,KAAK,SAAS,QAAQ,CAAC,CAAC,CAAC;AAClD"}
@@ -1 +1 @@
1
- {"version":3,"file":"mergeMediaQueries.js","names":[],"sources":["../../../src/plugins/postcss/mergeMediaQueries.ts"],"sourcesContent":["import sortMediaQueries from 'postcss-sort-media-queries'\nimport type postcss from 'postcss'\nimport type { MaizzleConfig } from '../../types/config.ts'\n\n/**\n * Sorts and merges CSS media queries using postcss-sort-media-queries.\n *\n * Enabled by default. Opt out with css: { media: false }.\n *\n * Config examples:\n * css: { media: true } // merge, mobile-first sort (default)\n * css: { media: { sort: 'desktop-first' } } // merge, desktop-first sort\n * css: { media: false } // disabled\n */\nexport function mergeMediaQueries(config: MaizzleConfig): postcss.Plugin | null {\n const media = config.css?.media\n\n if (media === false) return null\n\n const options = typeof media === 'object' ? media : {}\n const sort = options.sort ?? 'mobile-first'\n\n return sortMediaQueries({ sort }) as postcss.Plugin\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,kBAAkB,QAA8C;CAC9E,MAAM,QAAQ,OAAO,KAAK;CAE1B,IAAI,UAAU,OAAO,OAAO;CAK5B,OAAO,iBAAiB,EAAE,OAHV,OAAO,UAAU,WAAW,QAAQ,CAAC,GAChC,QAAQ,eAEE,CAAC;AAClC"}
1
+ {"version":3,"file":"mergeMediaQueries.js","names":[],"sources":["../../../src/plugins/postcss/mergeMediaQueries.ts"],"sourcesContent":["import sortMediaQueries from 'postcss-sort-media-queries'\nimport type postcss from 'postcss'\nimport type { MaizzleConfig } from '../../types/config.ts'\n\n/**\n * Sorts and merges CSS media queries using postcss-sort-media-queries.\n *\n * Enabled by default. Opt out with css: { media: false }.\n *\n * Config examples:\n * css: { media: true } // merge, mobile-first sort (default)\n * css: { media: { sort: 'desktop-first' } } // merge, desktop-first sort\n * css: { media: false } // disabled\n */\nexport function mergeMediaQueries(config: MaizzleConfig): postcss.Plugin | null {\n const media = config.css?.media\n\n if (media === false) return null\n\n const options = typeof media === 'object' ? media : {}\n const sort = options.sort ?? 'mobile-first'\n\n return sortMediaQueries({ sort }) as postcss.Plugin\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,kBAAkB,QAA8C;CAC9E,MAAM,QAAQ,OAAO,KAAK;CAE1B,IAAI,UAAU,OAAO,OAAO;CAK5B,OAAO,iBAAiB,EAAE,OAHV,OAAO,UAAU,WAAW,QAAQ,CAAC,EAAA,CAChC,QAAQ,eAEE,CAAC;AAClC"}