| 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 |
|