better-svelte-email 0.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.
Files changed (38) hide show
  1. package/README.md +422 -0
  2. package/dist/components/Body.svelte +9 -0
  3. package/dist/components/Body.svelte.d.ts +13 -0
  4. package/dist/components/Button.svelte +54 -0
  5. package/dist/components/Button.svelte.d.ts +21 -0
  6. package/dist/components/Container.svelte +28 -0
  7. package/dist/components/Container.svelte.d.ts +13 -0
  8. package/dist/components/Head.svelte +13 -0
  9. package/dist/components/Head.svelte.d.ts +6 -0
  10. package/dist/components/Html.svelte +19 -0
  11. package/dist/components/Html.svelte.d.ts +10 -0
  12. package/dist/components/Section.svelte +21 -0
  13. package/dist/components/Section.svelte.d.ts +13 -0
  14. package/dist/components/Text.svelte +17 -0
  15. package/dist/components/Text.svelte.d.ts +15 -0
  16. package/dist/components/__tests__/test-email.svelte +13 -0
  17. package/dist/components/__tests__/test-email.svelte.d.ts +26 -0
  18. package/dist/components/index.d.ts +7 -0
  19. package/dist/components/index.js +9 -0
  20. package/dist/emails/demo-email.svelte +108 -0
  21. package/dist/emails/demo-email.svelte.d.ts +13 -0
  22. package/dist/emails/test-email.svelte +15 -0
  23. package/dist/emails/test-email.svelte.d.ts +26 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +10 -0
  26. package/dist/preprocessor/head-injector.d.ts +9 -0
  27. package/dist/preprocessor/head-injector.js +57 -0
  28. package/dist/preprocessor/index.d.ts +25 -0
  29. package/dist/preprocessor/index.js +196 -0
  30. package/dist/preprocessor/parser.d.ts +14 -0
  31. package/dist/preprocessor/parser.js +249 -0
  32. package/dist/preprocessor/transformer.d.ts +18 -0
  33. package/dist/preprocessor/transformer.js +158 -0
  34. package/dist/preprocessor/types.d.ts +104 -0
  35. package/dist/preprocessor/types.js +1 -0
  36. package/dist/utils/index.d.ts +12 -0
  37. package/dist/utils/index.js +24 -0
  38. package/package.json +97 -0
@@ -0,0 +1,13 @@
1
+ <script>
2
+ import { Html, Head, Text, Button, Container } from '../index.js';
3
+ </script>
4
+
5
+ <Html>
6
+ <Head />
7
+ <Container class="bg-gray-100 p-8">
8
+ <Text class="text-lg font-bold text-blue-600">Hello World</Text>
9
+ <Button class="rounded bg-blue-500 px-4 py-2 text-white" href="https://example.com">
10
+ Click Me
11
+ </Button>
12
+ </Container>
13
+ </Html>
@@ -0,0 +1,26 @@
1
+ export default TestEmail;
2
+ type TestEmail = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const TestEmail: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -0,0 +1,7 @@
1
+ export { default as Html } from './Html.svelte';
2
+ export { default as Head } from './Head.svelte';
3
+ export { default as Body } from './Body.svelte';
4
+ export { default as Container } from './Container.svelte';
5
+ export { default as Section } from './Section.svelte';
6
+ export { default as Text } from './Text.svelte';
7
+ export { default as Button } from './Button.svelte';
@@ -0,0 +1,9 @@
1
+ // Email Components for better-svelte-email
2
+ // These components work with the preprocessor's styleString prop
3
+ export { default as Html } from './Html.svelte';
4
+ export { default as Head } from './Head.svelte';
5
+ export { default as Body } from './Body.svelte';
6
+ export { default as Container } from './Container.svelte';
7
+ export { default as Section } from './Section.svelte';
8
+ export { default as Text } from './Text.svelte';
9
+ export { default as Button } from './Button.svelte';
@@ -0,0 +1,108 @@
1
+ <script>
2
+ import { Html, Head, Body, Container, Section, Text, Button } from '../components/index.js';
3
+
4
+ let { userName = 'User', testMessage = 'This is a test email!' } = $props();
5
+ </script>
6
+
7
+ <Html>
8
+ <Head>
9
+ <style>
10
+ @media(min-width: 640px){.sm\:bg-green-600{background-color:rgb(22,163,74) !important}}
11
+ </style>
12
+ </Head>
13
+ <Body styleString="background-color:rgb(243,244,246); font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;">
14
+ <Container styleString="margin-left:auto;margin-right:auto; max-width:42rem; background-color:rgb(255,255,255); padding:2rem;">
15
+ <!-- Header -->
16
+ <Section styleString="margin-bottom:1.5rem; border-bottom-width:1px; border-color:rgb(229,231,235); padding-bottom:1.5rem;">
17
+ <Text as="h1" styleString="margin-bottom:0.5rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(17,24,39);">better-svelte-email Demo</Text>
18
+ <Text styleString="color:rgb(75,85,99);">
19
+ This email was generated using Tailwind classes and transformed by the preprocessor!
20
+ </Text>
21
+ </Section>
22
+
23
+ <!-- Main Content -->
24
+ <Section styleString="margin-bottom:1.5rem;">
25
+ <Text styleString="margin-bottom:1rem; font-size:1.125rem;line-height:1.75rem; color:rgb(31,41,55);">Hello {userName}! 👋</Text>
26
+
27
+ <Text styleString="margin-bottom:1rem; color:rgb(55,65,81);">{testMessage}</Text>
28
+
29
+ <Container styleString="margin-bottom:1.5rem; border-radius:0.5rem; background-color:rgb(239,246,255); padding:1rem;">
30
+ <Text styleString="margin-bottom:0.5rem; font-weight:600; color:rgb(30,58,138);">✨ Features Demonstrated:</Text>
31
+ <ul styleString="list-style-type:disc; padding-left:1.25rem; color:rgb(30,64,175);">
32
+ <li>Tailwind classes converted to inline styles</li>
33
+ <li>Responsive design with media queries</li>
34
+ <li>Email-safe CSS transformations</li>
35
+ <li>Build-time preprocessing (zero runtime cost)</li>
36
+ </ul>
37
+ </Container>
38
+
39
+ <!-- Buttons showcase -->
40
+ <Section styleString="margin-bottom:1.5rem;">
41
+ <Button
42
+ href="https://github.com/Konixy/better-svelte-email"
43
+ class="sm_bg_green_600" styleString="display:inline-block; border-radius:0.25rem; background-color:rgb(37,99,235); padding-left:1.5rem;padding-right:1.5rem; padding-top:0.75rem;padding-bottom:0.75rem; font-weight:600; color:rgb(255,255,255);"
44
+ >
45
+ View on GitHub
46
+ </Button>
47
+
48
+ <Button
49
+ href="https://svelte.dev"
50
+ styleString="margin-left:0.75rem; display:inline-block; border-radius:0.25rem; border-width:1px; border-color:rgb(209,213,219); background-color:rgb(255,255,255); padding-left:1.5rem;padding-right:1.5rem; padding-top:0.75rem;padding-bottom:0.75rem; font-weight:600; color:rgb(55,65,81);"
51
+ >
52
+ Learn Svelte 5
53
+ </Button>
54
+ </Section>
55
+ </Section>
56
+
57
+ <!-- Stats Grid -->
58
+ <Section styleString="margin-bottom:1.5rem;">
59
+ <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
60
+ <tbody>
61
+ <tr>
62
+ <td styleString="border-radius:0.5rem; background-color:rgb(250,245,255); padding:1rem; text-align:center;" style="width: 33.33%;">
63
+ <Text as="div" styleString="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(147,51,234);">52+</Text>
64
+ <Text as="div" styleString="font-size:0.875rem;line-height:1.25rem; color:rgb(126,34,206);">Tests Passing</Text>
65
+ </td>
66
+ <td style="width: 10px;"></td>
67
+ <td styleString="border-radius:0.5rem; background-color:rgb(240,253,244); padding:1rem; text-align:center;" style="width: 33.33%;">
68
+ <Text as="div" styleString="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(22,163,74);">100%</Text>
69
+ <Text as="div" styleString="font-size:0.875rem;line-height:1.25rem; color:rgb(21,128,61);">TypeScript</Text>
70
+ </td>
71
+ <td style="width: 10px;"></td>
72
+ <td styleString="border-radius:0.5rem; background-color:rgb(255,247,237); padding:1rem; text-align:center;" style="width: 33.33%;">
73
+ <Text as="div" styleString="margin-bottom:0.25rem; font-size:1.875rem;line-height:2.25rem; font-weight:700; color:rgb(234,88,12);">0ms</Text>
74
+ <Text as="div" styleString="font-size:0.875rem;line-height:1.25rem; color:rgb(194,65,12);">Runtime Cost</Text>
75
+ </td>
76
+ </tr>
77
+ </tbody>
78
+ </table>
79
+ </Section>
80
+
81
+ <!-- Code Example -->
82
+ <Container styleString="margin-bottom:1.5rem; border-radius:0.5rem; background-color:rgb(249,250,251); padding:1rem;">
83
+ <Text styleString="margin-bottom:0.5rem; font-size:0.875rem;line-height:1.25rem; font-weight:600; color:rgb(55,65,81);">How it works:</Text>
84
+ <pre
85
+ styleString="overflow-x:auto; border-radius:0.25rem; background-color:rgb(17,24,39); padding:0.75rem; font-size:0.75rem;line-height:1rem; color:rgb(243,244,246);"
86
+ style="margin: 0;"><code
87
+ >// Input
88
+ &lt;Button class="bg-blue-500 text-white p-4"&gt;
89
+ Click Me
90
+ &lt;/Button&gt;
91
+
92
+ // Output (after preprocessing)
93
+ &lt;Button styleString="background-color: rgb(59, 130, 246); ..."&gt;
94
+ Click Me
95
+ &lt;/Button&gt;</code
96
+ ></pre>
97
+ </Container>
98
+
99
+ <!-- Footer -->
100
+ <Section styleString="border-top-width:1px; border-color:rgb(229,231,235); padding-top:1.5rem; text-align:center;">
101
+ <Text styleString="margin-bottom:0.5rem; font-size:0.875rem;line-height:1.25rem; color:rgb(75,85,99);">Built with Svelte 5 & Tailwind CSS</Text>
102
+ <Text styleString="font-size:0.75rem;line-height:1rem; color:rgb(107,114,128);">
103
+ You're receiving this email because you tested the better-svelte-email demo.
104
+ </Text>
105
+ </Section>
106
+ </Container>
107
+ </Body>
108
+ </Html>
@@ -0,0 +1,13 @@
1
+ export default DemoEmail;
2
+ type DemoEmail = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const DemoEmail: import("svelte").Component<{
7
+ userName?: string;
8
+ testMessage?: string;
9
+ }, {}, "">;
10
+ type $$ComponentProps = {
11
+ userName?: string;
12
+ testMessage?: string;
13
+ };
@@ -0,0 +1,15 @@
1
+ <script>
2
+ import { Html, Head, Body, Text, Button, Container } from '../components/index.js';
3
+ </script>
4
+
5
+ <Html>
6
+ <Head />
7
+ <Body>
8
+ <Container styleString="background-color:rgb(243,244,246); padding:2rem;">
9
+ <Text styleString="font-size:1.125rem;line-height:1.75rem; font-weight:700; color:rgb(37,99,235);">Hello World</Text>
10
+ <Button styleString="border-radius:0.25rem; background-color:rgb(59,130,246); padding-left:1rem;padding-right:1rem; padding-top:0.5rem;padding-bottom:0.5rem; color:rgb(255,255,255);" href="https://example.com">
11
+ Click Me
12
+ </Button>
13
+ </Container>
14
+ </Body>
15
+ </Html>
@@ -0,0 +1,26 @@
1
+ export default TestEmail;
2
+ type TestEmail = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const TestEmail: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -0,0 +1,8 @@
1
+ export { Html, Head, Body, Container, Section, Text, Button } from './components/index.js';
2
+ export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
3
+ export type { PreprocessorOptions, ComponentTransform } from './preprocessor/index.js';
4
+ export type { ClassAttribute, TransformResult, MediaQueryStyle } from './preprocessor/types.js';
5
+ export { parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
6
+ export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
7
+ export { injectMediaQueries } from './preprocessor/head-injector.js';
8
+ export { styleToString, pxToPt } from './utils/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // Export email components
2
+ export { Html, Head, Body, Container, Section, Text, Button } from './components/index.js';
3
+ // Export the preprocessor
4
+ export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
5
+ // Export individual functions for advanced usage
6
+ export { parseClassAttributes, findHeadComponent } from './preprocessor/parser.js';
7
+ export { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './preprocessor/transformer.js';
8
+ export { injectMediaQueries } from './preprocessor/head-injector.js';
9
+ // Export utilities
10
+ export { styleToString, pxToPt } from './utils/index.js';
@@ -0,0 +1,9 @@
1
+ import type { MediaQueryStyle } from './types.js';
2
+ /**
3
+ * Inject media query styles into the <Head> component
4
+ */
5
+ export declare function injectMediaQueries(source: string, mediaQueries: MediaQueryStyle[]): {
6
+ code: string;
7
+ success: boolean;
8
+ error?: string;
9
+ };
@@ -0,0 +1,57 @@
1
+ import MagicString from 'magic-string';
2
+ import { findHeadComponent } from './parser.js';
3
+ /**
4
+ * Inject media query styles into the <Head> component
5
+ */
6
+ export function injectMediaQueries(source, mediaQueries) {
7
+ if (mediaQueries.length === 0) {
8
+ // No media queries to inject
9
+ return { code: source, success: true };
10
+ }
11
+ // Find the Head component
12
+ const headInfo = findHeadComponent(source);
13
+ if (!headInfo.found || headInfo.insertPosition === null) {
14
+ return {
15
+ code: source,
16
+ success: false,
17
+ error: 'No <Head> component found. Media queries cannot be injected.'
18
+ };
19
+ }
20
+ // Generate the style tag content
21
+ const styleContent = generateStyleTag(mediaQueries);
22
+ // Use MagicString for surgical insertion
23
+ const s = new MagicString(source);
24
+ // Check if Head is self-closing and convert it
25
+ const headStart = source.lastIndexOf('<Head', headInfo.insertPosition);
26
+ const headSegment = source.substring(headStart, headInfo.insertPosition + 10);
27
+ if (headSegment.includes('/>')) {
28
+ // Self-closing: convert to non-self-closing
29
+ // Check if there's a space before />
30
+ const spaceBeforeSelfClose = source[headInfo.insertPosition - 1] === ' ';
31
+ const replaceStart = spaceBeforeSelfClose
32
+ ? headInfo.insertPosition - 1
33
+ : headInfo.insertPosition;
34
+ // Replace [space]?/> with >
35
+ s.overwrite(replaceStart, headInfo.insertPosition + 2, '>');
36
+ // Insert style content
37
+ s.appendLeft(headInfo.insertPosition + 2, styleContent);
38
+ // Add closing tag
39
+ s.appendLeft(headInfo.insertPosition + 2, '</Head>');
40
+ }
41
+ else {
42
+ // Already has closing tag, just insert content
43
+ s.appendLeft(headInfo.insertPosition, styleContent);
44
+ }
45
+ return {
46
+ code: s.toString(),
47
+ success: true
48
+ };
49
+ }
50
+ /**
51
+ * Generate <style> tag with all media queries
52
+ */
53
+ function generateStyleTag(mediaQueries) {
54
+ // Combine all media queries
55
+ const allQueries = mediaQueries.map((mq) => mq.rules).join('\n');
56
+ return `\n\t<style>\n\t\t${allQueries}\n\t</style>\n`;
57
+ }
@@ -0,0 +1,25 @@
1
+ import type { PreprocessorGroup } from 'svelte/compiler';
2
+ import type { PreprocessorOptions, ComponentTransform } from './types.js';
3
+ /**
4
+ * Svelte 5 preprocessor for transforming Tailwind classes in email components
5
+ *
6
+ * @example
7
+ * ```javascript
8
+ * // svelte.config.js
9
+ * import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
10
+ *
11
+ * export default {
12
+ * preprocess: [
13
+ * vitePreprocess(),
14
+ * betterSvelteEmailPreprocessor({
15
+ * pathToEmailFolder: '/src/lib/emails',
16
+ * tailwindConfig: { ... }
17
+ * })
18
+ * ]
19
+ * };
20
+ * ```
21
+ *
22
+ * Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
23
+ */
24
+ export declare function betterSvelteEmailPreprocessor(options?: PreprocessorOptions): PreprocessorGroup;
25
+ export type { PreprocessorOptions, ComponentTransform };
@@ -0,0 +1,196 @@
1
+ import MagicString from 'magic-string';
2
+ import { parseClassAttributes } from './parser.js';
3
+ import { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './transformer.js';
4
+ import { injectMediaQueries } from './head-injector.js';
5
+ /**
6
+ * Svelte 5 preprocessor for transforming Tailwind classes in email components
7
+ *
8
+ * @example
9
+ * ```javascript
10
+ * // svelte.config.js
11
+ * import { betterSvelteEmailPreprocessor } from 'better-svelte-email/preprocessor';
12
+ *
13
+ * export default {
14
+ * preprocess: [
15
+ * vitePreprocess(),
16
+ * betterSvelteEmailPreprocessor({
17
+ * pathToEmailFolder: '/src/lib/emails',
18
+ * tailwindConfig: { ... }
19
+ * })
20
+ * ]
21
+ * };
22
+ * ```
23
+ *
24
+ * Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
25
+ */
26
+ export function betterSvelteEmailPreprocessor(options = {}) {
27
+ const { tailwindConfig, pathToEmailFolder = '/src/lib/emails', debug = false } = options;
28
+ // Initialize Tailwind converter once (performance optimization)
29
+ const tailwindConverter = createTailwindConverter(tailwindConfig);
30
+ // Return a Svelte 5 PreprocessorGroup
31
+ return {
32
+ name: 'better-svelte-email',
33
+ /**
34
+ * The markup preprocessor transforms the template/HTML portion
35
+ * This is where we extract and transform Tailwind classes
36
+ */
37
+ markup({ content, filename }) {
38
+ // Only process .svelte files in the configured email folder
39
+ if (!filename || !filename.includes(pathToEmailFolder)) {
40
+ // Return undefined to skip processing
41
+ return;
42
+ }
43
+ if (!filename.endsWith('.svelte')) {
44
+ return;
45
+ }
46
+ try {
47
+ // Process the email component
48
+ const result = processEmailComponent(content, filename, tailwindConverter, tailwindConfig);
49
+ // Log warnings if debug mode is enabled
50
+ if (result.warnings.length > 0) {
51
+ if (debug) {
52
+ console.warn(`[better-svelte-email] Warnings for ${filename}:`, result.warnings);
53
+ }
54
+ }
55
+ // Return the transformed code
56
+ // The preprocessor API expects { code: string } or { code: string, map: SourceMap }
57
+ return {
58
+ code: result.transformedCode
59
+ // Note: Source maps could be added here via MagicString's generateMap()
60
+ };
61
+ }
62
+ catch (error) {
63
+ console.error(`[better-svelte-email] Error processing ${filename}:`, error);
64
+ // On error, return undefined to use original content
65
+ // This prevents breaking the build for non-email files
66
+ return;
67
+ }
68
+ }
69
+ };
70
+ }
71
+ /**
72
+ * Process a single email component
73
+ */
74
+ function processEmailComponent(source, _filename, tailwindConverter, tailwindConfig) {
75
+ const warnings = [];
76
+ let transformedCode = source;
77
+ const allMediaQueries = [];
78
+ // Step 1: Parse and find all class attributes
79
+ const classAttributes = parseClassAttributes(source);
80
+ if (classAttributes.length === 0) {
81
+ // No classes to transform
82
+ return {
83
+ originalCode: source,
84
+ transformedCode: source,
85
+ mediaQueries: [],
86
+ hasHead: false,
87
+ warnings: []
88
+ };
89
+ }
90
+ // Step 2: Transform each class attribute
91
+ const s = new MagicString(transformedCode);
92
+ // Process in reverse order to maintain correct positions
93
+ const sortedAttributes = [...classAttributes].sort((a, b) => b.start - a.start);
94
+ for (const classAttr of sortedAttributes) {
95
+ if (!classAttr.isStatic) {
96
+ // Skip dynamic classes for now
97
+ warnings.push(`Dynamic class expression detected in ${classAttr.elementName}. ` +
98
+ `Only static classes can be transformed at build time.`);
99
+ continue;
100
+ }
101
+ // Transform the classes
102
+ const transformed = transformTailwindClasses(classAttr.raw, tailwindConverter);
103
+ // Collect warnings about invalid classes
104
+ if (transformed.invalidClasses.length > 0) {
105
+ warnings.push(`Invalid Tailwind classes in ${classAttr.elementName}: ${transformed.invalidClasses.join(', ')}`);
106
+ }
107
+ // Generate media queries for responsive classes
108
+ if (transformed.responsiveClasses.length > 0) {
109
+ const mediaQueries = generateMediaQueries(transformed.responsiveClasses, tailwindConverter, tailwindConfig);
110
+ allMediaQueries.push(...mediaQueries);
111
+ }
112
+ // Build the new attribute value
113
+ const newAttributes = buildNewAttributes(transformed.inlineStyles, transformed.responsiveClasses);
114
+ // Replace the class attribute with new attributes
115
+ replaceClassAttribute(s, classAttr, newAttributes);
116
+ }
117
+ transformedCode = s.toString();
118
+ // Step 3: Inject media queries into <Head>
119
+ if (allMediaQueries.length > 0) {
120
+ const injectionResult = injectMediaQueries(transformedCode, allMediaQueries);
121
+ if (!injectionResult.success) {
122
+ warnings.push(injectionResult.error || 'Failed to inject media queries');
123
+ }
124
+ else {
125
+ transformedCode = injectionResult.code;
126
+ }
127
+ }
128
+ return {
129
+ originalCode: source,
130
+ transformedCode,
131
+ mediaQueries: allMediaQueries,
132
+ hasHead: allMediaQueries.length > 0,
133
+ warnings
134
+ };
135
+ }
136
+ /**
137
+ * Build new attribute string from transformation result
138
+ */
139
+ function buildNewAttributes(inlineStyles, responsiveClasses) {
140
+ const parts = [];
141
+ // Add responsive classes if any
142
+ if (responsiveClasses.length > 0) {
143
+ const sanitizedClasses = responsiveClasses.map(sanitizeClassName);
144
+ parts.push(`class="${sanitizedClasses.join(' ')}"`);
145
+ }
146
+ // Add inline styles if any
147
+ if (inlineStyles) {
148
+ // Escape quotes in styles
149
+ const escapedStyles = inlineStyles.replace(/"/g, '&quot;');
150
+ parts.push(`styleString="${escapedStyles}"`);
151
+ }
152
+ return parts.join(' ');
153
+ }
154
+ /**
155
+ * Replace class attribute with new attributes using MagicString
156
+ */
157
+ function replaceClassAttribute(s, classAttr, newAttributes) {
158
+ // We need to replace the entire class="..." portion
159
+ // The positions from AST are for the value, not the attribute
160
+ // So we need to search backwards for class="
161
+ // Find the start of the attribute (look for class=")
162
+ const beforeAttr = s.original.substring(0, classAttr.start);
163
+ const attrStartMatch = beforeAttr.lastIndexOf('class="');
164
+ if (attrStartMatch === -1) {
165
+ console.warn('Could not find class attribute start position');
166
+ return;
167
+ }
168
+ // Find the end of the attribute (closing quote)
169
+ const afterValue = s.original.substring(classAttr.end);
170
+ const quotePos = afterValue.indexOf('"');
171
+ if (quotePos === -1) {
172
+ console.warn('Could not find class attribute end position');
173
+ return;
174
+ }
175
+ const fullAttrStart = attrStartMatch;
176
+ const fullAttrEnd = classAttr.end + quotePos + 1;
177
+ // Replace the entire class="..." with our new attributes
178
+ if (newAttributes) {
179
+ s.overwrite(fullAttrStart, fullAttrEnd, newAttributes);
180
+ }
181
+ else {
182
+ // No attributes to add - remove the class attribute entirely
183
+ // Also remove any extra whitespace
184
+ let removeStart = fullAttrStart;
185
+ let removeEnd = fullAttrEnd;
186
+ // Check if there's a space before
187
+ if (s.original[removeStart - 1] === ' ') {
188
+ removeStart--;
189
+ }
190
+ // Check if there's a space after
191
+ if (s.original[removeEnd] === ' ') {
192
+ removeEnd++;
193
+ }
194
+ s.remove(removeStart, removeEnd);
195
+ }
196
+ }
@@ -0,0 +1,14 @@
1
+ import type { ClassAttribute } 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 parseClassAttributes(source: string): ClassAttribute[];
7
+ /**
8
+ * Find the <Head> component in Svelte 5 AST
9
+ * Returns the position where we should inject styles
10
+ */
11
+ export declare function findHeadComponent(source: string): {
12
+ found: boolean;
13
+ insertPosition: number | null;
14
+ };