assign-gingerly 0.0.15 → 0.0.17

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 CHANGED
@@ -2180,6 +2180,216 @@ assignGingerly(element, attrs);
2180
2180
 
2181
2181
  </details>
2182
2182
 
2183
+ ## Building CSS Queries with `buildCSSQuery`
2184
+
2185
+ The `buildCSSQuery` function generates CSS selector strings that match elements with attributes defined in an enhancement configuration's `withAttrs`. This is particularly useful for libraries like mount-observer that need to find elements that should be enhanced.
2186
+
2187
+ ### Basic Usage
2188
+
2189
+ ```TypeScript
2190
+ import { buildCSSQuery } from 'assign-gingerly';
2191
+
2192
+ const config = {
2193
+ spawn: MyEnhancement,
2194
+ withAttrs: {
2195
+ base: 'my-component',
2196
+ theme: '${base}-theme'
2197
+ }
2198
+ };
2199
+
2200
+ const query = buildCSSQuery(config, 'div, span');
2201
+ console.log(query);
2202
+ // 'div[my-component], span[my-component], div[enh-my-component], span[enh-my-component],
2203
+ // div[my-component-theme], span[my-component-theme], div[enh-my-component-theme], span[enh-my-component-theme]'
2204
+
2205
+ // Use with querySelector
2206
+ const elements = document.querySelectorAll(query);
2207
+ ```
2208
+
2209
+ **Without selectors (matches any element):**
2210
+
2211
+ ```TypeScript
2212
+ const query = buildCSSQuery(config, '');
2213
+ console.log(query);
2214
+ // '[my-component], [enh-my-component], [my-component-theme], [enh-my-component-theme]'
2215
+
2216
+ // Matches any element with these attributes
2217
+ const elements = document.querySelectorAll(query);
2218
+ ```
2219
+
2220
+ ### How It Works
2221
+
2222
+ `buildCSSQuery` creates a cross-product of:
2223
+ 1. **Selectors**: The CSS selectors you provide (e.g., `'div, span'`)
2224
+ 2. **Attributes**: All attribute names from `withAttrs` (resolving template variables)
2225
+ 3. **Prefixes**: Both unprefixed and `enh-` prefixed versions
2226
+
2227
+ This ensures you find all elements that might be enhanced, regardless of whether they use the `enh-` prefix or not.
2228
+
2229
+ ### Template Variable Resolution
2230
+
2231
+ Template variables in `withAttrs` are automatically resolved:
2232
+
2233
+ ```TypeScript
2234
+ const config = {
2235
+ spawn: BeABeacon,
2236
+ withAttrs: {
2237
+ base: 'be-a-beacon',
2238
+ theme: '${base}-theme',
2239
+ size: '${base}-size'
2240
+ }
2241
+ };
2242
+
2243
+ buildCSSQuery(config, 'template, script');
2244
+ // Returns selectors for: be-a-beacon, be-a-beacon-theme, be-a-beacon-size
2245
+ // Each with both prefixed and unprefixed versions
2246
+ ```
2247
+
2248
+ ### Complex Selectors
2249
+
2250
+ The function supports any valid CSS selector:
2251
+
2252
+ ```TypeScript
2253
+ const config = {
2254
+ spawn: MyEnhancement,
2255
+ withAttrs: {
2256
+ base: 'data-enhanced'
2257
+ }
2258
+ };
2259
+
2260
+ // Classes and IDs
2261
+ buildCSSQuery(config, 'div.highlight, span#special');
2262
+ // 'div.highlight[data-enhanced], span#special[data-enhanced], ...'
2263
+
2264
+ // Combinators
2265
+ buildCSSQuery(config, 'div > span, ul li');
2266
+ // 'div > span[data-enhanced], ul li[data-enhanced], ...'
2267
+
2268
+ // Pseudo-classes
2269
+ buildCSSQuery(config, 'div:hover, span:first-child');
2270
+ // 'div:hover[data-enhanced], span:first-child[data-enhanced], ...'
2271
+
2272
+ // Attribute selectors
2273
+ buildCSSQuery(config, 'div[existing-attr]');
2274
+ // 'div[existing-attr][data-enhanced], ...'
2275
+ ```
2276
+
2277
+ ### Underscore-Prefixed Keys Excluded
2278
+
2279
+ Configuration keys starting with `_` are excluded from the query:
2280
+
2281
+ ```TypeScript
2282
+ const config = {
2283
+ spawn: MyEnhancement,
2284
+ withAttrs: {
2285
+ base: 'my-attr',
2286
+ _base: {
2287
+ mapsTo: 'something' // Config only, not an attribute
2288
+ },
2289
+ theme: '${base}-theme',
2290
+ _theme: {
2291
+ instanceOf: 'String' // Config only
2292
+ }
2293
+ }
2294
+ };
2295
+
2296
+ buildCSSQuery(config, 'div');
2297
+ // Only includes: my-attr and my-attr-theme
2298
+ // Does NOT include: _base or _theme
2299
+ ```
2300
+
2301
+ ### Edge Cases
2302
+
2303
+ **Empty selectors return attribute-only selectors:**
2304
+ ```TypeScript
2305
+ const config = {
2306
+ spawn: MyClass,
2307
+ withAttrs: {
2308
+ base: 'my-attr',
2309
+ theme: '${base}-theme'
2310
+ }
2311
+ };
2312
+
2313
+ buildCSSQuery(config, '');
2314
+ // '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
2315
+ // Matches any element with these attributes
2316
+ ```
2317
+
2318
+ **Empty withAttrs returns empty string:**
2319
+ ```TypeScript
2320
+ buildCSSQuery({ spawn: MyClass }, 'div'); // '' (no withAttrs)
2321
+ buildCSSQuery({ spawn: MyClass, withAttrs: {} }, 'div'); // '' (empty withAttrs)
2322
+ ```
2323
+
2324
+ **Deduplication:**
2325
+ ```TypeScript
2326
+ buildCSSQuery(config, 'div, div, div');
2327
+ // Duplicates are removed automatically
2328
+ ```
2329
+
2330
+ **Whitespace handling:**
2331
+ ```TypeScript
2332
+ buildCSSQuery(config, ' div , span , p ');
2333
+ // Whitespace is trimmed automatically
2334
+ ```
2335
+
2336
+ ### Use Cases
2337
+
2338
+ 1. **Mount Observer Integration**: Find elements that need enhancement
2339
+ ```TypeScript
2340
+ // Match any element with the attributes
2341
+ const query = buildCSSQuery(enhancementConfig, '');
2342
+ const observer = new MutationObserver(() => {
2343
+ const elements = document.querySelectorAll(query);
2344
+ elements.forEach(el => enhance(el));
2345
+ });
2346
+ ```
2347
+
2348
+ 2. **Specific Element Types**: Enhance only certain element types
2349
+ ```TypeScript
2350
+ const query = buildCSSQuery(config, 'template, script');
2351
+ document.querySelectorAll(query).forEach(el => {
2352
+ const instance = el.enh.get(config);
2353
+ });
2354
+ ```
2355
+
2356
+ 3. **Conditional Enhancement**: Find elements in specific contexts
2357
+ ```TypeScript
2358
+ const query = buildCSSQuery(config, '.container > div');
2359
+ const elements = document.querySelectorAll(query);
2360
+ ```
2361
+
2362
+ ### API Reference
2363
+
2364
+ ```TypeScript
2365
+ function buildCSSQuery(
2366
+ config: EnhancementConfig,
2367
+ selectors: string
2368
+ ): string
2369
+ ```
2370
+
2371
+ **Parameters:**
2372
+ - `config`: Enhancement configuration with `withAttrs` property
2373
+ - `selectors`: Comma-separated CSS selectors (e.g., `'div, span'`)
2374
+ - If empty string or whitespace only, returns attribute selectors without element prefix
2375
+ - This matches any element with the specified attributes
2376
+
2377
+ **Returns:**
2378
+ - CSS query string with cross-product of selectors and attributes
2379
+ - If selectors is empty: returns attribute-only selectors (e.g., `'[attr], [enh-attr]'`)
2380
+ - If withAttrs is missing or empty: returns empty string
2381
+
2382
+ **Throws:**
2383
+ - Error if template variables have circular references
2384
+ - Error if template variables reference undefined keys
2385
+
2386
+ ### Performance Notes
2387
+
2388
+ - The function is synchronous and fast
2389
+ - Resulting queries can be long with many attributes, but CSS engines handle this efficiently
2390
+ - Queries are deduplicated automatically
2391
+ - Consider caching the result if calling repeatedly with the same config
2392
+
2183
2393
  <!--
2184
2394
 
2185
2395
  ### Complete Example
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Resolves template variables in a string recursively
3
+ * @param template - Template string with ${var} placeholders
4
+ * @param patterns - The patterns object containing variable values
5
+ * @param resolvedCache - Cache of already resolved values
6
+ * @param visitedKeys - Set of keys being resolved (for cycle detection)
7
+ * @returns Resolved string
8
+ */
9
+ function resolveTemplate(template, patterns, resolvedCache, visitedKeys = new Set()) {
10
+ return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
11
+ // Check if already resolved
12
+ if (resolvedCache.has(varName)) {
13
+ return resolvedCache.get(varName);
14
+ }
15
+ // Check for circular reference
16
+ if (visitedKeys.has(varName)) {
17
+ throw new Error(`Circular reference detected in template variable: ${varName}`);
18
+ }
19
+ const value = patterns[varName];
20
+ if (value === undefined) {
21
+ throw new Error(`Undefined template variable: ${varName}`);
22
+ }
23
+ if (typeof value === 'string') {
24
+ // Recursively resolve
25
+ visitedKeys.add(varName);
26
+ const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
27
+ visitedKeys.delete(varName);
28
+ resolvedCache.set(varName, resolved);
29
+ return resolved;
30
+ }
31
+ // Non-string value, return as-is
32
+ return String(value);
33
+ });
34
+ }
35
+ /**
36
+ * Extracts attribute names from withAttrs configuration
37
+ * Resolves template variables and excludes underscore-prefixed config keys
38
+ * @param withAttrs - The attribute patterns configuration
39
+ * @returns Array of resolved attribute names
40
+ */
41
+ function extractAttributeNames(withAttrs) {
42
+ const names = [];
43
+ const resolvedCache = new Map();
44
+ // Add base if present
45
+ if ('base' in withAttrs && typeof withAttrs.base === 'string') {
46
+ names.push(withAttrs.base);
47
+ }
48
+ // Add other attributes (skip underscore-prefixed config keys)
49
+ for (const key in withAttrs) {
50
+ if (key === 'base' || key.startsWith('_')) {
51
+ continue;
52
+ }
53
+ const value = withAttrs[key];
54
+ if (typeof value === 'string') {
55
+ // Resolve template variables
56
+ const resolved = resolveTemplate(value, withAttrs, resolvedCache);
57
+ names.push(resolved);
58
+ }
59
+ }
60
+ return names;
61
+ }
62
+ /**
63
+ * Builds a CSS query selector that matches elements with attributes from withAttrs
64
+ * Creates a cross-product of selectors and attribute names (both prefixed and unprefixed)
65
+ *
66
+ * @param config - Enhancement configuration with withAttrs
67
+ * @param selectors - Comma-separated CSS selectors to match (e.g., 'template, script')
68
+ * If empty, returns just the attribute selectors without element prefix
69
+ * @returns CSS query string with cross-product of selectors and attributes
70
+ *
71
+ * @example
72
+ * const config = {
73
+ * spawn: MyClass,
74
+ * withAttrs: {
75
+ * base: 'my-attr',
76
+ * theme: '${base}-theme'
77
+ * }
78
+ * };
79
+ *
80
+ * // With selectors
81
+ * buildCSSQuery(config, 'div, span');
82
+ * // Returns: 'div[my-attr], span[my-attr], div[enh-my-attr], span[enh-my-attr],
83
+ * // div[my-attr-theme], span[my-attr-theme], div[enh-my-attr-theme], span[enh-my-attr-theme]'
84
+ *
85
+ * // Without selectors (matches any element)
86
+ * buildCSSQuery(config, '');
87
+ * // Returns: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
88
+ */
89
+ export function buildCSSQuery(config, selectors) {
90
+ // Validate inputs
91
+ if (!config.withAttrs) {
92
+ return '';
93
+ }
94
+ // Extract and resolve attribute names first
95
+ const attrNames = extractAttributeNames(config.withAttrs);
96
+ if (attrNames.length === 0) {
97
+ return '';
98
+ }
99
+ // Parse and normalize selectors
100
+ const selectorList = selectors
101
+ ? selectors.split(',').map(s => s.trim()).filter(s => s.length > 0)
102
+ : [];
103
+ // Build queries
104
+ const queries = [];
105
+ if (selectorList.length === 0) {
106
+ // No selectors provided - return just attribute selectors
107
+ for (const attrName of attrNames) {
108
+ queries.push(`[${attrName}]`);
109
+ queries.push(`[enh-${attrName}]`);
110
+ }
111
+ }
112
+ else {
113
+ // Build cross-product of selectors × attributes × prefixes
114
+ for (const selector of selectorList) {
115
+ for (const attrName of attrNames) {
116
+ // Unprefixed version
117
+ queries.push(`${selector}[${attrName}]`);
118
+ // enh- prefixed version
119
+ queries.push(`${selector}[enh-${attrName}]`);
120
+ }
121
+ }
122
+ }
123
+ // Deduplicate and join
124
+ const uniqueQueries = [...new Set(queries)];
125
+ return uniqueQueries.join(', ');
126
+ }
@@ -0,0 +1,152 @@
1
+ import { EnhancementConfig, AttrPatterns } from './types/assign-gingerly/types';
2
+
3
+ /**
4
+ * Resolves template variables in a string recursively
5
+ * @param template - Template string with ${var} placeholders
6
+ * @param patterns - The patterns object containing variable values
7
+ * @param resolvedCache - Cache of already resolved values
8
+ * @param visitedKeys - Set of keys being resolved (for cycle detection)
9
+ * @returns Resolved string
10
+ */
11
+ function resolveTemplate(
12
+ template: string,
13
+ patterns: Record<string, any>,
14
+ resolvedCache: Map<string, string>,
15
+ visitedKeys: Set<string> = new Set()
16
+ ): string {
17
+ return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
18
+ // Check if already resolved
19
+ if (resolvedCache.has(varName)) {
20
+ return resolvedCache.get(varName)!;
21
+ }
22
+
23
+ // Check for circular reference
24
+ if (visitedKeys.has(varName)) {
25
+ throw new Error(`Circular reference detected in template variable: ${varName}`);
26
+ }
27
+
28
+ const value = patterns[varName];
29
+
30
+ if (value === undefined) {
31
+ throw new Error(`Undefined template variable: ${varName}`);
32
+ }
33
+
34
+ if (typeof value === 'string') {
35
+ // Recursively resolve
36
+ visitedKeys.add(varName);
37
+ const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
38
+ visitedKeys.delete(varName);
39
+ resolvedCache.set(varName, resolved);
40
+ return resolved;
41
+ }
42
+
43
+ // Non-string value, return as-is
44
+ return String(value);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Extracts attribute names from withAttrs configuration
50
+ * Resolves template variables and excludes underscore-prefixed config keys
51
+ * @param withAttrs - The attribute patterns configuration
52
+ * @returns Array of resolved attribute names
53
+ */
54
+ function extractAttributeNames(withAttrs: AttrPatterns<any>): string[] {
55
+ const names: string[] = [];
56
+ const resolvedCache = new Map<string, string>();
57
+
58
+ // Add base if present
59
+ if ('base' in withAttrs && typeof withAttrs.base === 'string') {
60
+ names.push(withAttrs.base);
61
+ }
62
+
63
+ // Add other attributes (skip underscore-prefixed config keys)
64
+ for (const key in withAttrs) {
65
+ if (key === 'base' || key.startsWith('_')) {
66
+ continue;
67
+ }
68
+
69
+ const value = withAttrs[key];
70
+ if (typeof value === 'string') {
71
+ // Resolve template variables
72
+ const resolved = resolveTemplate(value, withAttrs, resolvedCache);
73
+ names.push(resolved);
74
+ }
75
+ }
76
+
77
+ return names;
78
+ }
79
+
80
+ /**
81
+ * Builds a CSS query selector that matches elements with attributes from withAttrs
82
+ * Creates a cross-product of selectors and attribute names (both prefixed and unprefixed)
83
+ *
84
+ * @param config - Enhancement configuration with withAttrs
85
+ * @param selectors - Comma-separated CSS selectors to match (e.g., 'template, script')
86
+ * If empty, returns just the attribute selectors without element prefix
87
+ * @returns CSS query string with cross-product of selectors and attributes
88
+ *
89
+ * @example
90
+ * const config = {
91
+ * spawn: MyClass,
92
+ * withAttrs: {
93
+ * base: 'my-attr',
94
+ * theme: '${base}-theme'
95
+ * }
96
+ * };
97
+ *
98
+ * // With selectors
99
+ * buildCSSQuery(config, 'div, span');
100
+ * // Returns: 'div[my-attr], span[my-attr], div[enh-my-attr], span[enh-my-attr],
101
+ * // div[my-attr-theme], span[my-attr-theme], div[enh-my-attr-theme], span[enh-my-attr-theme]'
102
+ *
103
+ * // Without selectors (matches any element)
104
+ * buildCSSQuery(config, '');
105
+ * // Returns: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
106
+ */
107
+ export function buildCSSQuery(
108
+ config: EnhancementConfig,
109
+ selectors: string
110
+ ): string {
111
+ // Validate inputs
112
+ if (!config.withAttrs) {
113
+ return '';
114
+ }
115
+
116
+ // Extract and resolve attribute names first
117
+ const attrNames = extractAttributeNames(config.withAttrs);
118
+
119
+ if (attrNames.length === 0) {
120
+ return '';
121
+ }
122
+
123
+ // Parse and normalize selectors
124
+ const selectorList = selectors
125
+ ? selectors.split(',').map(s => s.trim()).filter(s => s.length > 0)
126
+ : [];
127
+
128
+ // Build queries
129
+ const queries: string[] = [];
130
+
131
+ if (selectorList.length === 0) {
132
+ // No selectors provided - return just attribute selectors
133
+ for (const attrName of attrNames) {
134
+ queries.push(`[${attrName}]`);
135
+ queries.push(`[enh-${attrName}]`);
136
+ }
137
+ } else {
138
+ // Build cross-product of selectors × attributes × prefixes
139
+ for (const selector of selectorList) {
140
+ for (const attrName of attrNames) {
141
+ // Unprefixed version
142
+ queries.push(`${selector}[${attrName}]`);
143
+ // enh- prefixed version
144
+ queries.push(`${selector}[enh-${attrName}]`);
145
+ }
146
+ }
147
+ }
148
+
149
+ // Deduplicate and join
150
+ const uniqueQueries = [...new Set(queries)];
151
+ return uniqueQueries.join(', ');
152
+ }
package/index.js CHANGED
@@ -4,4 +4,5 @@ export { BaseRegistry } from './assignGingerly.js';
4
4
  export { waitForEvent } from './waitForEvent.js';
5
5
  export { ParserRegistry, globalParserRegistry } from './parserRegistry.js';
6
6
  export { parseWithAttrs } from './parseWithAttrs.js';
7
+ export { buildCSSQuery } from './buildCSSQuery.js';
7
8
  import './object-extension.js';
package/index.ts CHANGED
@@ -4,4 +4,5 @@ export {BaseRegistry} from './assignGingerly.js';
4
4
  export {waitForEvent} from './waitForEvent.js';
5
5
  export {ParserRegistry, globalParserRegistry} from './parserRegistry.js';
6
6
  export {parseWithAttrs} from './parseWithAttrs.js';
7
+ export {buildCSSQuery} from './buildCSSQuery.js';
7
8
  import './object-extension.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -43,6 +43,10 @@
43
43
  "./parseWithAttrs.js": {
44
44
  "default": "./parseWithAttrs.js",
45
45
  "types": "./parseWithAttrs.ts"
46
+ },
47
+ "./buildCSSQuery.js": {
48
+ "default": "./buildCSSQuery.js",
49
+ "types": "./buildCSSQuery.ts"
46
50
  }
47
51
  },
48
52
  "main": "index.js",