PluginProbe
Extendify / 3.0.5
Extendify v3.0.5
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / lib / variant-classes.js

variant-classes.js in Extendify 3.0.5, at src/Agent/lib/variant-classes.js

160 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Utility functions for patching variant classes in HTML.
3 * Handles class patterns like "is-style-outline--N" and "is-style-ext-preset--...--N"
4 * that WordPress may reorder when parsing blocks.
5 */
6
7 // escape for regex building
8 export const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
10 export const getVariantNumbersInTree = (rootEl, base) => {
11 if (!rootEl) return [];
12 // Match classes ending with --{number}
13 const re = new RegExp(`^${esc(base)}.*--(\\d+)$`, 'i');
14 const out = [];
15 const seen = new Set();
16 // include root + descendants
17 const all = [rootEl, ...rootEl.querySelectorAll(`[class*="${base}"]`)];
18 for (const el of all) {
19 for (const cls of el.classList) {
20 const m = cls.match(re);
21 if (!m) continue;
22
23 const n = Number(m[1]);
24 if (seen.has(n)) continue;
25
26 seen.add(n);
27 out.push(n);
28 }
29 }
30 return out;
31 };
32
33 /**
34 * Extracts variant class info as { prefix, number } pairs.
35 * prefix = everything before the final --N (used for matching element types)
36 */
37 export const getVariantClassInfo = (rootEl, base) => {
38 if (!rootEl) return [];
39 const re = new RegExp(`^(${esc(base)}.*)--(\\d+)$`, 'i');
40 const out = [];
41 const seenNumbers = new Set();
42 const all = [rootEl, ...rootEl.querySelectorAll(`[class*="${base}"]`)];
43 for (const el of all) {
44 for (const cls of el.classList) {
45 const m = cls.match(re);
46 if (!m) continue;
47 const prefix = m[1];
48 const number = Number(m[2]);
49 // Skip duplicate numbers (same variant on multiple elements)
50 if (seenNumbers.has(number)) continue;
51 seenNumbers.add(number);
52 out.push({ prefix, number });
53 }
54 }
55 return out;
56 };
57
58 export const getVariantClassInfoFromHtml = (html, base) => {
59 const wrapper = document.createElement('div');
60 wrapper.innerHTML = html;
61 return getVariantClassInfo(wrapper, base);
62 };
63
64 export const getVariantNumbersInHtml = (html, base) => {
65 const wrapper = document.createElement('div');
66 wrapper.innerHTML = html;
67 return getVariantNumbersInTree(wrapper, base);
68 };
69
70 export const applyVariantNumberMapToHtml = (html, base, numberMap) => {
71 if (!numberMap || !numberMap.size) return html;
72 const wrapper = document.createElement('div');
73 wrapper.innerHTML = html;
74
75 // Match classes ending with --{number}
76 const reToken = new RegExp(`^${esc(base)}.*--(\\d+)$`, 'i');
77
78 // target every element that *could* contain the class
79 const els = wrapper.querySelectorAll(`[class*="${base}"]`);
80 for (const el of els) {
81 const classes = Array.from(el.classList);
82 let changed = false;
83
84 for (const [i, cls] of classes.entries()) {
85 const m = cls.match(reToken);
86 if (!m) continue;
87
88 const oldN = Number(m[1]);
89 if (!numberMap.has(oldN)) continue;
90
91 const newN = numberMap.get(oldN);
92 // Replace the variant number at the end
93 const nextCls = cls.replace(/--(\d+)$/, `--${newN}`);
94 if (nextCls === cls) continue;
95
96 classes[i] = nextCls;
97 changed = true;
98 }
99
100 // keep the base style class too
101 if (!classes.includes(base)) {
102 classes.push(base);
103 changed = true;
104 }
105
106 if (changed) el.className = classes.join(' ');
107 }
108
109 return wrapper.innerHTML;
110 };
111
112 /**
113 * Patches variant classes in HTML to match the DOM element's numbering.
114 * Handles patterns like "is-style-outline--N" and "is-style-ext-preset--...--N"
115 * Note that that variant numbers are globally unique in each page and are autogenerated by WP.
116 */
117 export const patchVariantClasses = (html, el, bases) => {
118 let patchedHtml = html;
119 for (const base of bases) {
120 // Get variant info with prefixes for matching by element type
121 const targetInfo = getVariantClassInfo(el, base);
122 const currentInfo = getVariantClassInfoFromHtml(patchedHtml, base);
123
124 if (!targetInfo.length || !currentInfo.length) continue;
125
126 // Group by prefix - allows matching by type, with position-based fallback
127 // for multiple elements of the same type
128 const targetByPrefix = new Map();
129 for (const { prefix, number } of targetInfo) {
130 if (!targetByPrefix.has(prefix)) targetByPrefix.set(prefix, []);
131 targetByPrefix.get(prefix).push(number);
132 }
133
134 const currentByPrefix = new Map();
135 for (const { prefix, number } of currentInfo) {
136 if (!currentByPrefix.has(prefix)) currentByPrefix.set(prefix, []);
137 currentByPrefix.get(prefix).push(number);
138 }
139
140 // Map by matching prefixes, then by position within each prefix group
141 const map = new Map();
142 for (const [prefix, currentNums] of currentByPrefix) {
143 const targetNums = targetByPrefix.get(prefix);
144 if (!targetNums) continue;
145
146 const count = Math.min(currentNums.length, targetNums.length);
147 for (const [i, cur] of currentNums.entries()) {
148 if (i >= count) break;
149 if (cur === targetNums[i]) continue;
150 map.set(cur, targetNums[i]);
151 }
152 }
153
154 if (!map.size) continue;
155
156 patchedHtml = applyVariantNumberMapToHtml(patchedHtml, base, map);
157 }
158 return patchedHtml;
159 };
160