assign-gingerly 0.0.15 → 0.0.16

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,187 @@ 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
+ ### How It Works
2210
+
2211
+ `buildCSSQuery` creates a cross-product of:
2212
+ 1. **Selectors**: The CSS selectors you provide (e.g., `'div, span'`)
2213
+ 2. **Attributes**: All attribute names from `withAttrs` (resolving template variables)
2214
+ 3. **Prefixes**: Both unprefixed and `enh-` prefixed versions
2215
+
2216
+ This ensures you find all elements that might be enhanced, regardless of whether they use the `enh-` prefix or not.
2217
+
2218
+ ### Template Variable Resolution
2219
+
2220
+ Template variables in `withAttrs` are automatically resolved:
2221
+
2222
+ ```TypeScript
2223
+ const config = {
2224
+ spawn: BeABeacon,
2225
+ withAttrs: {
2226
+ base: 'be-a-beacon',
2227
+ theme: '${base}-theme',
2228
+ size: '${base}-size'
2229
+ }
2230
+ };
2231
+
2232
+ buildCSSQuery(config, 'template, script');
2233
+ // Returns selectors for: be-a-beacon, be-a-beacon-theme, be-a-beacon-size
2234
+ // Each with both prefixed and unprefixed versions
2235
+ ```
2236
+
2237
+ ### Complex Selectors
2238
+
2239
+ The function supports any valid CSS selector:
2240
+
2241
+ ```TypeScript
2242
+ const config = {
2243
+ spawn: MyEnhancement,
2244
+ withAttrs: {
2245
+ base: 'data-enhanced'
2246
+ }
2247
+ };
2248
+
2249
+ // Classes and IDs
2250
+ buildCSSQuery(config, 'div.highlight, span#special');
2251
+ // 'div.highlight[data-enhanced], span#special[data-enhanced], ...'
2252
+
2253
+ // Combinators
2254
+ buildCSSQuery(config, 'div > span, ul li');
2255
+ // 'div > span[data-enhanced], ul li[data-enhanced], ...'
2256
+
2257
+ // Pseudo-classes
2258
+ buildCSSQuery(config, 'div:hover, span:first-child');
2259
+ // 'div:hover[data-enhanced], span:first-child[data-enhanced], ...'
2260
+
2261
+ // Attribute selectors
2262
+ buildCSSQuery(config, 'div[existing-attr]');
2263
+ // 'div[existing-attr][data-enhanced], ...'
2264
+ ```
2265
+
2266
+ ### Underscore-Prefixed Keys Excluded
2267
+
2268
+ Configuration keys starting with `_` are excluded from the query:
2269
+
2270
+ ```TypeScript
2271
+ const config = {
2272
+ spawn: MyEnhancement,
2273
+ withAttrs: {
2274
+ base: 'my-attr',
2275
+ _base: {
2276
+ mapsTo: 'something' // Config only, not an attribute
2277
+ },
2278
+ theme: '${base}-theme',
2279
+ _theme: {
2280
+ instanceOf: 'String' // Config only
2281
+ }
2282
+ }
2283
+ };
2284
+
2285
+ buildCSSQuery(config, 'div');
2286
+ // Only includes: my-attr and my-attr-theme
2287
+ // Does NOT include: _base or _theme
2288
+ ```
2289
+
2290
+ ### Edge Cases
2291
+
2292
+ **Empty inputs return empty string:**
2293
+ ```TypeScript
2294
+ buildCSSQuery({ spawn: MyClass }, 'div'); // '' (no withAttrs)
2295
+ buildCSSQuery({ spawn: MyClass, withAttrs: {} }, 'div'); // '' (empty withAttrs)
2296
+ buildCSSQuery({ spawn: MyClass, withAttrs: { base: 'x' } }, ''); // '' (empty selectors)
2297
+ ```
2298
+
2299
+ **Deduplication:**
2300
+ ```TypeScript
2301
+ buildCSSQuery(config, 'div, div, div');
2302
+ // Duplicates are removed automatically
2303
+ ```
2304
+
2305
+ **Whitespace handling:**
2306
+ ```TypeScript
2307
+ buildCSSQuery(config, ' div , span , p ');
2308
+ // Whitespace is trimmed automatically
2309
+ ```
2310
+
2311
+ ### Use Cases
2312
+
2313
+ 1. **Mount Observer Integration**: Find elements that need enhancement
2314
+ ```TypeScript
2315
+ const query = buildCSSQuery(enhancementConfig, '*');
2316
+ const observer = new MutationObserver(() => {
2317
+ const elements = document.querySelectorAll(query);
2318
+ elements.forEach(el => enhance(el));
2319
+ });
2320
+ ```
2321
+
2322
+ 2. **Batch Enhancement**: Enhance all matching elements at once
2323
+ ```TypeScript
2324
+ const query = buildCSSQuery(config, 'template, script');
2325
+ document.querySelectorAll(query).forEach(el => {
2326
+ const instance = el.enh.get(config);
2327
+ });
2328
+ ```
2329
+
2330
+ 3. **Conditional Enhancement**: Find elements in specific contexts
2331
+ ```TypeScript
2332
+ const query = buildCSSQuery(config, '.container > div');
2333
+ const elements = document.querySelectorAll(query);
2334
+ ```
2335
+
2336
+ ### API Reference
2337
+
2338
+ ```TypeScript
2339
+ function buildCSSQuery(
2340
+ config: EnhancementConfig,
2341
+ selectors: string
2342
+ ): string
2343
+ ```
2344
+
2345
+ **Parameters:**
2346
+ - `config`: Enhancement configuration with `withAttrs` property
2347
+ - `selectors`: Comma-separated CSS selectors (e.g., `'div, span'`)
2348
+
2349
+ **Returns:**
2350
+ - CSS query string with cross-product of selectors and attributes
2351
+ - Empty string if `withAttrs` is missing or empty
2352
+
2353
+ **Throws:**
2354
+ - Error if template variables have circular references
2355
+ - Error if template variables reference undefined keys
2356
+
2357
+ ### Performance Notes
2358
+
2359
+ - The function is synchronous and fast
2360
+ - Resulting queries can be long with many attributes, but CSS engines handle this efficiently
2361
+ - Queries are deduplicated automatically
2362
+ - Consider caching the result if calling repeatedly with the same config
2363
+
2183
2364
  <!--
2184
2365
 
2185
2366
  ### Complete Example
@@ -0,0 +1,114 @@
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
+ * @returns CSS query string with cross-product of selectors and attributes
69
+ *
70
+ * @example
71
+ * const config = {
72
+ * spawn: MyClass,
73
+ * withAttrs: {
74
+ * base: 'my-attr',
75
+ * theme: '${base}-theme'
76
+ * }
77
+ * };
78
+ *
79
+ * buildCSSQuery(config, 'div, span');
80
+ * // Returns: 'div[my-attr], span[my-attr], div[enh-my-attr], span[enh-my-attr],
81
+ * // div[my-attr-theme], span[my-attr-theme], div[enh-my-attr-theme], span[enh-my-attr-theme]'
82
+ */
83
+ export function buildCSSQuery(config, selectors) {
84
+ // Validate inputs
85
+ if (!config.withAttrs || !selectors) {
86
+ return '';
87
+ }
88
+ // Parse and normalize selectors
89
+ const selectorList = selectors
90
+ .split(',')
91
+ .map(s => s.trim())
92
+ .filter(s => s.length > 0);
93
+ if (selectorList.length === 0) {
94
+ return '';
95
+ }
96
+ // Extract and resolve attribute names
97
+ const attrNames = extractAttributeNames(config.withAttrs);
98
+ if (attrNames.length === 0) {
99
+ return '';
100
+ }
101
+ // Build cross-product of selectors × attributes × prefixes
102
+ const queries = [];
103
+ for (const selector of selectorList) {
104
+ for (const attrName of attrNames) {
105
+ // Unprefixed version
106
+ queries.push(`${selector}[${attrName}]`);
107
+ // enh- prefixed version
108
+ queries.push(`${selector}[enh-${attrName}]`);
109
+ }
110
+ }
111
+ // Deduplicate and join
112
+ const uniqueQueries = [...new Set(queries)];
113
+ return uniqueQueries.join(', ');
114
+ }
@@ -0,0 +1,142 @@
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
+ * @returns CSS query string with cross-product of selectors and attributes
87
+ *
88
+ * @example
89
+ * const config = {
90
+ * spawn: MyClass,
91
+ * withAttrs: {
92
+ * base: 'my-attr',
93
+ * theme: '${base}-theme'
94
+ * }
95
+ * };
96
+ *
97
+ * buildCSSQuery(config, 'div, span');
98
+ * // Returns: 'div[my-attr], span[my-attr], div[enh-my-attr], span[enh-my-attr],
99
+ * // div[my-attr-theme], span[my-attr-theme], div[enh-my-attr-theme], span[enh-my-attr-theme]'
100
+ */
101
+ export function buildCSSQuery(
102
+ config: EnhancementConfig,
103
+ selectors: string
104
+ ): string {
105
+ // Validate inputs
106
+ if (!config.withAttrs || !selectors) {
107
+ return '';
108
+ }
109
+
110
+ // Parse and normalize selectors
111
+ const selectorList = selectors
112
+ .split(',')
113
+ .map(s => s.trim())
114
+ .filter(s => s.length > 0);
115
+
116
+ if (selectorList.length === 0) {
117
+ return '';
118
+ }
119
+
120
+ // Extract and resolve attribute names
121
+ const attrNames = extractAttributeNames(config.withAttrs);
122
+
123
+ if (attrNames.length === 0) {
124
+ return '';
125
+ }
126
+
127
+ // Build cross-product of selectors × attributes × prefixes
128
+ const queries: string[] = [];
129
+
130
+ for (const selector of selectorList) {
131
+ for (const attrName of attrNames) {
132
+ // Unprefixed version
133
+ queries.push(`${selector}[${attrName}]`);
134
+ // enh- prefixed version
135
+ queries.push(`${selector}[enh-${attrName}]`);
136
+ }
137
+ }
138
+
139
+ // Deduplicate and join
140
+ const uniqueQueries = [...new Set(queries)];
141
+ return uniqueQueries.join(', ');
142
+ }
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.16",
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",