better-svelte-email 1.0.0-beta.2 → 1.0.1
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/README.md +24 -204
- package/dist/components/index.js +1 -2
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -5
- package/dist/preview/EmailTreeNode.svelte +24 -9
- package/package.json +7 -19
- package/dist/preprocessor/head-injector.d.ts +0 -9
- package/dist/preprocessor/head-injector.js +0 -57
- package/dist/preprocessor/index.d.ts +0 -44
- package/dist/preprocessor/index.js +0 -227
- package/dist/preprocessor/parser.d.ts +0 -17
- package/dist/preprocessor/parser.js +0 -315
- package/dist/preprocessor/transformer.d.ts +0 -18
- package/dist/preprocessor/transformer.js +0 -158
- package/dist/preprocessor/types.d.ts +0 -125
- package/dist/preprocessor/types.js +0 -1
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import MagicString from 'magic-string';
|
|
2
|
-
import { parseAttributes } from './parser.js';
|
|
3
|
-
import { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './transformer.js';
|
|
4
|
-
import { injectMediaQueries } from './head-injector.js';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
/**
|
|
7
|
-
* Svelte 5 preprocessor for transforming Tailwind classes in email components
|
|
8
|
-
*
|
|
9
|
-
* @deprecated The preprocessor approach is deprecated. Use the `Renderer` class instead for better performance and flexibility.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```javascript
|
|
13
|
-
* // Old (deprecated):
|
|
14
|
-
* // svelte.config.js
|
|
15
|
-
* import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
|
|
16
|
-
*
|
|
17
|
-
* export default {
|
|
18
|
-
* preprocess: [
|
|
19
|
-
* vitePreprocess(),
|
|
20
|
-
* betterSvelteEmailPreprocessor({
|
|
21
|
-
* pathToEmailFolder: '/src/lib/emails',
|
|
22
|
-
* tailwindConfig: { ... }
|
|
23
|
-
* })
|
|
24
|
-
* ]
|
|
25
|
-
* };
|
|
26
|
-
*
|
|
27
|
-
* // New (recommended):
|
|
28
|
-
* import Renderer from 'better-svelte-email/renderer';
|
|
29
|
-
* import EmailComponent from './email.svelte';
|
|
30
|
-
*
|
|
31
|
-
* const renderer = new Renderer({
|
|
32
|
-
* theme: {
|
|
33
|
-
* extend: {
|
|
34
|
-
* colors: { brand: '#FF3E00' }
|
|
35
|
-
* }
|
|
36
|
-
* }
|
|
37
|
-
* });
|
|
38
|
-
*
|
|
39
|
-
* const html = await renderer.render(EmailComponent, {
|
|
40
|
-
* props: { name: 'John' }
|
|
41
|
-
* });
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
|
|
45
|
-
*/
|
|
46
|
-
export function betterSvelteEmailPreprocessor(options = {}) {
|
|
47
|
-
const { tailwindConfig, pathToEmailFolder = '/src/lib/emails', debug = true } = options;
|
|
48
|
-
// Initialize Tailwind converter once (performance optimization)
|
|
49
|
-
const tailwindConverter = createTailwindConverter(tailwindConfig);
|
|
50
|
-
// Return a Svelte 5 PreprocessorGroup
|
|
51
|
-
return {
|
|
52
|
-
name: 'better-svelte-email',
|
|
53
|
-
/**
|
|
54
|
-
* The markup preprocessor transforms the template/HTML portion
|
|
55
|
-
* This is where we extract and transform Tailwind classes
|
|
56
|
-
*/
|
|
57
|
-
markup({ content, filename }) {
|
|
58
|
-
// Only process .svelte files in the configured email folder
|
|
59
|
-
if (!filename || !filename.includes(pathToEmailFolder)) {
|
|
60
|
-
// Return undefined to skip processing
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (!filename.endsWith('.svelte')) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
// Process the email component
|
|
68
|
-
const result = processEmailComponent(content, filename, tailwindConverter, tailwindConfig);
|
|
69
|
-
// Log warnings if debug mode is enabled
|
|
70
|
-
if (result.warnings.length > 0) {
|
|
71
|
-
if (debug) {
|
|
72
|
-
console.warn(`[better-svelte-email] Warnings for ${path.relative(process.cwd(), filename)}:\n`, result.warnings.join('\n'));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Return the transformed code
|
|
76
|
-
// The preprocessor API expects { code: string } or { code: string, map: SourceMap }
|
|
77
|
-
return {
|
|
78
|
-
code: result.transformedCode
|
|
79
|
-
// Note: Source maps could be added here via MagicString's generateMap()
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
console.error(`[better-svelte-email] Error processing ${filename}:`, error);
|
|
84
|
-
// On error, return undefined to use original content
|
|
85
|
-
// This prevents breaking the build for non-email files
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Process a single email component
|
|
93
|
-
*/
|
|
94
|
-
function processEmailComponent(source, _filename, tailwindConverter, tailwindConfig) {
|
|
95
|
-
const warnings = [];
|
|
96
|
-
let transformedCode = source;
|
|
97
|
-
const allMediaQueries = [];
|
|
98
|
-
// Step 1: Parse and find all class attributes
|
|
99
|
-
const attributes = parseAttributes(source);
|
|
100
|
-
if (attributes.length === 0) {
|
|
101
|
-
// No classes to transform
|
|
102
|
-
return {
|
|
103
|
-
originalCode: source,
|
|
104
|
-
transformedCode: source,
|
|
105
|
-
mediaQueries: [],
|
|
106
|
-
hasHead: false,
|
|
107
|
-
warnings: []
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// Step 2: Transform each class attribute
|
|
111
|
-
const s = new MagicString(transformedCode);
|
|
112
|
-
// Process in reverse order to maintain correct positions
|
|
113
|
-
const sortedAttributes = [...attributes].sort((a, b) => b.class.start - a.class.start);
|
|
114
|
-
for (const attr of sortedAttributes) {
|
|
115
|
-
if (!attr.class.isStatic) {
|
|
116
|
-
// Skip dynamic classes for now
|
|
117
|
-
warnings.push(`Dynamic class expression detected in ${attr.class.elementName}. ` +
|
|
118
|
-
`Only static classes can be transformed at build time.`);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
// Transform the classes
|
|
122
|
-
const transformed = transformTailwindClasses(attr.class.raw, tailwindConverter);
|
|
123
|
-
// Collect warnings about invalid classes
|
|
124
|
-
if (transformed.invalidClasses.length > 0) {
|
|
125
|
-
warnings.push(`Invalid Tailwind classes in ${attr.class.elementName}: ${transformed.invalidClasses.join(', ')}`);
|
|
126
|
-
}
|
|
127
|
-
// Generate media queries for responsive classes
|
|
128
|
-
if (transformed.responsiveClasses.length > 0) {
|
|
129
|
-
const mediaQueries = generateMediaQueries(transformed.responsiveClasses, tailwindConverter, tailwindConfig);
|
|
130
|
-
allMediaQueries.push(...mediaQueries);
|
|
131
|
-
}
|
|
132
|
-
// Build the new attribute value
|
|
133
|
-
const newAttributes = buildNewAttributes(transformed.inlineStyles, transformed.responsiveClasses, attr.style?.raw);
|
|
134
|
-
// Remove the already existing style attribute if it exists
|
|
135
|
-
if (attr.style) {
|
|
136
|
-
removeStyleAttribute(s, attr.style);
|
|
137
|
-
}
|
|
138
|
-
// Replace the class attribute with new attributes
|
|
139
|
-
replaceClassAttribute(s, attr.class, newAttributes);
|
|
140
|
-
}
|
|
141
|
-
transformedCode = s.toString();
|
|
142
|
-
// Step 3: Inject media queries into <Head>
|
|
143
|
-
if (allMediaQueries.length > 0) {
|
|
144
|
-
const injectionResult = injectMediaQueries(transformedCode, allMediaQueries);
|
|
145
|
-
if (!injectionResult.success) {
|
|
146
|
-
warnings.push(injectionResult.error || 'Failed to inject media queries');
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
transformedCode = injectionResult.code;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
originalCode: source,
|
|
154
|
-
transformedCode,
|
|
155
|
-
mediaQueries: allMediaQueries,
|
|
156
|
-
hasHead: allMediaQueries.length > 0,
|
|
157
|
-
warnings
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Build new attribute string from transformation result
|
|
162
|
-
*/
|
|
163
|
-
function buildNewAttributes(inlineStyles, responsiveClasses, existingStyles) {
|
|
164
|
-
const parts = [];
|
|
165
|
-
// Add responsive classes if any
|
|
166
|
-
if (responsiveClasses.length > 0) {
|
|
167
|
-
const sanitizedClasses = responsiveClasses.map(sanitizeClassName);
|
|
168
|
-
parts.push(`class="${sanitizedClasses.join(' ')}"`);
|
|
169
|
-
}
|
|
170
|
-
// Add inline styles if any
|
|
171
|
-
if (inlineStyles) {
|
|
172
|
-
// Escape quotes in styles
|
|
173
|
-
const escapedStyles = inlineStyles.replace(/"/g, '"');
|
|
174
|
-
const withExisting = escapedStyles + (existingStyles ? existingStyles : '');
|
|
175
|
-
parts.push(`style="${withExisting}"`);
|
|
176
|
-
}
|
|
177
|
-
return parts.join(' ');
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Replace class attribute with new attributes using MagicString
|
|
181
|
-
*/
|
|
182
|
-
function replaceClassAttribute(s, classAttr, newAttributes) {
|
|
183
|
-
// We need to replace the entire class="..." portion
|
|
184
|
-
// The positions from AST are for the value, not the attribute
|
|
185
|
-
// So we need to search backwards for class="
|
|
186
|
-
// Find the start of the attribute (look for class=")
|
|
187
|
-
const beforeAttr = s.original.substring(0, classAttr.start);
|
|
188
|
-
const attrStartMatch = beforeAttr.lastIndexOf('class="');
|
|
189
|
-
if (attrStartMatch === -1) {
|
|
190
|
-
console.warn('Could not find class attribute start position');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
// Find the end of the attribute (closing quote)
|
|
194
|
-
const afterValue = s.original.substring(classAttr.end);
|
|
195
|
-
const quotePos = afterValue.indexOf('"');
|
|
196
|
-
if (quotePos === -1) {
|
|
197
|
-
console.warn('Could not find class attribute end position');
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const fullAttrStart = attrStartMatch;
|
|
201
|
-
const fullAttrEnd = classAttr.end + quotePos + 1;
|
|
202
|
-
// Replace the entire class="..." with our new attributes
|
|
203
|
-
if (newAttributes) {
|
|
204
|
-
s.overwrite(fullAttrStart, fullAttrEnd, newAttributes);
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
// No attributes to add - remove the class attribute entirely
|
|
208
|
-
// Also remove any extra whitespace
|
|
209
|
-
let removeStart = fullAttrStart;
|
|
210
|
-
let removeEnd = fullAttrEnd;
|
|
211
|
-
// Check if there's a space before
|
|
212
|
-
if (s.original[removeStart - 1] === ' ') {
|
|
213
|
-
removeStart--;
|
|
214
|
-
}
|
|
215
|
-
// Check if there's a space after
|
|
216
|
-
if (s.original[removeEnd] === ' ') {
|
|
217
|
-
removeEnd++;
|
|
218
|
-
}
|
|
219
|
-
s.remove(removeStart, removeEnd);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Remove style attribute with MagicString
|
|
224
|
-
*/
|
|
225
|
-
function removeStyleAttribute(s, styleAttr) {
|
|
226
|
-
s.remove(styleAttr.start, styleAttr.end);
|
|
227
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ClassAttribute, StyleAttribute } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Parse Svelte 5 source code and extract all class attributes
|
|
4
|
-
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#parse
|
|
5
|
-
*/
|
|
6
|
-
export declare function parseAttributes(source: string): {
|
|
7
|
-
class: ClassAttribute;
|
|
8
|
-
style?: StyleAttribute;
|
|
9
|
-
}[];
|
|
10
|
-
/**
|
|
11
|
-
* Find the <Head> component in Svelte 5 AST
|
|
12
|
-
* Returns the position where we should inject styles
|
|
13
|
-
*/
|
|
14
|
-
export declare function findHeadComponent(source: string): {
|
|
15
|
-
found: boolean;
|
|
16
|
-
insertPosition: number | null;
|
|
17
|
-
};
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import { parse } from 'svelte/compiler';
|
|
2
|
-
/**
|
|
3
|
-
* Parse Svelte 5 source code and extract all class attributes
|
|
4
|
-
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#parse
|
|
5
|
-
*/
|
|
6
|
-
export function parseAttributes(source) {
|
|
7
|
-
const attributes = [];
|
|
8
|
-
try {
|
|
9
|
-
// Parse the Svelte file into an AST
|
|
10
|
-
// Svelte 5 parse returns a Root node with modern AST structure
|
|
11
|
-
const ast = parse(source);
|
|
12
|
-
// Walk the html fragment (template portion) of the AST
|
|
13
|
-
if (ast.html && ast.html.children) {
|
|
14
|
-
for (const child of ast.html.children) {
|
|
15
|
-
walkNode(child, attributes, source);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
console.error('Failed to parse Svelte file:', error);
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
return attributes;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Recursively walk Svelte 5 AST nodes to find class attributes
|
|
27
|
-
*/
|
|
28
|
-
function walkNode(node, attributes, source) {
|
|
29
|
-
if (!node)
|
|
30
|
-
return;
|
|
31
|
-
// Svelte 5 AST structure:
|
|
32
|
-
// - Element: HTML elements like <div>, <button>
|
|
33
|
-
// - InlineComponent: Custom components like <Button>, <Head>
|
|
34
|
-
// - SlotElement: <svelte:element> and other svelte: elements
|
|
35
|
-
if (node.type === 'Element' ||
|
|
36
|
-
node.type === 'InlineComponent' ||
|
|
37
|
-
node.type === 'SlotElement' ||
|
|
38
|
-
node.type === 'Component') {
|
|
39
|
-
const elementName = node.name || 'unknown';
|
|
40
|
-
// Look for class and style attribute in Svelte 5 AST
|
|
41
|
-
const classAttr = node.attributes?.find((attr) => attr.type === 'Attribute' && attr.name === 'class');
|
|
42
|
-
const styleAttr = node.attributes?.find((attr) => attr.type === 'Attribute' && attr.name === 'style');
|
|
43
|
-
if (classAttr && classAttr.value) {
|
|
44
|
-
// Extract class value
|
|
45
|
-
const extractedClass = extractClassValue(classAttr, source);
|
|
46
|
-
let extractedStyle = null;
|
|
47
|
-
if (styleAttr && styleAttr.value) {
|
|
48
|
-
extractedStyle = extractStyleValue(styleAttr, source);
|
|
49
|
-
}
|
|
50
|
-
if (extractedClass) {
|
|
51
|
-
attributes.push({
|
|
52
|
-
class: {
|
|
53
|
-
raw: extractedClass.value,
|
|
54
|
-
start: extractedClass.start,
|
|
55
|
-
end: extractedClass.end,
|
|
56
|
-
elementName,
|
|
57
|
-
isStatic: extractedClass.isStatic
|
|
58
|
-
},
|
|
59
|
-
style: extractedStyle
|
|
60
|
-
? {
|
|
61
|
-
raw: extractedStyle.value,
|
|
62
|
-
start: extractedStyle.start,
|
|
63
|
-
end: extractedStyle.end,
|
|
64
|
-
elementName
|
|
65
|
-
}
|
|
66
|
-
: undefined
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Recursively process children
|
|
72
|
-
if (node.children) {
|
|
73
|
-
for (const child of node.children) {
|
|
74
|
-
walkNode(child, attributes, source);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Handle conditional blocks (#if, #each, etc.)
|
|
78
|
-
if (node.consequent) {
|
|
79
|
-
if (node.consequent.children) {
|
|
80
|
-
for (const child of node.consequent.children) {
|
|
81
|
-
walkNode(child, attributes, source);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (node.alternate) {
|
|
86
|
-
if (node.alternate.children) {
|
|
87
|
-
for (const child of node.alternate.children) {
|
|
88
|
-
walkNode(child, attributes, source);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// Handle #each blocks
|
|
93
|
-
if (node.body) {
|
|
94
|
-
if (node.body.children) {
|
|
95
|
-
for (const child of node.body.children) {
|
|
96
|
-
walkNode(child, attributes, source);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Extract the actual class value from a Svelte 5 attribute node
|
|
103
|
-
*/
|
|
104
|
-
function extractClassValue(classAttr, source) {
|
|
105
|
-
// Svelte 5 attribute value formats:
|
|
106
|
-
// 1. Static string: class="text-red-500"
|
|
107
|
-
// → value: [{ type: 'Text', data: 'text-red-500' }]
|
|
108
|
-
//
|
|
109
|
-
// 2. Expression: class={someVar}
|
|
110
|
-
// → value: [{ type: 'ExpressionTag', expression: {...} }]
|
|
111
|
-
//
|
|
112
|
-
// 3. Mixed: class="static {dynamic} more"
|
|
113
|
-
// → value: [{ type: 'Text' }, { type: 'ExpressionTag' }, { type: 'Text' }]
|
|
114
|
-
if (!classAttr.value || classAttr.value.length === 0) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
// Check if entirely static (only Text nodes)
|
|
118
|
-
const hasOnlyText = classAttr.value.every((v) => v.type === 'Text');
|
|
119
|
-
if (hasOnlyText) {
|
|
120
|
-
// Fully static - we can safely transform this
|
|
121
|
-
const textContent = classAttr.value.map((v) => v.data || '').join('');
|
|
122
|
-
const start = classAttr.value[0].start;
|
|
123
|
-
const end = classAttr.value[classAttr.value.length - 1].end;
|
|
124
|
-
return {
|
|
125
|
-
value: textContent,
|
|
126
|
-
start,
|
|
127
|
-
end,
|
|
128
|
-
isStatic: true
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
// Check if entirely dynamic (only ExpressionTag or MustacheTag)
|
|
132
|
-
const hasOnlyExpression = classAttr.value.length === 1 &&
|
|
133
|
-
(classAttr.value[0].type === 'ExpressionTag' || classAttr.value[0].type === 'MustacheTag');
|
|
134
|
-
if (hasOnlyExpression) {
|
|
135
|
-
// Fully dynamic - cannot transform at build time
|
|
136
|
-
const exprNode = classAttr.value[0];
|
|
137
|
-
const expressionCode = source.substring(exprNode.start, exprNode.end);
|
|
138
|
-
return {
|
|
139
|
-
value: expressionCode,
|
|
140
|
-
start: exprNode.start,
|
|
141
|
-
end: exprNode.end,
|
|
142
|
-
isStatic: false
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
// Mixed content (both Text and ExpressionTag)
|
|
146
|
-
// Extract only the static Text portions for partial transformation
|
|
147
|
-
let combinedValue = '';
|
|
148
|
-
const start = classAttr.value[0].start;
|
|
149
|
-
const end = classAttr.value[classAttr.value.length - 1].end;
|
|
150
|
-
let hasStaticContent = false;
|
|
151
|
-
for (const part of classAttr.value) {
|
|
152
|
-
if (part.type === 'Text' && part.data) {
|
|
153
|
-
combinedValue += part.data + ' ';
|
|
154
|
-
hasStaticContent = true;
|
|
155
|
-
}
|
|
156
|
-
// Skip ExpressionTag nodes
|
|
157
|
-
}
|
|
158
|
-
if (hasStaticContent) {
|
|
159
|
-
return {
|
|
160
|
-
value: combinedValue.trim(),
|
|
161
|
-
start,
|
|
162
|
-
end,
|
|
163
|
-
isStatic: false // Mixed is not fully static
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Extract the actual style value from a Svelte 5 attribute node
|
|
170
|
-
*/
|
|
171
|
-
function extractStyleValue(styleAttr, source) {
|
|
172
|
-
// Svelte 5 attribute value formats:
|
|
173
|
-
// 1. Static string: style="color: red;"
|
|
174
|
-
// → value: [{ type: 'Text', data: 'color: red;' }]
|
|
175
|
-
//
|
|
176
|
-
// 2. Expression: style={someVar}
|
|
177
|
-
// → value: [{ type: 'ExpressionTag', expression: {...} }]
|
|
178
|
-
//
|
|
179
|
-
// 3. Mixed: style="color: red; {dynamicStyle}"
|
|
180
|
-
// → value: [{ type: 'Text' }, { type: 'ExpressionTag' }]
|
|
181
|
-
if (!styleAttr.value || styleAttr.value.length === 0) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
// Check if entirely static (only Text nodes)
|
|
185
|
-
const hasOnlyText = styleAttr.value.every((v) => v.type === 'Text');
|
|
186
|
-
if (hasOnlyText) {
|
|
187
|
-
// Fully static - we can extract this
|
|
188
|
-
const textContent = styleAttr.value.map((v) => v.data || '').join('');
|
|
189
|
-
return {
|
|
190
|
-
value: textContent,
|
|
191
|
-
start: styleAttr.start,
|
|
192
|
-
end: styleAttr.end
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
// Check if entirely dynamic (only ExpressionTag or MustacheTag)
|
|
196
|
-
const hasOnlyExpression = styleAttr.value.length === 1 &&
|
|
197
|
-
(styleAttr.value[0].type === 'ExpressionTag' || styleAttr.value[0].type === 'MustacheTag');
|
|
198
|
-
if (hasOnlyExpression) {
|
|
199
|
-
// Fully dynamic - extract the expression code
|
|
200
|
-
const exprNode = styleAttr.value[0];
|
|
201
|
-
const expressionCode = source.substring(exprNode.start, exprNode.end);
|
|
202
|
-
return {
|
|
203
|
-
value: expressionCode,
|
|
204
|
-
start: exprNode.start,
|
|
205
|
-
end: exprNode.end
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
// Mixed content (both Text and ExpressionTag)
|
|
209
|
-
// Extract the full content including dynamic parts
|
|
210
|
-
const start = styleAttr.value[0].start;
|
|
211
|
-
const end = styleAttr.value[styleAttr.value.length - 1].end;
|
|
212
|
-
const fullContent = source.substring(start, end);
|
|
213
|
-
return {
|
|
214
|
-
value: fullContent,
|
|
215
|
-
start: styleAttr.start,
|
|
216
|
-
end: styleAttr.end
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Find the <Head> component in Svelte 5 AST
|
|
221
|
-
* Returns the position where we should inject styles
|
|
222
|
-
*/
|
|
223
|
-
export function findHeadComponent(source) {
|
|
224
|
-
try {
|
|
225
|
-
const ast = parse(source);
|
|
226
|
-
// Find Head component in the AST
|
|
227
|
-
if (ast.html && ast.html.children) {
|
|
228
|
-
for (const child of ast.html.children) {
|
|
229
|
-
const headInfo = findHeadInNode(child, source);
|
|
230
|
-
if (headInfo)
|
|
231
|
-
return headInfo;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return { found: false, insertPosition: null };
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
return { found: false, insertPosition: null };
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Recursively search for Head component in Svelte 5 AST
|
|
242
|
-
*/
|
|
243
|
-
function findHeadInNode(node, source) {
|
|
244
|
-
if (!node)
|
|
245
|
-
return null;
|
|
246
|
-
// Check if this is the Head component (InlineComponent type in Svelte 5)
|
|
247
|
-
if ((node.type === 'InlineComponent' || node.type === 'Component') && node.name === 'Head') {
|
|
248
|
-
// Svelte 5: Find the best insertion point for styles
|
|
249
|
-
// If Head has children, insert before first child
|
|
250
|
-
if (node.children && node.children.length > 0) {
|
|
251
|
-
return {
|
|
252
|
-
found: true,
|
|
253
|
-
insertPosition: node.children[0].start
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
// No children - need to insert before closing tag
|
|
257
|
-
// Find where the opening tag ends
|
|
258
|
-
const headStart = node.start;
|
|
259
|
-
const headEnd = node.end;
|
|
260
|
-
const headContent = source.substring(headStart, headEnd);
|
|
261
|
-
// Self-closing: <Head />
|
|
262
|
-
if (headContent.includes('/>')) {
|
|
263
|
-
// Convert to non-self-closing by inserting before />
|
|
264
|
-
const selfClosingPos = source.indexOf('/>', headStart);
|
|
265
|
-
return {
|
|
266
|
-
found: true,
|
|
267
|
-
insertPosition: selfClosingPos
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
// Regular closing tag: <Head></Head> or <Head>...</Head>
|
|
271
|
-
const closingTagPos = source.indexOf('</Head>', headStart);
|
|
272
|
-
if (closingTagPos !== -1) {
|
|
273
|
-
return {
|
|
274
|
-
found: true,
|
|
275
|
-
insertPosition: closingTagPos
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
// Fallback: insert right after opening tag
|
|
279
|
-
const openingTagEnd = source.indexOf('>', headStart);
|
|
280
|
-
if (openingTagEnd !== -1) {
|
|
281
|
-
return {
|
|
282
|
-
found: true,
|
|
283
|
-
insertPosition: openingTagEnd + 1
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// Search recursively through the AST
|
|
288
|
-
if (node.children) {
|
|
289
|
-
for (const child of node.children) {
|
|
290
|
-
const found = findHeadInNode(child, source);
|
|
291
|
-
if (found)
|
|
292
|
-
return found;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Check conditional branches
|
|
296
|
-
if (node.consequent) {
|
|
297
|
-
if (node.consequent.children) {
|
|
298
|
-
for (const child of node.consequent.children) {
|
|
299
|
-
const found = findHeadInNode(child, source);
|
|
300
|
-
if (found)
|
|
301
|
-
return found;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (node.alternate) {
|
|
306
|
-
if (node.alternate.children) {
|
|
307
|
-
for (const child of node.alternate.children) {
|
|
308
|
-
const found = findHeadInNode(child, source);
|
|
309
|
-
if (found)
|
|
310
|
-
return found;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type TailwindConfig } from 'tw-to-css';
|
|
2
|
-
import type { TransformResult, MediaQueryStyle } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Initialize Tailwind converter with config
|
|
5
|
-
*/
|
|
6
|
-
export declare function createTailwindConverter(config?: TailwindConfig): typeof import("tw-to-css").twi;
|
|
7
|
-
/**
|
|
8
|
-
* Transform Tailwind classes to inline styles and responsive classes
|
|
9
|
-
*/
|
|
10
|
-
export declare function transformTailwindClasses(classString: string, tailwindConverter: ReturnType<typeof createTailwindConverter>): TransformResult;
|
|
11
|
-
/**
|
|
12
|
-
* Generate media query CSS for responsive classes
|
|
13
|
-
*/
|
|
14
|
-
export declare function generateMediaQueries(responsiveClasses: string[], tailwindConverter: ReturnType<typeof createTailwindConverter>, tailwindConfig?: TailwindConfig): MediaQueryStyle[];
|
|
15
|
-
/**
|
|
16
|
-
* Sanitize class names for use in CSS (replace special characters)
|
|
17
|
-
*/
|
|
18
|
-
export declare function sanitizeClassName(className: string): string;
|