@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.
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +11 -0
- package/dist/build.js.map +1 -1
- package/dist/components/Body.vue +1 -1
- package/dist/components/CodeBlock.vue +1 -1
- package/dist/components/CodeInline.vue +72 -2
- package/dist/components/Column.vue +2 -1
- package/dist/components/Container.vue +1 -11
- package/dist/components/Heading.vue +1 -1
- package/dist/components/Img.vue +252 -7
- package/dist/components/Link.vue +1 -1
- package/dist/components/Preheader.vue +35 -5
- package/dist/components/Section.vue +9 -14
- package/dist/components/Tailwind.vue +4 -2
- package/dist/components/Text.vue +2 -2
- package/dist/components/Vml.vue +354 -0
- package/dist/components/utils.d.ts.map +1 -1
- package/dist/components/utils.js.map +1 -1
- package/dist/composables/defineConfig.d.ts +3 -4
- package/dist/composables/defineConfig.d.ts.map +1 -1
- package/dist/composables/defineConfig.js +3 -4
- package/dist/composables/defineConfig.js.map +1 -1
- package/dist/composables/renderContext.d.ts +0 -1
- package/dist/composables/renderContext.d.ts.map +1 -1
- package/dist/composables/renderContext.js.map +1 -1
- package/dist/composables/useBaseUrl.d.ts.map +1 -1
- package/dist/composables/useBaseUrl.js.map +1 -1
- package/dist/composables/useConfig.d.ts.map +1 -1
- package/dist/composables/useConfig.js.map +1 -1
- package/dist/composables/useCurrentTemplate.d.ts.map +1 -1
- package/dist/composables/useCurrentTemplate.js +10 -3
- package/dist/composables/useCurrentTemplate.js.map +1 -1
- package/dist/composables/useDoctype.d.ts.map +1 -1
- package/dist/composables/useDoctype.js.map +1 -1
- package/dist/composables/useEvent.js.map +1 -1
- package/dist/composables/useFont.d.ts.map +1 -1
- package/dist/composables/useFont.js.map +1 -1
- package/dist/composables/useOutlookFallback.d.ts.map +1 -1
- package/dist/composables/useOutlookFallback.js.map +1 -1
- package/dist/composables/usePlaintext.d.ts.map +1 -1
- package/dist/composables/usePlaintext.js.map +1 -1
- package/dist/composables/usePreheader.d.ts +6 -5
- package/dist/composables/usePreheader.d.ts.map +1 -1
- package/dist/composables/usePreheader.js +3 -3
- package/dist/composables/usePreheader.js.map +1 -1
- package/dist/composables/useTransformers.d.ts +1 -1
- package/dist/composables/useTransformers.d.ts.map +1 -1
- package/dist/composables/useTransformers.js +1 -1
- package/dist/composables/useTransformers.js.map +1 -1
- package/dist/composables/useUrlQuery.d.ts.map +1 -1
- package/dist/composables/useUrlQuery.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.js +12 -0
- package/dist/config/index.js.map +1 -1
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +5 -0
- package/dist/events/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/plaintext.d.ts.map +1 -1
- package/dist/plaintext.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/plugins/postcss/mergeMediaQueries.d.ts.map +1 -1
- package/dist/plugins/postcss/mergeMediaQueries.js.map +1 -1
- package/dist/plugins/postcss/pruneVars.d.ts.map +1 -1
- package/dist/plugins/postcss/pruneVars.js.map +1 -1
- package/dist/plugins/postcss/quoteFontFamilies.d.ts.map +1 -1
- package/dist/plugins/postcss/quoteFontFamilies.js.map +1 -1
- package/dist/plugins/postcss/removeDeclarations.d.ts.map +1 -1
- package/dist/plugins/postcss/removeDeclarations.js.map +1 -1
- package/dist/plugins/postcss/resolveMaizzleImports.d.ts.map +1 -1
- package/dist/plugins/postcss/resolveMaizzleImports.js.map +1 -1
- package/dist/plugins/postcss/resolveProps.d.ts.map +1 -1
- package/dist/plugins/postcss/resolveProps.js +14 -0
- package/dist/plugins/postcss/resolveProps.js.map +1 -1
- package/dist/plugins/postcss/tailwindCleanup.d.ts.map +1 -1
- package/dist/plugins/postcss/tailwindCleanup.js.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/dist/prepare.js.map +1 -1
- package/dist/render/active.d.ts.map +1 -1
- package/dist/render/active.js.map +1 -1
- package/dist/render/createRenderer.d.ts.map +1 -1
- package/dist/render/createRenderer.js +91 -3
- package/dist/render/createRenderer.js.map +1 -1
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +6 -0
- package/dist/render/index.js.map +1 -1
- package/dist/render/injectFonts.js.map +1 -1
- package/dist/render/plugins/codeBlockExtract.d.ts.map +1 -1
- package/dist/render/plugins/codeBlockExtract.js +4 -0
- package/dist/render/plugins/codeBlockExtract.js.map +1 -1
- package/dist/render/plugins/markdownExtract.d.ts.map +1 -1
- package/dist/render/plugins/markdownExtract.js.map +1 -1
- package/dist/render/plugins/rawExtract.d.ts.map +1 -1
- package/dist/render/plugins/rawExtract.js.map +1 -1
- package/dist/render/plugins/rowSourceLocation.d.ts.map +1 -1
- package/dist/render/plugins/rowSourceLocation.js.map +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +48 -15
- package/dist/serve.js.map +1 -1
- package/dist/server/compatibility.d.ts.map +1 -1
- package/dist/server/compatibility.js +48 -0
- package/dist/server/compatibility.js.map +1 -1
- package/dist/server/email.js.map +1 -1
- package/dist/server/linter.js +6 -0
- package/dist/server/linter.js.map +1 -1
- package/dist/server/sfc-utils.d.ts.map +1 -1
- package/dist/server/sfc-utils.js.map +1 -1
- package/dist/server/ui/App.vue +17 -16
- package/dist/server/ui/components/Markdown.vue +17 -0
- package/dist/server/ui/components/SidebarClose.vue +1 -1
- package/dist/server/ui/components/ui/checkbox/Checkbox.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandInput.vue +2 -2
- package/dist/server/ui/components/ui/dialog/DialogContent.vue +1 -1
- package/dist/server/ui/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +1 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
- package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +1 -1
- package/dist/server/ui/lib/emulated-dark-mode.ts +25 -10
- package/dist/server/ui/pages/Home.vue +1 -1
- package/dist/server/ui/pages/Preview.vue +32 -18
- package/dist/tests/render/_helpers.js.map +1 -1
- package/dist/transformers/addAttributes.d.ts +18 -8
- package/dist/transformers/addAttributes.d.ts.map +1 -1
- package/dist/transformers/addAttributes.js +22 -8
- package/dist/transformers/addAttributes.js.map +1 -1
- package/dist/transformers/attributeToStyle.d.ts.map +1 -1
- package/dist/transformers/attributeToStyle.js.map +1 -1
- package/dist/transformers/base.d.ts.map +1 -1
- package/dist/transformers/base.js +4 -0
- package/dist/transformers/base.js.map +1 -1
- package/dist/transformers/columnWidth.d.ts.map +1 -1
- package/dist/transformers/columnWidth.js +136 -150
- package/dist/transformers/columnWidth.js.map +1 -1
- package/dist/transformers/entities.d.ts.map +1 -1
- package/dist/transformers/entities.js +1 -0
- package/dist/transformers/entities.js.map +1 -1
- package/dist/transformers/filters/defaults.d.ts.map +1 -1
- package/dist/transformers/filters/defaults.js.map +1 -1
- package/dist/transformers/filters/index.d.ts.map +1 -1
- package/dist/transformers/filters/index.js.map +1 -1
- package/dist/transformers/format.d.ts.map +1 -1
- package/dist/transformers/format.js.map +1 -1
- package/dist/transformers/index.d.ts.map +1 -1
- package/dist/transformers/index.js +33 -5
- package/dist/transformers/index.js.map +1 -1
- package/dist/transformers/inlineCss.d.ts.map +1 -1
- package/dist/transformers/inlineCss.js +27 -9
- package/dist/transformers/inlineCss.js.map +1 -1
- package/dist/transformers/inlineLink.d.ts.map +1 -1
- package/dist/transformers/inlineLink.js.map +1 -1
- package/dist/transformers/minify.d.ts.map +1 -1
- package/dist/transformers/minify.js.map +1 -1
- package/dist/transformers/minifyCodeInline.d.ts +29 -0
- package/dist/transformers/minifyCodeInline.d.ts.map +1 -0
- package/dist/transformers/minifyCodeInline.js +36 -0
- package/dist/transformers/minifyCodeInline.js.map +1 -0
- package/dist/transformers/msoPlaceholders.d.ts +10 -5
- package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
- package/dist/transformers/msoPlaceholders.js +38 -7
- package/dist/transformers/msoPlaceholders.js.map +1 -1
- package/dist/transformers/purgeCss.d.ts.map +1 -1
- package/dist/transformers/purgeCss.js +29 -3
- package/dist/transformers/purgeCss.js.map +1 -1
- package/dist/transformers/removeAttributes.d.ts.map +1 -1
- package/dist/transformers/removeAttributes.js.map +1 -1
- package/dist/transformers/replaceStrings.d.ts.map +1 -1
- package/dist/transformers/replaceStrings.js.map +1 -1
- package/dist/transformers/safeSelectors.d.ts +37 -0
- package/dist/transformers/safeSelectors.d.ts.map +1 -0
- package/dist/transformers/{safeClassNames.js → safeSelectors.js} +37 -6
- package/dist/transformers/safeSelectors.js.map +1 -0
- package/dist/transformers/shorthandCss.d.ts.map +1 -1
- package/dist/transformers/shorthandCss.js +38 -7
- package/dist/transformers/shorthandCss.js.map +1 -1
- package/dist/transformers/sixHex.d.ts.map +1 -1
- package/dist/transformers/sixHex.js.map +1 -1
- package/dist/transformers/tailwindComponent.js +9 -0
- package/dist/transformers/tailwindComponent.js.map +1 -1
- package/dist/transformers/tailwindcss.d.ts.map +1 -1
- package/dist/transformers/tailwindcss.js +22 -0
- package/dist/transformers/tailwindcss.js.map +1 -1
- package/dist/transformers/urlQuery.d.ts.map +1 -1
- package/dist/transformers/urlQuery.js.map +1 -1
- package/dist/types/config.d.ts +6 -10
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/ast/parser.d.ts.map +1 -1
- package/dist/utils/ast/parser.js.map +1 -1
- package/dist/utils/ast/serializer.d.ts.map +1 -1
- package/dist/utils/ast/serializer.js +27 -17
- package/dist/utils/ast/serializer.js.map +1 -1
- package/dist/utils/ast/walker.d.ts.map +1 -1
- package/dist/utils/ast/walker.js.map +1 -1
- package/dist/utils/compileTailwindCss.d.ts.map +1 -1
- package/dist/utils/compileTailwindCss.js.map +1 -1
- package/dist/utils/componentSources.d.ts.map +1 -1
- package/dist/utils/componentSources.js.map +1 -1
- package/dist/utils/cssBox.d.ts +42 -0
- package/dist/utils/cssBox.d.ts.map +1 -0
- package/dist/utils/cssBox.js +156 -0
- package/dist/utils/cssBox.js.map +1 -0
- package/dist/utils/decodeStyleEntities.d.ts.map +1 -1
- package/dist/utils/decodeStyleEntities.js.map +1 -1
- package/dist/utils/detect.d.ts.map +1 -1
- package/dist/utils/detect.js.map +1 -1
- package/dist/utils/output-markers.d.ts.map +1 -1
- package/dist/utils/output-markers.js.map +1 -1
- package/dist/utils/url.d.ts.map +1 -1
- package/dist/utils/url.js.map +1 -1
- package/dist/utils/watchPaths.js.map +1 -1
- package/node_modules/@clack/core/CHANGELOG.md +6 -0
- package/node_modules/@clack/core/dist/index.d.mts +1 -1
- package/node_modules/@clack/core/dist/index.mjs +8 -8
- package/node_modules/@clack/core/dist/index.mjs.map +1 -1
- package/node_modules/@clack/core/package.json +1 -1
- package/node_modules/@clack/prompts/CHANGELOG.md +13 -0
- package/node_modules/@clack/prompts/README.md +2 -2
- package/node_modules/@clack/prompts/dist/index.d.mts +98 -0
- package/node_modules/@clack/prompts/dist/index.mjs +122 -121
- package/node_modules/@clack/prompts/dist/index.mjs.map +1 -1
- package/node_modules/@clack/prompts/package.json +2 -2
- package/node_modules/fast-wrap-ansi/lib/main.js +0 -1
- package/node_modules/fast-wrap-ansi/package.json +10 -10
- package/node_modules/maizzle/dist/commands/make/config.mjs +7 -6
- package/node_modules/maizzle/dist/commands/new.mjs +15 -84
- package/node_modules/maizzle/package.json +2 -2
- package/node_modules/tinyexec/README.md +8 -0
- package/node_modules/tinyexec/dist/main.d.mts +16 -1
- package/node_modules/tinyexec/dist/main.mjs +163 -457
- package/node_modules/tinyexec/package.json +12 -14
- package/package.json +3 -4
- package/dist/transformers/safeClassNames.d.ts +0 -22
- package/dist/transformers/safeClassNames.d.ts.map +0 -1
- package/dist/transformers/safeClassNames.js.map +0 -1
- package/node_modules/fast-wrap-ansi/lib/main.js.map +0 -1
- package/node_modules/tinyexec/dist/LICENSES.txt +0 -83
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","names":[],"sources":["../src/build.ts"],"mappings":";;UAeiB,WAAA;EACf,KAAA;EACA,MAAA,EAAQ,
|
|
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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"}
|
package/dist/components/Body.vue
CHANGED
|
@@ -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 `<` (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 `<` after a round-trip, and the decoder couldn't
|
|
77
|
+
* tell which to decode back to a real `<` (structural) vs leave
|
|
78
|
+
* as `<` (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-
|
|
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
|
|
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="
|
|
25
|
+
<component :is="tag" v-bind="{ ...$attrs, class: mergedClass }">
|
|
26
26
|
<slot />
|
|
27
27
|
</component>
|
|
28
28
|
</template>
|
package/dist/components/Img.vue
CHANGED
|
@@ -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
|
|
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, '&')
|
|
221
|
+
.replace(/"/g, '"')
|
|
222
|
+
.replace(/</g, '<')
|
|
223
|
+
.replace(/>/g, '>')
|
|
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
|
|
245
|
+
const imgClass = 'max-w-full align-middle'
|
|
61
246
|
</script>
|
|
62
247
|
|
|
63
248
|
<template>
|
|
64
|
-
<
|
|
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" :
|
|
309
|
+
<img v-bind="attrs" :src="src" :alt="alt" :width="imgWidth" :class="imgClass">
|
|
68
310
|
</picture>
|
|
69
|
-
<
|
|
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>
|
package/dist/components/Link.vue
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
3
|
-
|
|
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:
|
|
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"
|
|
43
|
+
<div style="display: none">{{ text }}<template v-for="i in fillerCount" :key="i"> ͏ </template> </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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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>
|