esm-styles 0.1.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/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # ESM Styles
2
+
3
+ A TypeScript library for converting JavaScript objects to CSS strings, allowing for a cleaner syntax when writing CSS-in-JS.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install esm-styles
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ ```javascript
16
+ import { getCss } from 'esm-styles'
17
+
18
+ const styles = {
19
+ body: {
20
+ margin: 0,
21
+ padding: 0,
22
+ fontFamily: 'sans-serif',
23
+
24
+ header: {
25
+ backgroundColor: '#333',
26
+ color: 'white',
27
+ padding: '1rem',
28
+ },
29
+
30
+ 'main, article': {
31
+ maxWidth: '800px',
32
+ margin: '0 auto',
33
+ padding: '1rem',
34
+ },
35
+
36
+ footer: {
37
+ textAlign: 'center',
38
+ padding: '1rem',
39
+ backgroundColor: '#f5f5f5',
40
+ },
41
+ },
42
+ }
43
+
44
+ const css = getCss(styles)
45
+ console.log(css)
46
+ ```
47
+
48
+ This will output CSS with nested selectors properly expanded:
49
+
50
+ ```css
51
+ body {
52
+ margin: 0;
53
+ padding: 0;
54
+ font-family: sans-serif;
55
+ }
56
+
57
+ body header {
58
+ background-color: #333;
59
+ color: white;
60
+ padding: 1rem;
61
+ }
62
+
63
+ body main,
64
+ body article {
65
+ max-width: 800px;
66
+ margin: 0 auto;
67
+ padding: 1rem;
68
+ }
69
+
70
+ body footer {
71
+ text-align: center;
72
+ padding: 1rem;
73
+ background-color: #f5f5f5;
74
+ }
75
+ ```
76
+
77
+ ### Advanced Features
78
+
79
+ #### Media Queries
80
+
81
+ ```javascript
82
+ import { getCss } from 'esm-styles'
83
+
84
+ const styles = {
85
+ body: {
86
+ fontSize: '16px',
87
+
88
+ '@media (max-width: 768px)': {
89
+ fontSize: '14px',
90
+ },
91
+ },
92
+ }
93
+
94
+ const css = getCss(styles)
95
+ ```
96
+
97
+ #### Named Media Queries
98
+
99
+ ```javascript
100
+ import { getCss } from 'esm-styles'
101
+
102
+ const styles = {
103
+ container: {
104
+ width: '1200px',
105
+ '@tablet': {
106
+ width: '100%',
107
+ padding: '0 20px',
108
+ },
109
+ '@mobile': {
110
+ padding: '0 10px',
111
+ },
112
+ },
113
+ }
114
+
115
+ const mediaQueries = {
116
+ tablet: '(max-width: 1024px)',
117
+ mobile: '(max-width: 480px)',
118
+ }
119
+
120
+ const css = getCss(styles, mediaQueries)
121
+ ```
122
+
123
+ #### Class and Tag Selectors
124
+
125
+ ```javascript
126
+ import { getCss } from 'esm-styles'
127
+
128
+ const styles = {
129
+ // Tag selector (div)
130
+ div: {
131
+ margin: '10px',
132
+
133
+ // Nested tag selector (p inside div)
134
+ p: {
135
+ lineHeight: 1.5,
136
+ },
137
+
138
+ // Class selector (with underscore prefix)
139
+ _highlight: {
140
+ backgroundColor: 'yellow',
141
+ },
142
+
143
+ // Descendant class selector (with double underscore)
144
+ __text: {
145
+ color: 'blue',
146
+ },
147
+ },
148
+ }
149
+
150
+ const css = getCss(styles)
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,13 @@
1
+ /**
2
+ * esm-styles
3
+ *
4
+ * A library for working with CSS styles in ESM
5
+ */
6
+ export * from './lib/types/index.js';
7
+ export { default as getCss } from './lib/getCss.js';
8
+ export { joinSelectors, cartesianSelectors } from './lib/utils/selectors.js';
9
+ export { formatContentValue } from './lib/utils/content.js';
10
+ export { jsKeyToCssKey, isEndValue } from './lib/utils/common.js';
11
+ export { processMediaQueries } from './lib/utils/media.js';
12
+ export { obj2css, prettifyCss } from './lib/utils/obj2css.js';
13
+ export declare function greet(): string;
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * esm-styles
3
+ *
4
+ * A library for working with CSS styles in ESM
5
+ */
6
+ // Export types
7
+ export * from './lib/types/index.js';
8
+ // Main function export
9
+ export { default as getCss } from './lib/getCss.js';
10
+ // Utils exports for advanced usage
11
+ export { joinSelectors, cartesianSelectors } from './lib/utils/selectors.js';
12
+ export { formatContentValue } from './lib/utils/content.js';
13
+ export { jsKeyToCssKey, isEndValue } from './lib/utils/common.js';
14
+ export { processMediaQueries } from './lib/utils/media.js';
15
+ export { obj2css, prettifyCss } from './lib/utils/obj2css.js';
16
+ export function greet() {
17
+ return 'Hello from esm-styles!';
18
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Main function for converting JavaScript objects to CSS
3
+ */
4
+ import { CssStyles, MediaQueries, MediaPrefixes, AutoConfig } from './types/index.js';
5
+ /**
6
+ * Converts a JavaScript style object to CSS string
7
+ * @param object - The JavaScript object to convert to CSS
8
+ * @param mediaQueries - Media query definitions (e.g., { 'phone': '(max-width: 499px)' })
9
+ * @param mediaPrefixes - Media prefix definitions (e.g., { 'dark': ':root.dark' })
10
+ * @param auto - Auto mode configuration (e.g., { 'dark': [':root.auto', 'screen and (prefers-color-scheme: dark)'] })
11
+ * @returns CSS string
12
+ */
13
+ export declare function getCss(object: CssStyles, mediaQueries?: MediaQueries, mediaPrefixes?: MediaPrefixes, auto?: AutoConfig): string;
14
+ export default getCss;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Main function for converting JavaScript objects to CSS
3
+ */
4
+ import { traverseObject, determineNodeType } from './utils/traversal.js';
5
+ import { indent } from './utils/common.js';
6
+ import { obj2css, prettifyCss } from './utils/obj2css.js';
7
+ import { joinSelectors, cartesianSelectors } from './utils/selectors.js';
8
+ import { formatContentValue } from './utils/content.js';
9
+ import { jsKeyToCssKey } from './utils/common.js';
10
+ import { processMediaQueries } from './utils/media.js';
11
+ /**
12
+ * Converts a JavaScript style object to CSS string
13
+ * @param object - The JavaScript object to convert to CSS
14
+ * @param mediaQueries - Media query definitions (e.g., { 'phone': '(max-width: 499px)' })
15
+ * @param mediaPrefixes - Media prefix definitions (e.g., { 'dark': ':root.dark' })
16
+ * @param auto - Auto mode configuration (e.g., { 'dark': [':root.auto', 'screen and (prefers-color-scheme: dark)'] })
17
+ * @returns CSS string
18
+ */
19
+ export function getCss(object, mediaQueries = {}, mediaPrefixes = {}, auto) {
20
+ // Initialize various objects to collect different types of CSS rules
21
+ let cssStyle = {};
22
+ const layerStatements = [];
23
+ let layerObject = {};
24
+ let containerObject = {};
25
+ let mediaObject = {};
26
+ let prefixObject = {};
27
+ // Process the object by traversing it and handling different node types
28
+ traverseObject(object, (node, path, _, __) => {
29
+ if (!path)
30
+ return node;
31
+ const nodeType = determineNodeType(node, path);
32
+ const pathParts = path.split('\\');
33
+ const key = pathParts.pop() || '';
34
+ switch (nodeType) {
35
+ case 'selector': {
36
+ // Handle CSS property-value pairs
37
+ const selector = joinSelectors(pathParts);
38
+ // Create cartesian product of selectors for comma-separated parts
39
+ if (pathParts.some((part) => part.includes(','))) {
40
+ const classPaths = pathParts.map((part) => part.split(',').map((p) => p.trim()));
41
+ const allPaths = cartesianSelectors(classPaths);
42
+ // Apply the property-value to each selector
43
+ allPaths.forEach((selectorPath) => {
44
+ const cssKey = jsKeyToCssKey(key);
45
+ let value = node;
46
+ // Special handling for content property
47
+ if (cssKey === 'content') {
48
+ value = formatContentValue(value);
49
+ }
50
+ // Create the CSS object and merge it
51
+ const cssObject = {
52
+ [selectorPath]: { [cssKey]: value },
53
+ };
54
+ cssStyle = mergeDeep(cssStyle, cssObject);
55
+ });
56
+ }
57
+ else {
58
+ // Simple case - no commas in selectors
59
+ const cssKey = jsKeyToCssKey(key);
60
+ let value = node;
61
+ // Special handling for content property
62
+ if (cssKey === 'content') {
63
+ value = formatContentValue(value);
64
+ }
65
+ // Create the CSS object and merge it
66
+ const cssObject = {
67
+ [selector]: { [cssKey]: value },
68
+ };
69
+ cssStyle = mergeDeep(cssStyle, cssObject);
70
+ }
71
+ break;
72
+ }
73
+ case 'layer statement': {
74
+ // Handle @layer statements
75
+ const statement = `${key}${node ? ' ' + node : ''};`;
76
+ if (!layerStatements.includes(statement)) {
77
+ layerStatements.push(statement);
78
+ }
79
+ break;
80
+ }
81
+ case 'layer block': {
82
+ // Handle @layer blocks
83
+ const selector = joinSelectors(pathParts);
84
+ const object = { [key]: { [selector]: node } };
85
+ layerObject = mergeDeep(layerObject, object);
86
+ break;
87
+ }
88
+ case 'container query block': {
89
+ // Handle @container queries
90
+ const selector = joinSelectors(pathParts);
91
+ const object = { [key]: { [selector]: node } };
92
+ containerObject = mergeDeep(containerObject, object);
93
+ break;
94
+ }
95
+ case 'media query or prefix': {
96
+ // Handle media queries and prefixes
97
+ const selector = joinSelectors(pathParts);
98
+ const name = key.replace(/^@\s*/, '');
99
+ if (mediaPrefixes[name]) {
100
+ // Handle media prefix
101
+ const prefix = mediaPrefixes[name];
102
+ const rules = selector ? { ['& ' + selector]: node } : node;
103
+ const mediaPrefixObject = { [prefix]: rules };
104
+ prefixObject = mergeDeep(prefixObject, mediaPrefixObject);
105
+ }
106
+ else {
107
+ // Handle media query
108
+ let mediaQuery = key;
109
+ if (key.startsWith('@media ')) {
110
+ // Use the media query as is, just remove @media prefix
111
+ mediaQuery = key.replace(/^@media\s+/, '');
112
+ }
113
+ else if (mediaQueries[name]) {
114
+ // Use the named media query from configuration
115
+ mediaQuery = mediaQueries[name];
116
+ }
117
+ else {
118
+ // Unknown media query type
119
+ console.warn(`Warning: Media query type ${key} is unknown`);
120
+ break;
121
+ }
122
+ const mediaQueryObject = {
123
+ ['@media ' + mediaQuery]: { [selector]: node },
124
+ };
125
+ mediaObject = mergeDeep(mediaObject, mediaQueryObject);
126
+ }
127
+ break;
128
+ }
129
+ }
130
+ return node;
131
+ });
132
+ // Convert the main CSS style object to string
133
+ let cssString = obj2css(cssStyle);
134
+ // Add layer statements if any
135
+ if (layerStatements.length > 0) {
136
+ cssString = layerStatements.join('\n') + '\n\n' + cssString;
137
+ }
138
+ // Process and add layer blocks if any
139
+ if (Object.keys(layerObject).length > 0) {
140
+ const layers = Object.keys(layerObject);
141
+ const layerCssString = layers
142
+ .map((layer) => {
143
+ // Type guard for object values
144
+ const layerStyles = layerObject[layer];
145
+ if (!isObject(layerStyles))
146
+ return '';
147
+ const layerContent = getCss(layerStyles, mediaQueries, mediaPrefixes, auto);
148
+ return layerContent ? `${layer} {\n${indent(layerContent)}\n}` : '';
149
+ })
150
+ .filter(Boolean)
151
+ .join('\n\n');
152
+ if (layerCssString) {
153
+ cssString += '\n\n' + layerCssString;
154
+ }
155
+ }
156
+ // Process and add container queries if any
157
+ if (Object.keys(containerObject).length > 0) {
158
+ const containerQueries = Object.keys(containerObject);
159
+ const containerCssString = containerQueries
160
+ .map((containerQuery) => {
161
+ // Type guard for object values
162
+ const containerStyles = containerObject[containerQuery];
163
+ if (!isObject(containerStyles))
164
+ return '';
165
+ const containerContent = getCss(containerStyles, mediaQueries, mediaPrefixes, auto);
166
+ return containerContent
167
+ ? `${containerQuery} {\n${indent(containerContent)}\n}`
168
+ : '';
169
+ })
170
+ .filter(Boolean)
171
+ .join('\n\n');
172
+ if (containerCssString) {
173
+ cssString += '\n\n' + containerCssString;
174
+ }
175
+ }
176
+ // Process and add media queries if any
177
+ if (Object.keys(mediaObject).length > 0) {
178
+ const mediaCssString = processMediaQueries(mediaObject, mediaQueries);
179
+ if (mediaCssString) {
180
+ cssString += '\n\n' + mediaCssString;
181
+ }
182
+ }
183
+ // Process and add media prefixes if any
184
+ if (Object.keys(prefixObject).length > 0) {
185
+ // First, process the prefix object as normal CSS
186
+ const mediaPrefixedCssString = getCss(prefixObject, mediaQueries, mediaPrefixes);
187
+ cssString += '\n\n' + mediaPrefixedCssString;
188
+ // Handle auto mode if configured
189
+ if (auto) {
190
+ // Create a modified prefix object for auto mode
191
+ const autoPrefixObject = {};
192
+ for (const key of Object.keys(auto)) {
193
+ const selector = mediaPrefixes[key];
194
+ if (!selector || !prefixObject[selector])
195
+ continue;
196
+ const [autoSelector, mediaQuery] = auto[key];
197
+ // Create media query for auto mode
198
+ autoPrefixObject[`@media ${mediaQuery}`] = {
199
+ [autoSelector]: prefixObject[selector],
200
+ };
201
+ }
202
+ // Process the auto prefix object if not empty
203
+ if (Object.keys(autoPrefixObject).length > 0) {
204
+ const autoCssString = processMediaQueries(autoPrefixObject, mediaQueries);
205
+ if (autoCssString) {
206
+ cssString += '\n\n' + autoCssString;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ // Prettify the final CSS string
212
+ return prettifyCss(cssString.replace(/__bs__/g, '\\'));
213
+ }
214
+ /**
215
+ * Deep merge two objects
216
+ * @param target - Target object to merge into
217
+ * @param source - Source object to merge from
218
+ * @returns Merged object
219
+ */
220
+ function mergeDeep(target, source) {
221
+ const output = { ...target };
222
+ if (isObject(target) && isObject(source)) {
223
+ Object.keys(source).forEach((key) => {
224
+ if (isObject(source[key])) {
225
+ if (!(key in target)) {
226
+ output[key] = source[key];
227
+ }
228
+ else {
229
+ output[key] = mergeDeep(target[key], source[key]);
230
+ }
231
+ }
232
+ else {
233
+ output[key] = source[key];
234
+ }
235
+ });
236
+ }
237
+ return output;
238
+ }
239
+ /**
240
+ * Checks if value is a non-null object
241
+ * @param item - Value to check
242
+ * @returns True if the value is a non-null object
243
+ */
244
+ function isObject(item) {
245
+ return item && typeof item === 'object' && !Array.isArray(item);
246
+ }
247
+ export default getCss;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Type definitions for the CSS in JS library
3
+ */
4
+ /**
5
+ * CSS property value - can be string, number, or boolean
6
+ */
7
+ export type CssValue = string | number | boolean | null;
8
+ /**
9
+ * CSS selector or CSS property name
10
+ */
11
+ export type CssKey = string;
12
+ /**
13
+ * CSS styles object that can contain nested selectors or properties
14
+ */
15
+ export interface CssStyles {
16
+ [key: CssKey]: CssValue | CssStyles;
17
+ }
18
+ /**
19
+ * Named media queries configuration
20
+ */
21
+ export interface MediaQueries {
22
+ [name: string]: string;
23
+ }
24
+ /**
25
+ * Media prefixes configuration
26
+ */
27
+ export interface MediaPrefixes {
28
+ [name: string]: string;
29
+ }
30
+ /**
31
+ * Auto mode configuration for color schemes
32
+ */
33
+ export interface AutoConfig {
34
+ [mode: string]: [string, string];
35
+ }
36
+ /**
37
+ * Configuration for the CSS generator
38
+ */
39
+ export interface CssConfig {
40
+ mediaQueries?: MediaQueries;
41
+ mediaPrefixes?: MediaPrefixes;
42
+ auto?: AutoConfig;
43
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for the CSS in JS library
3
+ */
4
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Utility functions for CSS processing
3
+ */
4
+ /**
5
+ * Determines the object type in a more precise way than typeof
6
+ * @param obj - Any value to check type
7
+ * @returns The lowercase string representing the object type
8
+ */
9
+ export declare function getObjectType(obj: any): string;
10
+ /**
11
+ * Checks if a value is a primitive end value (string, number, boolean, null)
12
+ * @param value - Value to check
13
+ * @returns True if the value is a primitive end value
14
+ */
15
+ export declare function isEndValue(value: any): boolean;
16
+ /**
17
+ * Indents a string with spaces
18
+ * @param str - String to indent
19
+ * @param spaces - Number of spaces to indent with
20
+ * @returns Indented string
21
+ */
22
+ export declare function indent(str: string, spaces?: number): string;
23
+ /**
24
+ * Converts a JavaScript property name in camelCase to CSS kebab-case
25
+ * @param key - Property name in camelCase
26
+ * @returns Property name in kebab-case
27
+ */
28
+ export declare function jsKeyToCssKey(key: string): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Utility functions for CSS processing
3
+ */
4
+ /**
5
+ * Determines the object type in a more precise way than typeof
6
+ * @param obj - Any value to check type
7
+ * @returns The lowercase string representing the object type
8
+ */
9
+ export function getObjectType(obj) {
10
+ return (Object.prototype.toString
11
+ .call(obj)
12
+ .match(/^\[object (\w+)\]$/)?.[1]
13
+ .toLowerCase() || 'unknown');
14
+ }
15
+ /**
16
+ * Checks if a value is a primitive end value (string, number, boolean, null)
17
+ * @param value - Value to check
18
+ * @returns True if the value is a primitive end value
19
+ */
20
+ export function isEndValue(value) {
21
+ return ['string', 'number', 'boolean', 'null'].includes(getObjectType(value));
22
+ }
23
+ /**
24
+ * Indents a string with spaces
25
+ * @param str - String to indent
26
+ * @param spaces - Number of spaces to indent with
27
+ * @returns Indented string
28
+ */
29
+ export function indent(str, spaces = 2) {
30
+ return str
31
+ .split('\n')
32
+ .map((s) => ' '.repeat(spaces) + s)
33
+ .join('\n');
34
+ }
35
+ /**
36
+ * Converts a JavaScript property name in camelCase to CSS kebab-case
37
+ * @param key - Property name in camelCase
38
+ * @returns Property name in kebab-case
39
+ */
40
+ export function jsKeyToCssKey(key) {
41
+ // Keep vendor prefixes intact
42
+ const prefix = key.match(/^[-]+/)?.[0] || '';
43
+ const baseKey = key.replace(/^[-]+/, '');
44
+ // Convert camelCase to kebab-case
45
+ const kebabKey = baseKey.replace(/([A-Z])/g, '-$1').toLowerCase();
46
+ return prefix + kebabKey;
47
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Utilities for handling CSS content property values
3
+ */
4
+ /**
5
+ * Converts a JavaScript string value to a valid CSS content property value
6
+ * @param value - String value to convert for CSS content property
7
+ * @returns CSS-compatible content value
8
+ */
9
+ export declare function formatContentValue(value: string | number | boolean | null): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Utilities for handling CSS content property values
3
+ */
4
+ /**
5
+ * Converts a JavaScript string value to a valid CSS content property value
6
+ * @param value - String value to convert for CSS content property
7
+ * @returns CSS-compatible content value
8
+ */
9
+ export function formatContentValue(value) {
10
+ if (value === null)
11
+ return 'none';
12
+ const stringValue = String(value);
13
+ // If it's already wrapped in quotes, return as is
14
+ if (/^['"].*['"]$/.test(stringValue)) {
15
+ return stringValue;
16
+ }
17
+ // Handle unicode and emoji characters
18
+ const formattedValue = Array.from(stringValue)
19
+ .map((char) => {
20
+ const codePoint = char.codePointAt(0);
21
+ if (!codePoint)
22
+ return char;
23
+ // Convert emoji and special characters to unicode escape sequences
24
+ // Control characters and non-ASCII characters need escaping
25
+ if (codePoint > 127 || codePoint < 32) {
26
+ return `\\${codePoint.toString(16).padStart(6, '0')}`;
27
+ }
28
+ return char;
29
+ })
30
+ .join('');
31
+ // Wrap in single quotes if not already wrapped
32
+ return `'${formattedValue}'`;
33
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Utilities for handling media queries
3
+ */
4
+ import { CssStyles, MediaQueries } from '../types/index.js';
5
+ /**
6
+ * Processes media query objects and converts them to CSS
7
+ * @param mediaObject - Object containing media queries and their styles
8
+ * @param mediaQueries - Named media query definitions
9
+ * @returns CSS string with processed media queries
10
+ */
11
+ export declare function processMediaQueries(mediaObject: CssStyles, mediaQueries?: MediaQueries): string;
12
+ /**
13
+ * Checks if a key represents a media query
14
+ * @param key - Key to check
15
+ * @returns True if the key is a media query
16
+ */
17
+ export declare function isMediaQuery(key: string): boolean;
18
+ /**
19
+ * Checks if a key is a named media query that needs to be resolved
20
+ * @param key - Key to check
21
+ * @param mediaQueries - Named media query definitions
22
+ * @returns True if the key is a named media query
23
+ */
24
+ export declare function isNamedMediaQuery(key: string, mediaQueries: MediaQueries): boolean;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Utilities for handling media queries
3
+ */
4
+ import { indent } from './common.js';
5
+ import { obj2css } from './obj2css.js';
6
+ import { isEndValue } from './common.js';
7
+ /**
8
+ * Processes media query objects and converts them to CSS
9
+ * @param mediaObject - Object containing media queries and their styles
10
+ * @param mediaQueries - Named media query definitions
11
+ * @returns CSS string with processed media queries
12
+ */
13
+ export function processMediaQueries(mediaObject, mediaQueries = {}) {
14
+ if (!mediaObject || Object.keys(mediaObject).length === 0) {
15
+ return '';
16
+ }
17
+ const mediaQueriesToProcess = Object.keys(mediaObject);
18
+ // Process each media query and convert to CSS
19
+ const mediaCssStrings = mediaQueriesToProcess.map((queryKey) => {
20
+ // Handle named media queries (using @name syntax)
21
+ const mediaQueryMatch = queryKey.match(/^@(\w+)$/);
22
+ let actualQuery = queryKey;
23
+ if (mediaQueryMatch &&
24
+ mediaQueryMatch[1] &&
25
+ mediaQueries[mediaQueryMatch[1]]) {
26
+ // Replace named query with actual query definition
27
+ actualQuery = mediaQueries[mediaQueryMatch[1]];
28
+ }
29
+ else if (queryKey.startsWith('@media ')) {
30
+ // Strip @media prefix if present
31
+ actualQuery = queryKey.replace(/^@media\s+/, '');
32
+ }
33
+ // Convert the styles for this media query to CSS
34
+ const styles = mediaObject[queryKey];
35
+ // Skip if the styles aren't an object
36
+ if (isEndValue(styles)) {
37
+ return '';
38
+ }
39
+ const stylesCss = obj2css(styles);
40
+ if (!stylesCss.trim()) {
41
+ return '';
42
+ }
43
+ // Wrap the styles in a media query block
44
+ return `@media ${actualQuery} {\n${indent(stylesCss)}\n}`;
45
+ });
46
+ // Join all media query blocks with newlines
47
+ return mediaCssStrings.filter((css) => css).join('\n\n');
48
+ }
49
+ /**
50
+ * Checks if a key represents a media query
51
+ * @param key - Key to check
52
+ * @returns True if the key is a media query
53
+ */
54
+ export function isMediaQuery(key) {
55
+ return key.startsWith('@media') || key.startsWith('@');
56
+ }
57
+ /**
58
+ * Checks if a key is a named media query that needs to be resolved
59
+ * @param key - Key to check
60
+ * @param mediaQueries - Named media query definitions
61
+ * @returns True if the key is a named media query
62
+ */
63
+ export function isNamedMediaQuery(key, mediaQueries) {
64
+ const match = key.match(/^@(\w+)$/);
65
+ return Boolean(match && match[1] && mediaQueries[match[1]]);
66
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Utilities for converting JavaScript objects to CSS strings
3
+ */
4
+ /**
5
+ * Converts a CSS object to a string representation
6
+ * @param cssObject - Object with CSS selectors and properties
7
+ * @returns CSS string
8
+ */
9
+ export declare function obj2css(cssObject: Record<string, any>): string;
10
+ /**
11
+ * Prettifies a CSS string by ensuring consistent spacing and formatting
12
+ * @param cssString - CSS string to prettify
13
+ * @returns Prettified CSS string
14
+ */
15
+ export declare function prettifyCss(cssString: string): string;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Utilities for converting JavaScript objects to CSS strings
3
+ */
4
+ /**
5
+ * Converts a CSS object to a string representation
6
+ * @param cssObject - Object with CSS selectors and properties
7
+ * @returns CSS string
8
+ */
9
+ export function obj2css(cssObject) {
10
+ // Convert object to JSON string with indentation
11
+ const json = JSON.stringify(cssObject, null, 2);
12
+ // Process the JSON string to convert to valid CSS
13
+ const css = json
14
+ // Replace double quotes with single quotes for strings
15
+ .replace(/\\"/g, "'")
16
+ // Convert object keys to CSS selectors
17
+ .replace(/"([^"]+)":\s*{/g, '$1 {')
18
+ // Remove commas between rule sets and add empty line
19
+ .replace(/},/g, '}')
20
+ // Remove remaining double quotes
21
+ .replace(/"/g, '')
22
+ // Restore quotes for inlined URLs
23
+ .replace(/url\('(.+)'\)/g, 'url("$1")')
24
+ // Convert property-value commas to semicolons
25
+ .replace(/,(\s*})/g, ';$1')
26
+ // Remove the outermost curly braces
27
+ .replace(/^{|}$/g, '')
28
+ // Convert commas between property values to semicolons
29
+ .replace(/,\s*(?=[a-z-]+:)/g, ';\n ')
30
+ // Remove indentation from the start of lines
31
+ .replace(/^( {2})/gm, '')
32
+ // Add semicolons before closing brackets if missing
33
+ .replace(/([^;])\s*}/g, '$1;\n}')
34
+ // Ensure space between property name and value
35
+ .replace(/:\s*/g, ': ')
36
+ // Clean up extra spaces
37
+ .replace(/\s+/g, ' ')
38
+ // Fix colon spacing in selectors
39
+ .replace(/([:#]) /g, '$1');
40
+ // Split into lines for additional processing
41
+ const lines = css.split('\n');
42
+ const result = [];
43
+ for (let i = 0; i < lines.length; i++) {
44
+ const line = lines[i];
45
+ // Handle property lines
46
+ if (line.includes(': ')) {
47
+ result.push(' ' + line.trim());
48
+ }
49
+ // Handle selector lines
50
+ else if (line.includes('{')) {
51
+ result.push(line.trim());
52
+ }
53
+ // Handle closing bracket lines
54
+ else if (line.includes('}')) {
55
+ result.push('}');
56
+ }
57
+ // Other lines
58
+ else {
59
+ result.push(line);
60
+ }
61
+ }
62
+ return result.join('\n');
63
+ }
64
+ /**
65
+ * Prettifies a CSS string by ensuring consistent spacing and formatting
66
+ * @param cssString - CSS string to prettify
67
+ * @returns Prettified CSS string
68
+ */
69
+ export function prettifyCss(cssString) {
70
+ return (cssString
71
+ // Ensure consistent newlines between rule sets
72
+ .replace(/}\s*/g, '}\n\n')
73
+ // Fix spacing inside brackets
74
+ .replace(/{\s*/g, ' {\n')
75
+ // Clean up extra newlines
76
+ .replace(/\n{3,}/g, '\n\n')
77
+ // Trim the string
78
+ .trim());
79
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Utilities for working with CSS selectors
3
+ */
4
+ /**
5
+ * Joins selector parts into a valid CSS selector
6
+ * @param path - Array of selector parts or a single selector
7
+ * @returns Combined CSS selector
8
+ */
9
+ export declare function joinSelectors(path: string | string[]): string;
10
+ /**
11
+ * Creates a Cartesian product of selector parts and joins them
12
+ * @param parts - Array of selector parts arrays
13
+ * @returns Array of all possible combined selectors
14
+ */
15
+ export declare function cartesianSelectors(parts: string[][]): string[];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Utilities for working with CSS selectors
3
+ */
4
+ import { isHtmlTag } from './tags.js';
5
+ /**
6
+ * Joins selector parts into a valid CSS selector
7
+ * @param path - Array of selector parts or a single selector
8
+ * @returns Combined CSS selector
9
+ */
10
+ export function joinSelectors(path) {
11
+ const array = typeof path === 'string' ? [path] : path;
12
+ let result = '';
13
+ for (const keyStr of array) {
14
+ // Skip empty key strings
15
+ if (!keyStr.trim())
16
+ continue;
17
+ const key = keyStr.replace(/&/g, '').trim();
18
+ if (isHtmlTag(key)) {
19
+ // If it's an HTML tag, just add it with a space
20
+ result += ` ${key}`;
21
+ }
22
+ else {
23
+ let prefix = '';
24
+ let selector = key;
25
+ // Handle reference to parent selector with &
26
+ if (/^&/.test(keyStr)) {
27
+ prefix += ' ';
28
+ }
29
+ // Handle double underscore for descendant of any level (T6)
30
+ if (/^__/.test(key)) {
31
+ prefix += ' ';
32
+ selector = selector.replace(/^__/, '');
33
+ }
34
+ // Handle single underscore for class selector (T5)
35
+ if (/^_/.test(key)) {
36
+ selector = selector.replace(/^_/, '');
37
+ if (!isHtmlTag(selector)) {
38
+ // If not a tag, increase level
39
+ prefix += ' ';
40
+ }
41
+ }
42
+ // Add dot for class selectors if needed
43
+ if (!/^[.:#*[~+>]/.test(selector)) {
44
+ if (!/\./.test(prefix))
45
+ prefix += '.';
46
+ }
47
+ // Special handling for universal selector
48
+ if (/^\*/.test(selector)) {
49
+ prefix = ' ' + prefix;
50
+ }
51
+ result += prefix + selector;
52
+ }
53
+ }
54
+ // Fix any double dots that might have been created
55
+ result = result.replace(/\.\s*\./g, '.');
56
+ // Clean up whitespace
57
+ result = result
58
+ // Remove extra spaces
59
+ .replace(/\s+/g, ' ')
60
+ // Preserve proper spacing around combinators
61
+ .replace(/\s*([>+~])\s*/g, ' $1 ')
62
+ // Fix pseudo-classes and pseudo-elements
63
+ .replace(/\s+:/g, ':')
64
+ // Fix attribute selectors
65
+ .replace(/\s+\[/g, '[')
66
+ // Trim whitespace
67
+ .trim();
68
+ return result;
69
+ }
70
+ /**
71
+ * Creates a Cartesian product of selector parts and joins them
72
+ * @param parts - Array of selector parts arrays
73
+ * @returns Array of all possible combined selectors
74
+ */
75
+ export function cartesianSelectors(parts) {
76
+ // Base case: If empty or just one part, return it flattened
77
+ if (parts.length === 0)
78
+ return [];
79
+ if (parts.length === 1)
80
+ return parts[0];
81
+ // Implementation of cartesian product
82
+ const result = parts.reduce((acc, curr) => acc.flatMap((x) => curr.map((y) => [...x, y])), [[]]);
83
+ return result.map((selectors) => {
84
+ // Handle comma-separated selectors by creating real CSS selector lists
85
+ return joinSelectors(selectors.filter(Boolean));
86
+ });
87
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * List of all HTML tags used to identify selectors
3
+ */
4
+ export declare const htmlTags: string[];
5
+ /**
6
+ * Checks if a string is an HTML tag name
7
+ * @param key - String to check
8
+ * @returns True if the string is an HTML tag name
9
+ */
10
+ export declare function isHtmlTag(key: string): boolean;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * List of all HTML tags used to identify selectors
3
+ */
4
+ export const htmlTags = [
5
+ 'a',
6
+ 'abbr',
7
+ 'address',
8
+ 'area',
9
+ 'article',
10
+ 'aside',
11
+ 'audio',
12
+ 'b',
13
+ 'base',
14
+ 'bdi',
15
+ 'bdo',
16
+ 'blockquote',
17
+ 'body',
18
+ 'br',
19
+ 'button',
20
+ 'canvas',
21
+ 'caption',
22
+ 'cite',
23
+ 'code',
24
+ 'col',
25
+ 'colgroup',
26
+ 'data',
27
+ 'datalist',
28
+ 'dd',
29
+ 'del',
30
+ 'details',
31
+ 'dfn',
32
+ 'dialog',
33
+ 'div',
34
+ 'dl',
35
+ 'dt',
36
+ 'em',
37
+ 'embed',
38
+ 'fieldset',
39
+ 'figcaption',
40
+ 'figure',
41
+ 'footer',
42
+ 'form',
43
+ 'h1',
44
+ 'h2',
45
+ 'h3',
46
+ 'h4',
47
+ 'h5',
48
+ 'h6',
49
+ 'head',
50
+ 'header',
51
+ 'hgroup',
52
+ 'hr',
53
+ 'html',
54
+ 'i',
55
+ 'iframe',
56
+ 'img',
57
+ 'input',
58
+ 'ins',
59
+ 'kbd',
60
+ 'label',
61
+ 'legend',
62
+ 'li',
63
+ 'link',
64
+ 'main',
65
+ 'map',
66
+ 'mark',
67
+ 'meta',
68
+ 'meter',
69
+ 'nav',
70
+ 'noscript',
71
+ 'object',
72
+ 'ol',
73
+ 'optgroup',
74
+ 'option',
75
+ 'output',
76
+ 'p',
77
+ 'param',
78
+ 'picture',
79
+ 'pre',
80
+ 'progress',
81
+ 'q',
82
+ 'rp',
83
+ 'rt',
84
+ 'ruby',
85
+ 's',
86
+ 'samp',
87
+ 'script',
88
+ 'section',
89
+ 'select',
90
+ 'slot',
91
+ 'small',
92
+ 'source',
93
+ 'span',
94
+ 'strong',
95
+ 'style',
96
+ 'sub',
97
+ 'summary',
98
+ 'sup',
99
+ 'svg',
100
+ 'table',
101
+ 'tbody',
102
+ 'td',
103
+ 'template',
104
+ 'textarea',
105
+ 'tfoot',
106
+ 'th',
107
+ 'thead',
108
+ 'time',
109
+ 'title',
110
+ 'tr',
111
+ 'track',
112
+ 'u',
113
+ 'ul',
114
+ 'var',
115
+ 'video',
116
+ 'wbr',
117
+ ];
118
+ /**
119
+ * Checks if a string is an HTML tag name
120
+ * @param key - String to check
121
+ * @returns True if the string is an HTML tag name
122
+ */
123
+ export function isHtmlTag(key) {
124
+ if (/^\w+[.#:[~+ ]/.test(key)) {
125
+ return htmlTags.includes(key.split(/[.#:[~+ ]/)[0]);
126
+ }
127
+ return htmlTags.includes(key);
128
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Utility for traversing and transforming nested objects
3
+ */
4
+ /**
5
+ * Type for node visitor functions
6
+ */
7
+ export type NodeVisitor<T> = (node: any, path: string, root: any, index: number) => T;
8
+ /**
9
+ * Traverses a nested object and applies a visitor function to each node
10
+ * @param node - The object to traverse
11
+ * @param visitor - Function to call for each node
12
+ * @param path - Current path in the object (for tracking)
13
+ * @param root - Root object (for reference)
14
+ * @param index - Current index in array (if applicable)
15
+ * @param separator - Path separator character
16
+ * @returns The result of the visitor function on the current node
17
+ */
18
+ export declare function traverseObject<T>(node: any, visitor: NodeVisitor<T>, path?: string, root?: any, index?: number, separator?: string): T;
19
+ /**
20
+ * Determines the type of a node in the CSS object
21
+ * @param node - The node to check
22
+ * @param path - Current path in the object
23
+ * @returns The identified node type as a string
24
+ */
25
+ export declare function determineNodeType(node: any, path?: string): string;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Utility for traversing and transforming nested objects
3
+ */
4
+ /**
5
+ * Traverses a nested object and applies a visitor function to each node
6
+ * @param node - The object to traverse
7
+ * @param visitor - Function to call for each node
8
+ * @param path - Current path in the object (for tracking)
9
+ * @param root - Root object (for reference)
10
+ * @param index - Current index in array (if applicable)
11
+ * @param separator - Path separator character
12
+ * @returns The result of the visitor function on the current node
13
+ */
14
+ export function traverseObject(node, visitor, path = '', root, index = -1, separator = '\\') {
15
+ const realRoot = root || node;
16
+ // Handle arrays
17
+ if (Array.isArray(node)) {
18
+ const processedItems = node.map((item, idx) => traverseObject(item, visitor, path, realRoot, idx, separator));
19
+ return visitor(processedItems, path, realRoot, index);
20
+ }
21
+ // Handle objects (but not null)
22
+ if (node !== null && typeof node === 'object') {
23
+ const processedObject = Object.keys(node).reduce((result, key) => {
24
+ const newPath = path ? `${path}${separator}${key}` : key;
25
+ const processedValue = traverseObject(node[key], visitor, newPath, realRoot, index, separator);
26
+ result[key] = processedValue;
27
+ return result;
28
+ }, {});
29
+ return visitor(processedObject, path, realRoot, index);
30
+ }
31
+ // Handle primitive values
32
+ return visitor(node, path, realRoot, index);
33
+ }
34
+ /**
35
+ * Determines the type of a node in the CSS object
36
+ * @param node - The node to check
37
+ * @param path - Current path in the object
38
+ * @returns The identified node type as a string
39
+ */
40
+ export function determineNodeType(node, path) {
41
+ if (!path)
42
+ return 'unknown';
43
+ const lastKey = path.split('\\').pop() || '';
44
+ // Layer statement
45
+ if (/^@layer/.test(lastKey) && typeof node === 'string') {
46
+ return 'layer statement';
47
+ }
48
+ // Layer block
49
+ if (/^@layer/.test(lastKey) && typeof node === 'object' && node !== null) {
50
+ return 'layer block';
51
+ }
52
+ // Container query
53
+ if (/^@container/.test(lastKey) &&
54
+ typeof node === 'object' &&
55
+ node !== null) {
56
+ return 'container query block';
57
+ }
58
+ // Media query or prefix
59
+ if (/^@/.test(lastKey)) {
60
+ return 'media query or prefix';
61
+ }
62
+ // CSS property-value pair (end value)
63
+ if (isPrimitiveValue(node)) {
64
+ return 'selector';
65
+ }
66
+ return 'unknown';
67
+ }
68
+ /**
69
+ * Checks if a value is a primitive that can be used as a CSS property value
70
+ * @param value - Value to check
71
+ * @returns True if the value is a primitive
72
+ */
73
+ function isPrimitiveValue(value) {
74
+ return (value === null ||
75
+ typeof value === 'string' ||
76
+ typeof value === 'number' ||
77
+ typeof value === 'boolean');
78
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "esm-styles",
3
+ "version": "0.1.0",
4
+ "description": "A library for working with ESM styles",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "lint": "eslint src --ext .ts",
15
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
16
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
17
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "esm",
22
+ "styles",
23
+ "css"
24
+ ],
25
+ "author": "romochka@gmail.com",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=14.16"
29
+ },
30
+ "devDependencies": {
31
+ "@types/jest": "^29.5.11",
32
+ "@types/node": "^20.11.0",
33
+ "@typescript-eslint/eslint-plugin": "^6.18.1",
34
+ "@typescript-eslint/parser": "^6.18.1",
35
+ "eslint": "^8.56.0",
36
+ "jest": "^29.7.0",
37
+ "ts-jest": "^29.1.1",
38
+ "typescript": "^5.3.3"
39
+ }
40
+ }