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

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