PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
← All changes | src/Agent/lib/subtree-manifest.js +72 -18 3.1.4 → 3.2.2 View file →
@@ -1,9 +1,8 @@
1 +import { BLOCK_ID_SEL, blockIdOf } from './block-el';
1 2 import { detectBlockType } from './block-type';
2 3 import { readComputedStyles } from './computed-styles';
3 4
4 -const BLOCK_ID_ATTR = 'data-extendify-agent-block-id';
5 -
6 5 // Signal-less but edit targets ("swap the columns", "round the image") — dropped, they're unaddressable.
7 6 const ALWAYS_KEPT_TYPES = new Set([
8 7 'core/column',
9 8 'core/columns',
@@ -31,8 +30,18 @@
31 30 delete styles.backgroundColor;
32 31 return styles && Object.keys(styles).length ? styles : null;
33 32 };
34 33
34 +// "A bit smaller" needs the picture's rendered width; the wrapper's is the row's.
35 +const PICTURE_TYPES = new Set(['core/image', 'core/site-logo']);
36 +
37 +const pictureWidth = (el) => {
38 + const img = el.matches?.('img') ? el : el.querySelector?.('img');
39 + if (!img) return null;
40 + const width = img.ownerDocument.defaultView.getComputedStyle(img).width;
41 + return width && width !== 'auto' && width !== '0px' ? width : null;
42 +};
43 +
35 44 // style.css lives in a scoped `wp-custom-css-*` rule, invisible in the block code.
36 45 const customCss = (el) => {
37 46 const cls = [...(el.classList ?? [])].find((c) =>
38 47 c.startsWith('wp-custom-css-'),
@@ -51,18 +60,30 @@
51 60 }
52 61 return null;
53 62 };
54 63
64 +const PART_SLUG_ATTR = 'data-extendify-part-slug';
65 +
66 +// One request carries one partSlug, so a nested part's blocks would resolve
67 +// against the wrong post.
68 +const partScope = (root) => {
69 + const rootSlug = root?.getAttribute?.(PART_SLUG_ATTR) ?? null;
70 + return (el) => (el?.getAttribute?.(PART_SLUG_ATTR) ?? null) === rootSlug;
71 +};
72 +
55 73 // Stops at nested tagged blocks so a container's text never bleeds from its children.
56 74 const ownSubtree = (el) => {
57 75 const clone = el.cloneNode(true);
58 - for (const nested of clone.querySelectorAll(`[${BLOCK_ID_ATTR}]`))
59 - nested.remove();
76 + for (const nested of clone.querySelectorAll(BLOCK_ID_SEL)) nested.remove();
60 77 return clone;
61 78 };
62 79
63 80 const ownText = (clone) => {
64 - const text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
81 + const readable = clone.cloneNode(true);
82 + for (const hidden of readable.querySelectorAll('[aria-hidden="true"]')) {
83 + hidden.remove();
84 + }
85 + const text = (readable.textContent || '').replace(/\s+/g, ' ').trim();
65 86 return text.length > 80 ? `${text.slice(0, 80)}…` : text;
66 87 };
67 88
68 89 const colorSlugs = (clone) => {
@@ -80,17 +101,45 @@
80 101 }
81 102 return out;
82 103 };
83 104
105 +// Menu and icon colours live on the container, never the link that was clicked.
106 +// detectBlockType refuses these types so a click can't select the wrapper.
107 +const STYLE_OWNERS = [
108 + { selector: '.wp-block-navigation', type: 'core/navigation' },
109 + { selector: '.wp-block-social-links', type: 'core/social-links' },
110 +];
111 +
112 +export const ownerAncestorFor = (root) => {
113 + if (!root?.parentElement) return null;
114 + const inScope = partScope(root);
115 + for (const { selector, type } of STYLE_OWNERS) {
116 + const el = root.parentElement.closest(selector);
117 + if (!el || !inScope(el)) continue;
118 + const blockId = blockIdOf(el);
119 + if (!blockId) continue;
120 + const styles = renderedStyles(el);
121 + return {
122 + blockId,
123 + type,
124 + ...colorSlugs(ownSubtree(el)),
125 + ...(styles && { styles }),
126 + };
127 + }
128 + return null;
129 +};
130 +
84 131 // Per-block summary of the selection — enough context for the agent to pick
85 132 // targets ("the red button"); schemas + full code are fetched after the pick.
86 133 export const buildSubtreeManifest = (root) => {
87 134 if (!root) return [];
88 135 const manifest = [];
136 + const inScope = partScope(root);
89 137 // A directly-selected block is its own (and only) target, so the root is included.
90 - const els = [root, ...root.querySelectorAll(`[${BLOCK_ID_ATTR}]`)];
138 + const els = [root, ...root.querySelectorAll(BLOCK_ID_SEL)].filter(inScope);
91 139 for (const [index, el] of els.entries()) {
92 - if (!el.getAttribute?.(BLOCK_ID_ATTR)) continue;
140 + const blockId = blockIdOf(el);
141 + if (!blockId) continue;
93 142 const type = detectBlockType(el);
94 143 if (!type) continue;
95 144 const clone = ownSubtree(el);
96 145 const text = ownText(clone);
@@ -103,12 +152,15 @@
103 152 !colors.backgroundColor &&
104 153 !colors.textColor
105 154 )
106 155 continue;
107 - const styles = renderedStyles(el);
156 + const width = PICTURE_TYPES.has(type) ? pictureWidth(el) : null;
157 + const styles = width
158 + ? { ...renderedStyles(el), width }
159 + : renderedStyles(el);
108 160 const css = customCss(el);
109 161 manifest.push({
110 - blockId: el.getAttribute(BLOCK_ID_ATTR),
162 + blockId,
111 163 type,
112 164 ...(text && { text }),
113 165 ...colors,
114 166 ...(styles && { styles }),
@@ -118,26 +170,28 @@
118 170 return manifest;
119 171 };
120 172
121 173 // Tagged blocks directly beneath el, past any untagged wrappers.
122 -const childBlockEls = (el) => {
174 +const childBlockEls = (el, inScope) => {
123 175 const out = [];
124 176 for (const child of el.children ?? []) {
125 - if (child.getAttribute?.(BLOCK_ID_ATTR)) out.push(child);
126 - else out.push(...childBlockEls(child));
177 + if (!inScope(child)) continue;
178 + if (blockIdOf(child)) out.push(child);
179 + else out.push(...childBlockEls(child, inScope));
127 180 }
128 181 return out;
129 182 };
130 183
131 184 // Keeps every tagged block; the flat manifest drops signal-less ones.
132 -const treeNodesFor = (el) =>
133 - childBlockEls(el).flatMap((child) => {
185 +const treeNodesFor = (el, inScope) =>
186 + childBlockEls(el, inScope).flatMap((child) => {
134 187 const type = detectBlockType(child);
135 - const children = treeNodesFor(child);
136 - if (!type || !child.getAttribute?.(BLOCK_ID_ATTR)) return children;
188 + const children = treeNodesFor(child, inScope);
189 + const blockId = blockIdOf(child);
190 + if (!type || !blockId) return children;
137 191 return [
138 192 {
139 - blockId: child.getAttribute(BLOCK_ID_ATTR),
193 + blockId,
140 194 type,
141 195 ...(children.length && { children }),
142 196 },
143 197 ];
@@ -144,5 +198,5 @@
144 198 });
145 199
146 200 // Nested block structure for the selection.
147 201 export const buildSubtreeTree = (root) =>
148 - root ? treeNodesFor({ children: [root] }) : [];
202 + root ? treeNodesFor({ children: [root] }, partScope(root)) : [];