@maizzle/framework 6.0.0-rc.22 → 6.0.0-rc.24
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 +22 -2
- package/dist/build.js.map +1 -1
- package/dist/components/Heading.vue +1 -1
- package/dist/components/Img.vue +60 -10
- package/dist/components/Link.vue +1 -1
- package/dist/components/Markdown.vue +48 -16
- package/dist/components/Preheader.vue +4 -2
- package/dist/components/Tailwind.vue +4 -2
- package/dist/components/Text.vue +1 -1
- 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.js.map +1 -1
- package/dist/composables/renderContext.d.ts +1 -0
- package/dist/composables/renderContext.d.ts.map +1 -1
- package/dist/composables/renderContext.js +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 +7 -0
- package/dist/composables/useConfig.d.ts.map +1 -1
- package/dist/composables/useConfig.js +8 -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/useOutputPath.d.ts +17 -0
- package/dist/composables/useOutputPath.d.ts.map +1 -0
- package/dist/composables/useOutputPath.js +23 -0
- package/dist/composables/useOutputPath.js.map +1 -0
- package/dist/composables/usePlaintext.d.ts.map +1 -1
- package/dist/composables/usePlaintext.js.map +1 -1
- package/dist/composables/usePreheader.d.ts.map +1 -1
- package/dist/composables/usePreheader.js.map +1 -1
- package/dist/composables/useTransformers.d.ts.map +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 -1
- package/dist/index.js +2 -1
- 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 +1 -0
- package/dist/render/createRenderer.d.ts.map +1 -1
- package/dist/render/createRenderer.js +105 -1
- package/dist/render/createRenderer.js.map +1 -1
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +7 -1
- 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 +51 -18
- 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 +16 -16
- 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/main.css +25 -0
- package/dist/server/ui/pages/Home.vue +1 -1
- package/dist/server/ui/pages/Preview.vue +37 -19
- package/dist/tests/render/_helpers.js.map +1 -1
- package/dist/transformers/addAttributes.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/transformers/entities.d.ts.map +1 -1
- 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 +26 -0
- package/dist/transformers/index.js.map +1 -1
- package/dist/transformers/inlineCss.d.ts.map +1 -1
- package/dist/transformers/inlineCss.js +25 -2
- 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.map +1 -1
- package/dist/transformers/minifyCodeInline.js.map +1 -1
- package/dist/transformers/msoPlaceholders.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/transformers/safeSelectors.js +13 -1
- package/dist/transformers/safeSelectors.js.map +1 -1
- package/dist/transformers/shorthandCss.d.ts.map +1 -1
- 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 +4 -8
- 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.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.map +1 -1
- package/dist/utils/cssBox.js.map +1 -1
- 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/node_modules/fast-wrap-ansi/lib/main.js.map +0 -1
- package/node_modules/tinyexec/dist/LICENSES.txt +0 -83
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"msoPlaceholders.js","names":["el"],"sources":["../../src/transformers/msoPlaceholders.ts"],"sourcesContent":["import safeParser from 'postcss-safe-parser'\nimport { walk } from '../utils/ast/index.ts'\nimport { horizontalBorderPx } from '../utils/cssBox.ts'\nimport type { ChildNode, Element } from 'domhandler'\n\nconst RE_MAX_WIDTH = /(?:^|;\\s*)max-width:\\s*([^;]+)/i\nconst RE_WIDTH = /(?:^|;\\s*)width:\\s*([^;]+)/i\nconst RE_PERCENT = /^[\\d.]+%$/\n\nfunction resolveWidth(value: string): string | null {\n const trimmed = value.trim()\n if (RE_PERCENT.test(trimmed)) return trimmed\n const m = trimmed.match(/^([\\d.]+)(px|rem|em|pt)?$/i)\n if (!m) return null\n const n = parseFloat(m[1])\n switch ((m[2] || 'px').toLowerCase()) {\n case 'px': return `${Math.round(n)}px`\n case 'rem':\n case 'em': return `${Math.round(n * 16)}px`\n case 'pt': return `${Math.round(n * 1.333)}px`\n default: return null\n }\n}\n\n/**\n * Resolve all `__MAIZZLE_MSO*__` placeholders inside MSO conditional comments\n * by reading inlined style + `data-*` markers on the paired elements.\n *\n * Two placeholder families:\n *\n * MSOW (`__MAIZZLE_MSOW_{id}__`) — emitted by `<Container>` and `<Section>`.\n * Source element is marked with `data-maizzle-msow-id`. Reads inlined\n * `max-width:` (falls back to `width:`) and normalizes to px. Falls\n * back to `data-maizzle-msow-fallback` (default `600px`) when the\n * value can't be parsed.\n *\n * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>` and\n * `<Section>`'s MSO `<td>`. Source element is marked with\n * `data-maizzle-mso-td-id`. Extracts from the inlined style:\n * - `background-color` (always, when present) so Word paints the cell.\n * - `padding*` (only when no horizontal border on the element, since\n * Word drops div padding without a border and a copy would\n * double-pad with one).\n * Appends the `data-maizzle-mso-style` value (the user's `msoStyle`\n * prop) last so it wins on duplicates. Empty input resolves to ''\n * so the placeholder collapses cleanly.\n *\n * Single collect-walk + single substitute-walk: the same Container div\n * carries both marker kinds, so one element visit fills both maps.\n */\nexport function msoPlaceholders(dom: ChildNode[]): ChildNode[] {\n const widths = new Map<string, string>()\n const tdStyles = new Map<string, string>()\n\n walk(dom, (node) => {\n const el = node as Element\n const a = el.attribs\n if (!a) return\n\n const msowId = a['data-maizzle-msow-id']\n const tdId = a['data-maizzle-mso-td-id']\n if (!msowId && !tdId) return\n\n const style = a.style ?? ''\n\n if (msowId) {\n delete a['data-maizzle-msow-id']\n const fallback = a['data-maizzle-msow-fallback'] ?? '600px'\n delete a['data-maizzle-msow-fallback']\n const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1]\n const resolved = raw ? resolveWidth(raw) : null\n widths.set(msowId, resolved ?? fallback)\n }\n\n if (tdId) {\n delete a['data-maizzle-mso-td-id']\n const msoStyle = (a['data-maizzle-mso-style'] ?? '').trim().replace(/;\\s*$/, '')\n delete a['data-maizzle-mso-style']\n\n /**\n * Build the MSO td's inline style from three sources, in CSS priority order\n * (earlier = lower, later wins on dupes):\n *\n * 1. `background-color` (always, when present) — Word paints the cell\n * under any padding area or inline-block gap, not just the div.\n *\n * 2. `padding*` (hoisted only when no horizontal border) — Word drops\n * div padding without a stabilizing border, so the td has to\n * carry it. With a border, Word renders div padding and a td\n * copy would double-pad.\n *\n * 3. The user's `mso-style` prop — last so it overrides anything the\n * auto-hoist computed.\n */\n const parts: string[] = []\n if (style) {\n const root = safeParser(style)\n\n let bgDecl: string | undefined\n root.walkDecls('background-color', (d) => {\n bgDecl = `background-color: ${d.value}${d.important ? ' !important' : ''}`\n })\n if (bgDecl) parts.push(bgDecl)\n\n if (horizontalBorderPx(root) === 0) {\n root.walkDecls((d) => {\n if (/^padding(-|$)/.test(d.prop)) {\n parts.push(`${d.prop}: ${d.value}${d.important ? ' !important' : ''}`)\n }\n })\n }\n }\n if (msoStyle) parts.push(msoStyle)\n\n tdStyles.set(tdId, parts.length ? ` style=\"${parts.join('; ')}\"` : '')\n }\n })\n\n if (widths.size === 0 && tdStyles.size === 0) return dom\n\n walk(dom, (node) => {\n if (node.type !== 'comment') return\n let data = (node as any).data as string\n if (!data) return\n const hasMsow = widths.size > 0 && data.includes('__MAIZZLE_MSOW_')\n const hasTd = tdStyles.size > 0 && data.includes('__MAIZZLE_MSOTDSTYLE_')\n if (!hasMsow && !hasTd) return\n\n if (hasMsow) {\n for (const [id, val] of widths) {\n data = data.replaceAll(`__MAIZZLE_MSOW_${id}__`, val)\n }\n }\n if (hasTd) {\n for (const [id, val] of tdStyles) {\n data = data.replaceAll(`__MAIZZLE_MSOTDSTYLE_${id}__`, val)\n }\n }\n ;(node as any).data = data\n })\n\n return dom\n}\n"],"mappings":";;;;;AAKA,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,aAAa;AAEnB,SAAS,aAAa,OAA8B;CAClD,MAAM,UAAU,MAAM,
|
|
1
|
+
{"version":3,"file":"msoPlaceholders.js","names":["el"],"sources":["../../src/transformers/msoPlaceholders.ts"],"sourcesContent":["import safeParser from 'postcss-safe-parser'\nimport { walk } from '../utils/ast/index.ts'\nimport { horizontalBorderPx } from '../utils/cssBox.ts'\nimport type { ChildNode, Element } from 'domhandler'\n\nconst RE_MAX_WIDTH = /(?:^|;\\s*)max-width:\\s*([^;]+)/i\nconst RE_WIDTH = /(?:^|;\\s*)width:\\s*([^;]+)/i\nconst RE_PERCENT = /^[\\d.]+%$/\n\nfunction resolveWidth(value: string): string | null {\n const trimmed = value.trim()\n if (RE_PERCENT.test(trimmed)) return trimmed\n const m = trimmed.match(/^([\\d.]+)(px|rem|em|pt)?$/i)\n if (!m) return null\n const n = parseFloat(m[1])\n switch ((m[2] || 'px').toLowerCase()) {\n case 'px': return `${Math.round(n)}px`\n case 'rem':\n case 'em': return `${Math.round(n * 16)}px`\n case 'pt': return `${Math.round(n * 1.333)}px`\n default: return null\n }\n}\n\n/**\n * Resolve all `__MAIZZLE_MSO*__` placeholders inside MSO conditional comments\n * by reading inlined style + `data-*` markers on the paired elements.\n *\n * Two placeholder families:\n *\n * MSOW (`__MAIZZLE_MSOW_{id}__`) — emitted by `<Container>` and `<Section>`.\n * Source element is marked with `data-maizzle-msow-id`. Reads inlined\n * `max-width:` (falls back to `width:`) and normalizes to px. Falls\n * back to `data-maizzle-msow-fallback` (default `600px`) when the\n * value can't be parsed.\n *\n * MSOTDSTYLE (`__MAIZZLE_MSOTDSTYLE_{id}__`) — emitted by `<Container>` and\n * `<Section>`'s MSO `<td>`. Source element is marked with\n * `data-maizzle-mso-td-id`. Extracts from the inlined style:\n * - `background-color` (always, when present) so Word paints the cell.\n * - `padding*` (only when no horizontal border on the element, since\n * Word drops div padding without a border and a copy would\n * double-pad with one).\n * Appends the `data-maizzle-mso-style` value (the user's `msoStyle`\n * prop) last so it wins on duplicates. Empty input resolves to ''\n * so the placeholder collapses cleanly.\n *\n * Single collect-walk + single substitute-walk: the same Container div\n * carries both marker kinds, so one element visit fills both maps.\n */\nexport function msoPlaceholders(dom: ChildNode[]): ChildNode[] {\n const widths = new Map<string, string>()\n const tdStyles = new Map<string, string>()\n\n walk(dom, (node) => {\n const el = node as Element\n const a = el.attribs\n if (!a) return\n\n const msowId = a['data-maizzle-msow-id']\n const tdId = a['data-maizzle-mso-td-id']\n if (!msowId && !tdId) return\n\n const style = a.style ?? ''\n\n if (msowId) {\n delete a['data-maizzle-msow-id']\n const fallback = a['data-maizzle-msow-fallback'] ?? '600px'\n delete a['data-maizzle-msow-fallback']\n const raw = style.match(RE_MAX_WIDTH)?.[1] ?? style.match(RE_WIDTH)?.[1]\n const resolved = raw ? resolveWidth(raw) : null\n widths.set(msowId, resolved ?? fallback)\n }\n\n if (tdId) {\n delete a['data-maizzle-mso-td-id']\n const msoStyle = (a['data-maizzle-mso-style'] ?? '').trim().replace(/;\\s*$/, '')\n delete a['data-maizzle-mso-style']\n\n /**\n * Build the MSO td's inline style from three sources, in CSS priority order\n * (earlier = lower, later wins on dupes):\n *\n * 1. `background-color` (always, when present) — Word paints the cell\n * under any padding area or inline-block gap, not just the div.\n *\n * 2. `padding*` (hoisted only when no horizontal border) — Word drops\n * div padding without a stabilizing border, so the td has to\n * carry it. With a border, Word renders div padding and a td\n * copy would double-pad.\n *\n * 3. The user's `mso-style` prop — last so it overrides anything the\n * auto-hoist computed.\n */\n const parts: string[] = []\n if (style) {\n const root = safeParser(style)\n\n let bgDecl: string | undefined\n root.walkDecls('background-color', (d) => {\n bgDecl = `background-color: ${d.value}${d.important ? ' !important' : ''}`\n })\n if (bgDecl) parts.push(bgDecl)\n\n if (horizontalBorderPx(root) === 0) {\n root.walkDecls((d) => {\n if (/^padding(-|$)/.test(d.prop)) {\n parts.push(`${d.prop}: ${d.value}${d.important ? ' !important' : ''}`)\n }\n })\n }\n }\n if (msoStyle) parts.push(msoStyle)\n\n tdStyles.set(tdId, parts.length ? ` style=\"${parts.join('; ')}\"` : '')\n }\n })\n\n if (widths.size === 0 && tdStyles.size === 0) return dom\n\n walk(dom, (node) => {\n if (node.type !== 'comment') return\n let data = (node as any).data as string\n if (!data) return\n const hasMsow = widths.size > 0 && data.includes('__MAIZZLE_MSOW_')\n const hasTd = tdStyles.size > 0 && data.includes('__MAIZZLE_MSOTDSTYLE_')\n if (!hasMsow && !hasTd) return\n\n if (hasMsow) {\n for (const [id, val] of widths) {\n data = data.replaceAll(`__MAIZZLE_MSOW_${id}__`, val)\n }\n }\n if (hasTd) {\n for (const [id, val] of tdStyles) {\n data = data.replaceAll(`__MAIZZLE_MSOTDSTYLE_${id}__`, val)\n }\n }\n ;(node as any).data = data\n })\n\n return dom\n}\n"],"mappings":";;;;;AAKA,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,aAAa;AAEnB,SAAS,aAAa,OAA8B;CAClD,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,WAAW,KAAK,OAAO,GAAG,OAAO;CACrC,MAAM,IAAI,QAAQ,MAAM,4BAA4B;CACpD,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,IAAI,WAAW,EAAE,EAAE;CACzB,SAAS,EAAE,MAAM,MAAM,YAAY,GAAnC;EACE,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,CAAC,EAAE;EACnC,KAAK;EACL,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,IAAI,EAAE,EAAE;EACxC,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,IAAI,KAAK,EAAE;EAC3C,SAAS,OAAO;CAClB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,gBAAgB,KAA+B;CAC7D,MAAM,yBAAS,IAAI,IAAoB;CACvC,MAAM,2BAAW,IAAI,IAAoB;CAEzC,KAAK,MAAM,SAAS;EAElB,MAAM,IAAIA,KAAG;EACb,IAAI,CAAC,GAAG;EAER,MAAM,SAAS,EAAE;EACjB,MAAM,OAAO,EAAE;EACf,IAAI,CAAC,UAAU,CAAC,MAAM;EAEtB,MAAM,QAAQ,EAAE,SAAS;EAEzB,IAAI,QAAQ;GACV,OAAO,EAAE;GACT,MAAM,WAAW,EAAE,iCAAiC;GACpD,OAAO,EAAE;GACT,MAAM,MAAM,MAAM,MAAM,YAAY,IAAI,MAAM,MAAM,MAAM,QAAQ,IAAI;GACtE,MAAM,WAAW,MAAM,aAAa,GAAG,IAAI;GAC3C,OAAO,IAAI,QAAQ,YAAY,QAAQ;EACzC;EAEA,IAAI,MAAM;GACR,OAAO,EAAE;GACT,MAAM,YAAY,EAAE,6BAA6B,IAAI,KAAK,EAAE,QAAQ,SAAS,EAAE;GAC/E,OAAO,EAAE;;;;;;;;;;;;;;;;GAiBT,MAAM,QAAkB,CAAC;GACzB,IAAI,OAAO;IACT,MAAM,OAAO,WAAW,KAAK;IAE7B,IAAI;IACJ,KAAK,UAAU,qBAAqB,MAAM;KACxC,SAAS,qBAAqB,EAAE,QAAQ,EAAE,YAAY,gBAAgB;IACxE,CAAC;IACD,IAAI,QAAQ,MAAM,KAAK,MAAM;IAE7B,IAAI,mBAAmB,IAAI,MAAM,GAC/B,KAAK,WAAW,MAAM;KACpB,IAAI,gBAAgB,KAAK,EAAE,IAAI,GAC7B,MAAM,KAAK,GAAG,EAAE,KAAK,IAAI,EAAE,QAAQ,EAAE,YAAY,gBAAgB,IAAI;IAEzE,CAAC;GAEL;GACA,IAAI,UAAU,MAAM,KAAK,QAAQ;GAEjC,SAAS,IAAI,MAAM,MAAM,SAAS,WAAW,MAAM,KAAK,IAAI,EAAE,KAAK,EAAE;EACvE;CACF,CAAC;CAED,IAAI,OAAO,SAAS,KAAK,SAAS,SAAS,GAAG,OAAO;CAErD,KAAK,MAAM,SAAS;EAClB,IAAI,KAAK,SAAS,WAAW;EAC7B,IAAI,OAAQ,KAAa;EACzB,IAAI,CAAC,MAAM;EACX,MAAM,UAAU,OAAO,OAAO,KAAK,KAAK,SAAS,iBAAiB;EAClE,MAAM,QAAQ,SAAS,OAAO,KAAK,KAAK,SAAS,uBAAuB;EACxE,IAAI,CAAC,WAAW,CAAC,OAAO;EAExB,IAAI,SACF,KAAK,MAAM,CAAC,IAAI,QAAQ,QACtB,OAAO,KAAK,WAAW,kBAAkB,GAAG,KAAK,GAAG;EAGxD,IAAI,OACF,KAAK,MAAM,CAAC,IAAI,QAAQ,UACtB,OAAO,KAAK,WAAW,wBAAwB,GAAG,KAAK,GAAG;EAG7D,KAAc,OAAO;CACxB,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purgeCss.d.ts","names":[],"sources":["../../src/transformers/purgeCss.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"purgeCss.d.ts","names":[],"sources":["../../src/transformers/purgeCss.ts"],"mappings":";;;;;;AAsCA;UAAiB,eAAA,SAAwB,OAAA,CAAQ,IAAA,CAAK,IAAA;;;;;;EAMpD,QAAA;AAAA;;;;;AAAQ;AAqBV;;;;;;;;AAAoE;AASpE;;;;iBATgB,QAAA,CAAS,IAAA,UAAc,OAAA,GAAS,eAAoB;;;;;;iBASpD,WAAA,CAAY,GAAA,EAAK,SAAA,IAAa,OAAA,GAAS,eAAA,GAAuB,SAAA"}
|
|
@@ -22,6 +22,7 @@ const DEFAULT_SAFELIST = [
|
|
|
22
22
|
".mail-detail-content",
|
|
23
23
|
"*edo*",
|
|
24
24
|
"#*",
|
|
25
|
+
".\\&*",
|
|
25
26
|
".lang*"
|
|
26
27
|
];
|
|
27
28
|
const DEFAULT_OPTIONS = {
|
|
@@ -63,10 +64,21 @@ function purgeCss(html, options = {}) {
|
|
|
63
64
|
function purgeCssDom(dom, options = {}) {
|
|
64
65
|
const userSafelist = Array.isArray(options.safelist) ? options.safelist : [];
|
|
65
66
|
const { safelist: _discard, ...restUserOptions } = options;
|
|
67
|
+
/**
|
|
68
|
+
* Merge user options on top of defaults. defu merges objects deeply;
|
|
69
|
+
* for arrays it appends user values. We want the user safelist
|
|
70
|
+
* appended to the default safelist, so we build whitelist
|
|
71
|
+
* manually.
|
|
72
|
+
*/
|
|
66
73
|
const combOptions = defu$1({
|
|
67
74
|
...restUserOptions,
|
|
68
75
|
whitelist: [...DEFAULT_SAFELIST, ...userSafelist]
|
|
69
76
|
}, DEFAULT_OPTIONS);
|
|
77
|
+
/**
|
|
78
|
+
* Deep purge first: DOM-aware selector removal via PostCSS + css-select.
|
|
79
|
+
* Runs before email-comb so that email-comb can clean up orphaned
|
|
80
|
+
* classes in HTML attributes left behind by removed CSS rules.
|
|
81
|
+
*/
|
|
70
82
|
const safelist = [...DEFAULT_SAFELIST, ...userSafelist];
|
|
71
83
|
dom = deepPurge(dom, safelist);
|
|
72
84
|
/**
|
|
@@ -83,7 +95,7 @@ function purgeCssDom(dom, options = {}) {
|
|
|
83
95
|
walk(dom, (node) => {
|
|
84
96
|
const el = node;
|
|
85
97
|
if (el.name !== "style" || !el.attribs) return;
|
|
86
|
-
if (!("embed" in el.attribs) && !("data-embed" in el.attribs)) return;
|
|
98
|
+
if (!("embed" in el.attribs) && !("data-embed" in el.attribs) && !("amp-custom" in el.attribs)) return;
|
|
87
99
|
const textNode = el.children?.find((c) => c.type === "text");
|
|
88
100
|
if (!textNode?.data) return;
|
|
89
101
|
const token = `.maizzle-keep-${stash.length}`;
|
|
@@ -107,13 +119,17 @@ function purgeCssDom(dom, options = {}) {
|
|
|
107
119
|
if (stash.length) walk(purgedDom, (node) => {
|
|
108
120
|
const el = node;
|
|
109
121
|
if (el.name !== "style" || !el.attribs) return;
|
|
110
|
-
if (!("embed" in el.attribs) && !("data-embed" in el.attribs)) return;
|
|
122
|
+
if (!("embed" in el.attribs) && !("data-embed" in el.attribs) && !("amp-custom" in el.attribs)) return;
|
|
111
123
|
const textNode = el.children?.find((c) => c.type === "text");
|
|
112
124
|
if (!textNode?.data) return;
|
|
113
125
|
const trimmed = textNode.data.trim();
|
|
114
126
|
const match = stash.find((s) => trimmed === `${s.token}{}` || trimmed.startsWith(`${s.token}{`));
|
|
115
127
|
if (match) textNode.data = match.original;
|
|
116
128
|
});
|
|
129
|
+
/**
|
|
130
|
+
* Clean up data-embed/embed attributes — no longer needed after purging.
|
|
131
|
+
* `amp-custom` stays as-is (it's the user-authored AMP4Email attr).
|
|
132
|
+
*/
|
|
117
133
|
walk(purgedDom, (node) => {
|
|
118
134
|
const el = node;
|
|
119
135
|
if (el.name === "style" && el.attribs) {
|
|
@@ -140,15 +156,25 @@ function deepPurge(dom, safelist) {
|
|
|
140
156
|
walk(dom, (node) => {
|
|
141
157
|
const el = node;
|
|
142
158
|
if (el.name !== "style" || !el.attribs) return;
|
|
143
|
-
if ("data-embed" in el.attribs || "embed" in el.attribs) return;
|
|
159
|
+
if ("data-embed" in el.attribs || "embed" in el.attribs || "amp-custom" in el.attribs) return;
|
|
144
160
|
const textNode = el.children?.find((c) => c.type === "text");
|
|
145
161
|
if (!textNode?.data?.trim()) return;
|
|
146
162
|
const root = safeParser(textNode.data);
|
|
147
163
|
root.walkRules((rule) => {
|
|
164
|
+
/**
|
|
165
|
+
* Skip rules inside @media or other at-rules — those may target
|
|
166
|
+
* states we can't match statically (hover, responsive, etc.).
|
|
167
|
+
*/
|
|
148
168
|
if (rule.parent?.type === "atrule") return;
|
|
149
169
|
const selectors = rule.selectors ?? [rule.selector];
|
|
150
170
|
const matched = selectors.filter((sel) => {
|
|
151
171
|
if (isSafelisted(sel, safelist)) return true;
|
|
172
|
+
/**
|
|
173
|
+
* Skip pseudo-classes/elements that can't be matched statically.
|
|
174
|
+
* Functional pseudos like :not(), :is(), :where(), :has() are
|
|
175
|
+
* matchable by css-select, so we only skip dynamic/state
|
|
176
|
+
* ones.
|
|
177
|
+
*/
|
|
152
178
|
if (/::[\w-]/.test(sel)) return true;
|
|
153
179
|
if (/(?<!:):(?!not\b|is\b|where\b|has\b)[\w-]/.test(sel.replace(/\\./g, ""))) return true;
|
|
154
180
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purgeCss.js","names":["merge"],"sources":["../../src/transformers/purgeCss.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport safeParser from 'postcss-safe-parser'\nimport { selectAll } from 'css-select'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Opts as CombOptions } from 'email-comb'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\nconst DEFAULT_SAFELIST: string[] = [\n '*body*', // Gmail\n '.gmail*', // Gmail\n '.apple*', // Apple Mail\n '.ios*', // Mail on iOS\n '.ox-*', // Open-Xchange\n '.outlook*', // Outlook.com\n '[data-ogs*', // Outlook.com\n '.bloop_container', // Airmail\n '.Singleton', // Apple Mail 10\n '.unused', // Notes 8\n '.moz-text-html', // Thunderbird\n '.mail-detail-content', // Comcast, Libero webmail\n '*edo*', // Edison (all)\n '#*', // Freenet uses #msgBody\n '.lang*', // Fenced code blocks\n]\n\nconst DEFAULT_OPTIONS = {\n backend: [\n { heads: '{{', tails: '}}' },\n { heads: '{%', tails: '%}' },\n ],\n whitelist: [...DEFAULT_SAFELIST],\n}\n\n/**\n * Options for the `purgeCss` transformer.\n */\nexport interface PurgeCssOptions extends Partial<Omit<CombOptions, 'whitelist'>> {\n /**\n * Selectors to preserve regardless of whether they're matched in the\n * markup. Appended to Maizzle's built-in safelist (Gmail, Apple Mail,\n * Outlook.com hooks, etc). Mapped to email-comb's `whitelist` option.\n */\n safelist?: string[]\n}\n\n/**\n * Remove unused CSS from an HTML string.\n *\n * Uses `email-comb` together with a DOM-aware deep-purge step to strip\n * CSS selectors and class/id references that are not matched anywhere\n * in the document body.\n *\n * @param html HTML string to transform.\n * @param options Email-comb options plus a Maizzle `safelist`.\n * @returns The transformed HTML string.\n *\n * @example\n * import { purgeCss } from '@maizzle/framework'\n *\n * const out = purgeCss('<style>.a{}.b{}</style><p class=\"a\">x</p>', {\n * safelist: ['.keep'],\n * })\n */\nexport function purgeCss(html: string, options: PurgeCssOptions = {}): string {\n return serialize(purgeCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link purgeCss} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function purgeCssDom(dom: ChildNode[], options: PurgeCssOptions = {}): ChildNode[] {\n const userSafelist = Array.isArray(options.safelist) ? options.safelist : []\n\n const { safelist: _discard, ...restUserOptions } = options\n\n // Merge user options on top of defaults.\n // defu merges objects deeply; for arrays it appends user values.\n // We want the user safelist appended to the default safelist,\n // so we build whitelist manually.\n const combOptions = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n // Deep purge first: DOM-aware selector removal using PostCSS + css-select.\n // Runs before email-comb so that email-comb can clean up orphaned classes\n // in HTML attributes left behind by removed CSS rules.\n const safelist = [...DEFAULT_SAFELIST, ...userSafelist]\n dom = deepPurge(dom, safelist)\n\n /**\n * Shield embed style tags from email-comb. Comb has no skip option,\n * so it strips CSS comments and drops class refs it can't match\n * against visible CSS. Swap each embed tag's body for a unique\n * stub rule (`.maizzle-keep-N{}`) so comb keeps the tag, then\n * whitelist that stub plus every selector from the original\n * CSS so comb leaves matching refs alone elsewhere — and\n * finally restore the original CSS once comb has run.\n */\n const stash: { token: string; original: string; textNode: any }[] = []\n const extraWhitelist: string[] = []\n walk(dom, (node) => {\n const el = node as Element\n if (el.name !== 'style' || !el.attribs) return\n if (!('embed' in el.attribs) && !('data-embed' in el.attribs)) return\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data) return\n const idx = stash.length\n const token = `.maizzle-keep-${idx}`\n extraWhitelist.push(token)\n for (const m of textNode.data.matchAll(/(?<![\\w-])[.#][a-zA-Z_][\\w-]*/g)) {\n extraWhitelist.push(m[0])\n }\n stash.push({ token, original: textNode.data, textNode })\n textNode.data = `${token}{}`\n })\n\n if (extraWhitelist.length) {\n combOptions.whitelist = [...(combOptions.whitelist as string[] ?? []), ...extraWhitelist]\n }\n\n const { result } = comb(serialize(dom), combOptions)\n\n /**\n * Comb returns a fresh string, so we work off the post-parse tree:\n * find each embed style tag whose body still starts with the stub\n * token we planted earlier and swap the original CSS back in.\n */\n let purgedDom = parse(result)\n\n if (stash.length) {\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name !== 'style' || !el.attribs) return\n if (!('embed' in el.attribs) && !('data-embed' in el.attribs)) return\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data) return\n const trimmed = textNode.data.trim()\n const match = stash.find(s => trimmed === `${s.token}{}` || trimmed.startsWith(`${s.token}{`))\n if (match) textNode.data = match.original\n })\n }\n\n // Clean up data-embed/embed attributes — no longer needed after purging\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n delete el.attribs['data-embed']\n delete el.attribs.embed\n }\n })\n\n return purgedDom\n}\n\n/**\n * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,\n * then checks each selector against the DOM with css-select.\n * Removes rules where no selector matches any element.\n */\nfunction isSafelisted(selector: string, safelist: string[]): boolean {\n return safelist.some((pattern) => {\n if (pattern.startsWith('*') && pattern.endsWith('*')) {\n return selector.includes(pattern.slice(1, -1))\n }\n if (pattern.endsWith('*')) {\n return selector.startsWith(pattern.slice(0, -1))\n }\n if (pattern.startsWith('*')) {\n return selector.endsWith(pattern.slice(1))\n }\n return selector === pattern\n })\n}\n\nfunction deepPurge(dom: ChildNode[], safelist: string[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (el.name !== 'style' || !el.attribs) return\n if ('data-embed' in el.attribs || 'embed' in el.attribs) return\n\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data?.trim()) return\n\n const root = safeParser(textNode.data)\n\n root.walkRules((rule) => {\n // Skip rules inside @media or other at-rules — those may target\n // states we can't match statically (hover, responsive, etc.)\n if (rule.parent?.type === 'atrule') return\n\n const selectors = rule.selectors ?? [rule.selector]\n const matched = selectors.filter((sel) => {\n // Keep safelisted selectors\n if (isSafelisted(sel, safelist)) return true\n\n // Skip pseudo-classes/elements that can't be matched statically.\n // Functional pseudos like :not(), :is(), :where(), :has() are\n // matchable by css-select, so we only skip dynamic/state ones.\n if (/::[\\w-]/.test(sel)) return true\n if (/(?<!:):(?!not\\b|is\\b|where\\b|has\\b)[\\w-]/.test(sel.replace(/\\\\./g, ''))) return true\n\n try {\n return selectAll(sel, dom).length > 0\n } catch {\n // If css-select can't parse the selector, keep it\n return true\n }\n })\n\n if (matched.length === 0) {\n rule.remove()\n } else if (matched.length < selectors.length) {\n rule.selectors = matched\n }\n })\n\n // Remove empty at-rules\n root.walkAtRules((atRule) => {\n if (atRule.nodes?.length === 0) {\n atRule.remove()\n }\n })\n\n const purgedCss = root.toString()\n\n if (purgedCss.trim()) {\n textNode.data = purgedCss\n } else {\n // Remove the style tag entirely if empty\n const parent = el.parent\n if (parent && 'children' in parent) {\n const idx = parent.children.indexOf(el as any)\n if (idx !== -1) parent.children.splice(idx, 1)\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,mBAA6B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,MAAM,kBAAkB;CACtB,SAAS,CACP;EAAE,OAAO;EAAM,OAAO;EAAM,EAC5B;EAAE,OAAO;EAAM,OAAO;EAAM,CAC7B;CACD,WAAW,CAAC,GAAG,iBAAiB;CACjC;;;;;;;;;;;;;;;;;;;AAgCD,SAAgB,SAAS,MAAc,UAA2B,EAAE,EAAU;CAC5E,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,QAAQ,CAAC;;;;;;;AAQrD,SAAgB,YAAY,KAAkB,UAA2B,EAAE,EAAe;CACxF,MAAM,eAAe,MAAM,QAAQ,QAAQ,SAAS,GAAG,QAAQ,WAAW,EAAE;CAE5E,MAAM,EAAE,UAAU,UAAU,GAAG,oBAAoB;CAMnD,MAAM,cAAcA,OAClB;EAAE,GAAG;EAAiB,WAAW,CAAC,GAAG,kBAAkB,GAAG,aAAa;EAAE,EACzE,gBACD;CAKD,MAAM,WAAW,CAAC,GAAG,kBAAkB,GAAG,aAAa;CACvD,MAAM,UAAU,KAAK,SAAS;;;;;;;;;;CAW9B,MAAM,QAA8D,EAAE;CACtE,MAAM,iBAA2B,EAAE;CACnC,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,CAAC,GAAG,SAAS;EACxC,IAAI,EAAE,WAAW,GAAG,YAAY,EAAE,gBAAgB,GAAG,UAAU;EAC/D,MAAM,WAAW,GAAG,UAAU,MAAM,MAAW,EAAE,SAAS,OAAO;EACjE,IAAI,CAAC,UAAU,MAAM;EAErB,MAAM,QAAQ,iBADF,MAAM;EAElB,eAAe,KAAK,MAAM;EAC1B,KAAK,MAAM,KAAK,SAAS,KAAK,SAAS,iCAAiC,EACtE,eAAe,KAAK,EAAE,GAAG;EAE3B,MAAM,KAAK;GAAE;GAAO,UAAU,SAAS;GAAM;GAAU,CAAC;EACxD,SAAS,OAAO,GAAG,MAAM;GACzB;CAEF,IAAI,eAAe,QACjB,YAAY,YAAY,CAAC,GAAI,YAAY,aAAyB,EAAE,EAAG,GAAG,eAAe;CAG3F,MAAM,EAAE,WAAW,KAAK,UAAU,IAAI,EAAE,YAAY;;;;;;CAOpD,IAAI,YAAY,MAAM,OAAO;CAE7B,IAAI,MAAM,QACR,KAAK,YAAY,SAAS;EACxB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,CAAC,GAAG,SAAS;EACxC,IAAI,EAAE,WAAW,GAAG,YAAY,EAAE,gBAAgB,GAAG,UAAU;EAC/D,MAAM,WAAW,GAAG,UAAU,MAAM,MAAW,EAAE,SAAS,OAAO;EACjE,IAAI,CAAC,UAAU,MAAM;EACrB,MAAM,UAAU,SAAS,KAAK,MAAM;EACpC,MAAM,QAAQ,MAAM,MAAK,MAAK,YAAY,GAAG,EAAE,MAAM,OAAO,QAAQ,WAAW,GAAG,EAAE,MAAM,GAAG,CAAC;EAC9F,IAAI,OAAO,SAAS,OAAO,MAAM;GACjC;CAIJ,KAAK,YAAY,SAAS;EACxB,MAAM,KAAK;EACX,IAAI,GAAG,SAAS,WAAW,GAAG,SAAS;GACrC,OAAO,GAAG,QAAQ;GAClB,OAAO,GAAG,QAAQ;;GAEpB;CAEF,OAAO;;;;;;;AAQT,SAAS,aAAa,UAAkB,UAA6B;CACnE,OAAO,SAAS,MAAM,YAAY;EAChC,IAAI,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,EAClD,OAAO,SAAS,SAAS,QAAQ,MAAM,GAAG,GAAG,CAAC;EAEhD,IAAI,QAAQ,SAAS,IAAI,EACvB,OAAO,SAAS,WAAW,QAAQ,MAAM,GAAG,GAAG,CAAC;EAElD,IAAI,QAAQ,WAAW,IAAI,EACzB,OAAO,SAAS,SAAS,QAAQ,MAAM,EAAE,CAAC;EAE5C,OAAO,aAAa;GACpB;;AAGJ,SAAS,UAAU,KAAkB,UAAiC;CACpE,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EAEX,IAAI,GAAG,SAAS,WAAW,CAAC,GAAG,SAAS;EACxC,IAAI,gBAAgB,GAAG,WAAW,WAAW,GAAG,SAAS;EAEzD,MAAM,WAAW,GAAG,UAAU,MAAM,MAAW,EAAE,SAAS,OAAO;EACjE,IAAI,CAAC,UAAU,MAAM,MAAM,EAAE;EAE7B,MAAM,OAAO,WAAW,SAAS,KAAK;EAEtC,KAAK,WAAW,SAAS;GAGvB,IAAI,KAAK,QAAQ,SAAS,UAAU;GAEpC,MAAM,YAAY,KAAK,aAAa,CAAC,KAAK,SAAS;GACnD,MAAM,UAAU,UAAU,QAAQ,QAAQ;IAExC,IAAI,aAAa,KAAK,SAAS,EAAE,OAAO;IAKxC,IAAI,UAAU,KAAK,IAAI,EAAE,OAAO;IAChC,IAAI,2CAA2C,KAAK,IAAI,QAAQ,QAAQ,GAAG,CAAC,EAAE,OAAO;IAErF,IAAI;KACF,OAAO,UAAU,KAAK,IAAI,CAAC,SAAS;YAC9B;KAEN,OAAO;;KAET;GAEF,IAAI,QAAQ,WAAW,GACrB,KAAK,QAAQ;QACR,IAAI,QAAQ,SAAS,UAAU,QACpC,KAAK,YAAY;IAEnB;EAGF,KAAK,aAAa,WAAW;GAC3B,IAAI,OAAO,OAAO,WAAW,GAC3B,OAAO,QAAQ;IAEjB;EAEF,MAAM,YAAY,KAAK,UAAU;EAEjC,IAAI,UAAU,MAAM,EAClB,SAAS,OAAO;OACX;GAEL,MAAM,SAAS,GAAG;GAClB,IAAI,UAAU,cAAc,QAAQ;IAClC,MAAM,MAAM,OAAO,SAAS,QAAQ,GAAU;IAC9C,IAAI,QAAQ,IAAI,OAAO,SAAS,OAAO,KAAK,EAAE;;;GAGlD;CAEF,OAAO"}
|
|
1
|
+
{"version":3,"file":"purgeCss.js","names":["merge"],"sources":["../../src/transformers/purgeCss.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport safeParser from 'postcss-safe-parser'\nimport { selectAll } from 'css-select'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Opts as CombOptions } from 'email-comb'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\nconst DEFAULT_SAFELIST: string[] = [\n '*body*', // Gmail\n '.gmail*', // Gmail\n '.apple*', // Apple Mail\n '.ios*', // Mail on iOS\n '.ox-*', // Open-Xchange\n '.outlook*', // Outlook.com\n '[data-ogs*', // Outlook.com\n '.bloop_container', // Airmail\n '.Singleton', // Apple Mail 10\n '.unused', // Notes 8\n '.moz-text-html', // Thunderbird\n '.mail-detail-content', // Comcast, Libero webmail\n '*edo*', // Edison (all)\n '#*', // Freenet uses #msgBody\n '.\\\\&*', // Yahoo Mail wraps content in a class literally named \"&\"\n '.lang*', // Fenced code blocks\n]\n\nconst DEFAULT_OPTIONS = {\n backend: [\n { heads: '{{', tails: '}}' },\n { heads: '{%', tails: '%}' },\n ],\n whitelist: [...DEFAULT_SAFELIST],\n}\n\n/**\n * Options for the `purgeCss` transformer.\n */\nexport interface PurgeCssOptions extends Partial<Omit<CombOptions, 'whitelist'>> {\n /**\n * Selectors to preserve regardless of whether they're matched in the\n * markup. Appended to Maizzle's built-in safelist (Gmail, Apple Mail,\n * Outlook.com hooks, etc). Mapped to email-comb's `whitelist` option.\n */\n safelist?: string[]\n}\n\n/**\n * Remove unused CSS from an HTML string.\n *\n * Uses `email-comb` together with a DOM-aware deep-purge step to strip\n * CSS selectors and class/id references that are not matched anywhere\n * in the document body.\n *\n * @param html HTML string to transform.\n * @param options Email-comb options plus a Maizzle `safelist`.\n * @returns The transformed HTML string.\n *\n * @example\n * import { purgeCss } from '@maizzle/framework'\n *\n * const out = purgeCss('<style>.a{}.b{}</style><p class=\"a\">x</p>', {\n * safelist: ['.keep'],\n * })\n */\nexport function purgeCss(html: string, options: PurgeCssOptions = {}): string {\n return serialize(purgeCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link purgeCss} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function purgeCssDom(dom: ChildNode[], options: PurgeCssOptions = {}): ChildNode[] {\n const userSafelist = Array.isArray(options.safelist) ? options.safelist : []\n\n const { safelist: _discard, ...restUserOptions } = options\n\n /**\n * Merge user options on top of defaults. defu merges objects deeply;\n * for arrays it appends user values. We want the user safelist\n * appended to the default safelist, so we build whitelist\n * manually.\n */\n const combOptions = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n /**\n * Deep purge first: DOM-aware selector removal via PostCSS + css-select.\n * Runs before email-comb so that email-comb can clean up orphaned\n * classes in HTML attributes left behind by removed CSS rules.\n */\n const safelist = [...DEFAULT_SAFELIST, ...userSafelist]\n dom = deepPurge(dom, safelist)\n\n /**\n * Shield embed style tags from email-comb. Comb has no skip option,\n * so it strips CSS comments and drops class refs it can't match\n * against visible CSS. Swap each embed tag's body for a unique\n * stub rule (`.maizzle-keep-N{}`) so comb keeps the tag, then\n * whitelist that stub plus every selector from the original\n * CSS so comb leaves matching refs alone elsewhere — and\n * finally restore the original CSS once comb has run.\n */\n const stash: { token: string; original: string; textNode: any }[] = []\n const extraWhitelist: string[] = []\n walk(dom, (node) => {\n const el = node as Element\n if (el.name !== 'style' || !el.attribs) return\n if (!('embed' in el.attribs) && !('data-embed' in el.attribs) && !('amp-custom' in el.attribs)) return\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data) return\n const idx = stash.length\n const token = `.maizzle-keep-${idx}`\n extraWhitelist.push(token)\n for (const m of textNode.data.matchAll(/(?<![\\w-])[.#][a-zA-Z_][\\w-]*/g)) {\n extraWhitelist.push(m[0])\n }\n stash.push({ token, original: textNode.data, textNode })\n textNode.data = `${token}{}`\n })\n\n if (extraWhitelist.length) {\n combOptions.whitelist = [...(combOptions.whitelist as string[] ?? []), ...extraWhitelist]\n }\n\n const { result } = comb(serialize(dom), combOptions)\n\n /**\n * Comb returns a fresh string, so we work off the post-parse tree:\n * find each embed style tag whose body still starts with the stub\n * token we planted earlier and swap the original CSS back in.\n */\n let purgedDom = parse(result)\n\n if (stash.length) {\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name !== 'style' || !el.attribs) return\n if (!('embed' in el.attribs) && !('data-embed' in el.attribs) && !('amp-custom' in el.attribs)) return\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data) return\n const trimmed = textNode.data.trim()\n const match = stash.find(s => trimmed === `${s.token}{}` || trimmed.startsWith(`${s.token}{`))\n if (match) textNode.data = match.original\n })\n }\n\n /**\n * Clean up data-embed/embed attributes — no longer needed after purging.\n * `amp-custom` stays as-is (it's the user-authored AMP4Email attr).\n */\n walk(purgedDom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n delete el.attribs['data-embed']\n delete el.attribs.embed\n }\n })\n\n return purgedDom\n}\n\n/**\n * Deep purge: uses PostCSS to parse CSS in non-embedded style tags,\n * then checks each selector against the DOM with css-select.\n * Removes rules where no selector matches any element.\n */\nfunction isSafelisted(selector: string, safelist: string[]): boolean {\n return safelist.some((pattern) => {\n if (pattern.startsWith('*') && pattern.endsWith('*')) {\n return selector.includes(pattern.slice(1, -1))\n }\n if (pattern.endsWith('*')) {\n return selector.startsWith(pattern.slice(0, -1))\n }\n if (pattern.startsWith('*')) {\n return selector.endsWith(pattern.slice(1))\n }\n return selector === pattern\n })\n}\n\nfunction deepPurge(dom: ChildNode[], safelist: string[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (el.name !== 'style' || !el.attribs) return\n if ('data-embed' in el.attribs || 'embed' in el.attribs || 'amp-custom' in el.attribs) return\n\n const textNode = el.children?.find((c: any) => c.type === 'text') as any\n if (!textNode?.data?.trim()) return\n\n const root = safeParser(textNode.data)\n\n root.walkRules((rule) => {\n /**\n * Skip rules inside @media or other at-rules — those may target\n * states we can't match statically (hover, responsive, etc.).\n */\n if (rule.parent?.type === 'atrule') return\n\n const selectors = rule.selectors ?? [rule.selector]\n const matched = selectors.filter((sel) => {\n // Keep safelisted selectors\n if (isSafelisted(sel, safelist)) return true\n\n /**\n * Skip pseudo-classes/elements that can't be matched statically.\n * Functional pseudos like :not(), :is(), :where(), :has() are\n * matchable by css-select, so we only skip dynamic/state\n * ones.\n */\n if (/::[\\w-]/.test(sel)) return true\n if (/(?<!:):(?!not\\b|is\\b|where\\b|has\\b)[\\w-]/.test(sel.replace(/\\\\./g, ''))) return true\n\n try {\n return selectAll(sel, dom).length > 0\n } catch {\n // If css-select can't parse the selector, keep it\n return true\n }\n })\n\n if (matched.length === 0) {\n rule.remove()\n } else if (matched.length < selectors.length) {\n rule.selectors = matched\n }\n })\n\n // Remove empty at-rules\n root.walkAtRules((atRule) => {\n if (atRule.nodes?.length === 0) {\n atRule.remove()\n }\n })\n\n const purgedCss = root.toString()\n\n if (purgedCss.trim()) {\n textNode.data = purgedCss\n } else {\n // Remove the style tag entirely if empty\n const parent = el.parent\n if (parent && 'children' in parent) {\n const idx = parent.children.indexOf(el as any)\n if (idx !== -1) parent.children.splice(idx, 1)\n }\n }\n })\n\n return dom\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"removeAttributes.d.ts","names":[],"sources":["../../src/transformers/removeAttributes.ts"],"mappings":";;;;;AAMA;UAAiB,mBAAA;;EAEf,IAAA;EAAA
|
|
1
|
+
{"version":3,"file":"removeAttributes.d.ts","names":[],"sources":["../../src/transformers/removeAttributes.ts"],"mappings":";;;;;AAMA;UAAiB,mBAAA;;EAEf,IAAA;EAAA;;;;AAOuB;AAOzB;EAPE,KAAA,YAAiB,MAAM;AAAA;;AAOuC;AAwBhE;;KAxBY,qBAAA,YAAiC,mBAAmB;;;;;;AAwBuB;AASvF;;;;;;;;;;;;;;AAA0G;;iBAT1F,gBAAA,CAAiB,IAAA,UAAc,UAAA,GAAY,qBAAqB;;;;;;iBAShE,mBAAA,CAAoB,GAAA,EAAK,SAAA,IAAa,UAAA,GAAY,qBAAA,KAA+B,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"removeAttributes.js","names":[],"sources":["../../src/transformers/removeAttributes.ts"],"sourcesContent":["import type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Single attribute-removal rule.\n */\nexport interface RemoveAttributeRule {\n /** Attribute name to match. */\n name: string\n /**\n * Match condition for the attribute's value:\n * - `string` — remove when the value matches exactly.\n * - `RegExp` — remove when the value matches the regex.\n * - `boolean` / omitted — remove when the value is empty.\n */\n value?: string | RegExp | boolean\n}\n\n/**\n * Entry passed to {@link removeAttributes}. A bare string targets the named\n * attribute and removes it when its value is empty.\n */\nexport type RemoveAttributeOption = string | RemoveAttributeRule\n\n/**\n * Remove HTML attributes from elements.\n *\n * Empty `style` and `class` attributes are always stripped, regardless of\n * what you pass. Your entries are appended to those defaults.\n *\n * - `'data-src'` — remove when the value is empty.\n * - `{ name: 'id', value: 'test' }` — remove when the value matches exactly.\n * - `{ name: 'data-id', value: /\\d/ }` — remove when the value matches the regex.\n *\n * @param html HTML string to transform.\n * @param attributes Additional attribute-removal rules to apply on top of the defaults.\n * @returns The transformed HTML string.\n *\n * @example\n * import { removeAttributes } from '@maizzle/framework'\n *\n * const out = removeAttributes('<p style=\"\" data-x=\"\">x</p>', [\n * 'data-x',\n * { name: 'role', value: 'none' },\n * ])\n */\nexport function removeAttributes(html: string, attributes: RemoveAttributeOption[] = []): string {\n return serialize(removeAttributesDom(parse(html), attributes))\n}\n\n/**\n * DOM-form of {@link removeAttributes} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function removeAttributesDom(dom: ChildNode[], attributes: RemoveAttributeOption[] = []): ChildNode[] {\n // Empty style/class are always stripped; user entries are appended.\n const attributesToRemove: RemoveAttributeOption[] = ['style', 'class', ...attributes]\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.attribs) return\n\n for (const attr of attributesToRemove) {\n let attrName: string\n let attrValue: string | RegExp | boolean | undefined\n\n if (typeof attr === 'string') {\n attrName = attr\n attrValue = true // Remove when value is empty (boolean true or empty string)\n } else {\n attrName = attr.name\n attrValue = attr.value\n }\n\n const currentValue = el.attribs[attrName]\n\n // Skip if attribute doesn't exist\n if (currentValue === undefined) continue\n\n let shouldRemove = false\n\n if (typeof attrValue === 'boolean') {\n // Remove if value is empty (boolean true is treated as no-value attribute)\n shouldRemove = currentValue === '' || (currentValue as unknown) === true\n } else if (typeof attrValue === 'string') {\n // Remove if value matches exactly\n shouldRemove = currentValue === attrValue\n } else if (attrValue instanceof RegExp) {\n // Remove if value matches regex\n shouldRemove = attrValue.test(currentValue)\n } else {\n // Default: remove if empty\n shouldRemove = currentValue === ''\n }\n\n if (shouldRemove) {\n delete el.attribs[attrName]\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,iBAAiB,MAAc,aAAsC,
|
|
1
|
+
{"version":3,"file":"removeAttributes.js","names":[],"sources":["../../src/transformers/removeAttributes.ts"],"sourcesContent":["import type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Single attribute-removal rule.\n */\nexport interface RemoveAttributeRule {\n /** Attribute name to match. */\n name: string\n /**\n * Match condition for the attribute's value:\n * - `string` — remove when the value matches exactly.\n * - `RegExp` — remove when the value matches the regex.\n * - `boolean` / omitted — remove when the value is empty.\n */\n value?: string | RegExp | boolean\n}\n\n/**\n * Entry passed to {@link removeAttributes}. A bare string targets the named\n * attribute and removes it when its value is empty.\n */\nexport type RemoveAttributeOption = string | RemoveAttributeRule\n\n/**\n * Remove HTML attributes from elements.\n *\n * Empty `style` and `class` attributes are always stripped, regardless of\n * what you pass. Your entries are appended to those defaults.\n *\n * - `'data-src'` — remove when the value is empty.\n * - `{ name: 'id', value: 'test' }` — remove when the value matches exactly.\n * - `{ name: 'data-id', value: /\\d/ }` — remove when the value matches the regex.\n *\n * @param html HTML string to transform.\n * @param attributes Additional attribute-removal rules to apply on top of the defaults.\n * @returns The transformed HTML string.\n *\n * @example\n * import { removeAttributes } from '@maizzle/framework'\n *\n * const out = removeAttributes('<p style=\"\" data-x=\"\">x</p>', [\n * 'data-x',\n * { name: 'role', value: 'none' },\n * ])\n */\nexport function removeAttributes(html: string, attributes: RemoveAttributeOption[] = []): string {\n return serialize(removeAttributesDom(parse(html), attributes))\n}\n\n/**\n * DOM-form of {@link removeAttributes} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function removeAttributesDom(dom: ChildNode[], attributes: RemoveAttributeOption[] = []): ChildNode[] {\n // Empty style/class are always stripped; user entries are appended.\n const attributesToRemove: RemoveAttributeOption[] = ['style', 'class', ...attributes]\n\n walk(dom, (node) => {\n const el = node as Element\n if (!el.attribs) return\n\n for (const attr of attributesToRemove) {\n let attrName: string\n let attrValue: string | RegExp | boolean | undefined\n\n if (typeof attr === 'string') {\n attrName = attr\n attrValue = true // Remove when value is empty (boolean true or empty string)\n } else {\n attrName = attr.name\n attrValue = attr.value\n }\n\n const currentValue = el.attribs[attrName]\n\n // Skip if attribute doesn't exist\n if (currentValue === undefined) continue\n\n let shouldRemove = false\n\n if (typeof attrValue === 'boolean') {\n // Remove if value is empty (boolean true is treated as no-value attribute)\n shouldRemove = currentValue === '' || (currentValue as unknown) === true\n } else if (typeof attrValue === 'string') {\n // Remove if value matches exactly\n shouldRemove = currentValue === attrValue\n } else if (attrValue instanceof RegExp) {\n // Remove if value matches regex\n shouldRemove = attrValue.test(currentValue)\n } else {\n // Default: remove if empty\n shouldRemove = currentValue === ''\n }\n\n if (shouldRemove) {\n delete el.attribs[attrName]\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,iBAAiB,MAAc,aAAsC,CAAC,GAAW;CAC/F,OAAO,UAAU,oBAAoB,MAAM,IAAI,GAAG,UAAU,CAAC;AAC/D;;;;;;AAOA,SAAgB,oBAAoB,KAAkB,aAAsC,CAAC,GAAgB;CAE3G,MAAM,qBAA8C;EAAC;EAAS;EAAS,GAAG;CAAU;CAEpF,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EACX,IAAI,CAAC,GAAG,SAAS;EAEjB,KAAK,MAAM,QAAQ,oBAAoB;GACrC,IAAI;GACJ,IAAI;GAEJ,IAAI,OAAO,SAAS,UAAU;IAC5B,WAAW;IACX,YAAY;GACd,OAAO;IACL,WAAW,KAAK;IAChB,YAAY,KAAK;GACnB;GAEA,MAAM,eAAe,GAAG,QAAQ;GAGhC,IAAI,iBAAiB,KAAA,GAAW;GAEhC,IAAI,eAAe;GAEnB,IAAI,OAAO,cAAc,WAEvB,eAAe,iBAAiB,MAAO,iBAA6B;QAC/D,IAAI,OAAO,cAAc,UAE9B,eAAe,iBAAiB;QAC3B,IAAI,qBAAqB,QAE9B,eAAe,UAAU,KAAK,YAAY;QAG1C,eAAe,iBAAiB;GAGlC,IAAI,cACF,OAAO,GAAG,QAAQ;EAEtB;CACF,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replaceStrings.d.ts","names":[],"sources":["../../src/transformers/replaceStrings.ts"],"mappings":";;;;;AAWA;;;;;;;iBAAgB,cAAA,CAAe,IAAA,UAAc,MAAA,GAAQ,
|
|
1
|
+
{"version":3,"file":"replaceStrings.d.ts","names":[],"sources":["../../src/transformers/replaceStrings.ts"],"mappings":";;;;;AAWA;;;;;;;iBAAgB,cAAA,CAAe,IAAA,UAAc,MAAA,GAAQ,aAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replaceStrings.js","names":[],"sources":["../../src/transformers/replaceStrings.ts"],"sourcesContent":["import type { MaizzleConfig } from '../types/config.ts'\n\n/**\n * Replace strings transformer.\n *\n * Replaces strings in the HTML using the key-value pairs defined in\n * `config.replaceStrings`. Each key is treated as a regular expression\n * pattern (case-insensitive, global), and the value is the replacement.\n *\n * Character classes must be escaped in keys, e.g. `\\\\s` for `\\s`.\n */\nexport function replaceStrings(html: string, config: MaizzleConfig = {}): string {\n const replacements = config.replaceStrings\n\n if (!replacements || Object.keys(replacements).length === 0) return html\n\n return Object.entries(replacements).reduce(\n (result, [pattern, replacement]) => result.replace(new RegExp(pattern, 'gi'), replacement),\n html,\n )\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAgB,eAAe,MAAc,SAAwB,
|
|
1
|
+
{"version":3,"file":"replaceStrings.js","names":[],"sources":["../../src/transformers/replaceStrings.ts"],"sourcesContent":["import type { MaizzleConfig } from '../types/config.ts'\n\n/**\n * Replace strings transformer.\n *\n * Replaces strings in the HTML using the key-value pairs defined in\n * `config.replaceStrings`. Each key is treated as a regular expression\n * pattern (case-insensitive, global), and the value is the replacement.\n *\n * Character classes must be escaped in keys, e.g. `\\\\s` for `\\s`.\n */\nexport function replaceStrings(html: string, config: MaizzleConfig = {}): string {\n const replacements = config.replaceStrings\n\n if (!replacements || Object.keys(replacements).length === 0) return html\n\n return Object.entries(replacements).reduce(\n (result, [pattern, replacement]) => result.replace(new RegExp(pattern, 'gi'), replacement),\n html,\n )\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAgB,eAAe,MAAc,SAAwB,CAAC,GAAW;CAC/E,MAAM,eAAe,OAAO;CAE5B,IAAI,CAAC,gBAAgB,OAAO,KAAK,YAAY,EAAE,WAAW,GAAG,OAAO;CAEpE,OAAO,OAAO,QAAQ,YAAY,EAAE,QACjC,QAAQ,CAAC,SAAS,iBAAiB,OAAO,QAAQ,IAAI,OAAO,SAAS,IAAI,GAAG,WAAW,GACzF,IACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safeSelectors.d.ts","names":[],"sources":["../../src/transformers/safeSelectors.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"safeSelectors.d.ts","names":[],"sources":["../../src/transformers/safeSelectors.ts"],"mappings":";;;;;;AA2HA;;;;;;;;AAAkE;AASlE;;;;;;;;;;;;iBATgB,aAAA,CAAc,IAAA,UAAc,MAAA,GAAQ,SAAc;;AASmB;;;;iBAArE,gBAAA,CAAiB,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
|
|
@@ -51,7 +51,19 @@ function processCssSelectors(css, replacements) {
|
|
|
51
51
|
const selectorRegex = new RegExp(`\\\\(${Object.keys(replacements).map(escapeForRegex).join("|")})`, "g");
|
|
52
52
|
return postcss([(root) => {
|
|
53
53
|
root.walkRules((rule) => {
|
|
54
|
-
rule.selector = rule.selector.replace(selectorRegex, (
|
|
54
|
+
rule.selector = rule.selector.replace(selectorRegex, (matched, char, offset, str) => {
|
|
55
|
+
/**
|
|
56
|
+
* Yahoo Mail wraps content in a class literally named `&`, so
|
|
57
|
+
* the selector `.\&` must be preserved. Detect it as a
|
|
58
|
+
* `\&` that follows a `.` and ends the class atom
|
|
59
|
+
* (space, combinator, comma, `{`, or end-of-str).
|
|
60
|
+
*/
|
|
61
|
+
if (char === "&" && str[offset - 1] === ".") {
|
|
62
|
+
const next = str[offset + 2];
|
|
63
|
+
if (next === void 0 || /[\s,{>~+)]/.test(next)) return matched;
|
|
64
|
+
}
|
|
65
|
+
return replacements[char] ?? matched;
|
|
66
|
+
}).replaceAll("\\2c ", "_");
|
|
55
67
|
});
|
|
56
68
|
}]).process(css, { parser: safeParser }).css;
|
|
57
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safeSelectors.js","names":[],"sources":["../../src/transformers/safeSelectors.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\nimport type { CssConfig } from '../types/config.ts'\n\nconst DEFAULT_REPLACEMENTS: Record<string, string> = {\n ':': '-',\n '/': '-',\n '%': 'pc',\n '.': '_',\n ',': '_',\n '#': '_',\n '[': '',\n ']': '',\n '(': '',\n ')': '',\n '{': '',\n '}': '',\n '!': '-i',\n '&': 'and-',\n '<': 'lt-',\n '=': 'eq-',\n '>': 'gt-',\n '|': 'or-',\n '@': 'at-',\n '?': 'q-',\n '\\\\': '-',\n '\"': '-',\n \"'\": '-',\n '*': '-',\n '+': '-',\n ';': '-',\n '^': '-',\n '`': '-',\n '~': '-',\n '$': '-',\n}\n\nfunction escapeForRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Replace escaped special characters in CSS selectors.\n *\n * Tailwind generates selectors like `.sm\\:text-base`. This function\n * replaces the `\\:` with `-` (or whatever the configured replacement is)\n * so the selector becomes `.sm-text-base`, which is safe for email clients.\n */\nfunction processCssSelectors(css: string, replacements: Record<string, string>): string {\n // Matches \\<char> in CSS selectors — e.g. \\: \\/ \\. \\[ etc.\n const selectorRegex = new RegExp(\n `\\\\\\\\(${Object.keys(replacements).map(escapeForRegex).join('|')})`,\n 'g',\n )\n\n return postcss([\n (root: postcss.Root) => {\n root.walkRules((rule: postcss.Rule) => {\n rule.selector = rule.selector\n .replace(selectorRegex, (
|
|
1
|
+
{"version":3,"file":"safeSelectors.js","names":[],"sources":["../../src/transformers/safeSelectors.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\nimport type { CssConfig } from '../types/config.ts'\n\nconst DEFAULT_REPLACEMENTS: Record<string, string> = {\n ':': '-',\n '/': '-',\n '%': 'pc',\n '.': '_',\n ',': '_',\n '#': '_',\n '[': '',\n ']': '',\n '(': '',\n ')': '',\n '{': '',\n '}': '',\n '!': '-i',\n '&': 'and-',\n '<': 'lt-',\n '=': 'eq-',\n '>': 'gt-',\n '|': 'or-',\n '@': 'at-',\n '?': 'q-',\n '\\\\': '-',\n '\"': '-',\n \"'\": '-',\n '*': '-',\n '+': '-',\n ';': '-',\n '^': '-',\n '`': '-',\n '~': '-',\n '$': '-',\n}\n\nfunction escapeForRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Replace escaped special characters in CSS selectors.\n *\n * Tailwind generates selectors like `.sm\\:text-base`. This function\n * replaces the `\\:` with `-` (or whatever the configured replacement is)\n * so the selector becomes `.sm-text-base`, which is safe for email clients.\n */\nfunction processCssSelectors(css: string, replacements: Record<string, string>): string {\n // Matches \\<char> in CSS selectors — e.g. \\: \\/ \\. \\[ etc.\n const selectorRegex = new RegExp(\n `\\\\\\\\(${Object.keys(replacements).map(escapeForRegex).join('|')})`,\n 'g',\n )\n\n return postcss([\n (root: postcss.Root) => {\n root.walkRules((rule: postcss.Rule) => {\n rule.selector = rule.selector\n .replace(selectorRegex, (matched, char, offset, str) => {\n /**\n * Yahoo Mail wraps content in a class literally named `&`, so\n * the selector `.\\&` must be preserved. Detect it as a\n * `\\&` that follows a `.` and ends the class atom\n * (space, combinator, comma, `{`, or end-of-str).\n */\n if (char === '&' && str[offset - 1] === '.') {\n const next = str[offset + 2]\n if (next === undefined || /[\\s,{>~+)]/.test(next)) return matched\n }\n return replacements[char] ?? matched\n })\n // Handle CSS unicode escape for comma (\\2c → _)\n .replaceAll('\\\\2c ', '_')\n })\n },\n ]).process(css, { parser: safeParser }).css\n}\n\n/**\n * Replace unsafe special characters in a class attribute value.\n *\n * Splits on whitespace and replaces each char from the replacements map\n * in each class token individually.\n */\nfunction processClassAttr(classStr: string, replacements: Record<string, string>): string {\n return classStr\n .split(/\\s+/)\n .filter(Boolean)\n .map((cls) => {\n for (const [from, to] of Object.entries(replacements)) {\n cls = cls.split(from).join(to)\n }\n return cls\n })\n .join(' ')\n}\n\n/**\n * Safe selectors transformer.\n *\n * Replaces unsafe characters (`:`, `/`, `[`, `]`, etc.) in:\n * - CSS selectors inside `<style>` tags\n * - HTML `class` attributes\n *\n * This makes Tailwind utility classes like `sm:text-base` safe for\n * email clients that cannot handle escaped characters in class names.\n *\n * Enabled by default. Disable by setting `css.safe` to `false`.\n * Customize replacements by passing a `Record<string, string>` — user\n * values are merged on top of the defaults.\n *\n * @param html HTML string to transform.\n * @param config CSS config (see {@link CssConfig}).\n * @returns The transformed HTML string.\n *\n * @example\n * import { safeSelectors } from '@maizzle/framework'\n *\n * const out = safeSelectors('<div class=\"sm:text-base\"></div>')\n */\nexport function safeSelectors(html: string, config: CssConfig = {}): string {\n return serialize(safeSelectorsDom(parse(html), config))\n}\n\n/**\n * DOM-form of {@link safeSelectors} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function safeSelectorsDom(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.safe ?? true\n\n if (!option) return dom\n\n const replacements: Record<string, string> =\n option && typeof option === 'object'\n ? { ...DEFAULT_REPLACEMENTS, ...option }\n : DEFAULT_REPLACEMENTS\n\n walk(dom, (node) => {\n const el = node as Element\n\n // Process CSS selectors inside <style> tags\n if (el.name === 'style' && el.children?.length) {\n const text = el.children.find((c) => c.type === 'text') as any\n if (text?.data?.trim()) {\n text.data = processCssSelectors(text.data, replacements)\n }\n }\n\n // Replace special chars in class attributes\n if ('attribs' in el && el.attribs?.class) {\n el.attribs.class = processClassAttr(el.attribs.class, replacements)\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;;AAMA,MAAM,uBAA+C;CACnD,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CACN,MAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;AAEA,SAAS,eAAe,GAAmB;CACzC,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;AASA,SAAS,oBAAoB,KAAa,cAA8C;CAEtF,MAAM,gBAAgB,IAAI,OACxB,QAAQ,OAAO,KAAK,YAAY,EAAE,IAAI,cAAc,EAAE,KAAK,GAAG,EAAE,IAChE,GACF;CAEA,OAAO,QAAQ,EACZ,SAAuB;EACtB,KAAK,WAAW,SAAuB;GACrC,KAAK,WAAW,KAAK,SAClB,QAAQ,gBAAgB,SAAS,MAAM,QAAQ,QAAQ;;;;;;;IAOtD,IAAI,SAAS,OAAO,IAAI,SAAS,OAAO,KAAK;KAC3C,MAAM,OAAO,IAAI,SAAS;KAC1B,IAAI,SAAS,KAAA,KAAa,aAAa,KAAK,IAAI,GAAG,OAAO;IAC5D;IACA,OAAO,aAAa,SAAS;GAC/B,CAAC,EAEA,WAAW,SAAS,GAAG;EAC5B,CAAC;CACH,CACF,CAAC,EAAE,QAAQ,KAAK,EAAE,QAAQ,WAAW,CAAC,EAAE;AAC1C;;;;;;;AAQA,SAAS,iBAAiB,UAAkB,cAA8C;CACxF,OAAO,SACJ,MAAM,KAAK,EACX,OAAO,OAAO,EACd,KAAK,QAAQ;EACZ,KAAK,MAAM,CAAC,MAAM,OAAO,OAAO,QAAQ,YAAY,GAClD,MAAM,IAAI,MAAM,IAAI,EAAE,KAAK,EAAE;EAE/B,OAAO;CACT,CAAC,EACA,KAAK,GAAG;AACb;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,cAAc,MAAc,SAAoB,CAAC,GAAW;CAC1E,OAAO,UAAU,iBAAiB,MAAM,IAAI,GAAG,MAAM,CAAC;AACxD;;;;;;AAOA,SAAgB,iBAAiB,KAAkB,SAAoB,CAAC,GAAgB;CACtF,MAAM,SAAS,OAAO,QAAQ;CAE9B,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,eACJ,UAAU,OAAO,WAAW,WACxB;EAAE,GAAG;EAAsB,GAAG;CAAO,IACrC;CAEN,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EAGX,IAAI,GAAG,SAAS,WAAW,GAAG,UAAU,QAAQ;GAC9C,MAAM,OAAO,GAAG,SAAS,MAAM,MAAM,EAAE,SAAS,MAAM;GACtD,IAAI,MAAM,MAAM,KAAK,GACnB,KAAK,OAAO,oBAAoB,KAAK,MAAM,YAAY;EAE3D;EAGA,IAAI,aAAa,MAAM,GAAG,SAAS,OACjC,GAAG,QAAQ,QAAQ,iBAAiB,GAAG,QAAQ,OAAO,YAAY;CAEtE,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shorthandCss.d.ts","names":[],"sources":["../../src/transformers/shorthandCss.ts"],"mappings":";;;;;AASA;UAAiB,mBAAA
|
|
1
|
+
{"version":3,"file":"shorthandCss.d.ts","names":[],"sources":["../../src/transformers/shorthandCss.ts"],"mappings":";;;;;AASA;UAAiB,mBAAA;;;AAOX;AAyBN;;;EAzBE,IAAI;AAAA;;;;AAyBsE;AAS5E;;;;;;;;;;;;;;AAA+F;;;;iBAT/E,YAAA,CAAa,IAAA,UAAc,OAAA,GAAS,mBAAwB;;;;;;iBAS5D,eAAA,CAAgB,GAAA,EAAK,SAAA,IAAa,OAAA,GAAS,mBAAA,GAA2B,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shorthandCss.js","names":[],"sources":["../../src/transformers/shorthandCss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport mergeLonghand from 'postcss-merge-longhand'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Options for the `shorthandCss` transformer.\n */\nexport interface ShorthandCssOptions {\n /**\n * Restrict the transform to a list of HTML tag names. Omit to apply to\n * every element with a `style` attribute.\n *\n * @example ['td', 'div']\n */\n tags?: string[]\n}\n\n/**\n * Rewrite longhand CSS inside inline `style` attributes with shorthand\n * syntax. Works with margin, padding, and border when all sides are\n * specified.\n *\n * For example:\n * `margin-left: 2px; margin-right: 2px; margin-top: 4px; margin-bottom: 4px`\n * becomes:\n * `margin: 4px 2px`\n *\n * @param html HTML string to transform.\n * @param options Optional Maizzle options (`tags`).\n * @returns The transformed HTML string.\n *\n * @example\n * import { shorthandCss } from '@maizzle/framework'\n *\n * const out = shorthandCss(\n * '<p style=\"margin-top: 4px; margin-right: 2px; margin-bottom: 4px; margin-left: 2px;\">x</p>',\n * { tags: ['p'] },\n * )\n */\nexport function shorthandCss(html: string, options: ShorthandCssOptions = {}): string {\n return serialize(shorthandCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link shorthandCss} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function shorthandCssDom(dom: ChildNode[], options: ShorthandCssOptions = {}): ChildNode[] {\n const allowedTags = options.tags ?? []\n const hasTagFilter = allowedTags.length > 0\n\n /**\n * Merge longhand within a single inline-style value. Returns the merged\n * string when shorter, otherwise the original. Wraps the value in a\n * dummy selector since postcss-merge-longhand operates on rules.\n */\n const mergeStyleValue = (styleValue: string): string => {\n try {\n const { css } = postcss()\n .use(mergeLonghand)\n .process(`div { ${styleValue} }`, { parser: safeParser })\n const match = css.match(/div\\s*\\{\\s*([^}]+)\\s*\\}/)\n if (match && match[1]) {\n const merged = match[1].trim()\n if (merged !== styleValue) return merged\n }\n }\n catch {}\n return styleValue\n }\n\n walk(dom, (node) => {\n /**\n * MSO conditional comments carry their own inline-style attributes\n * (e.g. `<!--[if mso]><td style=\"…\"><![endif]-->`) as opaque text.\n * The element walker can't see them, so without this branch the td/\n * v:rect styles inside comments stay longhand even when the visible\n * div has already been merged. Match each `style=\"…\"` substring,\n * run it through mergeLonghand, splice back.\n *\n * Tag filter intentionally bypassed: the user can't address MSO td\n * elements (they don't parse as elements), and these comments\n * always wrap email-layout primitives anyway.\n */\n if (node.type === 'comment') {\n const data = (node as any).data as string\n if (!data || !data.includes('style=\"')) return\n const newData = data.replace(/style=\"([^\"]*)\"/g, (full, value) => {\n const merged = mergeStyleValue(value)\n return merged === value ? full : `style=\"${merged}\"`\n })\n if (newData !== data) (node as any).data = newData\n return\n }\n\n const el = node as Element\n\n if (!el.attribs?.style) return\n if (hasTagFilter && !allowedTags.includes(el.name)) return\n\n const merged = mergeStyleValue(el.attribs.style)\n if (merged !== el.attribs.style) el.attribs.style = merged\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,aAAa,MAAc,UAA+B,
|
|
1
|
+
{"version":3,"file":"shorthandCss.js","names":[],"sources":["../../src/transformers/shorthandCss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport mergeLonghand from 'postcss-merge-longhand'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\n/**\n * Options for the `shorthandCss` transformer.\n */\nexport interface ShorthandCssOptions {\n /**\n * Restrict the transform to a list of HTML tag names. Omit to apply to\n * every element with a `style` attribute.\n *\n * @example ['td', 'div']\n */\n tags?: string[]\n}\n\n/**\n * Rewrite longhand CSS inside inline `style` attributes with shorthand\n * syntax. Works with margin, padding, and border when all sides are\n * specified.\n *\n * For example:\n * `margin-left: 2px; margin-right: 2px; margin-top: 4px; margin-bottom: 4px`\n * becomes:\n * `margin: 4px 2px`\n *\n * @param html HTML string to transform.\n * @param options Optional Maizzle options (`tags`).\n * @returns The transformed HTML string.\n *\n * @example\n * import { shorthandCss } from '@maizzle/framework'\n *\n * const out = shorthandCss(\n * '<p style=\"margin-top: 4px; margin-right: 2px; margin-bottom: 4px; margin-left: 2px;\">x</p>',\n * { tags: ['p'] },\n * )\n */\nexport function shorthandCss(html: string, options: ShorthandCssOptions = {}): string {\n return serialize(shorthandCssDom(parse(html), options))\n}\n\n/**\n * DOM-form of {@link shorthandCss} used by the internal transformer\n * pipeline. Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function shorthandCssDom(dom: ChildNode[], options: ShorthandCssOptions = {}): ChildNode[] {\n const allowedTags = options.tags ?? []\n const hasTagFilter = allowedTags.length > 0\n\n /**\n * Merge longhand within a single inline-style value. Returns the merged\n * string when shorter, otherwise the original. Wraps the value in a\n * dummy selector since postcss-merge-longhand operates on rules.\n */\n const mergeStyleValue = (styleValue: string): string => {\n try {\n const { css } = postcss()\n .use(mergeLonghand)\n .process(`div { ${styleValue} }`, { parser: safeParser })\n const match = css.match(/div\\s*\\{\\s*([^}]+)\\s*\\}/)\n if (match && match[1]) {\n const merged = match[1].trim()\n if (merged !== styleValue) return merged\n }\n }\n catch {}\n return styleValue\n }\n\n walk(dom, (node) => {\n /**\n * MSO conditional comments carry their own inline-style attributes\n * (e.g. `<!--[if mso]><td style=\"…\"><![endif]-->`) as opaque text.\n * The element walker can't see them, so without this branch the td/\n * v:rect styles inside comments stay longhand even when the visible\n * div has already been merged. Match each `style=\"…\"` substring,\n * run it through mergeLonghand, splice back.\n *\n * Tag filter intentionally bypassed: the user can't address MSO td\n * elements (they don't parse as elements), and these comments\n * always wrap email-layout primitives anyway.\n */\n if (node.type === 'comment') {\n const data = (node as any).data as string\n if (!data || !data.includes('style=\"')) return\n const newData = data.replace(/style=\"([^\"]*)\"/g, (full, value) => {\n const merged = mergeStyleValue(value)\n return merged === value ? full : `style=\"${merged}\"`\n })\n if (newData !== data) (node as any).data = newData\n return\n }\n\n const el = node as Element\n\n if (!el.attribs?.style) return\n if (hasTagFilter && !allowedTags.includes(el.name)) return\n\n const merged = mergeStyleValue(el.attribs.style)\n if (merged !== el.attribs.style) el.attribs.style = merged\n })\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,aAAa,MAAc,UAA+B,CAAC,GAAW;CACpF,OAAO,UAAU,gBAAgB,MAAM,IAAI,GAAG,OAAO,CAAC;AACxD;;;;;;AAOA,SAAgB,gBAAgB,KAAkB,UAA+B,CAAC,GAAgB;CAChG,MAAM,cAAc,QAAQ,QAAQ,CAAC;CACrC,MAAM,eAAe,YAAY,SAAS;;;;;;CAO1C,MAAM,mBAAmB,eAA+B;EACtD,IAAI;GACF,MAAM,EAAE,QAAQ,QAAQ,EACrB,IAAI,aAAa,EACjB,QAAQ,SAAS,WAAW,KAAK,EAAE,QAAQ,WAAW,CAAC;GAC1D,MAAM,QAAQ,IAAI,MAAM,yBAAyB;GACjD,IAAI,SAAS,MAAM,IAAI;IACrB,MAAM,SAAS,MAAM,GAAG,KAAK;IAC7B,IAAI,WAAW,YAAY,OAAO;GACpC;EACF,QACM,CAAC;EACP,OAAO;CACT;CAEA,KAAK,MAAM,SAAS;;;;;;;;;;;;;EAalB,IAAI,KAAK,SAAS,WAAW;GAC3B,MAAM,OAAQ,KAAa;GAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,SAAS,UAAS,GAAG;GACxC,MAAM,UAAU,KAAK,QAAQ,qBAAqB,MAAM,UAAU;IAChE,MAAM,SAAS,gBAAgB,KAAK;IACpC,OAAO,WAAW,QAAQ,OAAO,UAAU,OAAO;GACpD,CAAC;GACD,IAAI,YAAY,MAAM,KAAc,OAAO;GAC3C;EACF;EAEA,MAAM,KAAK;EAEX,IAAI,CAAC,GAAG,SAAS,OAAO;EACxB,IAAI,gBAAgB,CAAC,YAAY,SAAS,GAAG,IAAI,GAAG;EAEpD,MAAM,SAAS,gBAAgB,GAAG,QAAQ,KAAK;EAC/C,IAAI,WAAW,GAAG,QAAQ,OAAO,GAAG,QAAQ,QAAQ;CACtD,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sixHex.d.ts","names":[],"sources":["../../src/transformers/sixHex.ts"],"mappings":";;;;;AAkBA;;;;;
|
|
1
|
+
{"version":3,"file":"sixHex.d.ts","names":[],"sources":["../../src/transformers/sixHex.ts"],"mappings":";;;;;AAkBA;;;;AAAmC;AASnC;;;;;iBATgB,MAAA,CAAO,IAAY;;;AASmB;;;iBAAtC,SAAA,CAAU,GAAA,EAAK,SAAA,KAAc,SAAS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sixHex.js","names":[],"sources":["../../src/transformers/sixHex.ts"],"sourcesContent":["import { conv } from 'color-shorthand-hex-to-six-digit'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\nconst targets = new Set(['bgcolor', 'color'])\n\n/**\n * Convert 3-digit HEX color codes to 6-digit in `bgcolor` and `color`\n * attributes, for better email client compatibility.\n *\n * @param html HTML string to transform.\n * @returns The transformed HTML string.\n *\n * @example\n * import { sixHex } from '@maizzle/framework'\n *\n * const out = sixHex('<font color=\"#abc\">x</font>')\n */\nexport function sixHex(html: string): string {\n return serialize(sixHexDom(parse(html)))\n}\n\n/**\n * DOM-form of {@link sixHex} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function sixHexDom(dom: ChildNode[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (!el.attribs) {\n return\n }\n\n for (const attr of targets) {\n const value = el.attribs[attr]\n\n if (value) {\n el.attribs[attr] = conv(value)\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;AAIA,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW,
|
|
1
|
+
{"version":3,"file":"sixHex.js","names":[],"sources":["../../src/transformers/sixHex.ts"],"sourcesContent":["import { conv } from 'color-shorthand-hex-to-six-digit'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize, walk } from '../utils/ast/index.ts'\n\nconst targets = new Set(['bgcolor', 'color'])\n\n/**\n * Convert 3-digit HEX color codes to 6-digit in `bgcolor` and `color`\n * attributes, for better email client compatibility.\n *\n * @param html HTML string to transform.\n * @returns The transformed HTML string.\n *\n * @example\n * import { sixHex } from '@maizzle/framework'\n *\n * const out = sixHex('<font color=\"#abc\">x</font>')\n */\nexport function sixHex(html: string): string {\n return serialize(sixHexDom(parse(html)))\n}\n\n/**\n * DOM-form of {@link sixHex} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function sixHexDom(dom: ChildNode[]): ChildNode[] {\n walk(dom, (node) => {\n const el = node as Element\n\n if (!el.attribs) {\n return\n }\n\n for (const attr of targets) {\n const value = el.attribs[attr]\n\n if (value) {\n el.attribs[attr] = conv(value)\n }\n }\n })\n\n return dom\n}\n"],"mappings":";;;;;;AAIA,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW,OAAO,CAAC;;;;;;;;;;;;;AAc5C,SAAgB,OAAO,MAAsB;CAC3C,OAAO,UAAU,UAAU,MAAM,IAAI,CAAC,CAAC;AACzC;;;;;;AAOA,SAAgB,UAAU,KAA+B;CACvD,KAAK,MAAM,SAAS;EAClB,MAAM,KAAK;EAEX,IAAI,CAAC,GAAG,SACN;EAGF,KAAK,MAAM,QAAQ,SAAS;GAC1B,MAAM,QAAQ,GAAG,QAAQ;GAEzB,IAAI,OACF,GAAG,QAAQ,QAAQ,KAAK,KAAK;EAEjC;CACF,CAAC;CAED,OAAO;AACT"}
|
|
@@ -43,6 +43,10 @@ async function tailwindComponent(dom, blocks, config, filePath) {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
const el = node;
|
|
46
|
+
/**
|
|
47
|
+
* Always assign to the OUTERMOST active marker (stack[0]) so nested
|
|
48
|
+
* <Tailwind> blocks merge their classes into the parent's scope.
|
|
49
|
+
*/
|
|
46
50
|
if (el.attribs?.class && stack.length > 0) map.get(stack[0]).classes.add(el.attribs.class);
|
|
47
51
|
});
|
|
48
52
|
const fromPath = filePath ?? resolve(process.cwd(), "template.vue");
|
|
@@ -51,6 +55,11 @@ async function tailwindComponent(dom, blocks, config, filePath) {
|
|
|
51
55
|
if (!head && n.name === "head") head = n;
|
|
52
56
|
});
|
|
53
57
|
if (!head) throw new Error("`Tailwind` component requires `Head` component to be present in the template.");
|
|
58
|
+
/**
|
|
59
|
+
* Compile + inject one <style raw> per outermost block. `raw` opts
|
|
60
|
+
* the existing tailwindcss transformer out of recompiling
|
|
61
|
+
* already-compiled CSS.
|
|
62
|
+
*/
|
|
54
63
|
for (const meta of map.values()) {
|
|
55
64
|
if (meta.nested) continue;
|
|
56
65
|
const css = (await compileTailwindCss(buildCssInput(meta.configCss, meta.classes), config, `${fromPath}?tw=${meta.id}`)).trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailwindComponent.js","names":[],"sources":["../../src/transformers/tailwindComponent.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport type { ChildNode, Element, Comment } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { compileTailwindCss } from '../utils/compileTailwindCss.ts'\nimport type { TailwindBlock } from '../composables/renderContext.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nconst DEFAULT_SEED = '@import \"@maizzle/tailwindcss\" source(none);'\n\ninterface BlockMeta {\n id: string\n configCss?: string\n nested: boolean\n classes: Set<string>\n}\n\nconst OPEN_RE = /^mz-tw:(\\S+)$/\nconst CLOSE_RE = /^\\/mz-tw:(\\S+)$/\n\n/**\n * Compile Tailwind CSS for each top-level <Tailwind> block in the render\n * context. Nested <Tailwind> instances are flattened: their classes flow\n * up to the outermost block, their `#config` slot (if any) is ignored.\n * One <style> per outermost block is appended to <head>; marker comments\n * are stripped after.\n */\nexport async function tailwindComponent(\n dom: ChildNode[],\n blocks: TailwindBlock[],\n config: MaizzleConfig,\n filePath?: string,\n): Promise<ChildNode[]> {\n if (!blocks.length) return dom\n\n const map = new Map<string, BlockMeta>()\n for (const b of blocks) {\n map.set(b.id, { id: b.id, configCss: b.css, nested: false, classes: new Set() })\n }\n\n const stack: string[] = []\n const markers: Comment[] = []\n\n walk(dom, (node) => {\n if (node.type === 'comment') {\n const data = (node as Comment).data\n const open = data.match(OPEN_RE)\n const close = data.match(CLOSE_RE)\n if (open) {\n const id = open[1]\n const meta = map.get(id)\n if (meta && stack.length > 0) meta.nested = true\n if (meta) stack.push(id)\n markers.push(node as Comment)\n } else if (close) {\n const id = close[1]\n if (stack[stack.length - 1] === id) stack.pop()\n markers.push(node as Comment)\n }\n return\n }\n\n const el = node as Element\n
|
|
1
|
+
{"version":3,"file":"tailwindComponent.js","names":[],"sources":["../../src/transformers/tailwindComponent.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport type { ChildNode, Element, Comment } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { compileTailwindCss } from '../utils/compileTailwindCss.ts'\nimport type { TailwindBlock } from '../composables/renderContext.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nconst DEFAULT_SEED = '@import \"@maizzle/tailwindcss\" source(none);'\n\ninterface BlockMeta {\n id: string\n configCss?: string\n nested: boolean\n classes: Set<string>\n}\n\nconst OPEN_RE = /^mz-tw:(\\S+)$/\nconst CLOSE_RE = /^\\/mz-tw:(\\S+)$/\n\n/**\n * Compile Tailwind CSS for each top-level <Tailwind> block in the render\n * context. Nested <Tailwind> instances are flattened: their classes flow\n * up to the outermost block, their `#config` slot (if any) is ignored.\n * One <style> per outermost block is appended to <head>; marker comments\n * are stripped after.\n */\nexport async function tailwindComponent(\n dom: ChildNode[],\n blocks: TailwindBlock[],\n config: MaizzleConfig,\n filePath?: string,\n): Promise<ChildNode[]> {\n if (!blocks.length) return dom\n\n const map = new Map<string, BlockMeta>()\n for (const b of blocks) {\n map.set(b.id, { id: b.id, configCss: b.css, nested: false, classes: new Set() })\n }\n\n const stack: string[] = []\n const markers: Comment[] = []\n\n walk(dom, (node) => {\n if (node.type === 'comment') {\n const data = (node as Comment).data\n const open = data.match(OPEN_RE)\n const close = data.match(CLOSE_RE)\n if (open) {\n const id = open[1]\n const meta = map.get(id)\n if (meta && stack.length > 0) meta.nested = true\n if (meta) stack.push(id)\n markers.push(node as Comment)\n } else if (close) {\n const id = close[1]\n if (stack[stack.length - 1] === id) stack.pop()\n markers.push(node as Comment)\n }\n return\n }\n\n const el = node as Element\n /**\n * Always assign to the OUTERMOST active marker (stack[0]) so nested\n * <Tailwind> blocks merge their classes into the parent's scope.\n */\n if (el.attribs?.class && stack.length > 0) {\n map.get(stack[0])!.classes.add(el.attribs.class)\n }\n })\n\n const fromPath = filePath ?? resolve(process.cwd(), 'template.vue')\n\n let head: Element | undefined\n walk(dom, (n) => {\n if (!head && (n as Element).name === 'head') head = n as Element\n })\n\n if (!head) {\n throw new Error('`Tailwind` component requires `Head` component to be present in the template.')\n }\n\n /**\n * Compile + inject one <style raw> per outermost block. `raw` opts\n * the existing tailwindcss transformer out of recompiling\n * already-compiled CSS.\n */\n for (const meta of map.values()) {\n if (meta.nested) continue\n\n const cssInput = buildCssInput(meta.configCss, meta.classes)\n const css = (await compileTailwindCss(cssInput, config, `${fromPath}?tw=${meta.id}`)).trim()\n if (!css) continue\n\n const styleNode: Element = {\n type: 'tag',\n name: 'style',\n attribs: { raw: '' },\n children: [],\n parent: head,\n prev: null,\n next: null,\n } as any\n\n const textNode = {\n type: 'text',\n data: css,\n parent: styleNode,\n prev: null,\n next: null,\n } as any\n\n styleNode.children = [textNode]\n head.children.push(styleNode)\n }\n\n // Strip marker comments from their parents\n for (const c of markers) {\n const parent = c.parent as Element | null\n if (!parent?.children) continue\n const i = parent.children.indexOf(c)\n if (i >= 0) parent.children.splice(i, 1)\n }\n\n return dom\n}\n\nfunction buildCssInput(configCss: string | undefined, classes: Set<string>): string {\n const seed = configCss ?? DEFAULT_SEED\n\n if (!classes.size) return seed\n\n const inline = [...classes].join(' ').replace(/\"/g, '\\\\\"')\n return `${seed}\\n@source inline(\"${inline}\");`\n}\n"],"mappings":";;;;;AAOA,MAAM,eAAe;AASrB,MAAM,UAAU;AAChB,MAAM,WAAW;;;;;;;;AASjB,eAAsB,kBACpB,KACA,QACA,QACA,UACsB;CACtB,IAAI,CAAC,OAAO,QAAQ,OAAO;CAE3B,MAAM,sBAAM,IAAI,IAAuB;CACvC,KAAK,MAAM,KAAK,QACd,IAAI,IAAI,EAAE,IAAI;EAAE,IAAI,EAAE;EAAI,WAAW,EAAE;EAAK,QAAQ;EAAO,yBAAS,IAAI,IAAI;CAAE,CAAC;CAGjF,MAAM,QAAkB,CAAC;CACzB,MAAM,UAAqB,CAAC;CAE5B,KAAK,MAAM,SAAS;EAClB,IAAI,KAAK,SAAS,WAAW;GAC3B,MAAM,OAAQ,KAAiB;GAC/B,MAAM,OAAO,KAAK,MAAM,OAAO;GAC/B,MAAM,QAAQ,KAAK,MAAM,QAAQ;GACjC,IAAI,MAAM;IACR,MAAM,KAAK,KAAK;IAChB,MAAM,OAAO,IAAI,IAAI,EAAE;IACvB,IAAI,QAAQ,MAAM,SAAS,GAAG,KAAK,SAAS;IAC5C,IAAI,MAAM,MAAM,KAAK,EAAE;IACvB,QAAQ,KAAK,IAAe;GAC9B,OAAO,IAAI,OAAO;IAChB,MAAM,KAAK,MAAM;IACjB,IAAI,MAAM,MAAM,SAAS,OAAO,IAAI,MAAM,IAAI;IAC9C,QAAQ,KAAK,IAAe;GAC9B;GACA;EACF;EAEA,MAAM,KAAK;;;;;EAKX,IAAI,GAAG,SAAS,SAAS,MAAM,SAAS,GACtC,IAAI,IAAI,MAAM,EAAE,EAAG,QAAQ,IAAI,GAAG,QAAQ,KAAK;CAEnD,CAAC;CAED,MAAM,WAAW,YAAY,QAAQ,QAAQ,IAAI,GAAG,cAAc;CAElE,IAAI;CACJ,KAAK,MAAM,MAAM;EACf,IAAI,CAAC,QAAS,EAAc,SAAS,QAAQ,OAAO;CACtD,CAAC;CAED,IAAI,CAAC,MACH,MAAM,IAAI,MAAM,+EAA+E;;;;;;CAQjG,KAAK,MAAM,QAAQ,IAAI,OAAO,GAAG;EAC/B,IAAI,KAAK,QAAQ;EAGjB,MAAM,OAAO,MAAM,mBADF,cAAc,KAAK,WAAW,KAAK,OACP,GAAG,QAAQ,GAAG,SAAS,MAAM,KAAK,IAAI,GAAG,KAAK;EAC3F,IAAI,CAAC,KAAK;EAEV,MAAM,YAAqB;GACzB,MAAM;GACN,MAAM;GACN,SAAS,EAAE,KAAK,GAAG;GACnB,UAAU,CAAC;GACX,QAAQ;GACR,MAAM;GACN,MAAM;EACR;EAUA,UAAU,WAAW,CAAC;GAPpB,MAAM;GACN,MAAM;GACN,QAAQ;GACR,MAAM;GACN,MAAM;EAGqB,CAAC;EAC9B,KAAK,SAAS,KAAK,SAAS;CAC9B;CAGA,KAAK,MAAM,KAAK,SAAS;EACvB,MAAM,SAAS,EAAE;EACjB,IAAI,CAAC,QAAQ,UAAU;EACvB,MAAM,IAAI,OAAO,SAAS,QAAQ,CAAC;EACnC,IAAI,KAAK,GAAG,OAAO,SAAS,OAAO,GAAG,CAAC;CACzC;CAEA,OAAO;AACT;AAEA,SAAS,cAAc,WAA+B,SAA8B;CAClF,MAAM,OAAO,aAAa;CAE1B,IAAI,CAAC,QAAQ,MAAM,OAAO;CAG1B,OAAO,GAAG,KAAK,oBADA,CAAC,GAAG,OAAO,EAAE,KAAK,GAAG,EAAE,QAAQ,MAAM,MACZ,EAAE;AAC5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailwindcss.d.ts","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"tailwindcss.d.ts","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"mappings":";;;;;;AA6EA;;;;;;;;;;;;;;iBAAsB,WAAA,CAAY,GAAA,EAAK,SAAA,IAAa,MAAA,EAAQ,aAAA,EAAe,QAAA,YAAoB,OAAA,CAAQ,SAAA"}
|
|
@@ -28,6 +28,13 @@ function buildSourceDirectives(dom, config, fromDir) {
|
|
|
28
28
|
const directives = [];
|
|
29
29
|
const excludePaths = [resolve(config.output?.path ?? "dist"), ...(config.css?.exclude ?? []).map((p) => resolve(p))];
|
|
30
30
|
for (const p of excludePaths) directives.push(`@source not "${relative(fromDir, resolve(p))}";`);
|
|
31
|
+
/**
|
|
32
|
+
* Inline source: collect all class attribute values from the rendered DOM.
|
|
33
|
+
* After Vue SSR, the DOM contains every class from every component
|
|
34
|
+
* (built-in framework components, user components, dynamic
|
|
35
|
+
* bindings). We pass these raw values to Tailwind's
|
|
36
|
+
* scanner via @source inline().
|
|
37
|
+
*/
|
|
31
38
|
const classes = [];
|
|
32
39
|
walk(dom, (n) => {
|
|
33
40
|
const cls = n.attribs?.class;
|
|
@@ -57,6 +64,12 @@ async function tailwindcss(dom, config, filePath) {
|
|
|
57
64
|
walk(dom, (node) => {
|
|
58
65
|
if (node.name !== "style") return;
|
|
59
66
|
const el = node;
|
|
67
|
+
/**
|
|
68
|
+
* `raw` opts out of compilation entirely (marker is consumed here).
|
|
69
|
+
* `embed`/`data-embed` only signal "preserve tag after inlining"
|
|
70
|
+
* — they still need to go through compile so Tailwind/@apply
|
|
71
|
+
* resolves.
|
|
72
|
+
*/
|
|
60
73
|
if ("raw" in (el.attribs || {})) {
|
|
61
74
|
delete el.attribs.raw;
|
|
62
75
|
return;
|
|
@@ -74,6 +87,11 @@ async function tailwindcss(dom, config, filePath) {
|
|
|
74
87
|
const sourceDirectives = styleTags.some(({ cssContent }) => usesTailwind(cssContent)) ? buildSourceDirectives(dom, config, fromDir) : "";
|
|
75
88
|
for (let i = 0; i < styleTags.length; i++) {
|
|
76
89
|
const { node, cssContent } = styleTags[i];
|
|
90
|
+
/**
|
|
91
|
+
* Only add source directives to style tags that import Tailwind —
|
|
92
|
+
* plain CSS doesn't need them and @tailwindcss/postcss would
|
|
93
|
+
* leave the directives unconsumed in the output.
|
|
94
|
+
*/
|
|
77
95
|
const fullCss = usesTailwind(cssContent) ? `${cssContent}\n${sourceDirectives}` : cssContent;
|
|
78
96
|
try {
|
|
79
97
|
node.children = [{
|
|
@@ -82,6 +100,10 @@ async function tailwindcss(dom, config, filePath) {
|
|
|
82
100
|
parent: node
|
|
83
101
|
}];
|
|
84
102
|
} catch {
|
|
103
|
+
/**
|
|
104
|
+
* If CSS processing fails, still replace with decoded content
|
|
105
|
+
* so HTML entities don't break the CSS.
|
|
106
|
+
*/
|
|
85
107
|
node.children = [{
|
|
86
108
|
type: "text",
|
|
87
109
|
data: cssContent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailwindcss.js","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { decodeStyleEntities } from '../utils/decodeStyleEntities.ts'\nimport { compileTailwindCss } from '../utils/compileTailwindCss.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\n/**\n * Check if CSS content uses Tailwind features that require source scanning.\n *\n * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source\n * directives. Plain CSS without Tailwind imports doesn't need scanning\n * and would pass through @source directives unconsumed.\n */\nfunction usesTailwind(css: string): boolean {\n return /((@import|@reference)\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Build @source directives for Tailwind CSS scanning.\n *\n * Configures two types of sources:\n * 1. Exclusions for output dir and user-configured paths\n * 2. Inline source with all class attribute values from the rendered DOM,\n * capturing classes from all components (built-in + user), dynamic\n * expressions, and the template itself — Tailwind's scanner handles\n * the actual class extraction from these raw values\n */\nfunction buildSourceDirectives(dom: ChildNode[], config: MaizzleConfig, fromDir: string): string {\n const directives: string[] = []\n\n // Exclude output dir and user-configured paths\n const excludePaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\n for (const p of excludePaths) {\n directives.push(`@source not \"${relative(fromDir, resolve(p))}\";`)\n }\n\n
|
|
1
|
+
{"version":3,"file":"tailwindcss.js","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { decodeStyleEntities } from '../utils/decodeStyleEntities.ts'\nimport { compileTailwindCss } from '../utils/compileTailwindCss.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\n/**\n * Check if CSS content uses Tailwind features that require source scanning.\n *\n * Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source\n * directives. Plain CSS without Tailwind imports doesn't need scanning\n * and would pass through @source directives unconsumed.\n */\nfunction usesTailwind(css: string): boolean {\n return /((@import|@reference)\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Build @source directives for Tailwind CSS scanning.\n *\n * Configures two types of sources:\n * 1. Exclusions for output dir and user-configured paths\n * 2. Inline source with all class attribute values from the rendered DOM,\n * capturing classes from all components (built-in + user), dynamic\n * expressions, and the template itself — Tailwind's scanner handles\n * the actual class extraction from these raw values\n */\nfunction buildSourceDirectives(dom: ChildNode[], config: MaizzleConfig, fromDir: string): string {\n const directives: string[] = []\n\n // Exclude output dir and user-configured paths\n const excludePaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\n for (const p of excludePaths) {\n directives.push(`@source not \"${relative(fromDir, resolve(p))}\";`)\n }\n\n /**\n * Inline source: collect all class attribute values from the rendered DOM.\n * After Vue SSR, the DOM contains every class from every component\n * (built-in framework components, user components, dynamic\n * bindings). We pass these raw values to Tailwind's\n * scanner via @source inline().\n */\n const classes: string[] = []\n walk(dom, (n) => {\n const cls = (n as Element).attribs?.class\n if (cls) classes.push(cls)\n })\n\n if (classes.length) {\n directives.push(`@source inline(\"${classes.join(' ')}\");`)\n }\n\n return directives.join('\\n')\n}\n\n/**\n * Tailwind CSS transformer.\n *\n * Compiles CSS inside <style> tags in the DOM using\n * @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.\n *\n * Configures Tailwind sources to scan:\n * - Rendered class attributes (via `@source inline`) for all classes from all components\n * - User project files (via Tailwind's auto-detection from base/from path)\n *\n * User `@source` and `@source not directives` in style tags are preserved.\n * Source directives are only added to style tags that import Tailwind.\n *\n * Runs as the first transformer in the pipeline so that subsequent\n * transformers (inliner, purge, etc.) work with fully compiled CSS.\n */\nexport async function tailwindcss(dom: ChildNode[], config: MaizzleConfig, filePath?: string): Promise<ChildNode[]> {\n const styleTags: { node: Element; cssContent: string }[] = []\n\n walk(dom, (node) => {\n if ((node as Element).name !== 'style') return\n\n const el = node as Element\n const attrs = el.attribs || {}\n\n /**\n * `raw` opts out of compilation entirely (marker is consumed here).\n * `embed`/`data-embed` only signal \"preserve tag after inlining\"\n * — they still need to go through compile so Tailwind/@apply\n * resolves.\n */\n if ('raw' in attrs) {\n delete el.attribs.raw\n return\n }\n\n // Get text content from children and decode HTML entities\n const rawContent = el.children\n .filter(child => child.type === 'text')\n .map(child => (child as any).data)\n .join('')\n\n if (!rawContent.trim()) return\n\n styleTags.push({ node: el, cssContent: decodeStyleEntities(rawContent) })\n })\n\n if (!styleTags.length) return dom\n\n const fromPath = filePath ?? resolve(process.cwd(), 'template.vue')\n const fromDir = dirname(fromPath)\n\n // Only compute source directives if at least one style tag uses Tailwind\n const hasTailwindStyles = styleTags.some(({ cssContent }) => usesTailwind(cssContent))\n const sourceDirectives = hasTailwindStyles\n ? buildSourceDirectives(dom, config, fromDir)\n : ''\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n\n /**\n * Only add source directives to style tags that import Tailwind —\n * plain CSS doesn't need them and @tailwindcss/postcss would\n * leave the directives unconsumed in the output.\n */\n const fullCss = usesTailwind(cssContent)\n ? `${cssContent}\\n${sourceDirectives}`\n : cssContent\n\n try {\n const optimized = await compileTailwindCss(fullCss, config, `${fromPath}?style=${i}`)\n\n // Replace the style tag's children with the compiled CSS\n node.children = [{\n type: 'text',\n data: optimized,\n parent: node,\n } as any]\n } catch {\n /**\n * If CSS processing fails, still replace with decoded content\n * so HTML entities don't break the CSS.\n */\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAS,aAAa,KAAsB;CAC1C,OAAO,+EAA+E,KAAK,GAAG;AAChG;;;;;;;;;;;AAYA,SAAS,sBAAsB,KAAkB,QAAuB,SAAyB;CAC/F,MAAM,aAAuB,CAAC;CAG9B,MAAM,eAAe,CACnB,QAAQ,OAAO,QAAQ,QAAQ,MAAM,GACrC,IAAI,OAAO,KAAK,WAAW,CAAC,GAAG,KAAI,MAAK,QAAQ,CAAC,CAAC,CACpD;CAEA,KAAK,MAAM,KAAK,cACd,WAAW,KAAK,gBAAgB,SAAS,SAAS,QAAQ,CAAC,CAAC,EAAE,GAAG;;;;;;;;CAUnE,MAAM,UAAoB,CAAC;CAC3B,KAAK,MAAM,MAAM;EACf,MAAM,MAAO,EAAc,SAAS;EACpC,IAAI,KAAK,QAAQ,KAAK,GAAG;CAC3B,CAAC;CAED,IAAI,QAAQ,QACV,WAAW,KAAK,mBAAmB,QAAQ,KAAK,GAAG,EAAE,IAAI;CAG3D,OAAO,WAAW,KAAK,IAAI;AAC7B;;;;;;;;;;;;;;;;;AAkBA,eAAsB,YAAY,KAAkB,QAAuB,UAAyC;CAClH,MAAM,YAAqD,CAAC;CAE5D,KAAK,MAAM,SAAS;EAClB,IAAK,KAAiB,SAAS,SAAS;EAExC,MAAM,KAAK;;;;;;;EASX,IAAI,UARU,GAAG,WAAW,CAAC,IAQT;GAClB,OAAO,GAAG,QAAQ;GAClB;EACF;EAGA,MAAM,aAAa,GAAG,SACnB,QAAO,UAAS,MAAM,SAAS,MAAM,EACrC,KAAI,UAAU,MAAc,IAAI,EAChC,KAAK,EAAE;EAEV,IAAI,CAAC,WAAW,KAAK,GAAG;EAExB,UAAU,KAAK;GAAE,MAAM;GAAI,YAAY,oBAAoB,UAAU;EAAE,CAAC;CAC1E,CAAC;CAED,IAAI,CAAC,UAAU,QAAQ,OAAO;CAE9B,MAAM,WAAW,YAAY,QAAQ,QAAQ,IAAI,GAAG,cAAc;CAClE,MAAM,UAAU,QAAQ,QAAQ;CAIhC,MAAM,mBADoB,UAAU,MAAM,EAAE,iBAAiB,aAAa,UAAU,CAC3C,IACrC,sBAAsB,KAAK,QAAQ,OAAO,IAC1C;CAEJ,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;EACzC,MAAM,EAAE,MAAM,eAAe,UAAU;;;;;;EAOvC,MAAM,UAAU,aAAa,UAAU,IACnC,GAAG,WAAW,IAAI,qBAClB;EAEJ,IAAI;GAIF,KAAK,WAAW,CAAC;IACf,MAAM;IACN,MAAM,MALgB,mBAAmB,SAAS,QAAQ,GAAG,SAAS,SAAS,GAAG;IAMlF,QAAQ;GACV,CAAQ;EACV,QAAQ;;;;;GAKN,KAAK,WAAW,CAAC;IACf,MAAM;IACN,MAAM;IACN,QAAQ;GACV,CAAQ;EACV;CACF;CAEA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urlQuery.d.ts","names":[],"sources":["../../src/transformers/urlQuery.ts"],"mappings":";;;;;;AAiDA
|
|
1
|
+
{"version":3,"file":"urlQuery.d.ts","names":[],"sources":["../../src/transformers/urlQuery.ts"],"mappings":";;;;;;AAiDA;;;;;;;;;;AAG+B;AAU/B;;;;;;;;;iBAbgB,QAAA,CACd,IAAA,UACA,MAAA,GAAQ,MAAA,mBACR,OAAA,GAAS,eAAoB;;;;;;iBAUf,WAAA,CACd,GAAA,EAAK,SAAA,IACL,MAAA,GAAQ,MAAA,mBACR,OAAA,GAAS,eAAA,GACR,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urlQuery.js","names":[],"sources":["../../src/transformers/urlQuery.ts"],"sourcesContent":["import queryString from 'query-string'\nimport { selectAll } from 'css-select'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize } from '../utils/ast/index.ts'\nimport { isAbsoluteUrl } from '../utils/url.ts'\nimport type { UrlQueryOptions } from '../types/config.ts'\n\nconst DEFAULT_ATTRIBUTES = ['src', 'href', 'poster', 'srcset', 'background']\nconst DEFAULT_TAGS = ['a']\n\n/**\n * Append query parameters to a URL string using query-string.\n */\nfunction appendParams(\n url: string,\n params: Record<string, unknown>,\n qsOptions: queryString.StringifyOptions,\n strict: boolean,\n): string {\n if (strict && !isAbsoluteUrl(url)) return url\n\n return queryString.stringifyUrl(\n { url, query: params as queryString.StringifiableRecord },\n qsOptions,\n )\n}\n\n/**\n * Append query parameters to URLs found in matching attributes/elements.\n *\n * @param html HTML string to transform.\n * @param params Query parameters to append (e.g. `{ utm_source: 'newsletter' }`).\n * @param options Behaviour overrides — `tags` (CSS selectors, default `['a']`),\n * `attributes` (default `['src', 'href', 'poster', 'srcset', 'background']`),\n * `strict` (default `true`, only rewrites absolute URLs),\n * `qs` (forwarded to `query-string`, default `{ encode: false }`).\n * @returns The transformed HTML string.\n *\n * @example\n * import { urlQuery } from '@maizzle/framework'\n *\n * const out = urlQuery(\n * '<a href=\"https://example.com\">x</a>',\n * { utm_source: 'newsletter' },\n * )\n *\n * // Restrict to specific tags / attributes:\n * urlQuery(html, { ref: 'email' }, { tags: ['a', 'img'], attributes: ['href', 'src'] })\n */\nexport function urlQuery(\n html: string,\n params: Record<string, unknown> = {},\n options: UrlQueryOptions = {},\n): string {\n return serialize(urlQueryDom(parse(html), params, options))\n}\n\n/**\n * DOM-form of {@link urlQuery} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function urlQueryDom(\n dom: ChildNode[],\n params: Record<string, unknown> = {},\n options: UrlQueryOptions = {},\n): ChildNode[] {\n if (!params || Object.keys(params).length === 0) return dom\n\n const tags = options.tags ?? DEFAULT_TAGS\n const attributes = options.attributes ?? DEFAULT_ATTRIBUTES\n const strict = options.strict ?? true\n const qsOptions: queryString.StringifyOptions = { encode: false, ...((options.qs ?? {}) as queryString.StringifyOptions) }\n\n // Use css-select to find all elements matching any of the tag selectors\n const selector = tags.join(', ')\n const elements = selectAll(selector, dom) as Element[]\n\n for (const el of elements) {\n for (const attr of attributes) {\n const value = el.attribs[attr]\n if (!value) continue\n\n const updated = appendParams(value, params, qsOptions, strict)\n if (updated !== value) {\n el.attribs[attr] = updated\n }\n }\n }\n\n return dom\n}\n"],"mappings":";;;;;;;AAOA,MAAM,qBAAqB;CAAC;CAAO;CAAQ;CAAU;CAAU;
|
|
1
|
+
{"version":3,"file":"urlQuery.js","names":[],"sources":["../../src/transformers/urlQuery.ts"],"sourcesContent":["import queryString from 'query-string'\nimport { selectAll } from 'css-select'\nimport type { ChildNode, Element } from 'domhandler'\nimport { parse, serialize } from '../utils/ast/index.ts'\nimport { isAbsoluteUrl } from '../utils/url.ts'\nimport type { UrlQueryOptions } from '../types/config.ts'\n\nconst DEFAULT_ATTRIBUTES = ['src', 'href', 'poster', 'srcset', 'background']\nconst DEFAULT_TAGS = ['a']\n\n/**\n * Append query parameters to a URL string using query-string.\n */\nfunction appendParams(\n url: string,\n params: Record<string, unknown>,\n qsOptions: queryString.StringifyOptions,\n strict: boolean,\n): string {\n if (strict && !isAbsoluteUrl(url)) return url\n\n return queryString.stringifyUrl(\n { url, query: params as queryString.StringifiableRecord },\n qsOptions,\n )\n}\n\n/**\n * Append query parameters to URLs found in matching attributes/elements.\n *\n * @param html HTML string to transform.\n * @param params Query parameters to append (e.g. `{ utm_source: 'newsletter' }`).\n * @param options Behaviour overrides — `tags` (CSS selectors, default `['a']`),\n * `attributes` (default `['src', 'href', 'poster', 'srcset', 'background']`),\n * `strict` (default `true`, only rewrites absolute URLs),\n * `qs` (forwarded to `query-string`, default `{ encode: false }`).\n * @returns The transformed HTML string.\n *\n * @example\n * import { urlQuery } from '@maizzle/framework'\n *\n * const out = urlQuery(\n * '<a href=\"https://example.com\">x</a>',\n * { utm_source: 'newsletter' },\n * )\n *\n * // Restrict to specific tags / attributes:\n * urlQuery(html, { ref: 'email' }, { tags: ['a', 'img'], attributes: ['href', 'src'] })\n */\nexport function urlQuery(\n html: string,\n params: Record<string, unknown> = {},\n options: UrlQueryOptions = {},\n): string {\n return serialize(urlQueryDom(parse(html), params, options))\n}\n\n/**\n * DOM-form of {@link urlQuery} used by the internal transformer pipeline.\n * Takes a parsed DOM, returns a parsed DOM — avoids redundant\n * serialize/parse round-trips when chained with other transformers.\n */\nexport function urlQueryDom(\n dom: ChildNode[],\n params: Record<string, unknown> = {},\n options: UrlQueryOptions = {},\n): ChildNode[] {\n if (!params || Object.keys(params).length === 0) return dom\n\n const tags = options.tags ?? DEFAULT_TAGS\n const attributes = options.attributes ?? DEFAULT_ATTRIBUTES\n const strict = options.strict ?? true\n const qsOptions: queryString.StringifyOptions = { encode: false, ...((options.qs ?? {}) as queryString.StringifyOptions) }\n\n // Use css-select to find all elements matching any of the tag selectors\n const selector = tags.join(', ')\n const elements = selectAll(selector, dom) as Element[]\n\n for (const el of elements) {\n for (const attr of attributes) {\n const value = el.attribs[attr]\n if (!value) continue\n\n const updated = appendParams(value, params, qsOptions, strict)\n if (updated !== value) {\n el.attribs[attr] = updated\n }\n }\n }\n\n return dom\n}\n"],"mappings":";;;;;;;AAOA,MAAM,qBAAqB;CAAC;CAAO;CAAQ;CAAU;CAAU;AAAY;AAC3E,MAAM,eAAe,CAAC,GAAG;;;;AAKzB,SAAS,aACP,KACA,QACA,WACA,QACQ;CACR,IAAI,UAAU,CAAC,cAAc,GAAG,GAAG,OAAO;CAE1C,OAAO,YAAY,aACjB;EAAE;EAAK,OAAO;CAA0C,GACxD,SACF;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,SACd,MACA,SAAkC,CAAC,GACnC,UAA2B,CAAC,GACpB;CACR,OAAO,UAAU,YAAY,MAAM,IAAI,GAAG,QAAQ,OAAO,CAAC;AAC5D;;;;;;AAOA,SAAgB,YACd,KACA,SAAkC,CAAC,GACnC,UAA2B,CAAC,GACf;CACb,IAAI,CAAC,UAAU,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG,OAAO;CAExD,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,YAA0C;EAAE,QAAQ;EAAO,GAAK,QAAQ,MAAM,CAAC;CAAoC;CAIzH,MAAM,WAAW,UADA,KAAK,KAAK,IACO,GAAG,GAAG;CAExC,KAAK,MAAM,MAAM,UACf,KAAK,MAAM,QAAQ,YAAY;EAC7B,MAAM,QAAQ,GAAG,QAAQ;EACzB,IAAI,CAAC,OAAO;EAEZ,MAAM,UAAU,aAAa,OAAO,QAAQ,WAAW,MAAM;EAC7D,IAAI,YAAY,OACd,GAAG,QAAQ,QAAQ;CAEvB;CAGF,OAAO;AACT"}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -2,11 +2,7 @@ import { RemoveValue } from "../plugins/postcss/removeDeclarations.js";
|
|
|
2
2
|
import { TemplateInfo } from "../events/index.js";
|
|
3
3
|
import { Directive, Plugin } from "vue";
|
|
4
4
|
import { Options } from "juice";
|
|
5
|
-
import * as _$oxfmt from "oxfmt";
|
|
6
|
-
import * as _$html_crush0 from "html-crush";
|
|
7
5
|
import { InlineConfig } from "vite";
|
|
8
|
-
import * as _$string_strip_html0 from "string-strip-html";
|
|
9
|
-
import * as _$shiki from "shiki";
|
|
10
6
|
import { Options as Options$1 } from "unplugin-vue-markdown/types";
|
|
11
7
|
|
|
12
8
|
//#region src/types/config.d.ts
|
|
@@ -353,7 +349,7 @@ interface HtmlConfig {
|
|
|
353
349
|
*
|
|
354
350
|
* Set to `true` to enable with defaults, or pass options.
|
|
355
351
|
*/
|
|
356
|
-
format?: boolean |
|
|
352
|
+
format?: boolean | import('oxfmt').FormatOptions;
|
|
357
353
|
/**
|
|
358
354
|
* Minify the HTML output.
|
|
359
355
|
*
|
|
@@ -362,7 +358,7 @@ interface HtmlConfig {
|
|
|
362
358
|
*
|
|
363
359
|
* @see https://codsen.com/os/html-crush
|
|
364
360
|
*/
|
|
365
|
-
minify?: boolean | Partial<
|
|
361
|
+
minify?: boolean | Partial<import('html-crush').Opts>;
|
|
366
362
|
}
|
|
367
363
|
type FilterFunction = (str: string, value: string) => string;
|
|
368
364
|
type FiltersConfig = false | Record<string, FilterFunction>;
|
|
@@ -372,7 +368,7 @@ interface MarkdownConfig extends Options$1 {
|
|
|
372
368
|
*
|
|
373
369
|
* @default 'github-light'
|
|
374
370
|
*/
|
|
375
|
-
shikiTheme?:
|
|
371
|
+
shikiTheme?: import('shiki').BundledTheme;
|
|
376
372
|
}
|
|
377
373
|
interface VueConfig {
|
|
378
374
|
/**
|
|
@@ -447,7 +443,7 @@ interface PlaintextConfig {
|
|
|
447
443
|
*
|
|
448
444
|
* @see https://codsen.com/os/string-strip-html
|
|
449
445
|
*/
|
|
450
|
-
options?: Partial<
|
|
446
|
+
options?: Partial<import('string-strip-html').Opts>;
|
|
451
447
|
}
|
|
452
448
|
/**
|
|
453
449
|
* Source directory entry for component auto-import.
|