PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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 / block-schema.js

block-schema.js in Extendify 3.2.1, at src/Agent/lib/block-schema.js

191 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Patch schema from the block's real `supports` — the model never hand-authors HTML.
2 // Structure only: the backend overlays the model-facing field descriptions.
3
4 import { SETTING_TEXT_BLOCKS } from '@agent/lib/setting-text-blocks';
5
6 const str = { type: 'string' };
7 const obj = (properties) => ({
8 type: 'object',
9 additionalProperties: false,
10 properties,
11 });
12
13 // Color channels default on once color support is present, unless opted out.
14 const colorChannel = (color, channel) => {
15 if (color === true) return true;
16 if (!color || typeof color !== 'object') return false;
17 return color[channel] !== false;
18 };
19
20 // Border/spacing/typography/dimensions are opt-in: `true`, or a truthy per-feature key.
21 const feat = (support, key) =>
22 support === true ||
23 (!!support && support[key] != null && support[key] !== false);
24
25 const prune = (properties) => {
26 const out = {};
27 for (const [key, value] of Object.entries(properties)) {
28 if (value == null) continue;
29 if (value.type === 'object' && !Object.keys(value.properties).length)
30 continue;
31 out[key] = value;
32 }
33 return out;
34 };
35
36 const styleSchema = (supports) => {
37 const border = supports.border ?? supports.__experimentalBorder;
38 const spacing = supports.spacing;
39 const typography = supports.typography;
40 const dimensions = supports.dimensions;
41
42 const borderStyle = prune({
43 color: feat(border, 'color') ? str : null,
44 width: feat(border, 'width') ? str : null,
45 style: feat(border, 'style') ? str : null,
46 radius: feat(border, 'radius') ? str : null,
47 });
48
49 const spacingStyle = prune({
50 padding: feat(spacing, 'padding') ? str : null,
51 margin: feat(spacing, 'margin') ? str : null,
52 blockGap: feat(spacing, 'blockGap') ? str : null,
53 });
54
55 const typographyStyle = prune({
56 textAlign:
57 feat(typography, 'textAlign') ||
58 feat(typography, '__experimentalTextAlign')
59 ? str
60 : null,
61 lineHeight: feat(typography, 'lineHeight') ? str : null,
62 fontFamily: feat(typography, '__experimentalFontFamily') ? str : null,
63 fontWeight: feat(typography, '__experimentalFontWeight') ? str : null,
64 fontStyle: feat(typography, '__experimentalFontStyle') ? str : null,
65 textTransform: feat(typography, '__experimentalTextTransform') ? str : null,
66 textDecoration: feat(typography, '__experimentalTextDecoration')
67 ? str
68 : null,
69 letterSpacing: feat(typography, '__experimentalLetterSpacing') ? str : null,
70 });
71
72 const dimensionsStyle = prune({
73 minHeight: feat(dimensions, 'minHeight') ? str : null,
74 aspectRatio: feat(dimensions, 'aspectRatio') ? str : null,
75 });
76
77 const style = prune({
78 border: Object.keys(borderStyle).length ? obj(borderStyle) : null,
79 spacing: Object.keys(spacingStyle).length ? obj(spacingStyle) : null,
80 typography: Object.keys(typographyStyle).length
81 ? obj(typographyStyle)
82 : null,
83 dimensions: Object.keys(dimensionsStyle).length
84 ? obj(dimensionsStyle)
85 : null,
86 shadow: supports.shadow ? str : null,
87 // The arbitrary-CSS escape hatch — always offered.
88 css: str,
89 });
90
91 return obj(style);
92 };
93
94 const ALIGN_OPTIONS = ['left', 'center', 'right', 'wide', 'full'];
95
96 // supports.align is `true` (everything) or the block's own subset.
97 const alignSchema = (align) => {
98 if (!align) return null;
99 return {
100 type: 'string',
101 enum: align === true ? ALIGN_OPTIONS : align,
102 };
103 };
104
105 // Positioning knobs only — allowedBlocks/default/allowEditing are editor config.
106 const layoutSchema = (supports) => {
107 const layout = supports.layout ?? supports.__experimentalLayout;
108 if (!layout || layout.allowEditing === false) return null;
109 return obj({
110 type: str,
111 contentSize: str,
112 wideSize: str,
113 justifyContent: str,
114 orientation: str,
115 flexWrap: str,
116 verticalAlignment: str,
117 });
118 };
119
120 // Where a block keeps its editable text, most-specific first: `text` (button),
121 // `content` (paragraph, heading), `label` (menu item, search).
122 export const RICH_TEXT_ATTRIBUTES = ['text', 'content', 'label'];
123
124 // Identity attributes (ref, id, className, lock, metadata) stay out —
125 // bad values there break the block, not just its look.
126 const SIMPLE_ATTRIBUTES = [
127 'level',
128 'ordered',
129 'reversed',
130 'start',
131 'width',
132 'height',
133 'verticalAlignment',
134 'isStackedOnMobile',
135 // media-text's image side is an attribute, not child order.
136 'mediaPosition',
137 ];
138
139 const simpleAttrSchema = (definition) => {
140 if (!definition) return null;
141 const schema = {};
142 if (definition.type) schema.type = definition.type;
143 if (definition.enum) schema.enum = definition.enum;
144 return Object.keys(schema).length ? schema : str;
145 };
146
147 export const buildBlockSchema = (blockType) => {
148 if (!blockType) return null;
149 const attributes = blockType.attributes ?? {};
150 const supports = blockType.supports ?? {};
151 const typography = supports.typography;
152
153 const simple = Object.fromEntries(
154 SIMPLE_ATTRIBUTES.map((name) => [name, simpleAttrSchema(attributes[name])]),
155 );
156
157 const properties = prune({
158 ...simple,
159 align: attributes.align ? alignSchema(supports.align) : null,
160 // Quote and pullquote align text with an attribute, not a typography support.
161 textAlign: attributes.textAlign
162 ? { type: 'string', enum: ['left', 'center', 'right'] }
163 : null,
164 layout: attributes.layout ? layoutSchema(supports) : null,
165 // Model-facing name; block-patch maps it back to the real home.
166 text:
167 RICH_TEXT_ATTRIBUTES.some((name) => attributes[name]) ||
168 SETTING_TEXT_BLOCKS[blockType.name]
169 ? str
170 : null,
171 url: attributes.url ? str : null,
172 backgroundColor:
173 attributes.backgroundColor && colorChannel(supports.color, 'background')
174 ? str
175 : null,
176 textColor:
177 attributes.textColor && colorChannel(supports.color, 'text') ? str : null,
178 // Gradients omitted: the theme's preset slugs are too opaque for the model
179 // to pick reliably, so gradients go through style.css instead.
180 fontSize: attributes.fontSize && feat(typography, 'fontSize') ? str : null,
181 fontFamily:
182 attributes.fontFamily && feat(typography, '__experimentalFontFamily')
183 ? str
184 : null,
185 style: styleSchema(supports),
186 });
187
188 if (!Object.keys(properties).length) return null;
189 return obj(properties);
190 };
191