@maizzle/framework 6.0.0-0 → 6.0.0-10
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 +147 -0
- package/package.json +3 -5
- package/src/posthtml/index.js +10 -49
- package/src/posthtml/plugins/combineMediaQueries.js +1 -1
- package/src/posthtml/plugins/postcss/compileCss.js +128 -0
- package/src/posthtml/plugins/removeRawStyleAttributes.js +30 -0
- package/src/transformers/inline.js +124 -69
- package/src/transformers/purge.js +15 -11
- package/src/transformers/safeClassNames.js +2 -1
- package/src/utils/getConfigByFilePath.js +16 -7
- package/types/config.d.ts +25 -3
- package/types/css/combineMediaQueries.d.ts +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,153 @@ 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-10] - 2025-07-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- fix: resolve props default options typo 8673a1e
|
|
12
|
+
|
|
13
|
+
## [6.0.0-9] - 2025-07-16
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- refactor: resolving css props acacc14
|
|
18
|
+
|
|
19
|
+
## [6.0.0-8] - 2025-07-16
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- refactor: css inlining d443c31
|
|
24
|
+
|
|
25
|
+
## [6.0.0-7] - 2025-07-15
|
|
26
|
+
|
|
27
|
+
This release integrates the `v5.2.2` patch release, which fixes an issue where both local and production `build.content` paths were used when building for production. When specified in a `config.{env}.js` file, build.content must not be merged with the one in the base `config.js`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- fix: ensure content paths are unique to each build environment 46c292b
|
|
32
|
+
|
|
33
|
+
## [6.0.0-6] - 2025-07-14
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- use extension when importing file 99835d3
|
|
38
|
+
|
|
39
|
+
## [6.0.0-5] - 2025-07-14
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- run css compilation before components too 272bbdb
|
|
44
|
+
|
|
45
|
+
## [6.0.0-4] - 2025-07-14
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- added support for skipping CSS compilation on individual `<style>` tags by adding any of the following attributes: `raw`, `plain`, `as-is`, `uncompiled`, `unprocessed`
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- refactored CSS compilation into a custom PostHTML plugin
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- removed `posthtml-postcss` dependency
|
|
58
|
+
|
|
59
|
+
## [6.0.0-3] - 2025-07-14
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- safelist spark targeting selectors f29efb8
|
|
64
|
+
- safelist targeting for superhuman beb6b41
|
|
65
|
+
- safelist notion mail targeting 5cfd68f
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- safelisting outlook targeting b8b21f3
|
|
70
|
+
- safelisting selectors acb7b80
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- safelist class names for container queries b828c44
|
|
75
|
+
- purge safelisting patterns d6b9c48
|
|
76
|
+
- safelisting comcast targeting selector 9677421
|
|
77
|
+
- preserve yahoo mail targeting selectors 2f3429f
|
|
78
|
+
|
|
79
|
+
## [6.0.0-2] - 2025-07-11
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- fixed an issue with duplicate CSS selectors for utilities that cannot be disabled in Tailwind CSS v4, like `text-decoration`
|
|
84
|
+
- fixed an issue where some Tailwind directives like `@layer` or `@property` were still present in the final build, even though they were not used
|
|
85
|
+
|
|
86
|
+
## [6.0.0-1] - 2025-07-11
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
|
|
90
|
+
- types for `css.combineMediaQueries` and `css.lightningcss` options in the config
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- made `combineMediaQueries` configurable through the `css.combineMediaQueries` option, which can be set to an object with options for `postcss-sort-media-queries` or to `false` to disable it
|
|
95
|
+
- made `lightningcss` configurable through the `css.lightningcss` option
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- fixed an issue with CSS syntax lowering, we now use `lightningcss` directly instead of `postcss-lightningcss` which was it to trip on CSS inside `style=""` attributes
|
|
100
|
+
|
|
101
|
+
## [6.0.0-0] - 2025-07-10
|
|
102
|
+
|
|
103
|
+
This release adds initial support for Tailwind CSS v4 in Maizzle.
|
|
104
|
+
|
|
105
|
+
To jump right in, simply use the `next` branch of the Starter:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
git clone -b next https://github.com/maizzle/maizzle.git
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or, if you have a project:
|
|
112
|
+
|
|
113
|
+
1. Update `package.json`:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"private": true,
|
|
118
|
+
"type": "module",
|
|
119
|
+
"scripts": {
|
|
120
|
+
"dev": "maizzle serve",
|
|
121
|
+
"build": "maizzle build"
|
|
122
|
+
},
|
|
123
|
+
"dependencies": {
|
|
124
|
+
"@maizzle/framework": "next",
|
|
125
|
+
"@maizzle/tailwindcss": "latest"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
2. Update the `style` tag in `layouts/main.html`:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<style>
|
|
134
|
+
@import "@maizzle/tailwindcss";
|
|
135
|
+
|
|
136
|
+
img {
|
|
137
|
+
@apply max-w-full align-middle;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
There are still things missign and/or broken:
|
|
143
|
+
|
|
144
|
+
- inline CSS like the `line-height` on spacers is broken/missing
|
|
145
|
+
- `tailwindcss-email-variants` and `tailwindcss-mso` are not ported yet
|
|
146
|
+
- some CSS duplication and artifacts still present in the final build
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
- feat: add tailwindcss v4 support 8b5596e
|
|
151
|
+
|
|
152
|
+
https://github.com/maizzle/framework/compare/v5.2.1...v6.0.0-0
|
|
153
|
+
|
|
7
154
|
## [5.2.1] - 2025-06-25
|
|
8
155
|
|
|
9
156
|
This is just a maintenance release to update dependencies.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maizzle/framework",
|
|
3
|
-
"version": "6.0.0-
|
|
3
|
+
"version": "6.0.0-10",
|
|
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",
|
|
@@ -65,14 +65,13 @@
|
|
|
65
65
|
"is-url-superb": "^6.1.0",
|
|
66
66
|
"istextorbinary": "^9.5.0",
|
|
67
67
|
"juice": "^11.0.0",
|
|
68
|
+
"lightningcss": "^1.30.1",
|
|
68
69
|
"lodash-es": "^4.17.21",
|
|
69
70
|
"morphdom": "^2.7.4",
|
|
70
71
|
"ora": "^8.1.0",
|
|
71
72
|
"pathe": "^2.0.0",
|
|
72
73
|
"postcss": "^8.4.49",
|
|
73
|
-
"postcss-
|
|
74
|
-
"postcss-css-variables": "^0.19.0",
|
|
75
|
-
"postcss-lightningcss": "^1.0.1",
|
|
74
|
+
"postcss-custom-properties": "^14.0.6",
|
|
76
75
|
"postcss-safe-parser": "^7.0.0",
|
|
77
76
|
"postcss-sort-media-queries": "^5.2.0",
|
|
78
77
|
"posthtml": "^0.16.6",
|
|
@@ -86,7 +85,6 @@
|
|
|
86
85
|
"posthtml-markdownit": "^3.1.0",
|
|
87
86
|
"posthtml-mso": "^3.1.0",
|
|
88
87
|
"posthtml-parser": "^0.12.1",
|
|
89
|
-
"posthtml-postcss": "^1.0.2",
|
|
90
88
|
"posthtml-postcss-merge-longhand": "^3.1.2",
|
|
91
89
|
"posthtml-render": "^3.0.0",
|
|
92
90
|
"posthtml-safe-class-names": "^4.1.0",
|
package/src/posthtml/index.js
CHANGED
|
@@ -6,56 +6,17 @@ 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
12
|
import combineMediaQueries from './plugins/combineMediaQueries.js'
|
|
13
|
+
import defaultComponentsConfig from './defaultComponentsConfig.js'
|
|
14
|
+
import removeRawStyleAttributes from './plugins/removeRawStyleAttributes.js'
|
|
14
15
|
|
|
15
16
|
// PostCSS
|
|
16
|
-
import
|
|
17
|
-
import postcssCalc from 'postcss-calc'
|
|
18
|
-
import cssVariables from 'postcss-css-variables'
|
|
19
|
-
import postcssSafeParser from 'postcss-safe-parser'
|
|
20
|
-
import postcssLightningCss from 'postcss-lightningcss'
|
|
21
|
-
|
|
22
|
-
import defaultComponentsConfig from './defaultComponentsConfig.js'
|
|
17
|
+
import { compileCss } from './plugins/postcss/compileCss.js'
|
|
23
18
|
|
|
24
19
|
export async function process(html = '', config = {}) {
|
|
25
|
-
/**
|
|
26
|
-
* Configure PostCSS pipeline. Plugins defined and added here
|
|
27
|
-
* will apply to all `<style>` tags in the HTML.
|
|
28
|
-
*/
|
|
29
|
-
const resolveCSSProps = get(config, 'css.resolveProps')
|
|
30
|
-
const resolveCalc = get(config, 'css.resolveCalc') !== false
|
|
31
|
-
? get(config, 'css.resolveCalc', { precision: 2 }) // it's true by default, use default precision 2
|
|
32
|
-
: false
|
|
33
|
-
|
|
34
|
-
const postcssPlugin = posthtmlPostcss(
|
|
35
|
-
[
|
|
36
|
-
tailwindcss(get(config, 'css.tailwind', {})),
|
|
37
|
-
resolveCSSProps !== false && cssVariables(resolveCSSProps),
|
|
38
|
-
resolveCalc !== false && postcssCalc(resolveCalc),
|
|
39
|
-
postcssLightningCss({
|
|
40
|
-
lightningcssOptions: {
|
|
41
|
-
errorRecovery: true,
|
|
42
|
-
minify: false,
|
|
43
|
-
targets: {
|
|
44
|
-
ie: 1,
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}),
|
|
48
|
-
...get(config, 'postcss.plugins', []),
|
|
49
|
-
],
|
|
50
|
-
merge(
|
|
51
|
-
get(config, 'postcss.options', {}),
|
|
52
|
-
{
|
|
53
|
-
from: config.cwd || './',
|
|
54
|
-
parser: postcssSafeParser
|
|
55
|
-
}
|
|
56
|
-
)
|
|
57
|
-
)
|
|
58
|
-
|
|
59
20
|
/**
|
|
60
21
|
* Define PostHTML options by merging user-provided ones
|
|
61
22
|
* on top of a default configuration.
|
|
@@ -111,17 +72,17 @@ export async function process(html = '', config = {}) {
|
|
|
111
72
|
|
|
112
73
|
return posthtml([
|
|
113
74
|
...beforePlugins,
|
|
114
|
-
|
|
115
|
-
envAttributes(config.env),
|
|
116
|
-
expandLinkTag(),
|
|
117
|
-
postcssPlugin,
|
|
75
|
+
compileCss(config),
|
|
118
76
|
fetchPlugin,
|
|
119
77
|
components(componentsConfig),
|
|
78
|
+
fetchPlugin,
|
|
120
79
|
expandLinkTag(),
|
|
121
|
-
postcssPlugin,
|
|
122
80
|
envTags(config.env),
|
|
123
81
|
envAttributes(config.env),
|
|
124
|
-
|
|
82
|
+
compileCss(config),
|
|
83
|
+
get(config, 'css.combineMediaQueries') !== false
|
|
84
|
+
&& combineMediaQueries(get(config, 'css.combineMediaQueries', { sort: 'mobile-first' })),
|
|
85
|
+
removeRawStyleAttributes(),
|
|
125
86
|
...get(
|
|
126
87
|
config,
|
|
127
88
|
'posthtml.plugins.after',
|
|
@@ -129,7 +90,7 @@ export async function process(html = '', config = {}) {
|
|
|
129
90
|
? []
|
|
130
91
|
: get(config, 'posthtml.plugins', [])
|
|
131
92
|
),
|
|
132
|
-
])
|
|
93
|
+
].filter(Boolean))
|
|
133
94
|
.process(html, posthtmlOptions)
|
|
134
95
|
.then(result => ({
|
|
135
96
|
config: merge(config, { page: config }),
|
|
@@ -4,7 +4,7 @@ import sortMediaQueries from 'postcss-sort-media-queries'
|
|
|
4
4
|
const plugin = (options = {}) => tree => {
|
|
5
5
|
const process = node => {
|
|
6
6
|
// Check if this is a style tag with content
|
|
7
|
-
if (node.tag === 'style' && node.content && Array.isArray(node.content)) {
|
|
7
|
+
if (node && node.tag === 'style' && node.content && Array.isArray(node.content)) {
|
|
8
8
|
// Get the CSS content from the style tag
|
|
9
9
|
const cssContent = node.content.join('')
|
|
10
10
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import postcss from 'postcss'
|
|
2
|
+
import get from 'lodash-es/get.js'
|
|
3
|
+
import { defu as merge } from 'defu'
|
|
4
|
+
import { transform } from 'lightningcss'
|
|
5
|
+
import tailwindcss from '@tailwindcss/postcss'
|
|
6
|
+
import postcssSafeParser from 'postcss-safe-parser'
|
|
7
|
+
import customProperties from 'postcss-custom-properties'
|
|
8
|
+
|
|
9
|
+
const attributes = new Set([
|
|
10
|
+
'raw',
|
|
11
|
+
'plain',
|
|
12
|
+
'as-is',
|
|
13
|
+
'uncompiled',
|
|
14
|
+
'unprocessed',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
// export attributes
|
|
18
|
+
export const validAttributeNames = attributes
|
|
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. Tags marked with attribute
|
|
25
|
+
* names found in `attributes` will be skipped.
|
|
26
|
+
*/
|
|
27
|
+
export function compileCss(config = {}) {
|
|
28
|
+
return tree => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const stylePromises = []
|
|
31
|
+
|
|
32
|
+
tree.walk(node => {
|
|
33
|
+
if (node && node.tag === 'style' && node.content) {
|
|
34
|
+
if (node.attrs && Object.keys(node.attrs).some(attr => attributes.has(attr))) {
|
|
35
|
+
return node
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const css = Array.isArray(node.content)
|
|
39
|
+
? node.content.join('')
|
|
40
|
+
: node.content
|
|
41
|
+
|
|
42
|
+
const promise = processCss(css, config)
|
|
43
|
+
.then(processedCss => {
|
|
44
|
+
node.content = [processedCss]
|
|
45
|
+
})
|
|
46
|
+
.catch(error => {
|
|
47
|
+
console.warn('Error processing CSS in style tag:', error.message)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
stylePromises.push(promise)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return node
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
Promise.all(stylePromises)
|
|
57
|
+
.then(() => resolve(tree))
|
|
58
|
+
.catch(reject)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function processCss(css, config) {
|
|
64
|
+
/**
|
|
65
|
+
* PostCSS pipeline. Plugins defined and added here
|
|
66
|
+
* will apply to all `<style>` tags in the HTML,
|
|
67
|
+
* unless marked to be excluded.
|
|
68
|
+
*/
|
|
69
|
+
let resolveCSSProps = get(config, 'css.resolveProps')
|
|
70
|
+
if (resolveCSSProps !== false) {
|
|
71
|
+
resolveCSSProps = merge(resolveCSSProps, { preserve: false })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let lightningCssOptions = get(config, 'css.lightning')
|
|
75
|
+
if (lightningCssOptions !== false) {
|
|
76
|
+
lightningCssOptions = merge(
|
|
77
|
+
lightningCssOptions,
|
|
78
|
+
{
|
|
79
|
+
targets: {
|
|
80
|
+
ie: 1,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = await postcss([
|
|
88
|
+
tailwindcss(get(config, 'css.tailwind', {})),
|
|
89
|
+
resolveCSSProps && customProperties(resolveCSSProps),
|
|
90
|
+
...get(config, 'postcss.plugins', []),
|
|
91
|
+
].filter(Boolean)).process(css, merge(
|
|
92
|
+
get(config, 'postcss.options', {}),
|
|
93
|
+
{
|
|
94
|
+
from: config.cwd || './',
|
|
95
|
+
parser: postcssSafeParser
|
|
96
|
+
}
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Lightning CSS processing
|
|
101
|
+
*
|
|
102
|
+
* We use this to lower the modern Tailwind CSS 4 syntax
|
|
103
|
+
* to be more email-friendly.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
if (result.css?.trim() && lightningCssOptions !== false) {
|
|
107
|
+
try {
|
|
108
|
+
const { code } = transform(
|
|
109
|
+
merge(
|
|
110
|
+
lightningCssOptions,
|
|
111
|
+
{
|
|
112
|
+
code: Buffer.from(result.css)
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return code.toString()
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.warn('Failed to lower syntax with Lightning CSS:', error.message)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result.css
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn('Error compiling CSS:', error.message)
|
|
126
|
+
return css
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { validAttributeNames } from './postcss/compileCss.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove specific attributes from `<style>` tags in PostHTML.
|
|
5
|
+
*
|
|
6
|
+
* Use to clean up <style> tag attributes after proccessing
|
|
7
|
+
* has taken place. A "raw" attribute is used to indicate
|
|
8
|
+
* that the content should not be compiled.
|
|
9
|
+
* @returns {Function} PostHTML plugin
|
|
10
|
+
*/
|
|
11
|
+
const plugin = () => tree => {
|
|
12
|
+
const process = node => {
|
|
13
|
+
if (node && node.tag === 'style') {
|
|
14
|
+
if (node.attrs && Object.keys(node.attrs).some(attr => validAttributeNames.has(attr))) {
|
|
15
|
+
// Remove the attribute
|
|
16
|
+
for (const attr of Object.keys(node.attrs)) {
|
|
17
|
+
if (validAttributeNames.has(attr)) {
|
|
18
|
+
delete node.attrs[attr]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return tree.walk(process)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default plugin
|
|
@@ -6,18 +6,29 @@ import * as cheerio from 'cheerio/slim'
|
|
|
6
6
|
import remove from 'lodash-es/remove.js'
|
|
7
7
|
import { render } from 'posthtml-render'
|
|
8
8
|
import isEmpty from 'lodash-es/isEmpty.js'
|
|
9
|
+
import { match } from 'posthtml/lib/api.js'
|
|
9
10
|
import safeParser from 'postcss-safe-parser'
|
|
10
11
|
import isObject from 'lodash-es/isObject.js'
|
|
11
12
|
import { parser as parse } from 'posthtml-parser'
|
|
12
13
|
import { useAttributeSizes } from './useAttributeSizes.js'
|
|
13
14
|
import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* PostHTML plugin to inline CSS
|
|
18
|
+
* @param {*} options `css.inline` object from config
|
|
19
|
+
* @returns {Function} PostHTML tree
|
|
20
|
+
*/
|
|
21
|
+
export default (options = {}) => tree => {
|
|
22
|
+
return inline(render(tree), options)
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Function to inline CSS styles with Posthtml and Juice.
|
|
27
|
+
*
|
|
28
|
+
* @param {*} html HTML string to process
|
|
29
|
+
* @param {*} options Options for Juice
|
|
30
|
+
* @returns {string} HTML with inlined styles
|
|
31
|
+
*/
|
|
21
32
|
export async function inline(html = '', options = {}) {
|
|
22
33
|
// Exit early if no HTML is passed
|
|
23
34
|
if (typeof html !== 'string' || html === '') {
|
|
@@ -32,21 +43,26 @@ export async function inline(html = '', options = {}) {
|
|
|
32
43
|
options.safelist = new Set([
|
|
33
44
|
...get(options, 'safelist', []),
|
|
34
45
|
...[
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
46
|
+
'body', // Gmail
|
|
47
|
+
'gmail', // Gmail
|
|
48
|
+
'apple', // Apple Mail
|
|
49
|
+
'ios', // Mail on iOS
|
|
50
|
+
'ox-', // Open-Xchange
|
|
51
|
+
'yahoo', // Yahoo! Mail
|
|
52
|
+
'outlook', // Outlook Mac and Android
|
|
41
53
|
'[data-ogs', // Outlook.com
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
54
|
+
'bloop_container', // Airmail
|
|
55
|
+
'Singleton', // Apple Mail 10
|
|
56
|
+
'unused', // Notes 8
|
|
57
|
+
'moz-text-html', // Thunderbird
|
|
58
|
+
'mail-detail-content', // Comcast, Libero webmail
|
|
59
|
+
'mail-content', // Notion
|
|
47
60
|
'edo', // Edison (all)
|
|
48
61
|
'#msgBody', // Freenet uses #msgBody
|
|
49
|
-
'
|
|
62
|
+
'lang', // Fenced code blocks
|
|
63
|
+
'ShadowHTML', // Superhuman
|
|
64
|
+
'spark', // Spark
|
|
65
|
+
'at-', // Safe class names for container queries
|
|
50
66
|
],
|
|
51
67
|
])
|
|
52
68
|
|
|
@@ -63,32 +79,34 @@ export async function inline(html = '', options = {}) {
|
|
|
63
79
|
})
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
const $ = cheerio.load(html, {
|
|
67
|
-
xml: {
|
|
68
|
-
decodeEntities: false,
|
|
69
|
-
xmlMode: false,
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Add a `data-embed` attribute to style tags that have the embed attribute
|
|
74
|
-
$('style[embed]:not([data-embed])').each((_i, el) => {
|
|
75
|
-
$(el).attr('data-embed', '')
|
|
76
|
-
})
|
|
77
|
-
$('style[data-embed]:not([embed])').each((_i, el) => {
|
|
78
|
-
$(el).attr('embed', '')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
82
|
/**
|
|
82
83
|
* Inline the CSS
|
|
83
84
|
*
|
|
84
85
|
* If customCSS is passed, inline that CSS specifically
|
|
85
86
|
* Otherwise, use Juice's default inlining
|
|
86
87
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const tree = parse(html, getPosthtmlOptions())
|
|
89
|
+
tree.match = match
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add a `data-embed` attribute to style tags that have the `embed`
|
|
93
|
+
* attribute, so that Juice can skip them.
|
|
94
|
+
*/
|
|
95
|
+
tree.match({ tag: 'style' }, (node) => {
|
|
96
|
+
if (node.attrs && node.attrs.embed !== undefined) {
|
|
97
|
+
node.attrs['data-embed'] = true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (node.attrs && node.attrs['data-embed'] !== undefined) {
|
|
101
|
+
node.attrs.embed = true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return node
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let inlined_html = css
|
|
108
|
+
? juice.inlineContent(render(tree), css, { removeStyleTags, ...options })
|
|
109
|
+
: juice(render(tree), { removeStyleTags, ...options })
|
|
92
110
|
|
|
93
111
|
/**
|
|
94
112
|
* Prefer attribute sizes
|
|
@@ -99,23 +117,34 @@ export async function inline(html = '', options = {}) {
|
|
|
99
117
|
* natural size.
|
|
100
118
|
*/
|
|
101
119
|
if (options.useAttributeSizes) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
})
|
|
107
|
-
)
|
|
120
|
+
inlined_html = await useAttributeSizes(inlined_html, {
|
|
121
|
+
width: juice.widthElements,
|
|
122
|
+
height: juice.heightElements,
|
|
123
|
+
}, getPosthtmlOptions())
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
* Remove inlined selectors from the HTML
|
|
128
|
+
*
|
|
129
|
+
*/
|
|
130
|
+
const inlined_tree = parse(inlined_html, getPosthtmlOptions())
|
|
131
|
+
inlined_tree.match = match
|
|
132
|
+
|
|
133
|
+
const preservedAtRules = get(options, 'preservedAtRules', ['media'])
|
|
134
|
+
const selectors = new Set()
|
|
135
|
+
|
|
136
|
+
inlined_tree.match({ tag: 'style' }, node => {
|
|
137
|
+
// If this is an embedded style tag, exit early
|
|
138
|
+
if (node.attrs && ('data-embed' in node.attrs || 'embed' in node.attrs)) {
|
|
139
|
+
return node
|
|
140
|
+
}
|
|
141
|
+
|
|
115
142
|
// Parse the CSS
|
|
116
143
|
const { root } = postcss()
|
|
117
144
|
.process(
|
|
118
|
-
|
|
145
|
+
Array.isArray(node.content)
|
|
146
|
+
? node.content.join('')
|
|
147
|
+
: node.content,
|
|
119
148
|
{
|
|
120
149
|
from: undefined,
|
|
121
150
|
parser: safeParser
|
|
@@ -129,14 +158,15 @@ export async function inline(html = '', options = {}) {
|
|
|
129
158
|
|
|
130
159
|
const combinedRegex = new RegExp(combinedPattern)
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Preserve selectors in at rules
|
|
161
|
+
// Preserve selectors in predefined at-rules
|
|
135
162
|
root.walkAtRules(rule => {
|
|
136
|
-
if (
|
|
163
|
+
if (preservedAtRules.includes(rule.name)) {
|
|
137
164
|
rule.walkRules(rule => {
|
|
138
165
|
options.safelist.add(rule.selector)
|
|
139
166
|
})
|
|
167
|
+
} else {
|
|
168
|
+
// Remove the at rule if it's not predefined
|
|
169
|
+
rule.remove()
|
|
140
170
|
}
|
|
141
171
|
})
|
|
142
172
|
|
|
@@ -152,11 +182,12 @@ export async function inline(html = '', options = {}) {
|
|
|
152
182
|
prop: get(rule.nodes[0], 'prop')
|
|
153
183
|
})
|
|
154
184
|
}
|
|
155
|
-
// Preserve pseudo selectors
|
|
156
185
|
else {
|
|
186
|
+
// Preserve pseudo selectors
|
|
157
187
|
options.safelist.add(selector)
|
|
158
188
|
}
|
|
159
189
|
|
|
190
|
+
|
|
160
191
|
if (options.removeInlinedSelectors) {
|
|
161
192
|
// Remove the rule in the <style> tag as long as it's not a preserved class
|
|
162
193
|
if (!options.safelist.has(selector) && !combinedRegex.test(selector)) {
|
|
@@ -164,30 +195,42 @@ export async function inline(html = '', options = {}) {
|
|
|
164
195
|
}
|
|
165
196
|
|
|
166
197
|
// Update the <style> tag contents
|
|
167
|
-
|
|
198
|
+
node.content = root.toString()
|
|
168
199
|
}
|
|
169
200
|
})
|
|
170
201
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* 1. `preferUnitlessValues` - Replace unit values with `0` where possible
|
|
175
|
-
* 2. `removeInlinedSelectors` - Remove inlined selectors from the HTML
|
|
176
|
-
*/
|
|
202
|
+
// This is inlined_tree
|
|
203
|
+
return node
|
|
204
|
+
})
|
|
177
205
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
/**
|
|
207
|
+
* CSS optimizations
|
|
208
|
+
*
|
|
209
|
+
* `preferUnitlessValues` - Replace unit values with `0` where possible
|
|
210
|
+
* `removeInlinedSelectors` - Remove inlined selectors from the HTML
|
|
211
|
+
*/
|
|
181
212
|
|
|
213
|
+
const $ = cheerio.load(render(inlined_tree), {
|
|
214
|
+
xml: {
|
|
215
|
+
decodeEntities: false,
|
|
216
|
+
xmlMode: false,
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Loop over selectors that we found in the <style> tags
|
|
221
|
+
selectors.forEach(({ name, prop }) => {
|
|
222
|
+
try {
|
|
223
|
+
const elements = $(name).get()
|
|
182
224
|
// If the property is excluded from inlining, skip
|
|
183
225
|
if (!juice.excludedProperties.includes(prop)) {
|
|
184
226
|
// Find the selector in the HTML
|
|
185
227
|
elements.forEach((el) => {
|
|
186
228
|
// Get a `property|value` list from the inline style attribute
|
|
187
229
|
const styleAttr = $(el).attr('style')
|
|
230
|
+
// Store the element's inline styles
|
|
188
231
|
const inlineStyles = {}
|
|
189
232
|
|
|
190
|
-
//
|
|
233
|
+
// `preferUnitlessValues`
|
|
191
234
|
if (styleAttr) {
|
|
192
235
|
try {
|
|
193
236
|
const root = postcss.parse(`* { ${styleAttr} }`)
|
|
@@ -213,13 +256,13 @@ export async function inline(html = '', options = {}) {
|
|
|
213
256
|
'style',
|
|
214
257
|
Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
|
|
215
258
|
)
|
|
216
|
-
} catch {}
|
|
259
|
+
} catch { }
|
|
217
260
|
}
|
|
218
261
|
|
|
219
262
|
// Get the classes from the element's class attribute
|
|
220
263
|
const classes = $(el).attr('class')
|
|
221
264
|
|
|
222
|
-
//
|
|
265
|
+
// `removeInlinedSelectors`
|
|
223
266
|
if (options.removeInlinedSelectors && classes) {
|
|
224
267
|
const classList = classes.split(' ')
|
|
225
268
|
|
|
@@ -240,12 +283,24 @@ export async function inline(html = '', options = {}) {
|
|
|
240
283
|
}
|
|
241
284
|
})
|
|
242
285
|
}
|
|
243
|
-
}
|
|
286
|
+
} catch { }
|
|
244
287
|
})
|
|
245
288
|
|
|
246
|
-
|
|
247
|
-
|
|
289
|
+
const optimized_tree = parse($.html(), getPosthtmlOptions())
|
|
290
|
+
optimized_tree.match = match
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Remove the `embed` attribute from style tags
|
|
294
|
+
*/
|
|
295
|
+
optimized_tree.match({ tag: 'style' }, (node) => {
|
|
296
|
+
// If this is an embedded style tag, exit early
|
|
297
|
+
if (node.attrs && node.attrs.embed !== undefined) {
|
|
298
|
+
delete node.attrs.embed
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return node
|
|
248
302
|
})
|
|
249
303
|
|
|
250
|
-
return
|
|
304
|
+
// Finally, return the inlined html
|
|
305
|
+
return render(optimized_tree)
|
|
251
306
|
}
|
|
@@ -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
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
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
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
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
|
-
'
|
|
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 = {
|
|
@@ -118,16 +118,25 @@ export async function readFileConfig(config) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* Override the `content` key in `baseConfig` with the one in `envConfig
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* order to avoid compiling unnecessary files.
|
|
121
|
+
* Override the `build.content` key in `baseConfig` with the one in `envConfig`
|
|
122
|
+
* if present. We do this so that each build uses its own `content` paths,
|
|
123
|
+
* in order to avoid compiling unnecessary files.
|
|
125
124
|
*/
|
|
126
|
-
if (Array.isArray(envConfig.content)) {
|
|
127
|
-
baseConfig.
|
|
125
|
+
if (envConfig.build && Array.isArray(envConfig.build.content)) {
|
|
126
|
+
baseConfig.build = baseConfig.build || {}
|
|
127
|
+
baseConfig.build.content = envConfig.build.content
|
|
128
|
+
// Remove build.content from envConfig to prevent merging duplicates
|
|
129
|
+
envConfig = { ...envConfig, build: { ...envConfig.build, content: undefined } }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Merge envConfig and baseConfig, but ensure build.content is not duplicated
|
|
133
|
+
const merged = merge(envConfig, baseConfig)
|
|
134
|
+
|
|
135
|
+
if (baseConfig.build && Array.isArray(baseConfig.build.content)) {
|
|
136
|
+
merged.build.content = baseConfig.build.content
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
return
|
|
139
|
+
return merged
|
|
131
140
|
} catch (_error) {
|
|
132
141
|
throw new Error('Could not compute config')
|
|
133
142
|
}
|
package/types/config.d.ts
CHANGED
|
@@ -14,9 +14,10 @@ import type { BaseURLConfig } from 'posthtml-base-url';
|
|
|
14
14
|
import type URLParametersConfig from './urlParameters';
|
|
15
15
|
import type { PostCssCalcOptions } from 'postcss-calc';
|
|
16
16
|
import type { PostHTMLFetchConfig } from 'posthtml-fetch';
|
|
17
|
-
import type { Config as TailwindConfig } from 'tailwindcss';
|
|
18
17
|
import type { PostHTMLComponents } from 'posthtml-component';
|
|
19
18
|
import type { PostHTMLExpressions } from 'posthtml-expressions';
|
|
19
|
+
import type { TransformOptions, CustomAtRules } from 'lightningcss';
|
|
20
|
+
import type { PostCSSSortMediaQueriesOptions } from './css/combineMediaQueries';
|
|
20
21
|
|
|
21
22
|
export default interface Config {
|
|
22
23
|
/**
|
|
@@ -217,9 +218,30 @@ export default interface Config {
|
|
|
217
218
|
sixHex?: boolean;
|
|
218
219
|
|
|
219
220
|
/**
|
|
220
|
-
*
|
|
221
|
+
* Configure Lightning CSS.
|
|
222
|
+
*
|
|
223
|
+
* This is used mainly for lowering CSS syntax to be more compatible with email clients.
|
|
224
|
+
*/
|
|
225
|
+
lightningcss?: TransformOptions<CustomAtRules>;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Combine media queries in your CSS.
|
|
229
|
+
*
|
|
230
|
+
* @default { sort: 'mobile-first' }
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```
|
|
234
|
+
* export default {
|
|
235
|
+
* css: {
|
|
236
|
+
* combineMediaQueries: {
|
|
237
|
+
* sort: 'desktop-first',
|
|
238
|
+
* onlyTopLevel: true,
|
|
239
|
+
* }
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
221
243
|
*/
|
|
222
|
-
|
|
244
|
+
combineMediaQueries?: PostCSSSortMediaQueriesOptions;
|
|
223
245
|
}
|
|
224
246
|
|
|
225
247
|
/**
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Plugin } from 'postcss';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort function for media queries.
|
|
5
|
+
* Takes two media query strings and returns a number indicating their relative order.
|
|
6
|
+
*/
|
|
7
|
+
export type SortFunction = (a: string, b: string) => number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration object for sort-css-media-queries library.
|
|
11
|
+
*/
|
|
12
|
+
export interface SortConfiguration {
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for the postcss-sort-media-queries plugin.
|
|
18
|
+
*/
|
|
19
|
+
export interface PostCSSSortMediaQueriesOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Sorting method for media queries.
|
|
22
|
+
*
|
|
23
|
+
* @default 'mobile-first'
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```
|
|
27
|
+
* // Use built-in mobile-first sorting
|
|
28
|
+
* sort: 'mobile-first'
|
|
29
|
+
*
|
|
30
|
+
* // Use built-in desktop-first sorting
|
|
31
|
+
* sort: 'desktop-first'
|
|
32
|
+
*
|
|
33
|
+
* // Use custom sorting function
|
|
34
|
+
* sort: (a, b) => a.localeCompare(b)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
sort?: 'mobile-first' | 'desktop-first' | SortFunction;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Custom configuration object for the sort-css-media-queries library.
|
|
41
|
+
* When provided, it will be used to create a custom sort function.
|
|
42
|
+
*
|
|
43
|
+
* @default false
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```
|
|
47
|
+
* configuration: {
|
|
48
|
+
* unitlessMqAlwaysFirst: true,
|
|
49
|
+
* sort: 'mobile-first'
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
configuration?: false | SortConfiguration;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether to only process media queries at the top level (direct children of root).
|
|
57
|
+
* When true, nested media queries will be ignored.
|
|
58
|
+
*
|
|
59
|
+
* @default false
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```
|
|
63
|
+
* onlyTopLevel: true
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
onlyTopLevel?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* PostCSS plugin that sorts CSS media queries.
|
|
71
|
+
*
|
|
72
|
+
* @param options - Plugin configuration options
|
|
73
|
+
* @returns PostCSS plugin instance
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```
|
|
77
|
+
* import postcss from 'postcss';
|
|
78
|
+
* import sortMediaQueries from 'postcss-sort-media-queries';
|
|
79
|
+
*
|
|
80
|
+
* postcss([
|
|
81
|
+
* sortMediaQueries({
|
|
82
|
+
* sort: 'mobile-first',
|
|
83
|
+
* onlyTopLevel: false
|
|
84
|
+
* })
|
|
85
|
+
* ])
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function postcssSortMediaQueries(options?: PostCSSSortMediaQueriesOptions): Plugin;
|
|
89
|
+
|
|
90
|
+
export default postcssSortMediaQueries;
|