better-svelte-email 0.3.5 → 1.0.0-beta.0
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.svelte +7 -3
- package/dist/components/Button.svelte +10 -12
- package/dist/components/Button.svelte.d.ts +2 -2
- package/dist/index.d.ts +2 -6
- package/dist/index.js +3 -5
- package/dist/preprocessor/index.d.ts +19 -0
- package/dist/preprocessor/index.js +19 -0
- package/dist/preview/index.d.ts +34 -5
- package/dist/preview/index.js +91 -44
- package/dist/render/index.d.ts +66 -0
- package/dist/render/index.js +138 -0
- package/dist/render/utils/compatibility/sanitize-class-name.d.ts +7 -0
- package/dist/render/utils/compatibility/sanitize-class-name.js +35 -0
- package/dist/render/utils/css/extract-rules-per-class.d.ts +5 -0
- package/dist/render/utils/css/extract-rules-per-class.js +37 -0
- package/dist/render/utils/css/get-custom-properties.d.ts +8 -0
- package/dist/render/utils/css/get-custom-properties.js +37 -0
- package/dist/render/utils/css/is-rule-inlinable.d.ts +2 -0
- package/dist/render/utils/css/is-rule-inlinable.js +6 -0
- package/dist/render/utils/css/make-inline-styles-for.d.ts +3 -0
- package/dist/render/utils/css/make-inline-styles-for.js +57 -0
- package/dist/render/utils/css/resolve-all-css-variables.d.ts +8 -0
- package/dist/render/utils/css/resolve-all-css-variables.js +123 -0
- package/dist/render/utils/css/resolve-calc-expressions.d.ts +5 -0
- package/dist/render/utils/css/resolve-calc-expressions.js +126 -0
- package/dist/render/utils/css/sanitize-declarations.d.ts +15 -0
- package/dist/render/utils/css/sanitize-declarations.js +354 -0
- package/dist/render/utils/css/sanitize-non-inlinable-rules.d.ts +11 -0
- package/dist/render/utils/css/sanitize-non-inlinable-rules.js +33 -0
- package/dist/render/utils/css/sanitize-stylesheet.d.ts +2 -0
- package/dist/render/utils/css/sanitize-stylesheet.js +8 -0
- package/dist/render/utils/css/unwrap-value.d.ts +2 -0
- package/dist/render/utils/css/unwrap-value.js +6 -0
- package/dist/render/utils/html/is-valid-node.d.ts +2 -0
- package/dist/render/utils/html/is-valid-node.js +3 -0
- package/dist/render/utils/html/remove-attributes-functions.d.ts +2 -0
- package/dist/render/utils/html/remove-attributes-functions.js +10 -0
- package/dist/render/utils/html/walk.d.ts +15 -0
- package/dist/render/utils/html/walk.js +36 -0
- package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.d.ts +4 -0
- package/dist/render/utils/tailwindcss/add-inlined-styles-to-element.js +61 -0
- package/dist/render/utils/tailwindcss/pixel-based-preset.d.ts +2 -0
- package/dist/render/utils/tailwindcss/pixel-based-preset.js +58 -0
- package/dist/render/utils/tailwindcss/setup-tailwind.d.ts +7 -0
- package/dist/render/utils/tailwindcss/setup-tailwind.js +67 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/index.js +899 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/preflight.js +396 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/theme.js +465 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.d.ts +2 -0
- package/dist/render/utils/tailwindcss/tailwind-stylesheets/utilities.js +4 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +13 -10
- package/package.json +17 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { render as svelteRender } from 'svelte/server';
|
|
2
|
+
import { parse, serialize } from 'parse5';
|
|
3
|
+
import { walk } from './utils/html/walk.js';
|
|
4
|
+
import { setupTailwind } from './utils/tailwindcss/setup-tailwind.js';
|
|
5
|
+
import { sanitizeStyleSheet } from './utils/css/sanitize-stylesheet.js';
|
|
6
|
+
import { extractRulesPerClass } from './utils/css/extract-rules-per-class.js';
|
|
7
|
+
import { getCustomProperties } from './utils/css/get-custom-properties.js';
|
|
8
|
+
import { generate, List } from 'css-tree';
|
|
9
|
+
import { sanitizeNonInlinableRules } from './utils/css/sanitize-non-inlinable-rules.js';
|
|
10
|
+
import { addInlinedStylesToElement } from './utils/tailwindcss/add-inlined-styles-to-element.js';
|
|
11
|
+
import { isValidNode } from './utils/html/is-valid-node.js';
|
|
12
|
+
import { removeAttributesFunctions } from './utils/html/remove-attributes-functions.js';
|
|
13
|
+
import { convert } from 'html-to-text';
|
|
14
|
+
/**
|
|
15
|
+
* Email renderer that converts Svelte components to email-safe HTML with inlined Tailwind styles.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import Renderer from 'better-svelte-email/renderer';
|
|
20
|
+
* import EmailComponent from './email.svelte';
|
|
21
|
+
*
|
|
22
|
+
* const renderer = new Renderer({
|
|
23
|
+
* theme: {
|
|
24
|
+
* extend: {
|
|
25
|
+
* colors: {
|
|
26
|
+
* brand: '#FF3E00'
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* const html = await renderer.render(EmailComponent, {
|
|
33
|
+
* props: { name: 'John' }
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export default class Renderer {
|
|
38
|
+
tailwindConfig;
|
|
39
|
+
constructor(tailwindConfig = {}) {
|
|
40
|
+
this.tailwindConfig = tailwindConfig;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Renders a Svelte component to email-safe HTML with inlined Tailwind CSS.
|
|
44
|
+
*
|
|
45
|
+
* Automatically:
|
|
46
|
+
* - Converts Tailwind classes to inline styles
|
|
47
|
+
* - Injects media queries into `<head>` for responsive classes
|
|
48
|
+
* - Replaces DOCTYPE with XHTML 1.0 Transitional
|
|
49
|
+
* - Removes comments and Svelte artifacts
|
|
50
|
+
*
|
|
51
|
+
* @param component - The Svelte component to render
|
|
52
|
+
* @param options - Render options including props, context, and idPrefix
|
|
53
|
+
* @returns Email-safe HTML string
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const html = await renderer.render(EmailComponent, {
|
|
58
|
+
* props: { username: 'john_doe', resetUrl: 'https://...' }
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
render = async (component, options) => {
|
|
63
|
+
const { body } = svelteRender(component, options);
|
|
64
|
+
let ast = parse(body);
|
|
65
|
+
ast = removeAttributesFunctions(ast);
|
|
66
|
+
let classesUsed = [];
|
|
67
|
+
const tailwindSetup = await setupTailwind(this.tailwindConfig);
|
|
68
|
+
walk(ast, (node) => {
|
|
69
|
+
if (isValidNode(node)) {
|
|
70
|
+
const classAttr = node.attrs?.find((attr) => attr.name === 'class');
|
|
71
|
+
if (classAttr && classAttr.value) {
|
|
72
|
+
const classes = classAttr.value.split(/\s+/).filter(Boolean);
|
|
73
|
+
classesUsed = [...classesUsed, ...classes];
|
|
74
|
+
tailwindSetup.addUtilities(classes);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return node;
|
|
78
|
+
});
|
|
79
|
+
const styleSheet = tailwindSetup.getStyleSheet();
|
|
80
|
+
sanitizeStyleSheet(styleSheet);
|
|
81
|
+
const { inlinable: inlinableRules, nonInlinable: nonInlinableRules } = extractRulesPerClass(styleSheet, classesUsed);
|
|
82
|
+
const customProperties = getCustomProperties(styleSheet);
|
|
83
|
+
const nonInlineStyles = {
|
|
84
|
+
type: 'StyleSheet',
|
|
85
|
+
children: new List().fromArray(Array.from(nonInlinableRules.values()))
|
|
86
|
+
};
|
|
87
|
+
sanitizeNonInlinableRules(nonInlineStyles);
|
|
88
|
+
const hasNonInlineStylesToApply = nonInlinableRules.size > 0;
|
|
89
|
+
let appliedNonInlineStyles = false;
|
|
90
|
+
let hasHead = false;
|
|
91
|
+
const unknownClasses = [];
|
|
92
|
+
ast = walk(ast, (node) => {
|
|
93
|
+
if (isValidNode(node)) {
|
|
94
|
+
const elementWithInlinedStyles = addInlinedStylesToElement(node, inlinableRules, nonInlinableRules, customProperties, unknownClasses);
|
|
95
|
+
if (node.nodeName === 'head') {
|
|
96
|
+
hasHead = true;
|
|
97
|
+
}
|
|
98
|
+
return elementWithInlinedStyles;
|
|
99
|
+
}
|
|
100
|
+
return node;
|
|
101
|
+
});
|
|
102
|
+
let serialized = serialize(ast);
|
|
103
|
+
if (unknownClasses.length > 0) {
|
|
104
|
+
console.warn(`[better-svelte-email] You are using the following classes that were not recognized: ${unknownClasses.join(' ')}.`);
|
|
105
|
+
}
|
|
106
|
+
if (hasHead && hasNonInlineStylesToApply) {
|
|
107
|
+
appliedNonInlineStyles = true;
|
|
108
|
+
serialized = serialized.replace('<head>', '<head>' + '<style>' + generate(nonInlineStyles) + '</style>');
|
|
109
|
+
}
|
|
110
|
+
if (hasNonInlineStylesToApply && !appliedNonInlineStyles) {
|
|
111
|
+
throw new Error(`You are trying to use the following Tailwind classes that cannot be inlined: ${Array.from(nonInlinableRules.keys()).join(' ')}.
|
|
112
|
+
For the media queries to work properly on rendering, they need to be added into a <style> tag inside of a <head> tag,
|
|
113
|
+
the render function tried finding a <head> element but just wasn't able to find it.
|
|
114
|
+
|
|
115
|
+
Make sure that you have a <head> element at any depth.
|
|
116
|
+
This can also be our <Head> component.
|
|
117
|
+
|
|
118
|
+
If you do already have a <head> element at some depth,
|
|
119
|
+
please file a bug https://github.com/Konixy/better-svelte-email/issues/new?assignees=&labels=bug&projects=.`);
|
|
120
|
+
}
|
|
121
|
+
// Replace various DOCTYPE formats with XHTML 1.0 Transitional
|
|
122
|
+
serialized = serialized.replace(/<!DOCTYPE\s+html[^>]*>/i, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">');
|
|
123
|
+
return serialized;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Render HTML as plain text
|
|
128
|
+
* @param markup - HTML string
|
|
129
|
+
* @returns Plain text string
|
|
130
|
+
*/
|
|
131
|
+
export const toPlainText = (markup) => {
|
|
132
|
+
return convert(markup, {
|
|
133
|
+
selectors: [
|
|
134
|
+
{ selector: 'img', format: 'skip' },
|
|
135
|
+
{ selector: '#__better-svelte-email-preview', format: 'skip' }
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces special characters to avoid problems on email clients.
|
|
3
|
+
*
|
|
4
|
+
* @param className - This should not come with any escaped charcters, it should come the same
|
|
5
|
+
* as is on the `className` attribute on React elements.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeClassName(className: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const digitToNameMap = {
|
|
2
|
+
'0': 'zero',
|
|
3
|
+
'1': 'one',
|
|
4
|
+
'2': 'two',
|
|
5
|
+
'3': 'three',
|
|
6
|
+
'4': 'four',
|
|
7
|
+
'5': 'five',
|
|
8
|
+
'6': 'six',
|
|
9
|
+
'7': 'seven',
|
|
10
|
+
'8': 'eight',
|
|
11
|
+
'9': 'nine'
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Replaces special characters to avoid problems on email clients.
|
|
15
|
+
*
|
|
16
|
+
* @param className - This should not come with any escaped charcters, it should come the same
|
|
17
|
+
* as is on the `className` attribute on React elements.
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeClassName(className) {
|
|
20
|
+
return className
|
|
21
|
+
.replaceAll('+', 'plus')
|
|
22
|
+
.replaceAll('[', '')
|
|
23
|
+
.replaceAll('%', 'pc')
|
|
24
|
+
.replaceAll(']', '')
|
|
25
|
+
.replaceAll('(', '')
|
|
26
|
+
.replaceAll(')', '')
|
|
27
|
+
.replaceAll('!', 'imprtnt')
|
|
28
|
+
.replaceAll('>', 'gt')
|
|
29
|
+
.replaceAll('<', 'lt')
|
|
30
|
+
.replaceAll('=', 'eq')
|
|
31
|
+
.replace(/^[0-9]/, (digit) => {
|
|
32
|
+
return digitToNameMap[digit];
|
|
33
|
+
})
|
|
34
|
+
.replace(/[^a-zA-Z0-9\-_]/g, '_');
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { string, walk } from 'css-tree';
|
|
2
|
+
import { isRuleInlinable } from './is-rule-inlinable.js';
|
|
3
|
+
export function extractRulesPerClass(root, classes) {
|
|
4
|
+
const classSet = new Set(classes);
|
|
5
|
+
const inlinableRules = new Map();
|
|
6
|
+
const nonInlinableRules = new Map();
|
|
7
|
+
walk(root, {
|
|
8
|
+
visit: 'Rule',
|
|
9
|
+
enter(rule) {
|
|
10
|
+
const selectorClasses = [];
|
|
11
|
+
walk(rule, {
|
|
12
|
+
visit: 'ClassSelector',
|
|
13
|
+
enter(classSelector) {
|
|
14
|
+
selectorClasses.push(string.decode(classSelector.name));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (isRuleInlinable(rule)) {
|
|
18
|
+
for (const className of selectorClasses) {
|
|
19
|
+
if (classSet.has(className)) {
|
|
20
|
+
inlinableRules.set(className, rule);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
for (const className of selectorClasses) {
|
|
26
|
+
if (classSet.has(className)) {
|
|
27
|
+
nonInlinableRules.set(className, rule);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
inlinable: inlinableRules,
|
|
35
|
+
nonInlinable: nonInlinableRules
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CssNode, type Declaration } from 'css-tree';
|
|
2
|
+
export interface CustomProperty {
|
|
3
|
+
syntax?: Declaration;
|
|
4
|
+
inherits?: Declaration;
|
|
5
|
+
initialValue?: Declaration;
|
|
6
|
+
}
|
|
7
|
+
export type CustomProperties = Map<string, CustomProperty>;
|
|
8
|
+
export declare function getCustomProperties(node: CssNode): Map<string, CustomProperty>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { generate, walk } from 'css-tree';
|
|
2
|
+
export function getCustomProperties(node) {
|
|
3
|
+
const customProperties = new Map();
|
|
4
|
+
walk(node, {
|
|
5
|
+
visit: 'Atrule',
|
|
6
|
+
enter(atrule) {
|
|
7
|
+
if (atrule.name === 'property' && atrule.prelude) {
|
|
8
|
+
const prelude = generate(atrule.prelude);
|
|
9
|
+
if (prelude.startsWith('--')) {
|
|
10
|
+
let syntax;
|
|
11
|
+
let inherits;
|
|
12
|
+
let initialValue;
|
|
13
|
+
walk(atrule, {
|
|
14
|
+
visit: 'Declaration',
|
|
15
|
+
enter(declaration) {
|
|
16
|
+
if (declaration.property === 'syntax') {
|
|
17
|
+
syntax = declaration;
|
|
18
|
+
}
|
|
19
|
+
if (declaration.property === 'inherits') {
|
|
20
|
+
inherits = declaration;
|
|
21
|
+
}
|
|
22
|
+
if (declaration.property === 'initial-value') {
|
|
23
|
+
initialValue = declaration;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
customProperties.set(prelude, {
|
|
28
|
+
syntax,
|
|
29
|
+
inherits,
|
|
30
|
+
initialValue
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return customProperties;
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { find } from 'css-tree';
|
|
2
|
+
export function isRuleInlinable(rule) {
|
|
3
|
+
const hasAtRuleInside = find(rule, (node) => node.type === 'Atrule') !== null;
|
|
4
|
+
const hasPseudoSelector = find(rule, (node) => node.type === 'PseudoClassSelector' || node.type === 'PseudoElementSelector') !== null;
|
|
5
|
+
return !hasAtRuleInside && !hasPseudoSelector;
|
|
6
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { generate, walk } from 'css-tree';
|
|
2
|
+
import { unwrapValue } from './unwrap-value.js';
|
|
3
|
+
export function makeInlineStylesFor(inlinableRules, customProperties) {
|
|
4
|
+
let styles = '';
|
|
5
|
+
const localVariableDeclarations = new Map();
|
|
6
|
+
for (const rule of inlinableRules) {
|
|
7
|
+
walk(rule, {
|
|
8
|
+
visit: 'Declaration',
|
|
9
|
+
enter(declaration) {
|
|
10
|
+
if (declaration.property.startsWith('--')) {
|
|
11
|
+
localVariableDeclarations.set(declaration.property, declaration);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
for (const rule of inlinableRules) {
|
|
17
|
+
walk(rule, {
|
|
18
|
+
visit: 'Function',
|
|
19
|
+
enter(func, funcParentListItem) {
|
|
20
|
+
if (func.name === 'var') {
|
|
21
|
+
let variableName;
|
|
22
|
+
walk(func, {
|
|
23
|
+
visit: 'Identifier',
|
|
24
|
+
enter(identifier) {
|
|
25
|
+
variableName = identifier.name;
|
|
26
|
+
return this.break;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (variableName) {
|
|
30
|
+
const definition = localVariableDeclarations.get(variableName);
|
|
31
|
+
if (definition) {
|
|
32
|
+
funcParentListItem.data = unwrapValue(definition.value);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// For most variables tailwindcss defines, they also define a custom
|
|
36
|
+
// property for them with an initial value that we can inline here
|
|
37
|
+
const customProperty = customProperties.get(variableName);
|
|
38
|
+
if (customProperty?.initialValue) {
|
|
39
|
+
funcParentListItem.data = unwrapValue(customProperty.initialValue.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
walk(rule, {
|
|
47
|
+
visit: 'Declaration',
|
|
48
|
+
enter(declaration) {
|
|
49
|
+
if (declaration.property.startsWith('--')) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
styles += `${declaration.property}: ${generate(declaration.value)} ${declaration.important ? '!important' : ''};`;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return styles;
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CssNode, type Declaration } from 'css-tree';
|
|
2
|
+
export interface VariableDefinition {
|
|
3
|
+
declaration: Declaration;
|
|
4
|
+
path: CssNode[];
|
|
5
|
+
variableName: string;
|
|
6
|
+
definition: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveAllCssVariables(node: CssNode): void;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { generate, parse, walk } from 'css-tree';
|
|
2
|
+
function doSelectorsIntersect(first, second) {
|
|
3
|
+
const firstStringified = generate(first);
|
|
4
|
+
const secondStringified = generate(second);
|
|
5
|
+
if (firstStringified === secondStringified) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
let hasSomeUniversal = false;
|
|
9
|
+
const walker = (node, _parentListItem, parentList) => {
|
|
10
|
+
if (hasSomeUniversal)
|
|
11
|
+
return;
|
|
12
|
+
if (node.type === 'PseudoClassSelector' && node.name === 'root') {
|
|
13
|
+
hasSomeUniversal = true;
|
|
14
|
+
}
|
|
15
|
+
if (node.type === 'TypeSelector' && node.name === '*' && parentList.size === 1) {
|
|
16
|
+
hasSomeUniversal = true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
walk(first, walker);
|
|
20
|
+
walk(second, walker);
|
|
21
|
+
if (hasSomeUniversal) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export function resolveAllCssVariables(node) {
|
|
27
|
+
const variableDefinitions = new Set();
|
|
28
|
+
const variableUses = new Set();
|
|
29
|
+
const path = [];
|
|
30
|
+
walk(node, {
|
|
31
|
+
leave() {
|
|
32
|
+
path.shift();
|
|
33
|
+
},
|
|
34
|
+
enter(node) {
|
|
35
|
+
if (node.type === 'Declaration') {
|
|
36
|
+
const declaration = node;
|
|
37
|
+
// Ignores @layer (properties) { ... } to avoid variable resolution conflicts
|
|
38
|
+
if (path.some((ancestor) => ancestor.type === 'Atrule' &&
|
|
39
|
+
ancestor.name === 'layer' &&
|
|
40
|
+
ancestor.prelude !== null &&
|
|
41
|
+
generate(ancestor.prelude).includes('properties'))) {
|
|
42
|
+
path.unshift(node);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (/--[\S]+/.test(declaration.property)) {
|
|
46
|
+
variableDefinitions.add({
|
|
47
|
+
declaration,
|
|
48
|
+
path: [...path],
|
|
49
|
+
variableName: declaration.property,
|
|
50
|
+
definition: generate(declaration.value)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
function parseVariableUsesFrom(node) {
|
|
55
|
+
walk(node, {
|
|
56
|
+
visit: 'Function',
|
|
57
|
+
enter(funcNode) {
|
|
58
|
+
if (funcNode.name === 'var') {
|
|
59
|
+
const children = funcNode.children.toArray();
|
|
60
|
+
const name = generate(children[0]);
|
|
61
|
+
const fallback =
|
|
62
|
+
// The second argument should be an "," Operator Node,
|
|
63
|
+
// such that the actual fallback is only in the third argument
|
|
64
|
+
children[2] ? generate(children[2]) : undefined;
|
|
65
|
+
variableUses.add({
|
|
66
|
+
declaration,
|
|
67
|
+
path: [...path],
|
|
68
|
+
fallback,
|
|
69
|
+
variableName: name,
|
|
70
|
+
raw: generate(funcNode)
|
|
71
|
+
});
|
|
72
|
+
if (fallback?.includes('var(')) {
|
|
73
|
+
const parsedFallback = parse(fallback, {
|
|
74
|
+
context: 'value'
|
|
75
|
+
});
|
|
76
|
+
parseVariableUsesFrom(parsedFallback);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
parseVariableUsesFrom(declaration.value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
path.unshift(node);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
for (const use of variableUses) {
|
|
89
|
+
let hasReplaced = false;
|
|
90
|
+
for (const definition of variableDefinitions) {
|
|
91
|
+
if (use.variableName !== definition.variableName) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (use.path[0]?.type === 'Block' &&
|
|
95
|
+
use.path[1]?.type === 'Atrule' &&
|
|
96
|
+
use.path[2]?.type === 'Block' &&
|
|
97
|
+
use.path[3]?.type === 'Rule' &&
|
|
98
|
+
definition.path[0].type === 'Block' &&
|
|
99
|
+
definition.path[1].type === 'Rule' &&
|
|
100
|
+
doSelectorsIntersect(use.path[3].prelude, definition.path[1].prelude)) {
|
|
101
|
+
use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, definition.definition), {
|
|
102
|
+
context: 'value'
|
|
103
|
+
});
|
|
104
|
+
hasReplaced = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (use.path[0]?.type === 'Block' &&
|
|
108
|
+
use.path[1]?.type === 'Rule' &&
|
|
109
|
+
definition.path[0]?.type === 'Block' &&
|
|
110
|
+
definition.path[1]?.type === 'Rule' &&
|
|
111
|
+
doSelectorsIntersect(use.path[1].prelude, definition.path[1].prelude)) {
|
|
112
|
+
use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, definition.definition), {
|
|
113
|
+
context: 'value'
|
|
114
|
+
});
|
|
115
|
+
hasReplaced = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!hasReplaced && use.fallback) {
|
|
120
|
+
use.declaration.value = parse(generate(use.declaration.value).replaceAll(use.raw, use.fallback), { context: 'value' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { walk } from 'css-tree';
|
|
2
|
+
/**
|
|
3
|
+
* Intentionally only resolves `*` and `/` operations without dealing with parenthesis, because this is the only thing required to run Tailwind v4
|
|
4
|
+
*/
|
|
5
|
+
export function resolveCalcExpressions(node) {
|
|
6
|
+
walk(node, {
|
|
7
|
+
visit: 'Function',
|
|
8
|
+
enter(func, funcListItem) {
|
|
9
|
+
if (func.name === 'calc') {
|
|
10
|
+
/*
|
|
11
|
+
[
|
|
12
|
+
{ type: 'Dimension', loc: null, value: '0.25', unit: 'rem' },
|
|
13
|
+
{ type: 'Operator', loc: null, value: '*' },
|
|
14
|
+
{ type: 'Number', loc: null, value: '2' }
|
|
15
|
+
{ type: 'Percentage', loc: null, value: '2' }
|
|
16
|
+
]
|
|
17
|
+
*/
|
|
18
|
+
func.children.forEach((child, item) => {
|
|
19
|
+
const left = item.prev;
|
|
20
|
+
const right = item.next;
|
|
21
|
+
if (left &&
|
|
22
|
+
right &&
|
|
23
|
+
child.type === 'Operator' &&
|
|
24
|
+
(left.data.type === 'Dimension' ||
|
|
25
|
+
left.data.type === 'Number' ||
|
|
26
|
+
left.data.type === 'Percentage') &&
|
|
27
|
+
(right.data.type === 'Dimension' ||
|
|
28
|
+
right.data.type === 'Number' ||
|
|
29
|
+
right.data.type === 'Percentage')) {
|
|
30
|
+
if (child.value === '*' || child.value === '/') {
|
|
31
|
+
const value = (() => {
|
|
32
|
+
if (child.value === '*') {
|
|
33
|
+
return String(Number.parseFloat(left.data.value) * Number.parseFloat(right.data.value));
|
|
34
|
+
}
|
|
35
|
+
if (right.data.value === '0') {
|
|
36
|
+
return '0';
|
|
37
|
+
}
|
|
38
|
+
return String(Number.parseFloat(left.data.value) / Number.parseFloat(right.data.value));
|
|
39
|
+
})();
|
|
40
|
+
if (left.data.type === 'Dimension' && right.data.type === 'Number') {
|
|
41
|
+
item.data = {
|
|
42
|
+
type: 'Dimension',
|
|
43
|
+
unit: left.data.unit,
|
|
44
|
+
value
|
|
45
|
+
};
|
|
46
|
+
func.children.remove(left);
|
|
47
|
+
func.children.remove(right);
|
|
48
|
+
}
|
|
49
|
+
else if (left.data.type === 'Number' && right.data.type === 'Dimension') {
|
|
50
|
+
item.data = {
|
|
51
|
+
type: 'Dimension',
|
|
52
|
+
unit: right.data.unit,
|
|
53
|
+
value
|
|
54
|
+
};
|
|
55
|
+
func.children.remove(left);
|
|
56
|
+
func.children.remove(right);
|
|
57
|
+
}
|
|
58
|
+
else if (left.data.type === 'Number' && right.data.type === 'Number') {
|
|
59
|
+
item.data = {
|
|
60
|
+
type: 'Number',
|
|
61
|
+
value
|
|
62
|
+
};
|
|
63
|
+
func.children.remove(left);
|
|
64
|
+
func.children.remove(right);
|
|
65
|
+
}
|
|
66
|
+
else if (left.data.type === 'Dimension' &&
|
|
67
|
+
right.data.type === 'Dimension' &&
|
|
68
|
+
left.data.unit === right.data.unit) {
|
|
69
|
+
if (child.value === '/') {
|
|
70
|
+
item.data = {
|
|
71
|
+
type: 'Number',
|
|
72
|
+
value
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
item.data = {
|
|
77
|
+
type: 'Dimension',
|
|
78
|
+
unit: left.data.unit,
|
|
79
|
+
value
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
func.children.remove(left);
|
|
83
|
+
func.children.remove(right);
|
|
84
|
+
}
|
|
85
|
+
else if (left.data.type === 'Percentage' && right.data.type === 'Number') {
|
|
86
|
+
item.data = {
|
|
87
|
+
type: 'Percentage',
|
|
88
|
+
value
|
|
89
|
+
};
|
|
90
|
+
func.children.remove(left);
|
|
91
|
+
func.children.remove(right);
|
|
92
|
+
}
|
|
93
|
+
else if (left.data.type === 'Number' && right.data.type === 'Percentage') {
|
|
94
|
+
item.data = {
|
|
95
|
+
type: 'Percentage',
|
|
96
|
+
value
|
|
97
|
+
};
|
|
98
|
+
func.children.remove(left);
|
|
99
|
+
func.children.remove(right);
|
|
100
|
+
}
|
|
101
|
+
else if (left.data.type === 'Percentage' && right.data.type === 'Percentage') {
|
|
102
|
+
if (child.value === '/') {
|
|
103
|
+
item.data = {
|
|
104
|
+
type: 'Number',
|
|
105
|
+
value
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
item.data = {
|
|
110
|
+
type: 'Percentage',
|
|
111
|
+
value
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
func.children.remove(left);
|
|
115
|
+
func.children.remove(right);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (func.children.size === 1 && func.children.first) {
|
|
121
|
+
funcListItem.data = func.children.first;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CssNode } from 'css-tree';
|
|
2
|
+
/**
|
|
3
|
+
* Meant to do all the things necessary, in a per-declaration basis, to have the best email client
|
|
4
|
+
* support possible.
|
|
5
|
+
*
|
|
6
|
+
* Here's the transformations it does so far:
|
|
7
|
+
* - convert all `rgb` with space-based syntax into a comma based one;
|
|
8
|
+
* - convert all `oklch` values into `rgb`;
|
|
9
|
+
* - convert all hex values into `rgb`;
|
|
10
|
+
* - convert `padding-inline` into `padding-left` and `padding-right`;
|
|
11
|
+
* - convert `padding-block` into `padding-top` and `padding-bottom`;
|
|
12
|
+
* - convert `margin-inline` into `margin-left` and `margin-right`;
|
|
13
|
+
* - convert `margin-block` into `margin-top` and `margin-bottom`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sanitizeDeclarations(nodeContainingDeclarations: CssNode): void;
|