@maizzle/framework 6.0.0-rc.4 → 6.0.0-rc.6
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.mjs +3 -2
- package/dist/build.mjs.map +1 -1
- package/dist/components/Body.vue +42 -0
- package/dist/components/Button.vue +65 -14
- package/dist/components/CodeBlock.vue +75 -0
- package/dist/components/CodeInline.vue +44 -0
- package/dist/components/Column.vue +61 -0
- package/dist/components/Container.vue +40 -0
- package/dist/components/Head.vue +8 -0
- package/dist/components/Html.vue +53 -0
- package/dist/components/Image.vue +70 -0
- package/dist/components/Overlap.vue +60 -0
- package/dist/components/Preview.vue +20 -0
- package/dist/components/Row.vue +80 -0
- package/dist/components/Spacer.vue +50 -7
- package/dist/composables/renderContext.d.mts +5 -0
- package/dist/composables/renderContext.d.mts.map +1 -1
- package/dist/composables/renderContext.mjs.map +1 -1
- package/dist/composables/usePreviewText.d.mts +24 -0
- package/dist/composables/usePreviewText.d.mts.map +1 -0
- package/dist/composables/usePreviewText.mjs +29 -0
- package/dist/composables/usePreviewText.mjs.map +1 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/render/createRenderer.d.mts +1 -0
- package/dist/render/createRenderer.d.mts.map +1 -1
- package/dist/render/createRenderer.mjs +60 -1
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/render/index.mjs +3 -2
- package/dist/render/index.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +31 -20
- package/dist/serve.mjs.map +1 -1
- package/dist/server/ui/pages/Preview.vue +11 -5
- package/dist/transformers/entities.d.mts.map +1 -1
- package/dist/transformers/entities.mjs +3 -0
- package/dist/transformers/entities.mjs.map +1 -1
- package/dist/transformers/filters/defaults.d.mts +6 -0
- package/dist/transformers/filters/defaults.d.mts.map +1 -0
- package/dist/transformers/filters/defaults.mjs +78 -0
- package/dist/transformers/filters/defaults.mjs.map +1 -0
- package/dist/transformers/filters/index.d.mts +22 -0
- package/dist/transformers/filters/index.d.mts.map +1 -0
- package/dist/transformers/filters/index.mjs +67 -0
- package/dist/transformers/filters/index.mjs.map +1 -0
- package/dist/transformers/index.d.mts +9 -8
- package/dist/transformers/index.d.mts.map +1 -1
- package/dist/transformers/index.mjs +15 -10
- package/dist/transformers/index.mjs.map +1 -1
- package/dist/transformers/tailwindcss.d.mts +6 -2
- package/dist/transformers/tailwindcss.d.mts.map +1 -1
- package/dist/transformers/tailwindcss.mjs +49 -21
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +15 -1
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/utils/ast/serializer.d.mts +3 -2
- package/dist/utils/ast/serializer.d.mts.map +1 -1
- package/dist/utils/ast/serializer.mjs +24 -0
- package/dist/utils/ast/serializer.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -8,8 +8,12 @@ import { ChildNode } from "domhandler";
|
|
|
8
8
|
* Compiles CSS inside <style> tags in the DOM using
|
|
9
9
|
* @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Configures Tailwind sources to scan:
|
|
12
|
+
* - Rendered class attributes (via `@source inline`) for all classes from all components
|
|
13
|
+
* - User project files (via Tailwind's auto-detection from base/from path)
|
|
14
|
+
*
|
|
15
|
+
* User `@source` and `@source not directives` in style tags are preserved.
|
|
16
|
+
* Source directives are only added to style tags that import Tailwind.
|
|
13
17
|
*
|
|
14
18
|
* Runs as the first transformer in the pipeline so that subsequent
|
|
15
19
|
* transformers (inliner, purge, etc.) work with fully compiled CSS.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailwindcss.d.mts","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"tailwindcss.d.mts","names":[],"sources":["../../src/transformers/tailwindcss.ts"],"mappings":";;;;;;AAsJA;;;;;;;;;;;;;;iBAAsB,WAAA,CAAY,GAAA,EAAK,SAAA,IAAa,MAAA,EAAQ,aAAA,EAAe,QAAA,YAAoB,OAAA,CAAQ,SAAA"}
|
|
@@ -7,6 +7,7 @@ import { dirname, relative, resolve } from "node:path";
|
|
|
7
7
|
import postcss from "postcss";
|
|
8
8
|
import tailwindcssPostcss from "@tailwindcss/postcss";
|
|
9
9
|
import customProperties from "postcss-custom-properties";
|
|
10
|
+
import postcssCalc from "postcss-calc";
|
|
10
11
|
import safeParser from "postcss-safe-parser";
|
|
11
12
|
import { transform } from "lightningcss";
|
|
12
13
|
|
|
@@ -19,6 +20,7 @@ function createProcessor(config) {
|
|
|
19
20
|
optimize: false
|
|
20
21
|
}),
|
|
21
22
|
customProperties({ preserve: false }),
|
|
23
|
+
postcssCalc({}),
|
|
22
24
|
pruneVars_default()
|
|
23
25
|
]);
|
|
24
26
|
}
|
|
@@ -33,6 +35,16 @@ function decodeEntities(str) {
|
|
|
33
35
|
return str.replace(/"/g, "\"").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/'/g, "'");
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
38
|
+
* Check if CSS content uses Tailwind features that require source scanning.
|
|
39
|
+
*
|
|
40
|
+
* Only CSS that imports Tailwind (or @maizzle/tailwindcss) needs @source
|
|
41
|
+
* directives. Plain CSS without Tailwind imports doesn't need scanning
|
|
42
|
+
* and would pass through @source directives unconsumed.
|
|
43
|
+
*/
|
|
44
|
+
function usesTailwind(css) {
|
|
45
|
+
return /(@import\s+["'](tailwindcss|@maizzle\/tailwindcss)|@tailwind\s)/.test(css);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
36
48
|
* Lower modern CSS syntax using lightningcss.
|
|
37
49
|
*
|
|
38
50
|
* Targets IE 1 to maximize syntax lowering — converts modern features
|
|
@@ -60,19 +72,44 @@ async function optimizeCss(css, config) {
|
|
|
60
72
|
return (await postcss(plugins).process(css, { from: void 0 })).css;
|
|
61
73
|
}
|
|
62
74
|
/**
|
|
75
|
+
* Build @source directives for Tailwind CSS scanning.
|
|
76
|
+
*
|
|
77
|
+
* Configures two types of sources:
|
|
78
|
+
* 1. Exclusions for output dir and user-configured paths
|
|
79
|
+
* 2. Inline source with all class attribute values from the rendered DOM,
|
|
80
|
+
* capturing classes from all components (built-in + user), dynamic
|
|
81
|
+
* expressions, and the template itself — Tailwind's scanner handles
|
|
82
|
+
* the actual class extraction from these raw values
|
|
83
|
+
*/
|
|
84
|
+
function buildSourceDirectives(dom, config, fromDir) {
|
|
85
|
+
const directives = [];
|
|
86
|
+
const excludePaths = [resolve(config.output?.path ?? "dist"), ...(config.css?.exclude ?? []).map((p) => resolve(p))];
|
|
87
|
+
for (const p of excludePaths) directives.push(`@source not "${relative(fromDir, resolve(p))}";`);
|
|
88
|
+
const classes = [];
|
|
89
|
+
walk(dom, (n) => {
|
|
90
|
+
const cls = n.attribs?.class;
|
|
91
|
+
if (cls) classes.push(cls);
|
|
92
|
+
});
|
|
93
|
+
if (classes.length) directives.push(`@source inline("${classes.join(" ")}");`);
|
|
94
|
+
return directives.join("\n");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
63
97
|
* Tailwind CSS transformer.
|
|
64
98
|
*
|
|
65
99
|
* Compiles CSS inside <style> tags in the DOM using
|
|
66
100
|
* @tailwindcss/postcss, then lowers modern CSS syntax with lightningcss.
|
|
67
101
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
102
|
+
* Configures Tailwind sources to scan:
|
|
103
|
+
* - Rendered class attributes (via `@source inline`) for all classes from all components
|
|
104
|
+
* - User project files (via Tailwind's auto-detection from base/from path)
|
|
105
|
+
*
|
|
106
|
+
* User `@source` and `@source not directives` in style tags are preserved.
|
|
107
|
+
* Source directives are only added to style tags that import Tailwind.
|
|
70
108
|
*
|
|
71
109
|
* Runs as the first transformer in the pipeline so that subsequent
|
|
72
110
|
* transformers (inliner, purge, etc.) work with fully compiled CSS.
|
|
73
111
|
*/
|
|
74
112
|
async function tailwindcss(dom, config, filePath) {
|
|
75
|
-
const sourceNotPaths = [resolve(config.output?.path ?? "dist"), ...(config.css?.exclude ?? []).map((p) => resolve(p))];
|
|
76
113
|
const styleTags = [];
|
|
77
114
|
walk(dom, (node) => {
|
|
78
115
|
if (node.name !== "style") return;
|
|
@@ -89,33 +126,24 @@ async function tailwindcss(dom, config, filePath) {
|
|
|
89
126
|
}
|
|
90
127
|
const rawContent = el.children.filter((child) => child.type === "text").map((child) => child.data).join("");
|
|
91
128
|
if (!rawContent.trim()) return;
|
|
92
|
-
let cssContent = decodeEntities(rawContent);
|
|
93
|
-
if (filePath) {
|
|
94
|
-
if (sourceNotPaths.length) {
|
|
95
|
-
const fileDir = dirname(filePath);
|
|
96
|
-
const exclusions = sourceNotPaths.map((p) => `@source not "${relative(fileDir, resolve(p))}";`).join("\n");
|
|
97
|
-
cssContent = `${cssContent}\n${exclusions}`;
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
const classes = [];
|
|
101
|
-
walk(dom, (n) => {
|
|
102
|
-
const cls = n.attribs?.class;
|
|
103
|
-
if (cls) classes.push(cls);
|
|
104
|
-
});
|
|
105
|
-
if (classes.length) cssContent = `${cssContent}\n@source inline("${classes.join(" ")}");`;
|
|
106
|
-
}
|
|
107
129
|
styleTags.push({
|
|
108
130
|
node: el,
|
|
109
|
-
cssContent
|
|
131
|
+
cssContent: decodeEntities(rawContent)
|
|
110
132
|
});
|
|
111
133
|
});
|
|
134
|
+
if (!styleTags.length) return dom;
|
|
135
|
+
const fromPath = filePath ?? resolve(process.cwd(), "template.vue");
|
|
136
|
+
const fromDir = dirname(fromPath);
|
|
137
|
+
const sourceDirectives = styleTags.some(({ cssContent }) => usesTailwind(cssContent)) ? buildSourceDirectives(dom, config, fromDir) : "";
|
|
138
|
+
const processor = createProcessor(config);
|
|
112
139
|
for (let i = 0; i < styleTags.length; i++) {
|
|
113
140
|
const { node, cssContent } = styleTags[i];
|
|
141
|
+
const fullCss = usesTailwind(cssContent) ? `${cssContent}\n${sourceDirectives}` : cssContent;
|
|
114
142
|
try {
|
|
115
143
|
node.children = [{
|
|
116
144
|
type: "text",
|
|
117
|
-
data: await optimizeCss(lowerSyntax((await
|
|
118
|
-
from: `${
|
|
145
|
+
data: await optimizeCss(lowerSyntax((await processor.process(fullCss, {
|
|
146
|
+
from: `${fromPath}?style=${i}`,
|
|
119
147
|
parser: safeParser
|
|
120
148
|
})).css), config),
|
|
121
149
|
parent: node
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tailwindcss.mjs","names":["pruneVars"],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport tailwindcssPostcss from '@tailwindcss/postcss'\nimport customProperties from 'postcss-custom-properties'\nimport pruneVars from '../plugins/postcss/pruneVars.ts'\nimport safeParser from 'postcss-safe-parser'\nimport { transform } from 'lightningcss'\nimport { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { tailwindCleanup } from '../plugins/postcss/tailwindCleanup.ts'\nimport { mergeMediaQueries } from '../plugins/postcss/mergeMediaQueries.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nfunction createProcessor(config: MaizzleConfig) {\n return postcss([\n tailwindcssPostcss({\n base: config.css?.base,\n transformAssetUrls: false,\n optimize: false, // we run Lightning CSS manually\n }),\n customProperties({\n preserve: false,\n }),\n pruneVars(),\n ])\n}\n\n/**\n * Decode HTML entities that Vue SSR encodes inside <style> tags.\n *\n * Vue's renderToString HTML-encodes quotes and other characters\n * inside <style> tags within templates, breaking CSS like\n * `@import \"@maizzle/tailwindcss\"` → `@import "..."`\n */\nfunction decodeEntities(str: string): string {\n return str\n .replace(/"/g, '\"')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n}\n\n/**\n * Lower modern CSS syntax using lightningcss.\n *\n * Targets IE 1 to maximize syntax lowering — converts modern features\n * like nesting, oklch(), color-mix(), @property, etc. into simple CSS\n * that email clients can understand.\n */\nfunction lowerSyntax(css: string): string {\n const result = transform({\n filename: 'email.css',\n code: Buffer.from(css),\n minify: false,\n targets: {\n ie: 4 << 5,\n },\n })\n\n return result.code.toString()\n}\n\n/**\n * Run cleanup and media query merging on the compiled CSS.\n *\n * Removes unwanted selectors (:host, :lang) and at-rules (@layer, @property),\n * then sorts and merges media queries.\n */\nasync function optimizeCss(css: string, config: MaizzleConfig): Promise<string> {\n const plugins: postcss.Plugin[] = [...tailwindCleanup(config)]\n\n const mediaPlugin = mergeMediaQueries(config)\n if (mediaPlugin) plugins.push(mediaPlugin)\n\n const result = await postcss(plugins).process(css, { from: undefined })\n\n return result.css\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 * Uses the AST walker to find <style> tags and decodes HTML entities\n * that Vue SSR encodes (e.g. ") before CSS processing.\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 sourceNotPaths = [\n resolve(config.output?.path ?? 'dist'),\n ...(config.css?.exclude ?? []).map(p => resolve(p)),\n ]\n\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 // Skip marked style tags, but remove the marker attribute first\n const markerAttr = ['raw', 'embed', 'data-embed'].find(attr => attr in attrs)\n if (markerAttr) {\n delete el.attribs[markerAttr]\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 let cssContent = decodeEntities(rawContent)\n\n if (filePath) {\n if (sourceNotPaths.length) {\n const fileDir = dirname(filePath)\n const exclusions = sourceNotPaths\n .map(p => `@source not \"${relative(fileDir, resolve(p))}\";`)\n .join('\\n')\n\n cssContent = `${cssContent}\\n${exclusions}`\n }\n } else {\n // No file path (e.g. component input) — extract classes from the DOM\n // and tell Tailwind to scan them via @source inline()\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 cssContent = `${cssContent}\\n@source inline(\"${classes.join(' ')}\");`\n }\n }\n\n styleTags.push({ node: el, cssContent })\n })\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n try {\n const processor = createProcessor(config)\n const result = await processor.process(\n cssContent,\n {\n from: `${filePath ?? resolve(process.cwd(), 'template.vue')}?style=${i}`,\n parser: safeParser,\n }\n )\n\n const lowered = lowerSyntax(result.css)\n const optimized = await optimizeCss(lowered, config)\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 // If CSS processing fails, still replace with decoded content\n // so HTML entities don't break the CSS\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAS,gBAAgB,QAAuB;AAC9C,QAAO,QAAQ;EACb,mBAAmB;GACjB,MAAM,OAAO,KAAK;GAClB,oBAAoB;GACpB,UAAU;GACX,CAAC;EACF,iBAAiB,EACf,UAAU,OACX,CAAC;EACFA,mBAAW;EACZ,CAAC;;;;;;;;;AAUJ,SAAS,eAAe,KAAqB;AAC3C,QAAO,IACJ,QAAQ,WAAW,KAAI,CACvB,QAAQ,UAAU,IAAI,CACtB,QAAQ,SAAS,IAAI,CACrB,QAAQ,SAAS,IAAI,CACrB,QAAQ,UAAU,IAAI,CACtB,QAAQ,WAAW,IAAI;;;;;;;;;AAU5B,SAAS,YAAY,KAAqB;AAUxC,QATe,UAAU;EACvB,UAAU;EACV,MAAM,OAAO,KAAK,IAAI;EACtB,QAAQ;EACR,SAAS,EACP,IAAI,KACL;EACF,CAAC,CAEY,KAAK,UAAU;;;;;;;;AAS/B,eAAe,YAAY,KAAa,QAAwC;CAC9E,MAAM,UAA4B,CAAC,GAAG,gBAAgB,OAAO,CAAC;CAE9D,MAAM,cAAc,kBAAkB,OAAO;AAC7C,KAAI,YAAa,SAAQ,KAAK,YAAY;AAI1C,SAFe,MAAM,QAAQ,QAAQ,CAAC,QAAQ,KAAK,EAAE,MAAM,QAAW,CAAC,EAEzD;;;;;;;;;;;;;;AAehB,eAAsB,YAAY,KAAkB,QAAuB,UAAyC;CAClH,MAAM,iBAAiB,CACrB,QAAQ,OAAO,QAAQ,QAAQ,OAAO,EACtC,IAAI,OAAO,KAAK,WAAW,EAAE,EAAE,KAAI,MAAK,QAAQ,EAAE,CAAC,CACpD;CAED,MAAM,YAAqD,EAAE;AAE7D,MAAK,MAAM,SAAS;AAClB,MAAK,KAAiB,SAAS,QAAS;EAExC,MAAM,KAAK;EACX,MAAM,QAAQ,GAAG,WAAW,EAAE;EAG9B,MAAM,aAAa;GAAC;GAAO;GAAS;GAAa,CAAC,MAAK,SAAQ,QAAQ,MAAM;AAC7E,MAAI,YAAY;AACd,UAAO,GAAG,QAAQ;AAClB;;EAIF,MAAM,aAAa,GAAG,SACnB,QAAO,UAAS,MAAM,SAAS,OAAO,CACtC,KAAI,UAAU,MAAc,KAAK,CACjC,KAAK,GAAG;AAEX,MAAI,CAAC,WAAW,MAAM,CAAE;EAExB,IAAI,aAAa,eAAe,WAAW;AAE3C,MAAI,UACF;OAAI,eAAe,QAAQ;IACzB,MAAM,UAAU,QAAQ,SAAS;IACjC,MAAM,aAAa,eAChB,KAAI,MAAK,gBAAgB,SAAS,SAAS,QAAQ,EAAE,CAAC,CAAC,IAAI,CAC3D,KAAK,KAAK;AAEb,iBAAa,GAAG,WAAW,IAAI;;SAE5B;GAGL,MAAM,UAAoB,EAAE;AAC5B,QAAK,MAAM,MAAM;IACf,MAAM,MAAO,EAAc,SAAS;AACpC,QAAI,IAAK,SAAQ,KAAK,IAAI;KAC1B;AAEF,OAAI,QAAQ,OACV,cAAa,GAAG,WAAW,oBAAoB,QAAQ,KAAK,IAAI,CAAC;;AAIrE,YAAU,KAAK;GAAE,MAAM;GAAI;GAAY,CAAC;GACxC;AAEF,MAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;EACzC,MAAM,EAAE,MAAM,eAAe,UAAU;AACvC,MAAI;AAcF,QAAK,WAAW,CAAC;IACf,MAAM;IACN,MALgB,MAAM,YADR,aARD,MADG,gBAAgB,OAAO,CACV,QAC7B,YACA;KACE,MAAM,GAAG,YAAY,QAAQ,QAAQ,KAAK,EAAE,eAAe,CAAC,SAAS;KACrE,QAAQ;KACT,CACF,EAEkC,IAAI,EACM,OAAO;IAMlD,QAAQ;IACT,CAAQ;UACH;AAGN,QAAK,WAAW,CAAC;IACf,MAAM;IACN,MAAM;IACN,QAAQ;IACT,CAAQ;;;AAIb,QAAO"}
|
|
1
|
+
{"version":3,"file":"tailwindcss.mjs","names":["pruneVars"],"sources":["../../src/transformers/tailwindcss.ts"],"sourcesContent":["import postcss from 'postcss'\nimport tailwindcssPostcss from '@tailwindcss/postcss'\nimport customProperties from 'postcss-custom-properties'\nimport postcssCalc from 'postcss-calc'\nimport pruneVars from '../plugins/postcss/pruneVars.ts'\nimport safeParser from 'postcss-safe-parser'\nimport { transform } from 'lightningcss'\nimport { resolve, dirname, relative } from 'node:path'\nimport type { ChildNode, Element } from 'domhandler'\nimport { walk } from '../utils/ast/index.ts'\nimport { tailwindCleanup } from '../plugins/postcss/tailwindCleanup.ts'\nimport { mergeMediaQueries } from '../plugins/postcss/mergeMediaQueries.ts'\nimport type { MaizzleConfig } from '../types/config.ts'\n\nfunction createProcessor(config: MaizzleConfig) {\n return postcss([\n tailwindcssPostcss({\n base: config.css?.base,\n transformAssetUrls: false,\n optimize: false, // we run Lightning CSS manually\n }),\n customProperties({\n preserve: false,\n }),\n postcssCalc({}),\n pruneVars(),\n ])\n}\n\n/**\n * Decode HTML entities that Vue SSR encodes inside <style> tags.\n *\n * Vue's renderToString HTML-encodes quotes and other characters\n * inside <style> tags within templates, breaking CSS like\n * `@import \"@maizzle/tailwindcss\"` → `@import "..."`\n */\nfunction decodeEntities(str: string): string {\n return str\n .replace(/"/g, '\"')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n}\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\\s+[\"'](tailwindcss|@maizzle\\/tailwindcss)|@tailwind\\s)/.test(css)\n}\n\n/**\n * Lower modern CSS syntax using lightningcss.\n *\n * Targets IE 1 to maximize syntax lowering — converts modern features\n * like nesting, oklch(), color-mix(), @property, etc. into simple CSS\n * that email clients can understand.\n */\nfunction lowerSyntax(css: string): string {\n const result = transform({\n filename: 'email.css',\n code: Buffer.from(css),\n minify: false,\n targets: {\n ie: 4 << 5,\n },\n })\n\n return result.code.toString()\n}\n\n/**\n * Run cleanup and media query merging on the compiled CSS.\n *\n * Removes unwanted selectors (:host, :lang) and at-rules (@layer, @property),\n * then sorts and merges media queries.\n */\nasync function optimizeCss(css: string, config: MaizzleConfig): Promise<string> {\n const plugins: postcss.Plugin[] = [...tailwindCleanup(config)]\n\n const mediaPlugin = mergeMediaQueries(config)\n if (mediaPlugin) plugins.push(mediaPlugin)\n\n const result = await postcss(plugins).process(css, { from: undefined })\n\n return result.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 // 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 bindings).\n // We pass these raw values to Tailwind's scanner via @source inline().\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 // Skip marked style tags, but remove the marker attribute first\n const markerAttr = ['raw', 'embed', 'data-embed'].find(attr => attr in attrs)\n if (markerAttr) {\n delete el.attribs[markerAttr]\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: decodeEntities(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 // Create processor once — reused for all style tags in this template\n const processor = createProcessor(config)\n\n for (let i = 0; i < styleTags.length; i++) {\n const { node, cssContent } = styleTags[i]\n\n // Only add source directives to style tags that import Tailwind —\n // plain CSS doesn't need them and @tailwindcss/postcss would leave\n // the directives unconsumed in the output\n const fullCss = usesTailwind(cssContent)\n ? `${cssContent}\\n${sourceDirectives}`\n : cssContent\n\n try {\n const result = await processor.process(\n fullCss,\n {\n from: `${fromPath}?style=${i}`,\n parser: safeParser,\n }\n )\n\n const lowered = lowerSyntax(result.css)\n const optimized = await optimizeCss(lowered, config)\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 // If CSS processing fails, still replace with decoded content\n // so HTML entities don't break the CSS\n node.children = [{\n type: 'text',\n data: cssContent,\n parent: node,\n } as any]\n }\n }\n\n return dom\n}\n"],"mappings":"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"}
|
package/dist/types/config.d.mts
CHANGED
|
@@ -334,6 +334,8 @@ interface HtmlConfig {
|
|
|
334
334
|
*/
|
|
335
335
|
minify?: boolean | Record<string, unknown>;
|
|
336
336
|
}
|
|
337
|
+
type FilterFunction = (str: string, value: string) => string;
|
|
338
|
+
type FiltersConfig = false | Record<string, FilterFunction>;
|
|
337
339
|
interface MaizzleConfig {
|
|
338
340
|
/**
|
|
339
341
|
* Root directory for the Maizzle email project.
|
|
@@ -457,6 +459,18 @@ interface MaizzleConfig {
|
|
|
457
459
|
* }
|
|
458
460
|
*/
|
|
459
461
|
replaceStrings?: Record<string, string>;
|
|
462
|
+
/**
|
|
463
|
+
* Content filters that transform text inside HTML elements using custom attributes.
|
|
464
|
+
*
|
|
465
|
+
* Set to `false` to disable all filters. Pass an object to add custom filters
|
|
466
|
+
* (merged with built-in defaults).
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* filters: {
|
|
470
|
+
* uppercase: str => str.toUpperCase(),
|
|
471
|
+
* }
|
|
472
|
+
*/
|
|
473
|
+
filters?: FiltersConfig;
|
|
460
474
|
/** URL transformation settings (base URL, query string appending). */
|
|
461
475
|
url?: UrlConfig;
|
|
462
476
|
/** HTML post-processing settings (attributes, formatting, minification). */
|
|
@@ -490,5 +504,5 @@ interface MaizzleConfig {
|
|
|
490
504
|
[key: string]: any;
|
|
491
505
|
}
|
|
492
506
|
//#endregion
|
|
493
|
-
export { AttributesConfig, CssConfig, EntitiesConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
|
|
507
|
+
export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
|
|
494
508
|
//# sourceMappingURL=config.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":";;;;;UAAiB,eAAA;;;;;AAAjB;EAME,IAAA;;;;;;EAMA,UAAA;EAYK;;;AAGP;;EATE,MAAA;EAU0B;;;;;EAJ1B,EAAA,GAAK,MAAA;AAAA;AAAA,KAGK,QAAA,GAAW,MAAA;EACrB,QAAA,GAAW,eAAA;AAAA;AAAA,UAGI,SAAA;EA2BK;;;;;;;;;;;EAfpB,KAAA,GAAQ,QAAA;EAiBO;;;;;AAQjB;;;;;EAdE,IAAA;IA2KiB,+BAzKf,GAAA,WA4LmB;IA1LnB,IAAA,cAAkB,MAAA,SAAe,MAAA,6BA0LR;IAxLzB,UAAA,GAAa,MAAA,kBAsBf;IApBE,QAAA,YAkCF;IAhCE,SAAA;EAAA;AAAA;AAAA,UAIa,SAAA;EA2Db;;;;;EArDF,IAAA;EAwFE;;;;;;;EAhFF,KAAA,aAAkB,MAAA;EAoHhB;;;;;;;;;;;;;EAtGF,MAAA;IA6JO;;AAGT;;;;IAzJI,gBAAA;IAkLuD;;;;;IA5KvD,eAAA;IAiK2B;;;;;IA3J3B,sBAAA;IAsK6D;;AAGjE;;;IAnKI,oBAAA;IAmKyC;AAE7C;;;;IA/JI,QAAA;IAwLa;;;;;;;;;;IA7Kb,gBAAA,GAAmB,MAAA;IAuLrB;;;;;IAjLE,oBAAA;IA6LuB;;AAG3B;;;IA1LI,qBAAA;
|
|
1
|
+
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":"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"}
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AttributesConfig, CssConfig, EntitiesConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
|
|
2
|
-
export { type AttributesConfig, type CssConfig, type EntitiesConfig, type HtmlConfig, type MaizzleConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
|
|
1
|
+
import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
|
|
2
|
+
export { type AttributesConfig, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ChildNode
|
|
1
|
+
import { ChildNode } from "domhandler";
|
|
2
|
+
import { DomSerializerOptions } from "dom-serializer";
|
|
2
3
|
|
|
3
4
|
//#region src/utils/ast/serializer.d.ts
|
|
4
|
-
declare function serialize(dom: ChildNode[], options?:
|
|
5
|
+
declare function serialize(dom: ChildNode[], options?: DomSerializerOptions): string;
|
|
5
6
|
//#endregion
|
|
6
7
|
export { serialize };
|
|
7
8
|
//# sourceMappingURL=serializer.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.mts","names":[],"sources":["../../../src/utils/ast/serializer.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"serializer.d.mts","names":[],"sources":["../../../src/utils/ast/serializer.ts"],"mappings":";;;;iBA6BgB,SAAA,CAAU,GAAA,EAAK,SAAA,IAAa,OAAA,GAAU,oBAAA"}
|
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
import { walk } from "./walker.mjs";
|
|
1
2
|
import render from "dom-serializer";
|
|
2
3
|
|
|
3
4
|
//#region src/utils/ast/serializer.ts
|
|
5
|
+
/**
|
|
6
|
+
* Re-encode < and > as entities in text nodes inside <code> elements.
|
|
7
|
+
*
|
|
8
|
+
* The DOM parser decodes entities like < into raw < in text nodes.
|
|
9
|
+
* With encodeEntities: false the serializer outputs them as-is, which
|
|
10
|
+
* creates broken HTML (e.g. </a> inside a code block closes the real tag).
|
|
11
|
+
*
|
|
12
|
+
* We selectively fix this for <code> contents only, so the rest of the
|
|
13
|
+
* document (where encodeEntities: false is needed) is unaffected.
|
|
14
|
+
*/
|
|
15
|
+
function encodeCodeTextNodes(dom) {
|
|
16
|
+
walk(dom, (node) => {
|
|
17
|
+
const el = node;
|
|
18
|
+
if (el.name !== "code") return;
|
|
19
|
+
walk(el.children ?? [], (child) => {
|
|
20
|
+
if (child.type === "text") {
|
|
21
|
+
const text = child;
|
|
22
|
+
text.data = text.data.replace(/</g, "<").replace(/>/g, ">");
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
4
27
|
function serialize(dom, options) {
|
|
28
|
+
encodeCodeTextNodes(dom);
|
|
5
29
|
return render(dom, {
|
|
6
30
|
encodeEntities: false,
|
|
7
31
|
...options
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.mjs","names":[],"sources":["../../../src/utils/ast/serializer.ts"],"sourcesContent":["import render from 'dom-serializer'\nimport type { ChildNode
|
|
1
|
+
{"version":3,"file":"serializer.mjs","names":[],"sources":["../../../src/utils/ast/serializer.ts"],"sourcesContent":["import render from 'dom-serializer'\nimport type { ChildNode } from 'domhandler'\nimport type { DomSerializerOptions } from 'dom-serializer'\nimport { walk } from './walker.ts'\n\n/**\n * Re-encode < and > as entities in text nodes inside <code> elements.\n *\n * The DOM parser decodes entities like < into raw < in text nodes.\n * With encodeEntities: false the serializer outputs them as-is, which\n * creates broken HTML (e.g. </a> inside a code block closes the real tag).\n *\n * We selectively fix this for <code> contents only, so the rest of the\n * document (where encodeEntities: false is needed) is unaffected.\n */\nfunction encodeCodeTextNodes(dom: ChildNode[]): void {\n walk(dom, (node) => {\n const el = node as import('domhandler').Element\n if (el.name !== 'code') return\n\n walk(el.children ?? [], (child) => {\n if (child.type === 'text') {\n const text = child as import('domhandler').Text\n text.data = text.data.replace(/</g, '<').replace(/>/g, '>')\n }\n })\n })\n}\n\nexport function serialize(dom: ChildNode[], options?: DomSerializerOptions): string {\n encodeCodeTextNodes(dom)\n return render(dom, { encodeEntities: false, ...options })\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAS,oBAAoB,KAAwB;AACnD,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,OAAQ;AAExB,OAAK,GAAG,YAAY,EAAE,GAAG,UAAU;AACjC,OAAI,MAAM,SAAS,QAAQ;IACzB,MAAM,OAAO;AACb,SAAK,OAAO,KAAK,KAAK,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;IAEnE;GACF;;AAGJ,SAAgB,UAAU,KAAkB,SAAwC;AAClF,qBAAoB,IAAI;AACxB,QAAO,OAAO,KAAK;EAAE,gBAAgB;EAAO,GAAG;EAAS,CAAC"}
|