@maizzle/framework 6.0.0-2 → 6.0.0-4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [6.0.0-3] - 2025-07-14
8
+
9
+ ### Added
10
+
11
+ - added support for skipping CSS compilation on individual `<style>` tags by adding any of the following attributes: `raw`, `plain`, `as-is`, `uncompiled`, `unprocessed`
12
+
13
+ ### Changed
14
+
15
+ - refactored CSS compilation into a custom PostHTML plugin
16
+
17
+ ### Removed
18
+
19
+ - removed `posthtml-postcss` dependency
20
+
7
21
  ## [6.0.0-2] - 2025-07-11
8
22
 
9
23
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "6.0.0-2",
3
+ "version": "6.0.0-4",
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",
@@ -86,7 +86,6 @@
86
86
  "posthtml-markdownit": "^3.1.0",
87
87
  "posthtml-mso": "^3.1.0",
88
88
  "posthtml-parser": "^0.12.1",
89
- "posthtml-postcss": "^1.0.2",
90
89
  "posthtml-postcss-merge-longhand": "^3.1.2",
91
90
  "posthtml-render": "^3.0.0",
92
91
  "posthtml-safe-class-names": "^4.1.0",
@@ -6,51 +6,16 @@ import posthtml from 'posthtml'
6
6
  import posthtmlFetch from 'posthtml-fetch'
7
7
  import envTags from './plugins/envTags.js'
8
8
  import components from 'posthtml-component'
9
- import posthtmlPostcss from 'posthtml-postcss'
10
9
  import expandLinkTag from './plugins/expandLinkTag.js'
11
10
  import envAttributes from './plugins/envAttributes.js'
12
11
  import { getPosthtmlOptions } from './defaultConfig.js'
13
- import lowerCssSyntax from './plugins/lowerCssSyntax.js'
14
12
  import combineMediaQueries from './plugins/combineMediaQueries.js'
13
+ import defaultComponentsConfig from './defaultComponentsConfig.js'
15
14
 
16
15
  // PostCSS
17
- import tailwindcss from '@tailwindcss/postcss'
18
- import postcssCalc from 'postcss-calc'
19
- import cssVariables from 'postcss-css-variables'
20
- import postcssSafeParser from 'postcss-safe-parser'
21
- import removeDuplicateSelectors from './plugins/postcss/removeDuplicateSelectors.js'
22
- import cleanupTailwindArtifacts from './plugins/postcss/cleanupTailwindArtifacts.js'
23
-
24
- import defaultComponentsConfig from './defaultComponentsConfig.js'
16
+ import compileCss from './plugins/postcss/compileCss.js'
25
17
 
26
18
  export async function process(html = '', config = {}) {
27
- /**
28
- * Configure PostCSS pipeline. Plugins defined and added here
29
- * will apply to all `<style>` tags in the HTML.
30
- */
31
- const resolveCSSProps = get(config, 'css.resolveProps')
32
- const resolveCalc = get(config, 'css.resolveCalc') !== false
33
- ? get(config, 'css.resolveCalc', { precision: 2 }) // it's true by default, use default precision 2
34
- : false
35
-
36
- const postcssPlugin = posthtmlPostcss(
37
- [
38
- tailwindcss(get(config, 'css.tailwind', {})),
39
- resolveCSSProps !== false && cssVariables(resolveCSSProps),
40
- resolveCalc !== false && postcssCalc(resolveCalc),
41
- removeDuplicateSelectors(),
42
- cleanupTailwindArtifacts(get(config, 'css.cleanup', {})),
43
- ...get(config, 'postcss.plugins', []),
44
- ],
45
- merge(
46
- get(config, 'postcss.options', {}),
47
- {
48
- from: config.cwd || './',
49
- parser: postcssSafeParser
50
- }
51
- )
52
- )
53
-
54
19
  /**
55
20
  * Define PostHTML options by merging user-provided ones
56
21
  * on top of a default configuration.
@@ -106,18 +71,15 @@ export async function process(html = '', config = {}) {
106
71
 
107
72
  return posthtml([
108
73
  ...beforePlugins,
109
- envTags(config.env),
110
- envAttributes(config.env),
111
- expandLinkTag(),
112
- postcssPlugin,
113
74
  fetchPlugin,
114
75
  components(componentsConfig),
76
+ fetchPlugin,
115
77
  expandLinkTag(),
116
- postcssPlugin,
117
78
  envTags(config.env),
118
79
  envAttributes(config.env),
119
- lowerCssSyntax(get(config, 'css.lightningcss', {})),
120
- get(config, 'css.combineMediaQueries') !== false && combineMediaQueries(get(config, 'css.combineMediaQueries', { sort: 'mobile-first' })),
80
+ compileCss(config),
81
+ get(config, 'css.combineMediaQueries') !== false
82
+ && combineMediaQueries(get(config, 'css.combineMediaQueries', { sort: 'mobile-first' })),
121
83
  ...get(
122
84
  config,
123
85
  'posthtml.plugins.after',
@@ -0,0 +1,137 @@
1
+ import postcss from 'postcss'
2
+ import get from 'lodash-es/get.js'
3
+ import { defu as merge } from 'defu'
4
+ import postcssCalc from 'postcss-calc'
5
+ import { transform } from 'lightningcss'
6
+ import tailwindcss from '@tailwindcss/postcss'
7
+ import cssVariables from 'postcss-css-variables'
8
+ import postcssSafeParser from 'postcss-safe-parser'
9
+ import removeDuplicateSelectors from './removeDuplicateSelectors.js'
10
+ import cleanupTailwindArtifacts from './cleanupTailwindArtifacts.js'
11
+
12
+ const validAttributeNames = new Set([
13
+ 'raw',
14
+ 'plain',
15
+ 'as-is',
16
+ 'uncompiled',
17
+ 'unprocessed',
18
+ ])
19
+
20
+ /**
21
+ * PostHTML plugin to process Tailwind CSS within style tags.
22
+ *
23
+ * This plugin processes CSS content in `<style>` tags and
24
+ * compiles it with PostCSS. `<style>` tags marked as
25
+ * `no-process` will be skipped.
26
+ */
27
+ export default function compile(config = {}) {
28
+ return tree => {
29
+ return new Promise((resolve, reject) => {
30
+ const stylePromises = []
31
+
32
+ tree.walk(node => {
33
+ if (node.tag === 'style' && node.content) {
34
+ if (node.attrs && Object.keys(node.attrs).some(attr => validAttributeNames.has(attr))) {
35
+ // Remove the attribute
36
+ for (const attr of Object.keys(node.attrs)) {
37
+ if (validAttributeNames.has(attr)) {
38
+ delete node.attrs[attr]
39
+ }
40
+ }
41
+
42
+ return node
43
+ }
44
+
45
+ const css = Array.isArray(node.content)
46
+ ? node.content.join('')
47
+ : node.content
48
+
49
+ const promise = processCss(css, config)
50
+ .then(processedCss => {
51
+ node.content = [processedCss]
52
+ })
53
+ .catch(error => {
54
+ console.warn('Error processing CSS in style tag:', error.message)
55
+ })
56
+
57
+ stylePromises.push(promise)
58
+ }
59
+
60
+ return node
61
+ })
62
+
63
+ Promise.all(stylePromises)
64
+ .then(() => resolve(tree))
65
+ .catch(reject)
66
+ })
67
+ }
68
+ }
69
+
70
+ async function processCss(css, config) {
71
+ /**
72
+ * PostCSS pipeline. Plugins defined and added here
73
+ * will apply to all `<style>` tags in the HTML,
74
+ * unless marked to be excluded.
75
+ */
76
+ const resolveCSSProps = get(config, 'css.resolveProps')
77
+ const resolveCalc = get(config, 'css.resolveCalc') !== false
78
+ ? get(config, 'css.resolveCalc', { precision: 2 })
79
+ : false
80
+
81
+ const lightningCssOptions = merge(
82
+ get(config, 'css.lightning', {}),
83
+ {
84
+ targets: {
85
+ ie: 1,
86
+ },
87
+ }
88
+ )
89
+
90
+ try {
91
+ const processor = postcss([
92
+ tailwindcss(get(config, 'css.tailwind', {})),
93
+ resolveCSSProps !== false && cssVariables(resolveCSSProps),
94
+ resolveCalc !== false && postcssCalc(resolveCalc),
95
+ removeDuplicateSelectors(),
96
+ cleanupTailwindArtifacts(get(config, 'css.cleanup', {})),
97
+ ...get(config, 'postcss.plugins', []),
98
+ ].filter(Boolean))
99
+
100
+ const result = await processor.process(css, merge(
101
+ get(config, 'postcss.options', {}),
102
+ {
103
+ from: config.cwd || './',
104
+ parser: postcssSafeParser
105
+ }
106
+ ))
107
+
108
+ /**
109
+ * Lightning CSS processing
110
+ *
111
+ * We use this to lower the modern Tailwind CSS 4 syntax
112
+ * to be more email-friendly.
113
+ */
114
+
115
+ if (result.css?.trim()) {
116
+ try {
117
+ const { code } = transform(
118
+ merge(
119
+ lightningCssOptions,
120
+ {
121
+ code: Buffer.from(result.css)
122
+ }
123
+ )
124
+ )
125
+
126
+ return code.toString()
127
+ } catch (error) {
128
+ console.warn('Failed to lower syntax with Lightning CSS:', error.message)
129
+ }
130
+ }
131
+
132
+ return result.css
133
+ } catch (error) {
134
+ console.warn('Error compiling CSS:', error.message)
135
+ return css
136
+ }
137
+ }
@@ -32,21 +32,26 @@ export async function inline(html = '', options = {}) {
32
32
  options.safelist = new Set([
33
33
  ...get(options, 'safelist', []),
34
34
  ...[
35
- '.body', // Gmail
36
- '.gmail', // Gmail
37
- '.apple', // Apple Mail
38
- '.ios', // Mail on iOS
39
- '.ox-', // Open-Xchange
40
- '.outlook', // Outlook.com
35
+ 'body', // Gmail
36
+ 'gmail', // Gmail
37
+ 'apple', // Apple Mail
38
+ 'ios', // Mail on iOS
39
+ 'ox-', // Open-Xchange
40
+ 'yahoo', // Yahoo! Mail
41
+ 'outlook', // Outlook Mac and Android
41
42
  '[data-ogs', // Outlook.com
42
- '.bloop_container', // Airmail
43
- '.Singleton', // Apple Mail 10
44
- '.unused', // Notes 8
45
- '.moz-text-html', // Thunderbird
46
- '.mail-detail-content', // Comcast, Libero webmail
43
+ 'bloop_container', // Airmail
44
+ 'Singleton', // Apple Mail 10
45
+ 'unused', // Notes 8
46
+ 'moz-text-html', // Thunderbird
47
+ 'mail-detail-content', // Comcast, Libero webmail
48
+ 'mail-content', // Notion
47
49
  'edo', // Edison (all)
48
50
  '#msgBody', // Freenet uses #msgBody
49
- '.lang' // Fenced code blocks
51
+ 'lang', // Fenced code blocks
52
+ 'ShadowHTML', // Superhuman
53
+ 'spark', // Spark
54
+ 'at-', // Safe class names for container queries
50
55
  ],
51
56
  ])
52
57
 
@@ -9,20 +9,24 @@ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
9
9
  const posthtmlPlugin = options => tree => {
10
10
  const defaultSafelist = [
11
11
  '*body*', // Gmail
12
- '.gmail*', // Gmail
13
- '.apple*', // Apple Mail
14
- '.ios*', // Mail on iOS
15
- '.ox-*', // Open-Xchange
16
- '.outlook*', // Outlook.com
12
+ '*gmail*', // Gmail
13
+ '*apple*', // Apple Mail
14
+ '*ios*', // Mail on iOS
15
+ '*ox-*', // Open-Xchange
16
+ '*outlook*', // Outlook.com
17
17
  '[data-ogs*', // Outlook.com
18
- '.bloop_container', // Airmail
19
- '.Singleton', // Apple Mail 10
20
- '.unused', // Notes 8
21
- '.moz-text-html', // Thunderbird
22
- '.mail-detail-content', // Comcast, Libero webmail
18
+ '*bloop_container*', // Airmail
19
+ '*Singleton*', // Apple Mail 10
20
+ '*unused', // Notes 8
21
+ '*moz-text-html*', // Thunderbird
22
+ '*mail-detail-content*', // Comcast, Libero webmail
23
+ '*mail-content-*', // Notion
23
24
  '*edo*', // Edison (all)
24
25
  '#*', // Freenet uses #msgBody
25
- '.lang*' // Fenced code blocks
26
+ '*lang*', // Fenced code blocks
27
+ '*ShadowHTML*', // Superhuman
28
+ '*spark*', // Spark
29
+ '*at-*', // Safe class names for container queries
26
30
  ]
27
31
 
28
32
  const defaultOptions = {
@@ -12,7 +12,8 @@ export default function posthtmlPlugin(options = {}) {
12
12
  options = merge({
13
13
  replacements: {
14
14
  '{': '{',
15
- '}': '}'
15
+ '}': '}',
16
+ '&': '&',
16
17
  }
17
18
  }, options)
18
19
 
@@ -1,43 +0,0 @@
1
- import { defu as merge } from 'defu'
2
- import { transform } from 'lightningcss'
3
-
4
- const plugin = (options = {}) => tree => {
5
- options = merge(options, {
6
- targets: options.targets ? {} : {
7
- ie: 1,
8
- },
9
- })
10
-
11
- const process = node => {
12
- // Check if this is a style tag with content
13
- if (node.tag === 'style' && node.content && Array.isArray(node.content)) {
14
- // Get the CSS content from the style tag
15
- const cssContent = node.content.join('')
16
-
17
- if (cssContent.trim()) {
18
- try {
19
- const { code } = transform(
20
- merge(
21
- options,
22
- {
23
- code: Buffer.from(cssContent)
24
- }
25
- )
26
- )
27
-
28
- // Replace the content with processed CSS
29
- node.content = [code.toString()]
30
- } catch (error) {
31
- // If processing fails, leave the content unchanged
32
- console.warn('Failed to process media queries:', error.message)
33
- }
34
- }
35
- }
36
-
37
- return node
38
- }
39
-
40
- return tree.walk(process)
41
- }
42
-
43
- export default plugin