@maizzle/framework 6.0.0-1 → 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 +79 -0
- package/package.json +2 -4
- package/src/posthtml/index.js +9 -40
- 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 +176 -123
- package/src/transformers/purge.js +15 -11
- package/src/transformers/safeClassNames.js +2 -1
- package/src/utils/getConfigByFilePath.js +16 -7
- package/src/posthtml/plugins/lowerCssSyntax.js +0 -43
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ 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
|
+
|
|
7
86
|
## [6.0.0-1] - 2025-07-11
|
|
8
87
|
|
|
9
88
|
### Added
|
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",
|
|
@@ -71,8 +71,7 @@
|
|
|
71
71
|
"ora": "^8.1.0",
|
|
72
72
|
"pathe": "^2.0.0",
|
|
73
73
|
"postcss": "^8.4.49",
|
|
74
|
-
"postcss-
|
|
75
|
-
"postcss-css-variables": "^0.19.0",
|
|
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,47 +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
|
-
import lowerCssSyntax from './plugins/lowerCssSyntax.js'
|
|
14
12
|
import combineMediaQueries from './plugins/combineMediaQueries.js'
|
|
13
|
+
import defaultComponentsConfig from './defaultComponentsConfig.js'
|
|
14
|
+
import removeRawStyleAttributes from './plugins/removeRawStyleAttributes.js'
|
|
15
15
|
|
|
16
16
|
// PostCSS
|
|
17
|
-
import
|
|
18
|
-
import postcssCalc from 'postcss-calc'
|
|
19
|
-
import cssVariables from 'postcss-css-variables'
|
|
20
|
-
import postcssSafeParser from 'postcss-safe-parser'
|
|
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
|
-
...get(config, 'postcss.plugins', []),
|
|
40
|
-
],
|
|
41
|
-
merge(
|
|
42
|
-
get(config, 'postcss.options', {}),
|
|
43
|
-
{
|
|
44
|
-
from: config.cwd || './',
|
|
45
|
-
parser: postcssSafeParser
|
|
46
|
-
}
|
|
47
|
-
)
|
|
48
|
-
)
|
|
49
|
-
|
|
50
20
|
/**
|
|
51
21
|
* Define PostHTML options by merging user-provided ones
|
|
52
22
|
* on top of a default configuration.
|
|
@@ -102,18 +72,17 @@ export async function process(html = '', config = {}) {
|
|
|
102
72
|
|
|
103
73
|
return posthtml([
|
|
104
74
|
...beforePlugins,
|
|
105
|
-
|
|
106
|
-
envAttributes(config.env),
|
|
107
|
-
expandLinkTag(),
|
|
108
|
-
postcssPlugin,
|
|
75
|
+
compileCss(config),
|
|
109
76
|
fetchPlugin,
|
|
110
77
|
components(componentsConfig),
|
|
78
|
+
fetchPlugin,
|
|
111
79
|
expandLinkTag(),
|
|
112
|
-
postcssPlugin,
|
|
113
80
|
envTags(config.env),
|
|
114
81
|
envAttributes(config.env),
|
|
115
|
-
|
|
116
|
-
get(config, 'css.combineMediaQueries') !== false
|
|
82
|
+
compileCss(config),
|
|
83
|
+
get(config, 'css.combineMediaQueries') !== false
|
|
84
|
+
&& combineMediaQueries(get(config, 'css.combineMediaQueries', { sort: 'mobile-first' })),
|
|
85
|
+
removeRawStyleAttributes(),
|
|
117
86
|
...get(
|
|
118
87
|
config,
|
|
119
88
|
'posthtml.plugins.after',
|
|
@@ -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,90 +195,112 @@ 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
|
-
*/
|
|
177
|
-
|
|
178
|
-
// Loop over selectors that we found in the <style> tags
|
|
179
|
-
selectors.forEach(({ name, prop }) => {
|
|
180
|
-
try {
|
|
181
|
-
const elements = $(name).get()
|
|
182
|
-
|
|
183
|
-
// If the property is excluded from inlining, skip
|
|
184
|
-
if (!juice.excludedProperties.includes(prop)) {
|
|
185
|
-
// Find the selector in the HTML
|
|
186
|
-
elements.forEach((el) => {
|
|
187
|
-
// Get a `property|value` list from the inline style attribute
|
|
188
|
-
const styleAttr = $(el).attr('style')
|
|
189
|
-
const inlineStyles = {}
|
|
190
|
-
|
|
191
|
-
// 1. `preferUnitlessValues`
|
|
192
|
-
if (styleAttr) {
|
|
193
|
-
try {
|
|
194
|
-
const root = postcss.parse(`* { ${styleAttr} }`)
|
|
195
|
-
|
|
196
|
-
root.first.each((decl) => {
|
|
197
|
-
const property = decl.prop
|
|
198
|
-
let value = decl.value
|
|
199
|
-
|
|
200
|
-
if (value && options.preferUnitlessValues) {
|
|
201
|
-
value = value.replace(
|
|
202
|
-
/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g,
|
|
203
|
-
'0'
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (property) {
|
|
208
|
-
inlineStyles[property] = value
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// Update the element's style attribute with the new value
|
|
213
|
-
$(el).attr(
|
|
214
|
-
'style',
|
|
215
|
-
Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
|
|
216
|
-
)
|
|
217
|
-
} catch {}
|
|
218
|
-
}
|
|
202
|
+
// This is inlined_tree
|
|
203
|
+
return node
|
|
204
|
+
})
|
|
219
205
|
|
|
220
|
-
|
|
221
|
-
|
|
206
|
+
/**
|
|
207
|
+
* CSS optimizations
|
|
208
|
+
*
|
|
209
|
+
* `preferUnitlessValues` - Replace unit values with `0` where possible
|
|
210
|
+
* `removeInlinedSelectors` - Remove inlined selectors from the HTML
|
|
211
|
+
*/
|
|
222
212
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
213
|
+
const $ = cheerio.load(render(inlined_tree), {
|
|
214
|
+
xml: {
|
|
215
|
+
decodeEntities: false,
|
|
216
|
+
xmlMode: false,
|
|
217
|
+
}
|
|
218
|
+
})
|
|
226
219
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
220
|
+
// Loop over selectors that we found in the <style> tags
|
|
221
|
+
selectors.forEach(({ name, prop }) => {
|
|
222
|
+
try {
|
|
223
|
+
const elements = $(name).get()
|
|
224
|
+
// If the property is excluded from inlining, skip
|
|
225
|
+
if (!juice.excludedProperties.includes(prop)) {
|
|
226
|
+
// Find the selector in the HTML
|
|
227
|
+
elements.forEach((el) => {
|
|
228
|
+
// Get a `property|value` list from the inline style attribute
|
|
229
|
+
const styleAttr = $(el).attr('style')
|
|
230
|
+
// Store the element's inline styles
|
|
231
|
+
const inlineStyles = {}
|
|
232
|
+
|
|
233
|
+
// `preferUnitlessValues`
|
|
234
|
+
if (styleAttr) {
|
|
235
|
+
try {
|
|
236
|
+
const root = postcss.parse(`* { ${styleAttr} }`)
|
|
237
|
+
|
|
238
|
+
root.first.each((decl) => {
|
|
239
|
+
const property = decl.prop
|
|
240
|
+
let value = decl.value
|
|
241
|
+
|
|
242
|
+
if (value && options.preferUnitlessValues) {
|
|
243
|
+
value = value.replace(
|
|
244
|
+
/\b0(px|rem|em|%|vh|vw|vmin|vmax|in|cm|mm|pt|pc|ex|ch)\b/g,
|
|
245
|
+
'0'
|
|
246
|
+
)
|
|
232
247
|
}
|
|
233
248
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
$(el).attr('class', classList.join(' '))
|
|
237
|
-
} else {
|
|
238
|
-
$(el).removeAttr('class')
|
|
249
|
+
if (property) {
|
|
250
|
+
inlineStyles[property] = value
|
|
239
251
|
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Update the element's style attribute with the new value
|
|
255
|
+
$(el).attr(
|
|
256
|
+
'style',
|
|
257
|
+
Object.entries(inlineStyles).map(([property, value]) => `${property}: ${value}`).join('; ')
|
|
258
|
+
)
|
|
259
|
+
} catch { }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Get the classes from the element's class attribute
|
|
263
|
+
const classes = $(el).attr('class')
|
|
264
|
+
|
|
265
|
+
// `removeInlinedSelectors`
|
|
266
|
+
if (options.removeInlinedSelectors && classes) {
|
|
267
|
+
const classList = classes.split(' ')
|
|
268
|
+
|
|
269
|
+
// If the class has been inlined in the style attribute...
|
|
270
|
+
if (has(inlineStyles, prop)) {
|
|
271
|
+
// Try to remove the classes that have been inlined
|
|
272
|
+
if (![...options.safelist].some(item => item.includes(name))) {
|
|
273
|
+
remove(classList, classToRemove => name.includes(classToRemove))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Update the class list on the element with the new classes
|
|
277
|
+
if (classList.length > 0) {
|
|
278
|
+
$(el).attr('class', classList.join(' '))
|
|
279
|
+
} else {
|
|
280
|
+
$(el).removeAttr('class')
|
|
240
281
|
}
|
|
241
282
|
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
} catch { }
|
|
246
287
|
})
|
|
247
288
|
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
250
302
|
})
|
|
251
303
|
|
|
252
|
-
return
|
|
304
|
+
// Finally, return the inlined html
|
|
305
|
+
return render(optimized_tree)
|
|
253
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
|
}
|
|
@@ -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
|