mount-observer 0.1.4 → 0.1.5

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/attrChanges.ts DELETED
@@ -1,90 +0,0 @@
1
- import type { AttrChange, MountInit } from './types.d.ts';
2
-
3
- /**
4
- * Checks for attribute changes on a mounted element.
5
- * This module is dynamically loaded only when whereAttr is configured.
6
- */
7
- export function checkAttrChanges(
8
- element: Element,
9
- mountInit: MountInit,
10
- buildAttrCoordinateMapFn: (whereAttr: any, isCustomElement: boolean) => any,
11
- elementAttrStates: WeakMap<Element, Map<string, string | null>>,
12
- elementOnceAttrs: WeakMap<Element, Set<string>>
13
- ): AttrChange[] {
14
- if (!mountInit.whereAttr || !buildAttrCoordinateMapFn) {
15
- return [];
16
- }
17
-
18
- const isCustomElement = element.tagName.toLowerCase().includes('-');
19
- const attrCoordMap = buildAttrCoordinateMapFn(mountInit.whereAttr, isCustomElement);
20
-
21
- // Get or create the attribute state for this element
22
- let attrState = elementAttrStates.get(element);
23
- if (!attrState) {
24
- attrState = new Map<string, string | null>();
25
- elementAttrStates.set(element, attrState);
26
- }
27
-
28
- const changes: AttrChange[] = [];
29
- const currentAttrs = new Set<string>();
30
-
31
- // Check all possible attributes from the coordinate map
32
- for (const attrName of Object.keys(attrCoordMap)) {
33
- const coordinate = attrCoordMap[attrName];
34
- const currentValue = element.getAttribute(attrName);
35
- const previousValue = attrState.get(attrName);
36
-
37
- if (currentValue !== null) {
38
- currentAttrs.add(attrName);
39
- }
40
-
41
- // Check if this attribute has "once: true" in its map entry
42
- const mapEntry = mountInit.map?.[coordinate] || null;
43
- const isOnce = mapEntry?.once === true;
44
-
45
- // If "once" is true, check if we've already seen this attribute
46
- if (isOnce) {
47
- let onceAttrs = elementOnceAttrs.get(element);
48
- if (!onceAttrs) {
49
- onceAttrs = new Set<string>();
50
- elementOnceAttrs.set(element, onceAttrs);
51
- }
52
-
53
- // If we've already seen this attribute, skip it
54
- if (onceAttrs.has(attrName)) {
55
- continue;
56
- }
57
-
58
- // Mark this attribute as seen if it currently has a value
59
- if (currentValue !== null) {
60
- onceAttrs.add(attrName);
61
- }
62
- }
63
-
64
- // Include if: currently has value OR previously had value but now removed
65
- if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
66
- // Check if value changed
67
- if (currentValue !== previousValue) {
68
- const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
69
-
70
- changes.push({
71
- value: currentValue,
72
- attrNode,
73
- mapEntry,
74
- attrName,
75
- coordinate,
76
- element
77
- });
78
-
79
- // Update state
80
- if (currentValue !== null) {
81
- attrState.set(attrName, currentValue);
82
- } else {
83
- attrState.delete(attrName);
84
- }
85
- }
86
- }
87
- }
88
-
89
- return changes;
90
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Builds a map of attribute names to their coordinates based on whereAttr config
3
- */
4
- export function buildAttrCoordinateMap(whereAttr, isCustomElement) {
5
- const map = {};
6
- const rootPrefixes = isCustomElement
7
- ? (whereAttr.hasCERootIn || [])
8
- : (whereAttr.hasBuiltInRootIn || []);
9
- // Parse base attribute for custom delimiter
10
- const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
11
- // Build attribute names for each prefix
12
- for (const prefix of rootPrefixes) {
13
- const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
14
- // If no branches specified, just the base attribute
15
- if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
16
- map[baseAttrName] = '0';
17
- continue;
18
- }
19
- // Process each branch
20
- for (let i = 0; i < whereAttr.hasBranchIn.length; i++) {
21
- const branch = whereAttr.hasBranchIn[i];
22
- if (branch === '') {
23
- // Empty string means base attribute alone is valid
24
- map[baseAttrName] = '0';
25
- continue;
26
- }
27
- if (typeof branch === 'object') {
28
- // Process branch object
29
- processBranch(branch, baseAttrName, String(i), map);
30
- }
31
- }
32
- }
33
- return map;
34
- }
35
- /**
36
- * Recursively processes a branch object to build attribute-coordinate mappings
37
- */
38
- function processBranch(branch, parentAttrName, parentCoordinate, map) {
39
- for (const [key, subBranches] of Object.entries(branch)) {
40
- const { delimiter, name } = parseDelimiter(key);
41
- const attrName = parentAttrName + delimiter + name;
42
- // Process sub-branches
43
- if (!subBranches || subBranches.length === 0) {
44
- map[attrName] = parentCoordinate;
45
- continue;
46
- }
47
- for (let i = 0; i < subBranches.length; i++) {
48
- const subBranch = subBranches[i];
49
- const coordinate = `${parentCoordinate}.${i}`;
50
- if (subBranch === '') {
51
- // Empty string means this level alone is valid
52
- map[attrName] = parentCoordinate;
53
- continue;
54
- }
55
- if (typeof subBranch === 'string') {
56
- // Simple string sub-branch
57
- const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
58
- const subAttrName = attrName + subDelimiter + subName;
59
- map[subAttrName] = coordinate;
60
- continue;
61
- }
62
- if (typeof subBranch === 'object') {
63
- // Nested object - recursively process
64
- processBranch(subBranch, attrName, coordinate, map);
65
- }
66
- }
67
- }
68
- }
69
- /**
70
- * Parses a key to extract custom delimiter and name
71
- */
72
- function parseDelimiter(key) {
73
- const match = key.match(/^\[(.+?)\](.+)$/);
74
- if (match) {
75
- return {
76
- delimiter: match[1],
77
- name: match[2]
78
- };
79
- }
80
- return {
81
- delimiter: '-',
82
- name: key
83
- };
84
- }
85
- /**
86
- * Builds the full attribute name from prefix, base name, and delimiter
87
- */
88
- function buildAttributeName(prefix, baseName, delimiter) {
89
- if (prefix === '') {
90
- return baseName;
91
- }
92
- return prefix + delimiter + baseName;
93
- }
@@ -1,122 +0,0 @@
1
- import { WhereAttr, BranchValue } from './types.js';
2
-
3
- /**
4
- * Represents a mapping from attribute name to coordinate
5
- */
6
- export interface AttrCoordinateMap {
7
- [attrName: string]: string;
8
- }
9
-
10
- /**
11
- * Builds a map of attribute names to their coordinates based on whereAttr config
12
- */
13
- export function buildAttrCoordinateMap(whereAttr: WhereAttr, isCustomElement: boolean): AttrCoordinateMap {
14
- const map: AttrCoordinateMap = {};
15
- const rootPrefixes = isCustomElement
16
- ? (whereAttr.hasCERootIn || [])
17
- : (whereAttr.hasBuiltInRootIn || []);
18
-
19
- // Parse base attribute for custom delimiter
20
- const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
21
-
22
- // Build attribute names for each prefix
23
- for (const prefix of rootPrefixes) {
24
- const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
25
-
26
- // If no branches specified, just the base attribute
27
- if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
28
- map[baseAttrName] = '0';
29
- continue;
30
- }
31
-
32
- // Process each branch
33
- for (let i = 0; i < whereAttr.hasBranchIn.length; i++) {
34
- const branch = whereAttr.hasBranchIn[i];
35
-
36
- if (branch === '') {
37
- // Empty string means base attribute alone is valid
38
- map[baseAttrName] = '0';
39
- continue;
40
- }
41
-
42
- if (typeof branch === 'object') {
43
- // Process branch object
44
- processBranch(branch, baseAttrName, String(i), map);
45
- }
46
- }
47
- }
48
-
49
- return map;
50
- }
51
-
52
- /**
53
- * Recursively processes a branch object to build attribute-coordinate mappings
54
- */
55
- function processBranch(
56
- branch: { [key: string]: BranchValue[] },
57
- parentAttrName: string,
58
- parentCoordinate: string,
59
- map: AttrCoordinateMap
60
- ): void {
61
- for (const [key, subBranches] of Object.entries(branch)) {
62
- const { delimiter, name } = parseDelimiter(key);
63
- const attrName = parentAttrName + delimiter + name;
64
-
65
- // Process sub-branches
66
- if (!subBranches || subBranches.length === 0) {
67
- map[attrName] = parentCoordinate;
68
- continue;
69
- }
70
-
71
- for (let i = 0; i < subBranches.length; i++) {
72
- const subBranch = subBranches[i];
73
- const coordinate = `${parentCoordinate}.${i}`;
74
-
75
- if (subBranch === '') {
76
- // Empty string means this level alone is valid
77
- map[attrName] = parentCoordinate;
78
- continue;
79
- }
80
-
81
- if (typeof subBranch === 'string') {
82
- // Simple string sub-branch
83
- const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
84
- const subAttrName = attrName + subDelimiter + subName;
85
- map[subAttrName] = coordinate;
86
- continue;
87
- }
88
-
89
- if (typeof subBranch === 'object') {
90
- // Nested object - recursively process
91
- processBranch(subBranch, attrName, coordinate, map);
92
- }
93
- }
94
- }
95
- }
96
-
97
- /**
98
- * Parses a key to extract custom delimiter and name
99
- */
100
- function parseDelimiter(key: string): { delimiter: string; name: string } {
101
- const match = key.match(/^\[(.+?)\](.+)$/);
102
- if (match) {
103
- return {
104
- delimiter: match[1],
105
- name: match[2]
106
- };
107
- }
108
- return {
109
- delimiter: '-',
110
- name: key
111
- };
112
- }
113
-
114
- /**
115
- * Builds the full attribute name from prefix, base name, and delimiter
116
- */
117
- function buildAttributeName(prefix: string, baseName: string, delimiter: string): string {
118
- if (prefix === '') {
119
- return baseName;
120
- }
121
- return prefix + delimiter + baseName;
122
- }
package/whereAttr.js DELETED
@@ -1,174 +0,0 @@
1
- const selectorCache = new WeakMap();
2
- /**
3
- * Checks if an element matches the whereAttr configuration using CSS selector matching
4
- */
5
- export function matchesWhereAttr(element, whereAttr) {
6
- // Get or build the CSS selectors for this whereAttr config
7
- let selectors = selectorCache.get(whereAttr);
8
- if (!selectors) {
9
- selectors = {
10
- builtIn: buildWhereAttrSelector(false, whereAttr),
11
- custom: buildWhereAttrSelector(true, whereAttr)
12
- };
13
- selectorCache.set(whereAttr, selectors);
14
- }
15
- // Determine which selector to use based on element type
16
- const isCustomElement = element.tagName.toLowerCase().includes('-');
17
- const selector = isCustomElement ? selectors.custom : selectors.builtIn;
18
- // Use native CSS matching - optimized in Chrome/Blink
19
- return element.matches(selector);
20
- }
21
- /**
22
- * Builds a CSS selector string from the whereAttr configuration
23
- * @param isCustomElement - Whether to build selector for custom elements (true) or built-in elements (false)
24
- */
25
- function buildWhereAttrSelector(isCustomElement, whereAttr) {
26
- const rootPrefixes = isCustomElement
27
- ? (whereAttr.hasCERootIn || [])
28
- : (whereAttr.hasBuiltInRootIn || []);
29
- // Parse base attribute for custom delimiter
30
- const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
31
- const selectors = [];
32
- // Build selectors for each valid prefix
33
- for (const prefix of rootPrefixes) {
34
- const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
35
- const escapedBaseAttr = escapeAttributeName(baseAttrName);
36
- // If no branches specified, just having the base attribute is enough
37
- if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
38
- selectors.push(`[${escapedBaseAttr}]`);
39
- continue;
40
- }
41
- // Build selectors for each branch combination
42
- for (const branch of whereAttr.hasBranchIn) {
43
- if (branch === '') {
44
- // Empty string means base attribute alone is valid (no branch attributes)
45
- // This requires base attr AND none of the branch attrs
46
- // For CSS, we can only check for base attr presence
47
- // The "no branch attrs" check needs to be done separately
48
- selectors.push(`[${escapedBaseAttr}]`);
49
- continue;
50
- }
51
- if (typeof branch === 'object') {
52
- // Build selectors for each branch path
53
- for (const [key, subBranches] of Object.entries(branch)) {
54
- const branchSelectors = buildBranchSelectors(baseAttrName, key, subBranches);
55
- selectors.push(...branchSelectors);
56
- }
57
- }
58
- }
59
- }
60
- // Join all selectors with comma (OR logic)
61
- return selectors.join(',');
62
- }
63
- /**
64
- * Builds CSS selectors for a branch and its sub-branches
65
- */
66
- function buildBranchSelectors(baseAttrName, branchKey, subBranches) {
67
- const { delimiter: branchDelimiter, name: branchName } = parseDelimiter(branchKey);
68
- const branchAttrName = baseAttrName + branchDelimiter + branchName;
69
- const escapedBranchAttr = escapeAttributeName(branchAttrName);
70
- const selectors = [];
71
- // If no sub-branches specified, just the branch attribute itself
72
- if (!subBranches || subBranches.length === 0) {
73
- selectors.push(`[${escapedBranchAttr}]`);
74
- return selectors;
75
- }
76
- // Build selectors for each sub-branch - they form an OR condition
77
- for (const subBranch of subBranches) {
78
- if (subBranch === '') {
79
- // Empty string means this branch level alone is valid
80
- selectors.push(`[${escapedBranchAttr}]`);
81
- continue;
82
- }
83
- if (typeof subBranch === 'string') {
84
- // Simple string sub-branch - build the full path
85
- const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
86
- const subAttrName = branchAttrName + subDelimiter + subName;
87
- const escapedSubAttr = escapeAttributeName(subAttrName);
88
- selectors.push(`[${escapedSubAttr}]`);
89
- continue;
90
- }
91
- if (typeof subBranch === 'object') {
92
- // Nested object sub-branch - recursively build deeper paths
93
- for (const [key, nestedBranches] of Object.entries(subBranch)) {
94
- const nestedSelectors = buildNestedBranchSelectors(branchAttrName, key, nestedBranches);
95
- selectors.push(...nestedSelectors);
96
- }
97
- }
98
- }
99
- return selectors;
100
- }
101
- /**
102
- * Builds CSS selectors for nested branches recursively
103
- */
104
- function buildNestedBranchSelectors(parentAttrName, branchKey, subBranches) {
105
- const { delimiter, name } = parseDelimiter(branchKey);
106
- const attrName = parentAttrName + delimiter + name;
107
- const escapedAttr = escapeAttributeName(attrName);
108
- const selectors = [];
109
- // If no sub-branches specified, just this attribute
110
- if (!subBranches || subBranches.length === 0) {
111
- selectors.push(`[${escapedAttr}]`);
112
- return selectors;
113
- }
114
- // Build selectors for each sub-branch - they form an OR condition
115
- for (const subBranch of subBranches) {
116
- if (subBranch === '') {
117
- // Empty string means this level alone is valid
118
- selectors.push(`[${escapedAttr}]`);
119
- continue;
120
- }
121
- if (typeof subBranch === 'string') {
122
- // Simple string sub-branch - build the full path
123
- const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
124
- const subAttrName = attrName + subDelimiter + subName;
125
- const escapedSubAttr = escapeAttributeName(subAttrName);
126
- selectors.push(`[${escapedSubAttr}]`);
127
- continue;
128
- }
129
- if (typeof subBranch === 'object') {
130
- // Nested object - recursively build deeper paths
131
- for (const [key, nestedBranches] of Object.entries(subBranch)) {
132
- const nestedSelectors = buildNestedBranchSelectors(attrName, key, nestedBranches);
133
- selectors.push(...nestedSelectors);
134
- }
135
- }
136
- }
137
- return selectors;
138
- }
139
- /**
140
- * Escapes special characters in attribute names for CSS selectors
141
- * Uses CSS.escape() API which handles all special characters including :
142
- */
143
- function escapeAttributeName(attrName) {
144
- // CSS.escape() is available in all modern browsers
145
- // It properly escapes special characters like : . [ ] etc.
146
- return CSS.escape(attrName);
147
- }
148
- /**
149
- * Parses a key to extract custom delimiter and name
150
- * Format: [delimiter]name
151
- * Example: "[_]my-custom" returns { delimiter: "_", name: "my-custom" }
152
- */
153
- function parseDelimiter(key) {
154
- const match = key.match(/^\[(.+?)\](.+)$/);
155
- if (match) {
156
- return {
157
- delimiter: match[1],
158
- name: match[2]
159
- };
160
- }
161
- return {
162
- delimiter: '-',
163
- name: key
164
- };
165
- }
166
- /**
167
- * Builds the full attribute name from prefix, base name, and delimiter
168
- */
169
- function buildAttributeName(prefix, baseName, delimiter) {
170
- if (prefix === '') {
171
- return baseName;
172
- }
173
- return prefix + delimiter + baseName;
174
- }
package/whereAttr.ts DELETED
@@ -1,221 +0,0 @@
1
- import { WhereAttr } from './types.js';
2
-
3
- /**
4
- * Cache for compiled CSS selectors to avoid rebuilding them on every check
5
- * Stores both built-in and custom element selectors
6
- */
7
- interface CachedSelectors {
8
- builtIn: string;
9
- custom: string;
10
- }
11
-
12
- const selectorCache = new WeakMap<WhereAttr, CachedSelectors>();
13
-
14
- /**
15
- * Checks if an element matches the whereAttr configuration using CSS selector matching
16
- */
17
- export function matchesWhereAttr(element: Element, whereAttr: WhereAttr): boolean {
18
- // Get or build the CSS selectors for this whereAttr config
19
- let selectors = selectorCache.get(whereAttr);
20
- if (!selectors) {
21
- selectors = {
22
- builtIn: buildWhereAttrSelector(false, whereAttr),
23
- custom: buildWhereAttrSelector(true, whereAttr)
24
- };
25
- selectorCache.set(whereAttr, selectors);
26
- }
27
-
28
- // Determine which selector to use based on element type
29
- const isCustomElement = element.tagName.toLowerCase().includes('-');
30
- const selector = isCustomElement ? selectors.custom : selectors.builtIn;
31
-
32
- // Use native CSS matching - optimized in Chrome/Blink
33
- return element.matches(selector);
34
- }
35
-
36
- /**
37
- * Builds a CSS selector string from the whereAttr configuration
38
- * @param isCustomElement - Whether to build selector for custom elements (true) or built-in elements (false)
39
- */
40
- function buildWhereAttrSelector(isCustomElement: boolean, whereAttr: WhereAttr): string {
41
- const rootPrefixes = isCustomElement
42
- ? (whereAttr.hasCERootIn || [])
43
- : (whereAttr.hasBuiltInRootIn || []);
44
-
45
- // Parse base attribute for custom delimiter
46
- const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
47
-
48
- const selectors: string[] = [];
49
-
50
- // Build selectors for each valid prefix
51
- for (const prefix of rootPrefixes) {
52
- const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
53
- const escapedBaseAttr = escapeAttributeName(baseAttrName);
54
-
55
- // If no branches specified, just having the base attribute is enough
56
- if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
57
- selectors.push(`[${escapedBaseAttr}]`);
58
- continue;
59
- }
60
-
61
- // Build selectors for each branch combination
62
- for (const branch of whereAttr.hasBranchIn) {
63
- if (branch === '') {
64
- // Empty string means base attribute alone is valid (no branch attributes)
65
- // This requires base attr AND none of the branch attrs
66
- // For CSS, we can only check for base attr presence
67
- // The "no branch attrs" check needs to be done separately
68
- selectors.push(`[${escapedBaseAttr}]`);
69
- continue;
70
- }
71
-
72
- if (typeof branch === 'object') {
73
- // Build selectors for each branch path
74
- for (const [key, subBranches] of Object.entries(branch)) {
75
- const branchSelectors = buildBranchSelectors(baseAttrName, key, subBranches);
76
- selectors.push(...branchSelectors);
77
- }
78
- }
79
- }
80
- }
81
-
82
- // Join all selectors with comma (OR logic)
83
- return selectors.join(',');
84
- }
85
-
86
- /**
87
- * Builds CSS selectors for a branch and its sub-branches
88
- */
89
- function buildBranchSelectors(
90
- baseAttrName: string,
91
- branchKey: string,
92
- subBranches: any[]
93
- ): string[] {
94
- const { delimiter: branchDelimiter, name: branchName } = parseDelimiter(branchKey);
95
- const branchAttrName = baseAttrName + branchDelimiter + branchName;
96
- const escapedBranchAttr = escapeAttributeName(branchAttrName);
97
-
98
- const selectors: string[] = [];
99
-
100
- // If no sub-branches specified, just the branch attribute itself
101
- if (!subBranches || subBranches.length === 0) {
102
- selectors.push(`[${escapedBranchAttr}]`);
103
- return selectors;
104
- }
105
-
106
- // Build selectors for each sub-branch - they form an OR condition
107
- for (const subBranch of subBranches) {
108
- if (subBranch === '') {
109
- // Empty string means this branch level alone is valid
110
- selectors.push(`[${escapedBranchAttr}]`);
111
- continue;
112
- }
113
-
114
- if (typeof subBranch === 'string') {
115
- // Simple string sub-branch - build the full path
116
- const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
117
- const subAttrName = branchAttrName + subDelimiter + subName;
118
- const escapedSubAttr = escapeAttributeName(subAttrName);
119
- selectors.push(`[${escapedSubAttr}]`);
120
- continue;
121
- }
122
-
123
- if (typeof subBranch === 'object') {
124
- // Nested object sub-branch - recursively build deeper paths
125
- for (const [key, nestedBranches] of Object.entries(subBranch)) {
126
- const nestedSelectors = buildNestedBranchSelectors(branchAttrName, key, nestedBranches as any[]);
127
- selectors.push(...nestedSelectors);
128
- }
129
- }
130
- }
131
-
132
- return selectors;
133
- }
134
-
135
- /**
136
- * Builds CSS selectors for nested branches recursively
137
- */
138
- function buildNestedBranchSelectors(
139
- parentAttrName: string,
140
- branchKey: string,
141
- subBranches: any[]
142
- ): string[] {
143
- const { delimiter, name } = parseDelimiter(branchKey);
144
- const attrName = parentAttrName + delimiter + name;
145
- const escapedAttr = escapeAttributeName(attrName);
146
-
147
- const selectors: string[] = [];
148
-
149
- // If no sub-branches specified, just this attribute
150
- if (!subBranches || subBranches.length === 0) {
151
- selectors.push(`[${escapedAttr}]`);
152
- return selectors;
153
- }
154
-
155
- // Build selectors for each sub-branch - they form an OR condition
156
- for (const subBranch of subBranches) {
157
- if (subBranch === '') {
158
- // Empty string means this level alone is valid
159
- selectors.push(`[${escapedAttr}]`);
160
- continue;
161
- }
162
-
163
- if (typeof subBranch === 'string') {
164
- // Simple string sub-branch - build the full path
165
- const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
166
- const subAttrName = attrName + subDelimiter + subName;
167
- const escapedSubAttr = escapeAttributeName(subAttrName);
168
- selectors.push(`[${escapedSubAttr}]`);
169
- continue;
170
- }
171
-
172
- if (typeof subBranch === 'object') {
173
- // Nested object - recursively build deeper paths
174
- for (const [key, nestedBranches] of Object.entries(subBranch)) {
175
- const nestedSelectors = buildNestedBranchSelectors(attrName, key, nestedBranches as any[]);
176
- selectors.push(...nestedSelectors);
177
- }
178
- }
179
- }
180
-
181
- return selectors;
182
- }
183
-
184
- /**
185
- * Escapes special characters in attribute names for CSS selectors
186
- * Uses CSS.escape() API which handles all special characters including :
187
- */
188
- function escapeAttributeName(attrName: string): string {
189
- // CSS.escape() is available in all modern browsers
190
- // It properly escapes special characters like : . [ ] etc.
191
- return CSS.escape(attrName);
192
- }
193
-
194
- /**
195
- * Parses a key to extract custom delimiter and name
196
- * Format: [delimiter]name
197
- * Example: "[_]my-custom" returns { delimiter: "_", name: "my-custom" }
198
- */
199
- function parseDelimiter(key: string): { delimiter: string; name: string } {
200
- const match = key.match(/^\[(.+?)\](.+)$/);
201
- if (match) {
202
- return {
203
- delimiter: match[1],
204
- name: match[2]
205
- };
206
- }
207
- return {
208
- delimiter: '-',
209
- name: key
210
- };
211
- }
212
-
213
- /**
214
- * Builds the full attribute name from prefix, base name, and delimiter
215
- */
216
- function buildAttributeName(prefix: string, baseName: string, delimiter: string): string {
217
- if (prefix === '') {
218
- return baseName;
219
- }
220
- return prefix + delimiter + baseName;
221
- }