PluginProbe
Extendify / 3.1.5
Extendify v3.1.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 / subtree-manifest.js

subtree-manifest.js in Extendify 3.1.5, at src/Agent/lib/subtree-manifest.js

149 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { detectBlockType } from './block-type';
2 import { readComputedStyles } from './computed-styles';
3
4 const BLOCK_ID_ATTR = 'data-extendify-agent-block-id';
5
6 // Signal-less but edit targets ("swap the columns", "round the image") — dropped, they're unaddressable.
7 const ALWAYS_KEPT_TYPES = new Set([
8 'core/column',
9 'core/columns',
10 'core/image',
11 'core/cover',
12 'core/site-logo',
13 ]);
14
15 const isTransparent = (value) =>
16 !value ||
17 value === 'transparent' ||
18 value.replace(/\s/g, '') === 'rgba(0,0,0,0)';
19
20 // core/button renders text + background in an inner link, so read styles off the
21 // link — only for buttons, or a container would report a nested button's styles.
22 const styleSource = (el) =>
23 el.classList?.contains('wp-block-button')
24 ? (el.querySelector('.wp-block-button__link') ?? el)
25 : el;
26
27 // Narrow by appearance — a token like `tertiary` says nothing about being yellow.
28 const renderedStyles = (el) => {
29 const styles = readComputedStyles(styleSource(el));
30 if (styles && isTransparent(styles.backgroundColor))
31 delete styles.backgroundColor;
32 return styles && Object.keys(styles).length ? styles : null;
33 };
34
35 // style.css lives in a scoped `wp-custom-css-*` rule, invisible in the block code.
36 const customCss = (el) => {
37 const cls = [...(el.classList ?? [])].find((c) =>
38 c.startsWith('wp-custom-css-'),
39 );
40 if (!cls) return null;
41 for (const sheet of el.ownerDocument?.styleSheets ?? []) {
42 let rules;
43 try {
44 rules = sheet.cssRules;
45 } catch {
46 continue; // cross-origin sheet — can't read
47 }
48 for (const rule of rules) {
49 if (rule.selectorText?.includes(cls)) return rule.style?.cssText || null;
50 }
51 }
52 return null;
53 };
54
55 // Stops at nested tagged blocks so a container's text never bleeds from its children.
56 const ownSubtree = (el) => {
57 const clone = el.cloneNode(true);
58 for (const nested of clone.querySelectorAll(`[${BLOCK_ID_ATTR}]`))
59 nested.remove();
60 return clone;
61 };
62
63 const ownText = (clone) => {
64 const text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
65 return text.length > 80 ? `${text.slice(0, 80)}` : text;
66 };
67
68 const colorSlugs = (clone) => {
69 const out = {};
70 const nodes = [clone, ...clone.querySelectorAll('*')];
71 for (const node of nodes) {
72 for (const cls of node.classList ?? []) {
73 const bg = cls.match(/^has-(.+)-background-color$/);
74 if (bg) out.backgroundColor = bg[1];
75 else if (cls !== 'has-text-color') {
76 const text = cls.match(/^has-(.+)-color$/);
77 if (text) out.textColor = text[1];
78 }
79 }
80 }
81 return out;
82 };
83
84 // Per-block summary of the selection — enough context for the agent to pick
85 // targets ("the red button"); schemas + full code are fetched after the pick.
86 export const buildSubtreeManifest = (root) => {
87 if (!root) return [];
88 const manifest = [];
89 // A directly-selected block is its own (and only) target, so the root is included.
90 const els = [root, ...root.querySelectorAll(`[${BLOCK_ID_ATTR}]`)];
91 for (const [index, el] of els.entries()) {
92 if (!el.getAttribute?.(BLOCK_ID_ATTR)) continue;
93 const type = detectBlockType(el);
94 if (!type) continue;
95 const clone = ownSubtree(el);
96 const text = ownText(clone);
97 const colors = colorSlugs(clone);
98 // Drop signal-less descendants, never the root (baseline for relative edits).
99 if (
100 index > 0 &&
101 !ALWAYS_KEPT_TYPES.has(type) &&
102 !text &&
103 !colors.backgroundColor &&
104 !colors.textColor
105 )
106 continue;
107 const styles = renderedStyles(el);
108 const css = customCss(el);
109 manifest.push({
110 blockId: el.getAttribute(BLOCK_ID_ATTR),
111 type,
112 ...(text && { text }),
113 ...colors,
114 ...(styles && { styles }),
115 ...(css && { css }),
116 });
117 }
118 return manifest;
119 };
120
121 // Tagged blocks directly beneath el, past any untagged wrappers.
122 const childBlockEls = (el) => {
123 const out = [];
124 for (const child of el.children ?? []) {
125 if (child.getAttribute?.(BLOCK_ID_ATTR)) out.push(child);
126 else out.push(...childBlockEls(child));
127 }
128 return out;
129 };
130
131 // Keeps every tagged block; the flat manifest drops signal-less ones.
132 const treeNodesFor = (el) =>
133 childBlockEls(el).flatMap((child) => {
134 const type = detectBlockType(child);
135 const children = treeNodesFor(child);
136 if (!type || !child.getAttribute?.(BLOCK_ID_ATTR)) return children;
137 return [
138 {
139 blockId: child.getAttribute(BLOCK_ID_ATTR),
140 type,
141 ...(children.length && { children }),
142 },
143 ];
144 });
145
146 // Nested block structure for the selection.
147 export const buildSubtreeTree = (root) =>
148 root ? treeNodesFor({ children: [root] }) : [];
149