PluginProbe
Extendify / 3.2.0
Extendify v3.2.0
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.0, at src/Agent/lib/block-schema.js

187 lines 5.8 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 layout: attributes.layout ? layoutSchema(supports) : null,
161 // Model-facing name; block-patch maps it back to the real home.
162 text:
163 RICH_TEXT_ATTRIBUTES.some((name) => attributes[name]) ||
164 SETTING_TEXT_BLOCKS[blockType.name]
165 ? str
166 : null,
167 url: attributes.url ? str : null,
168 backgroundColor:
169 attributes.backgroundColor && colorChannel(supports.color, 'background')
170 ? str
171 : null,
172 textColor:
173 attributes.textColor && colorChannel(supports.color, 'text') ? str : null,
174 // Gradients omitted: the theme's preset slugs are too opaque for the model
175 // to pick reliably, so gradients go through style.css instead.
176 fontSize: attributes.fontSize && feat(typography, 'fontSize') ? str : null,
177 fontFamily:
178 attributes.fontFamily && feat(typography, '__experimentalFontFamily')
179 ? str
180 : null,
181 style: styleSchema(supports),
182 });
183
184 if (!Object.keys(properties).length) return null;
185 return obj(properties);
186 };
187