@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.8
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/components/Body.vue +105 -36
- package/dist/components/Button.vue +4 -1
- package/dist/components/CodeBlock.vue +11 -18
- package/dist/components/CodeInline.vue +6 -1
- package/dist/components/Column.vue +30 -5
- package/dist/components/Container.vue +10 -2
- package/dist/components/Divider.vue +28 -0
- package/dist/components/Head.vue +22 -0
- package/dist/components/Heading.vue +28 -0
- package/dist/components/Html.vue +98 -47
- package/dist/components/Layout.vue +93 -0
- package/dist/components/Link.vue +26 -0
- package/dist/components/Markdown.vue +83 -0
- package/dist/components/Outlook.vue +36 -0
- package/dist/components/Overlap.vue +25 -5
- package/dist/components/{Preview.vue → Preheader.vue} +1 -1
- package/dist/components/Row.vue +16 -5
- package/dist/components/Section.vue +83 -0
- package/dist/components/Text.vue +29 -0
- package/dist/components/Vml.vue +165 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
- package/dist/render/createRenderer.d.mts +2 -3
- package/dist/render/createRenderer.d.mts.map +1 -1
- package/dist/render/createRenderer.mjs +67 -4
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +84 -4
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +1 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +30 -16
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/email.d.mts +17 -0
- package/dist/server/email.d.mts.map +1 -0
- package/dist/server/email.mjs +41 -0
- package/dist/server/email.mjs.map +1 -0
- package/dist/server/linter.d.mts +1 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +60 -71
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/ui/App.vue +205 -69
- package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
- package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
- package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
- package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
- package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
- package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
- package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
- package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
- package/dist/server/ui/components/ui/toggle/index.ts +3 -3
- package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
- package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
- package/dist/server/ui/main.css +20 -20
- package/dist/server/ui/pages/Home.vue +12 -5
- package/dist/server/ui/pages/Preview.vue +495 -211
- package/dist/transformers/inlineCSS.d.mts +1 -14
- package/dist/transformers/inlineCSS.d.mts.map +1 -1
- package/dist/transformers/inlineCSS.mjs +25 -34
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/purgeCSS.d.mts.map +1 -1
- package/dist/transformers/purgeCSS.mjs +67 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.mjs +3 -7
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +47 -29
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/package.json +7 -3
- package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
- package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
- package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
- package/dist/server/ui/components/ui/resizable/index.ts +0 -3
|
@@ -9,20 +9,7 @@ import { ChildNode } from "domhandler";
|
|
|
9
9
|
* This is important for email client compatibility (especially Outlook on Windows).
|
|
10
10
|
*
|
|
11
11
|
* Enabled when `css.inline` is set to `true` or an object with options.
|
|
12
|
-
*
|
|
13
|
-
* Options:
|
|
14
|
-
* - removeStyleTags: Remove style tags after inlining (default: false)
|
|
15
|
-
* - removeInlinedSelectors: Remove classes after they've been inlined (default: true)
|
|
16
|
-
* - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)
|
|
17
|
-
* - safelist: Selectors that should not be removed after inlining
|
|
18
|
-
* - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)
|
|
19
|
-
* - applyWidthAttributes: Add width attributes based on inline CSS (default: true)
|
|
20
|
-
* - applyHeightAttributes: Add height attributes based on inline CSS (default: true)
|
|
21
|
-
* - widthElements: Elements that can receive width attributes (default: ['img', 'video'])
|
|
22
|
-
* - heightElements: Elements that can receive height attributes (default: ['img', 'video'])
|
|
23
|
-
* - excludedProperties: CSS properties to exclude from inlining
|
|
24
|
-
* - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })
|
|
25
|
-
* - customCSS: Additional CSS to inline
|
|
12
|
+
* All Juice options are supported and passed through directly.
|
|
26
13
|
*/
|
|
27
14
|
declare function inlineCSS(dom: ChildNode[], config?: CssConfig): ChildNode[];
|
|
28
15
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inlineCSS.d.mts","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"inlineCSS.d.mts","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"mappings":";;;;;;AAeA;;;;;;;iBAAgB,SAAA,CAAU,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
|
|
@@ -12,32 +12,17 @@ import juice from "juice";
|
|
|
12
12
|
* This is important for email client compatibility (especially Outlook on Windows).
|
|
13
13
|
*
|
|
14
14
|
* Enabled when `css.inline` is set to `true` or an object with options.
|
|
15
|
-
*
|
|
16
|
-
* Options:
|
|
17
|
-
* - removeStyleTags: Remove style tags after inlining (default: false)
|
|
18
|
-
* - removeInlinedSelectors: Remove classes after they've been inlined (default: true)
|
|
19
|
-
* - preferUnitlessValues: Convert 0px, 0em, etc. to 0 (default: true)
|
|
20
|
-
* - safelist: Selectors that should not be removed after inlining
|
|
21
|
-
* - styleToAttribute: Map CSS properties to HTML attributes (e.g., background-color -> bgcolor)
|
|
22
|
-
* - applyWidthAttributes: Add width attributes based on inline CSS (default: true)
|
|
23
|
-
* - applyHeightAttributes: Add height attributes based on inline CSS (default: true)
|
|
24
|
-
* - widthElements: Elements that can receive width attributes (default: ['img', 'video'])
|
|
25
|
-
* - heightElements: Elements that can receive height attributes (default: ['img', 'video'])
|
|
26
|
-
* - excludedProperties: CSS properties to exclude from inlining
|
|
27
|
-
* - codeBlocks: Fenced code blocks to ignore (default: { EJS: { start: '<%', end: '%>' }, HBS: { start: '{{', end: '}}' } })
|
|
28
|
-
* - customCSS: Additional CSS to inline
|
|
15
|
+
* All Juice options are supported and passed through directly.
|
|
29
16
|
*/
|
|
30
17
|
function inlineCSS(dom, config = {}) {
|
|
31
18
|
const inline = config.inline;
|
|
32
19
|
if (!inline) return dom;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
juice.
|
|
37
|
-
juice.
|
|
38
|
-
|
|
39
|
-
juice.heightElements = (options.heightElements ?? ["img", "video"]).map((i) => i.toUpperCase());
|
|
40
|
-
if (options.codeBlocks && typeof options.codeBlocks === "object") Object.entries(options.codeBlocks).forEach(([key, value]) => {
|
|
20
|
+
const { preferUnitlessValues = true, safelist, customCSS = "", styleToAttribute, excludedProperties, widthElements, heightElements, codeBlocks, ...juicePassthrough } = typeof inline === "object" ? inline : {};
|
|
21
|
+
juice.styleToAttribute = styleToAttribute ?? {};
|
|
22
|
+
juice.excludedProperties = ["--tw-shadow", ...excludedProperties ?? []];
|
|
23
|
+
juice.widthElements = (widthElements ?? ["img", "video"]).map((i) => i.toUpperCase());
|
|
24
|
+
juice.heightElements = (heightElements ?? ["img", "video"]).map((i) => i.toUpperCase());
|
|
25
|
+
if (codeBlocks && typeof codeBlocks === "object") Object.entries(codeBlocks).forEach(([key, value]) => {
|
|
41
26
|
if (value.start && value.end) juice.codeBlocks[key] = value;
|
|
42
27
|
});
|
|
43
28
|
walk(dom, (node) => {
|
|
@@ -45,32 +30,38 @@ function inlineCSS(dom, config = {}) {
|
|
|
45
30
|
if (el.name === "style" && el.attribs) {
|
|
46
31
|
if (el.attribs.embed && !("data-embed" in el.attribs)) el.attribs["data-embed"] = "";
|
|
47
32
|
if (el.attribs["data-embed"] && !("embed" in el.attribs)) el.attribs.embed = "";
|
|
33
|
+
if ("data-embed" in el.attribs) el.attribs["data-maizzle-embed"] = "";
|
|
48
34
|
}
|
|
49
35
|
});
|
|
50
36
|
const serialized = serialize(dom);
|
|
51
37
|
let inlinedHtml;
|
|
52
38
|
try {
|
|
53
|
-
|
|
54
|
-
removeStyleTags,
|
|
55
|
-
removeInlinedSelectors:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
...
|
|
62
|
-
extraCss: customCSS
|
|
39
|
+
inlinedHtml = juice(serialized, {
|
|
40
|
+
removeStyleTags: juicePassthrough.removeStyleTags ?? false,
|
|
41
|
+
removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true,
|
|
42
|
+
applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true,
|
|
43
|
+
applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true,
|
|
44
|
+
preservedSelectors: safelist ?? [],
|
|
45
|
+
...customCSS ? { extraCss: customCSS } : {},
|
|
46
|
+
inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? true,
|
|
47
|
+
...juicePassthrough
|
|
63
48
|
});
|
|
64
|
-
else inlinedHtml = juice(serialized, juiceOptions);
|
|
65
49
|
} catch {
|
|
66
50
|
return dom;
|
|
67
51
|
}
|
|
68
|
-
const preferUnitlessValues = options.preferUnitlessValues ?? true;
|
|
69
52
|
const result = parse(inlinedHtml);
|
|
70
53
|
if (preferUnitlessValues) walk(result, (node) => {
|
|
71
54
|
const el = node;
|
|
72
55
|
if (el.attribs?.style) el.attribs.style = el.attribs.style.replace(/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g, "0");
|
|
73
56
|
});
|
|
57
|
+
walk(result, (node) => {
|
|
58
|
+
const el = node;
|
|
59
|
+
if (el.name === "style" && el.attribs && "data-maizzle-embed" in el.attribs) {
|
|
60
|
+
el.attribs["data-embed"] = "";
|
|
61
|
+
el.attribs.embed = "";
|
|
62
|
+
delete el.attribs["data-maizzle-embed"];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
74
65
|
return result;
|
|
75
66
|
}
|
|
76
67
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inlineCSS.mjs","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"sourcesContent":["import juice from 'juice'\nimport { walk, parse, serialize } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Options as JuiceOptions } from 'juice'\nimport type { CssConfig } from '../types/config.ts'\n\
|
|
1
|
+
{"version":3,"file":"inlineCSS.mjs","names":[],"sources":["../../src/transformers/inlineCSS.ts"],"sourcesContent":["import juice from 'juice'\nimport { walk, parse, serialize } from '../utils/ast/index.ts'\nimport type { ChildNode, Element } from 'domhandler'\nimport type { Options as JuiceOptions } from 'juice'\nimport type { CssConfig } from '../types/config.ts'\n\n/**\n * Inline CSS transformer.\n *\n * Inlines CSS from `<style>` tags into inline style attributes on HTML elements.\n * This is important for email client compatibility (especially Outlook on Windows).\n *\n * Enabled when `css.inline` is set to `true` or an object with options.\n * All Juice options are supported and passed through directly.\n */\nexport function inlineCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const inline = config.inline\n\n // Disabled when inline is falsy or not an object/truthy\n if (!inline) {\n return dom\n }\n\n // Build options from config\n const options = typeof inline === 'object' ? inline : {}\n\n // Separate Maizzle-specific options from Juice options\n const {\n preferUnitlessValues = true,\n safelist,\n customCSS = '',\n styleToAttribute,\n excludedProperties,\n widthElements,\n heightElements,\n codeBlocks,\n ...juicePassthrough\n } = options\n\n // Configure Juice static properties\n juice.styleToAttribute = styleToAttribute ?? {}\n juice.excludedProperties = ['--tw-shadow', ...(excludedProperties ?? [])]\n juice.widthElements = (widthElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n juice.heightElements = (heightElements ?? ['img', 'video']).map(i => i.toUpperCase()) as unknown as HTMLElement[]\n\n // Add custom code blocks\n if (codeBlocks && typeof codeBlocks === 'object') {\n Object.entries(codeBlocks).forEach(([key, value]) => {\n if (value.start && value.end) {\n juice.codeBlocks[key] = value\n }\n })\n }\n\n // Handle style tags with embed attributes.\n // We add a marker attribute that persists through the pipeline,\n // then restore data-embed from it after Juice runs.\n walk(dom, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs) {\n // Sync data-embed ↔ embed\n if (el.attribs.embed && !('data-embed' in el.attribs)) {\n el.attribs['data-embed'] = ''\n }\n if (el.attribs['data-embed'] && !('embed' in el.attribs)) {\n el.attribs.embed = ''\n }\n\n // Add marker that persists through the pipeline\n if ('data-embed' in el.attribs) {\n el.attribs['data-maizzle-embed'] = ''\n }\n }\n })\n\n // Serialize for juice (juice requires a string)\n const serialized = serialize(dom)\n\n let inlinedHtml: string\n\n try {\n const juiceOptions: JuiceOptions = {\n removeStyleTags: juicePassthrough.removeStyleTags ?? false,\n removeInlinedSelectors: juicePassthrough.removeInlinedSelectors ?? true,\n applyWidthAttributes: juicePassthrough.applyWidthAttributes ?? true,\n applyHeightAttributes: juicePassthrough.applyHeightAttributes ?? true,\n preservedSelectors: safelist ?? [],\n ...customCSS ? { extraCss: customCSS } : {},\n inlineDuplicateProperties: juicePassthrough.inlineDuplicateProperties ?? true,\n ...juicePassthrough,\n }\n\n inlinedHtml = juice(serialized, juiceOptions)\n } catch {\n // If Juice fails, return the dom unchanged\n return dom\n }\n\n // Post-process for preferUnitlessValues\n const result = parse(inlinedHtml)\n\n if (preferUnitlessValues) {\n walk(result, (node) => {\n const el = node as Element\n if (el.attribs?.style) {\n el.attribs.style = el.attribs.style.replace(\n /\\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\\b/g,\n '0'\n )\n }\n })\n }\n\n // Restore data-embed from our marker, then remove the marker.\n // The purge step will handle final data-embed/embed removal.\n walk(result, (node) => {\n const el = node as Element\n if (el.name === 'style' && el.attribs && 'data-maizzle-embed' in el.attribs) {\n el.attribs['data-embed'] = ''\n el.attribs.embed = ''\n delete el.attribs['data-maizzle-embed']\n }\n })\n\n return result\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,UAAU,KAAkB,SAAoB,EAAE,EAAe;CAC/E,MAAM,SAAS,OAAO;AAGtB,KAAI,CAAC,OACH,QAAO;CAOT,MAAM,EACJ,uBAAuB,MACvB,UACA,YAAY,IACZ,kBACA,oBACA,eACA,gBACA,YACA,GAAG,qBAZW,OAAO,WAAW,WAAW,SAAS,EAAE;AAgBxD,OAAM,mBAAmB,oBAAoB,EAAE;AAC/C,OAAM,qBAAqB,CAAC,eAAe,GAAI,sBAAsB,EAAE,CAAE;AACzE,OAAM,iBAAiB,iBAAiB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AACnF,OAAM,kBAAkB,kBAAkB,CAAC,OAAO,QAAQ,EAAE,KAAI,MAAK,EAAE,aAAa,CAAC;AAGrF,KAAI,cAAc,OAAO,eAAe,SACtC,QAAO,QAAQ,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW;AACnD,MAAI,MAAM,SAAS,MAAM,IACvB,OAAM,WAAW,OAAO;GAE1B;AAMJ,MAAK,MAAM,SAAS;EAClB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS;AAErC,OAAI,GAAG,QAAQ,SAAS,EAAE,gBAAgB,GAAG,SAC3C,IAAG,QAAQ,gBAAgB;AAE7B,OAAI,GAAG,QAAQ,iBAAiB,EAAE,WAAW,GAAG,SAC9C,IAAG,QAAQ,QAAQ;AAIrB,OAAI,gBAAgB,GAAG,QACrB,IAAG,QAAQ,wBAAwB;;GAGvC;CAGF,MAAM,aAAa,UAAU,IAAI;CAEjC,IAAI;AAEJ,KAAI;AAYF,gBAAc,MAAM,YAXe;GACjC,iBAAiB,iBAAiB,mBAAmB;GACrD,wBAAwB,iBAAiB,0BAA0B;GACnE,sBAAsB,iBAAiB,wBAAwB;GAC/D,uBAAuB,iBAAiB,yBAAyB;GACjE,oBAAoB,YAAY,EAAE;GAClC,GAAG,YAAY,EAAE,UAAU,WAAW,GAAG,EAAE;GAC3C,2BAA2B,iBAAiB,6BAA6B;GACzE,GAAG;GACJ,CAE4C;SACvC;AAEN,SAAO;;CAIT,MAAM,SAAS,MAAM,YAAY;AAEjC,KAAI,qBACF,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,MACd,IAAG,QAAQ,QAAQ,GAAG,QAAQ,MAAM,QAClC,4DACA,IACD;GAEH;AAKJ,MAAK,SAAS,SAAS;EACrB,MAAM,KAAK;AACX,MAAI,GAAG,SAAS,WAAW,GAAG,WAAW,wBAAwB,GAAG,SAAS;AAC3E,MAAG,QAAQ,gBAAgB;AAC3B,MAAG,QAAQ,QAAQ;AACnB,UAAO,GAAG,QAAQ;;GAEpB;AAEF,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purgeCSS.d.mts","names":[],"sources":["../../src/transformers/purgeCSS.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"purgeCSS.d.mts","names":[],"sources":["../../src/transformers/purgeCSS.ts"],"mappings":";;;;;;AAkDA;;;;;;;;;;;;;iBAAgB,QAAA,CAAS,GAAA,EAAK,SAAA,IAAa,MAAA,GAAQ,SAAA,GAAiB,SAAA"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { parse } from "../utils/ast/parser.mjs";
|
|
2
|
+
import { walk } from "../utils/ast/walker.mjs";
|
|
2
3
|
import { serialize } from "../utils/ast/serializer.mjs";
|
|
3
4
|
import "../utils/ast/index.mjs";
|
|
4
5
|
import { defu } from "defu";
|
|
6
|
+
import postcss from "postcss";
|
|
7
|
+
import safeParser from "postcss-safe-parser";
|
|
8
|
+
import { selectAll } from "css-select";
|
|
5
9
|
import { comb } from "email-comb";
|
|
6
10
|
|
|
7
11
|
//#region src/transformers/purgeCSS.ts
|
|
@@ -57,8 +61,70 @@ function purgeCSS(dom, config = {}) {
|
|
|
57
61
|
...restUserOptions,
|
|
58
62
|
whitelist: [...DEFAULT_SAFELIST, ...userSafelist]
|
|
59
63
|
}, DEFAULT_OPTIONS);
|
|
64
|
+
const safelist = [...DEFAULT_SAFELIST, ...userSafelist];
|
|
65
|
+
dom = deepPurge(dom, safelist);
|
|
60
66
|
const { result } = comb(serialize(dom), options);
|
|
61
|
-
|
|
67
|
+
let purgedDom = parse(result);
|
|
68
|
+
walk(purgedDom, (node) => {
|
|
69
|
+
const el = node;
|
|
70
|
+
if (el.name === "style" && el.attribs) {
|
|
71
|
+
delete el.attribs["data-embed"];
|
|
72
|
+
delete el.attribs.embed;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return purgedDom;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Deep purge: uses PostCSS to parse CSS in non-embedded style tags,
|
|
79
|
+
* then checks each selector against the DOM with css-select.
|
|
80
|
+
* Removes rules where no selector matches any element.
|
|
81
|
+
*/
|
|
82
|
+
function isSafelisted(selector, safelist) {
|
|
83
|
+
return safelist.some((pattern) => {
|
|
84
|
+
if (pattern.startsWith("*") && pattern.endsWith("*")) return selector.includes(pattern.slice(1, -1));
|
|
85
|
+
if (pattern.endsWith("*")) return selector.startsWith(pattern.slice(0, -1));
|
|
86
|
+
if (pattern.startsWith("*")) return selector.endsWith(pattern.slice(1));
|
|
87
|
+
return selector === pattern;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function deepPurge(dom, safelist) {
|
|
91
|
+
walk(dom, (node) => {
|
|
92
|
+
const el = node;
|
|
93
|
+
if (el.name !== "style" || !el.attribs) return;
|
|
94
|
+
if ("data-embed" in el.attribs) return;
|
|
95
|
+
const textNode = el.children?.find((c) => c.type === "text");
|
|
96
|
+
if (!textNode?.data?.trim()) return;
|
|
97
|
+
const root = postcss.parse(textNode.data, { parser: safeParser });
|
|
98
|
+
root.walkRules((rule) => {
|
|
99
|
+
if (rule.parent?.type === "atrule") return;
|
|
100
|
+
const selectors = rule.selectors ?? [rule.selector];
|
|
101
|
+
const matched = selectors.filter((sel) => {
|
|
102
|
+
if (isSafelisted(sel, safelist)) return true;
|
|
103
|
+
if (/::[\w-]/.test(sel)) return true;
|
|
104
|
+
if (/(?<!:):(?!not\b|is\b|where\b|has\b)[\w-]/.test(sel.replace(/\\./g, ""))) return true;
|
|
105
|
+
try {
|
|
106
|
+
return selectAll(sel, dom).length > 0;
|
|
107
|
+
} catch {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (matched.length === 0) rule.remove();
|
|
112
|
+
else if (matched.length < selectors.length) rule.selectors = matched;
|
|
113
|
+
});
|
|
114
|
+
root.walkAtRules((atRule) => {
|
|
115
|
+
if (atRule.nodes?.length === 0) atRule.remove();
|
|
116
|
+
});
|
|
117
|
+
const purgedCss = root.toString();
|
|
118
|
+
if (purgedCss.trim()) textNode.data = purgedCss;
|
|
119
|
+
else {
|
|
120
|
+
const parent = el.parent;
|
|
121
|
+
if (parent && "children" in parent) {
|
|
122
|
+
const idx = parent.children.indexOf(el);
|
|
123
|
+
if (idx !== -1) parent.children.splice(idx, 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return dom;
|
|
62
128
|
}
|
|
63
129
|
|
|
64
130
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purgeCSS.mjs","names":["merge"],"sources":["../../src/transformers/purgeCSS.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport type { ChildNode } from 'domhandler'\nimport { parse, serialize } from '../utils/ast/index.ts'\nimport type { CssConfig } from '../types/config.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 * Remove unused CSS transformer.\n *\n * Uses `email-comb` to strip CSS selectors and corresponding class/id\n * references that are not matched anywhere in the HTML body.\n *\n * Enable by setting `css.purge: true` (or passing options).\n * The user-supplied options are merged on top of the defaults, so\n * `safelist` values are **appended** to the built-in safelist rather\n * than replacing it.\n *\n * Accepts `ChildNode[]` as input, serializes internally before passing\n * to email-comb (which requires a raw HTML string), then parses the\n * result back to `ChildNode[]` so it fits in the DOM pipeline.\n */\nexport function purgeCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.purge\n\n if (!option) return dom\n\n const userOptions = typeof option === 'object' ? option : {}\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 userSafelist = Array.isArray((userOptions as any).safelist)\n ? (userOptions as any).safelist as string[]\n : []\n\n const { safelist: _discard, ...restUserOptions } = userOptions as any\n\n const options = merge(\n { ...restUserOptions, whitelist: [...DEFAULT_SAFELIST, ...userSafelist] },\n DEFAULT_OPTIONS,\n )\n\n const { result } = comb(serialize(dom), options)\n\n
|
|
1
|
+
{"version":3,"file":"purgeCSS.mjs","names":["merge"],"sources":["../../src/transformers/purgeCSS.ts"],"sourcesContent":["import { comb } from 'email-comb'\nimport { defu as merge } from 'defu'\nimport postcss from 'postcss'\nimport safeParser from 'postcss-safe-parser'\nimport { selectAll } from 'css-select'\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_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 * Remove unused CSS transformer.\n *\n * Uses `email-comb` to strip CSS selectors and corresponding class/id\n * references that are not matched anywhere in the HTML body.\n *\n * Enable by setting `css.purge: true` (or passing options).\n * The user-supplied options are merged on top of the defaults, so\n * `safelist` values are **appended** to the built-in safelist rather\n * than replacing it.\n *\n * Accepts `ChildNode[]` as input, serializes internally before passing\n * to email-comb (which requires a raw HTML string), then parses the\n * result back to `ChildNode[]` so it fits in the DOM pipeline.\n */\nexport function purgeCSS(dom: ChildNode[], config: CssConfig = {}): ChildNode[] {\n const option = config.purge\n\n if (!option) return dom\n\n const userOptions = typeof option === 'object' ? option : {}\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 userSafelist = Array.isArray((userOptions as any).safelist)\n ? (userOptions as any).safelist as string[]\n : []\n\n const { safelist: _discard, ...restUserOptions } = userOptions as any\n\n const options = 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 const { result } = comb(serialize(dom), options)\n\n let purgedDom = parse(result)\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) 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 = postcss.parse(textNode.data, { parser: safeParser })\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":"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"}
|
|
@@ -115,15 +115,11 @@ async function tailwindcss(dom, config, filePath) {
|
|
|
115
115
|
if (node.name !== "style") return;
|
|
116
116
|
const el = node;
|
|
117
117
|
const attrs = el.attribs || {};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"embed",
|
|
121
|
-
"data-embed"
|
|
122
|
-
].find((attr) => attr in attrs);
|
|
123
|
-
if (markerAttr) {
|
|
124
|
-
delete el.attribs[markerAttr];
|
|
118
|
+
if ("raw" in attrs) {
|
|
119
|
+
delete el.attribs.raw;
|
|
125
120
|
return;
|
|
126
121
|
}
|
|
122
|
+
if ("embed" in attrs || "data-embed" in attrs) return;
|
|
127
123
|
const rawContent = el.children.filter((child) => child.type === "text").map((child) => child.data).join("");
|
|
128
124
|
if (!rawContent.trim()) return;
|
|
129
125
|
styleTags.push({
|
|
@@ -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 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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"}
|
|
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\n // Remove 'raw' marker but preserve 'embed'/'data-embed' for Juice\n if ('raw' in attrs) {\n delete el.attribs.raw\n return\n }\n\n if ('embed' in attrs || 'data-embed' in attrs) {\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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { RemoveValue } from "../plugins/postcss/removeDeclarations.mjs";
|
|
2
|
+
import { Options } from "juice";
|
|
2
3
|
import * as oxfmt from "oxfmt";
|
|
3
|
-
import * as
|
|
4
|
+
import * as shiki from "shiki";
|
|
5
|
+
import { Options as Options$1 } from "unplugin-vue-markdown/types";
|
|
4
6
|
|
|
5
7
|
//#region src/types/config.d.ts
|
|
6
8
|
interface UrlQueryOptions {
|
|
@@ -91,7 +93,7 @@ interface CssConfig {
|
|
|
91
93
|
* }
|
|
92
94
|
* }
|
|
93
95
|
*/
|
|
94
|
-
inline?: boolean | {
|
|
96
|
+
inline?: boolean | Options & {
|
|
95
97
|
/**
|
|
96
98
|
* Convert HTML attributes like `width`, `height`, `bgcolor`, and `valign`
|
|
97
99
|
* to inline CSS styles. Set to `true` for all, or pass an array of attribute names.
|
|
@@ -99,18 +101,6 @@ interface CssConfig {
|
|
|
99
101
|
* @default false
|
|
100
102
|
*/
|
|
101
103
|
attributeToStyle?: boolean | string[];
|
|
102
|
-
/**
|
|
103
|
-
* Remove `<style>` tags after inlining.
|
|
104
|
-
*
|
|
105
|
-
* @default false
|
|
106
|
-
*/
|
|
107
|
-
removeStyleTags?: boolean;
|
|
108
|
-
/**
|
|
109
|
-
* Remove selectors from `<style>` tags after they have been inlined.
|
|
110
|
-
*
|
|
111
|
-
* @default true
|
|
112
|
-
*/
|
|
113
|
-
removeInlinedSelectors?: boolean;
|
|
114
104
|
/**
|
|
115
105
|
* Convert `0px`, `0em` etc. to `0` in inline styles.
|
|
116
106
|
*
|
|
@@ -119,12 +109,14 @@ interface CssConfig {
|
|
|
119
109
|
preferUnitlessValues?: boolean;
|
|
120
110
|
/**
|
|
121
111
|
* CSS selectors to preserve in `<style>` tags, even after inlining.
|
|
112
|
+
* Mapped to Juice's `preservedSelectors` option.
|
|
122
113
|
*
|
|
123
114
|
* @default []
|
|
124
115
|
*/
|
|
125
116
|
safelist?: string[];
|
|
126
117
|
/**
|
|
127
118
|
* Duplicate CSS properties to HTML attributes.
|
|
119
|
+
* Mapped to Juice's static `styleToAttribute` property.
|
|
128
120
|
*
|
|
129
121
|
* @default {}
|
|
130
122
|
*
|
|
@@ -134,38 +126,30 @@ interface CssConfig {
|
|
|
134
126
|
* }
|
|
135
127
|
*/
|
|
136
128
|
styleToAttribute?: Record<string, string>;
|
|
137
|
-
/**
|
|
138
|
-
* Add `width` HTML attributes based on inline CSS width values.
|
|
139
|
-
*
|
|
140
|
-
* @default true
|
|
141
|
-
*/
|
|
142
|
-
applyWidthAttributes?: boolean;
|
|
143
|
-
/**
|
|
144
|
-
* Add `height` HTML attributes based on inline CSS height values.
|
|
145
|
-
*
|
|
146
|
-
* @default true
|
|
147
|
-
*/
|
|
148
|
-
applyHeightAttributes?: boolean;
|
|
149
129
|
/**
|
|
150
130
|
* Elements that can receive `width` HTML attributes.
|
|
131
|
+
* Mapped to Juice's static `widthElements` property.
|
|
151
132
|
*
|
|
152
133
|
* @default ['img', 'video']
|
|
153
134
|
*/
|
|
154
135
|
widthElements?: string[];
|
|
155
136
|
/**
|
|
156
137
|
* Elements that can receive `height` HTML attributes.
|
|
138
|
+
* Mapped to Juice's static `heightElements` property.
|
|
157
139
|
*
|
|
158
140
|
* @default ['img', 'video']
|
|
159
141
|
*/
|
|
160
142
|
heightElements?: string[];
|
|
161
143
|
/**
|
|
162
144
|
* CSS properties to exclude from inlining.
|
|
145
|
+
* Mapped to Juice's static `excludedProperties` property.
|
|
163
146
|
*
|
|
164
147
|
* @default []
|
|
165
148
|
*/
|
|
166
149
|
excludedProperties?: string[];
|
|
167
150
|
/**
|
|
168
151
|
* Template language code blocks to preserve during inlining.
|
|
152
|
+
* Mapped to Juice's static `codeBlocks` property.
|
|
169
153
|
*
|
|
170
154
|
* @default { EJS: { start: '<%', end: '%>' }, HBS: { start: '\{\{', end: '}}' } }
|
|
171
155
|
*/
|
|
@@ -175,6 +159,7 @@ interface CssConfig {
|
|
|
175
159
|
}>;
|
|
176
160
|
/**
|
|
177
161
|
* Additional CSS string to inline alongside `<style>` tag contents.
|
|
162
|
+
* Mapped to Juice's `extraCss` option.
|
|
178
163
|
*/
|
|
179
164
|
customCSS?: string;
|
|
180
165
|
};
|
|
@@ -336,6 +321,14 @@ interface HtmlConfig {
|
|
|
336
321
|
}
|
|
337
322
|
type FilterFunction = (str: string, value: string) => string;
|
|
338
323
|
type FiltersConfig = false | Record<string, FilterFunction>;
|
|
324
|
+
interface MarkdownConfig extends Options$1 {
|
|
325
|
+
/**
|
|
326
|
+
* The shiki theme to use for syntax highlighting in Markdown fenced code blocks.
|
|
327
|
+
*
|
|
328
|
+
* @default 'github-light'
|
|
329
|
+
*/
|
|
330
|
+
shikiTheme?: shiki.BundledTheme;
|
|
331
|
+
}
|
|
339
332
|
interface MaizzleConfig {
|
|
340
333
|
/**
|
|
341
334
|
* Root directory for the Maizzle email project.
|
|
@@ -352,8 +345,8 @@ interface MaizzleConfig {
|
|
|
352
345
|
* })
|
|
353
346
|
*/
|
|
354
347
|
root?: string;
|
|
355
|
-
/** Options
|
|
356
|
-
markdown?:
|
|
348
|
+
/** Options for Markdown template support, extending `unplugin-vue-markdown`. */
|
|
349
|
+
markdown?: MarkdownConfig;
|
|
357
350
|
/**
|
|
358
351
|
* Glob patterns for email template files to process.
|
|
359
352
|
*
|
|
@@ -431,6 +424,31 @@ interface MaizzleConfig {
|
|
|
431
424
|
* }
|
|
432
425
|
*/
|
|
433
426
|
watch?: string[];
|
|
427
|
+
/**
|
|
428
|
+
* Email sending configuration for the "Send test" feature in the dev UI.
|
|
429
|
+
*
|
|
430
|
+
* When not configured, falls back to Ethereal (free fake SMTP — emails
|
|
431
|
+
* are captured and viewable via a URL, never actually delivered).
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* server: {
|
|
435
|
+
* email: {
|
|
436
|
+
* to: ['test@example.com'],
|
|
437
|
+
* from: 'dev@maizzle.test',
|
|
438
|
+
* transport: {
|
|
439
|
+
* host: 'smtp.mailtrap.io',
|
|
440
|
+
* port: 587,
|
|
441
|
+
* auth: { user: '...', pass: '...' },
|
|
442
|
+
* },
|
|
443
|
+
* },
|
|
444
|
+
* }
|
|
445
|
+
*/
|
|
446
|
+
email?: {
|
|
447
|
+
/** Default recipient(s). */to?: string | string[]; /** Sender address. @default 'Maizzle <maizzle@ethereal.email>' */
|
|
448
|
+
from?: string; /** Default subject line. */
|
|
449
|
+
subject?: string; /** Nodemailer transport options (SMTP, SES, etc.). Omit to use Ethereal. */
|
|
450
|
+
transport?: Record<string, unknown>;
|
|
451
|
+
};
|
|
434
452
|
};
|
|
435
453
|
/** Tailwind CSS and email CSS optimization settings. */
|
|
436
454
|
css?: CssConfig;
|
|
@@ -504,5 +522,5 @@ interface MaizzleConfig {
|
|
|
504
522
|
[key: string]: any;
|
|
505
523
|
}
|
|
506
524
|
//#endregion
|
|
507
|
-
export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
|
|
525
|
+
export { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions };
|
|
508
526
|
//# sourceMappingURL=config.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/types/config.ts"],"mappings":"
|
|
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, 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
|
+
import { AttributesConfig, CssConfig, EntitiesConfig, FilterFunction, FiltersConfig, HtmlConfig, MaizzleConfig, MarkdownConfig, PostcssConfig, UrlConfig, UrlQuery, UrlQueryOptions } from "./config.mjs";
|
|
2
|
+
export { type AttributesConfig, type CssConfig, type EntitiesConfig, type FilterFunction, type FiltersConfig, type HtmlConfig, type MaizzleConfig, type MarkdownConfig, type PostcssConfig, type UrlConfig, type UrlQuery, type UrlQueryOptions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maizzle/framework",
|
|
3
|
-
"version": "6.0.0-rc.
|
|
3
|
+
"version": "6.0.0-rc.8",
|
|
4
4
|
"description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"maizzle"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"maizzle": "latest",
|
|
37
36
|
"@tailwindcss/postcss": "^4.1.18",
|
|
38
37
|
"@tailwindcss/vite": "^4.1.18",
|
|
39
38
|
"@unhead/vue": "^2.1.4",
|
|
@@ -49,11 +48,15 @@
|
|
|
49
48
|
"domhandler": "^5.0.3",
|
|
50
49
|
"email-comb": "^7.1.3",
|
|
51
50
|
"html-crush": "^6.1.3",
|
|
51
|
+
"html-to-image": "^1.11.13",
|
|
52
52
|
"htmlparser2": "^10.1.0",
|
|
53
53
|
"is-url-superb": "^6.1.0",
|
|
54
54
|
"jiti": "^2.6.1",
|
|
55
55
|
"juice": "^11.1.1",
|
|
56
56
|
"lucide-vue-next": "^1.0.0",
|
|
57
|
+
"maizzle": "latest",
|
|
58
|
+
"markdown-exit": "^1.0.0-beta.9",
|
|
59
|
+
"nodemailer": "^8.0.5",
|
|
57
60
|
"ora": "^9.3.0",
|
|
58
61
|
"oxfmt": "^0.35.0",
|
|
59
62
|
"postcss": "^8.5.6",
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
"typescript": "^5.9.3",
|
|
74
77
|
"unplugin-auto-import": "^21.0.0",
|
|
75
78
|
"unplugin-vue-components": "^31.0.0",
|
|
76
|
-
"unplugin-vue-markdown": "^
|
|
79
|
+
"unplugin-vue-markdown": "^30.0.0",
|
|
77
80
|
"vite": "^7.3.1",
|
|
78
81
|
"vue": "^3.5.28",
|
|
79
82
|
"vue-router": "^5.0.2"
|
|
@@ -81,6 +84,7 @@
|
|
|
81
84
|
"devDependencies": {
|
|
82
85
|
"@types/js-beautify": "^1.14.3",
|
|
83
86
|
"@types/node": "^25.2.3",
|
|
87
|
+
"@types/nodemailer": "^8.0.0",
|
|
84
88
|
"@types/postcss-safe-parser": "^5.0.4",
|
|
85
89
|
"@vue/test-utils": "^2.4.6",
|
|
86
90
|
"happy-dom": "^20.6.3",
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { SplitterResizeHandleEmits, SplitterResizeHandleProps } from "reka-ui"
|
|
3
|
-
import type { HTMLAttributes } from "vue"
|
|
4
|
-
import { reactiveOmit } from "@vueuse/core"
|
|
5
|
-
import { GripVertical } from "lucide-vue-next"
|
|
6
|
-
import { SplitterResizeHandle, useForwardPropsEmits } from "reka-ui"
|
|
7
|
-
import { cn } from "@/lib/utils"
|
|
8
|
-
|
|
9
|
-
const props = defineProps<SplitterResizeHandleProps & { class?: HTMLAttributes["class"], withHandle?: boolean }>()
|
|
10
|
-
const emits = defineEmits<SplitterResizeHandleEmits>()
|
|
11
|
-
|
|
12
|
-
const delegatedProps = reactiveOmit(props, "class", "withHandle")
|
|
13
|
-
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<SplitterResizeHandle
|
|
18
|
-
data-slot="resizable-handle"
|
|
19
|
-
v-bind="forwarded"
|
|
20
|
-
:class="cn('bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)"
|
|
21
|
-
>
|
|
22
|
-
<template v-if="props.withHandle">
|
|
23
|
-
<div class="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
|
24
|
-
<slot>
|
|
25
|
-
<GripVertical class="size-2.5" />
|
|
26
|
-
</slot>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
29
|
-
</SplitterResizeHandle>
|
|
30
|
-
</template>
|