@maizzle/framework
Version:
Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.
109 lines (95 loc) • 4.24 kB
text/typescript
export function normalizeToPixels(value: string | number): string {
if (typeof value === 'number' || Number.isFinite(Number(value))) {
return `${value}px`
}
return value
}
const counters: Record<string, number> = {}
/**
* Module-scoped sequential ID generator. Used by components to mint
* unique marker ids (e.g. `c1`, `c2`) for the post-render transformer.
*
* Must live here (not inside `<script setup>`) because Vue compiles
* `<script setup>` into the component's `setup()` function — any
* `let counter = 0` there resets per instance, causing id collisions.
*/
export function nextId(prefix: string): string {
counters[prefix] = (counters[prefix] ?? 0) + 1
return `${prefix}${counters[prefix]}`
}
export function hasWidthUtility(classStr: string): boolean {
return classStr.split(/\s+/).some((c) => {
const utility = c.split(':').pop() ?? ''
const clean = utility.replace(/^!/, '')
return /^(w-|max-w-|min-w-)/.test(clean)
})
}
export function hasWidthInStyle(styleStr: string): boolean {
return /(?:^|;\s*)(?:max-width|width)\s*:/i.test(styleStr)
}
export function hasHeightUtility(classStr: string): boolean {
return classStr.split(/\s+/).some((c) => {
const utility = c.split(':').pop() ?? ''
const clean = utility.replace(/^!/, '')
return /^(h-|max-h-|min-h-)/.test(clean)
})
}
export function hasHeightInStyle(styleStr: string): boolean {
return /(?:^|;\s*)(?:max-height|height)\s*:/i.test(styleStr)
}
/**
* Shared prop for components that emit MSO/VML fallback markup. The
* `null` default acts as the "unset" sentinel — `useOutlookFallback`
* treats `null` as inherit-from-ancestor (root default `true`),
* letting users override per-component without losing inheritance.
*/
export const outlookFallbackProp = {
type: Boolean,
default: null,
} as const
/**
* Default utility classes for a code-block `<pre>`. `whitespace-pre!` is
* forced important so Gmail's stylesheet can't reset it to `normal`, and
* `m-0` strips the browser's default `<pre>` margins so it isn't spaced
* away from its wrapper.
*/
export function codeBlockPreClass(bg: string): string {
return `font-mono bg-[${bg}] p-4 m-0 overflow-auto whitespace-pre! [word-wrap:normal] [word-break:normal] [word-spacing:normal]`
}
/**
* Build the email-safe table wrapper around highlighted code. Shared by the
* `<CodeBlock>` component and the Markdown fenced/indented code-block
* rules so both render identical markup: a full-width table whose
* cell carries the theme background, wrapping a `<pre>` styled
* with utility classes (not Shiki's raw inline styles).
*/
export function buildCodeBlock(
codeContent: string,
bg: string,
options: { preClass?: string; tdClass?: string; styleAttr?: string } = {},
): string {
const preClass = options.preClass ?? codeBlockPreClass(bg)
const tdClass = options.tdClass ?? `bg-[${bg}] max-w-0 mso-padding-alt-4`
const styleAttr = options.styleAttr ?? ''
// `data-juice-important` tells the CSS inliner to keep `!important` on this
// element's inlined declarations (e.g. `white-space: pre !important`), which
// it strips by default. Juice removes the attribute from the output.
return `<table class="w-full"><tr><td class="${tdClass}"><pre class="${preClass}"${styleAttr} data-juice-important><code>${codeContent}</code></pre></td></tr></table>`
}
/**
* Re-wrap a Shiki (or plain markdown-it) `<pre><code>` block as a CodeBlock,
* pulling the inner code and the theme background out of the highlighted
* HTML. Falls back to a white background for unhighlighted blocks.
*/
export function shikiToCodeBlock(highlighted: string): string {
const trimmed = highlighted.trim()
// Read the background only from the opening <pre> tag's style, never from
// the code body — otherwise a `background-color:` inside the snippet (e.g. a
// CSS example, or any unhighlighted block) would hijack the wrapper color.
const preTag = trimmed.match(/^<pre[^>]*>/)?.[0] ?? ''
const bg = preTag.match(/background-color:\s*(#[0-9a-fA-F]+)/)?.[1] ?? '#fff'
const codeContent = trimmed
.replace(/^<pre[^>]*><code[^>]*>/, '')
.replace(/<\/code><\/pre>$/, '')
return buildCodeBlock(codeContent, bg)
}