# extendify/3.2.0/src/Agent/lib/subtree-manifest.js

Extendify, version 3.2.0. 160 lines.

- Page: https://pluginprobe.com/plugins/extendify/3.2.0/code/src/Agent/lib/subtree-manifest.js
- Raw: https://pluginprobe.com/plugins/extendify/3.2.0/raw/src/Agent/lib/subtree-manifest.js
- Modified: 2026-09-09T18:23:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/extendify/3.2.0/code/src/Agent/lib/subtree-manifest.js#L10-L20`.

```javascript
import { BLOCK_ID_SEL, blockIdOf } from './block-el';
import { detectBlockType } from './block-type';
import { readComputedStyles } from './computed-styles';

// Signal-less but edit targets ("swap the columns", "round the image") — dropped, they're unaddressable.
const ALWAYS_KEPT_TYPES = new Set([
	'core/column',
	'core/columns',
	'core/image',
	'core/cover',
	'core/site-logo',
]);

const isTransparent = (value) =>
	!value ||
	value === 'transparent' ||
	value.replace(/\s/g, '') === 'rgba(0,0,0,0)';

// core/button renders text + background in an inner link, so read styles off the
// link — only for buttons, or a container would report a nested button's styles.
const styleSource = (el) =>
	el.classList?.contains('wp-block-button')
		? (el.querySelector('.wp-block-button__link') ?? el)
		: el;

// Narrow by appearance — a token like `tertiary` says nothing about being yellow.
const renderedStyles = (el) => {
	const styles = readComputedStyles(styleSource(el));
	if (styles && isTransparent(styles.backgroundColor))
		delete styles.backgroundColor;
	return styles && Object.keys(styles).length ? styles : null;
};

// style.css lives in a scoped `wp-custom-css-*` rule, invisible in the block code.
const customCss = (el) => {
	const cls = [...(el.classList ?? [])].find((c) =>
		c.startsWith('wp-custom-css-'),
	);
	if (!cls) return null;
	for (const sheet of el.ownerDocument?.styleSheets ?? []) {
		let rules;
		try {
			rules = sheet.cssRules;
		} catch {
			continue; // cross-origin sheet — can't read
		}
		for (const rule of rules) {
			if (rule.selectorText?.includes(cls)) return rule.style?.cssText || null;
		}
	}
	return null;
};

const PART_SLUG_ATTR = 'data-extendify-part-slug';

// One request carries one partSlug, so a nested part's blocks would resolve
// against the wrong post.
const partScope = (root) => {
	const rootSlug = root?.getAttribute?.(PART_SLUG_ATTR) ?? null;
	return (el) => (el?.getAttribute?.(PART_SLUG_ATTR) ?? null) === rootSlug;
};

// Stops at nested tagged blocks so a container's text never bleeds from its children.
const ownSubtree = (el) => {
	const clone = el.cloneNode(true);
	for (const nested of clone.querySelectorAll(BLOCK_ID_SEL)) nested.remove();
	return clone;
};

const ownText = (clone) => {
	const text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
	return text.length > 80 ? `${text.slice(0, 80)}…` : text;
};

const colorSlugs = (clone) => {
	const out = {};
	const nodes = [clone, ...clone.querySelectorAll('*')];
	for (const node of nodes) {
		for (const cls of node.classList ?? []) {
			const bg = cls.match(/^has-(.+)-background-color$/);
			if (bg) out.backgroundColor = bg[1];
			else if (cls !== 'has-text-color') {
				const text = cls.match(/^has-(.+)-color$/);
				if (text) out.textColor = text[1];
			}
		}
	}
	return out;
};

// Per-block summary of the selection — enough context for the agent to pick
// targets ("the red button"); schemas + full code are fetched after the pick.
export const buildSubtreeManifest = (root) => {
	if (!root) return [];
	const manifest = [];
	const inScope = partScope(root);
	// A directly-selected block is its own (and only) target, so the root is included.
	const els = [root, ...root.querySelectorAll(BLOCK_ID_SEL)].filter(inScope);
	for (const [index, el] of els.entries()) {
		const blockId = blockIdOf(el);
		if (!blockId) continue;
		const type = detectBlockType(el);
		if (!type) continue;
		const clone = ownSubtree(el);
		const text = ownText(clone);
		const colors = colorSlugs(clone);
		// Drop signal-less descendants, never the root (baseline for relative edits).
		if (
			index > 0 &&
			!ALWAYS_KEPT_TYPES.has(type) &&
			!text &&
			!colors.backgroundColor &&
			!colors.textColor
		)
			continue;
		const styles = renderedStyles(el);
		const css = customCss(el);
		manifest.push({
			blockId,
			type,
			...(text && { text }),
			...colors,
			...(styles && { styles }),
			...(css && { css }),
		});
	}
	return manifest;
};

// Tagged blocks directly beneath el, past any untagged wrappers.
const childBlockEls = (el, inScope) => {
	const out = [];
	for (const child of el.children ?? []) {
		if (!inScope(child)) continue;
		if (blockIdOf(child)) out.push(child);
		else out.push(...childBlockEls(child, inScope));
	}
	return out;
};

// Keeps every tagged block; the flat manifest drops signal-less ones.
const treeNodesFor = (el, inScope) =>
	childBlockEls(el, inScope).flatMap((child) => {
		const type = detectBlockType(child);
		const children = treeNodesFor(child, inScope);
		const blockId = blockIdOf(child);
		if (!type || !blockId) return children;
		return [
			{
				blockId,
				type,
				...(children.length && { children }),
			},
		];
	});

// Nested block structure for the selection.
export const buildSubtreeTree = (root) =>
	root ? treeNodesFor({ children: [root] }, partScope(root)) : [];

```
