@diviops/mcp-server 1.5.13 → 1.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/data/verified-attrs-backlog.json +6 -6
- package/data/verified-attrs.json +32 -18
- package/dist/compatibility.d.ts +25 -0
- package/dist/index.js +349 -4
- package/dist/preset-cli/button-emitter.d.ts +1 -1
- package/dist/preset-cli/button-emitter.js +1 -1
- package/dist/preset-cli/cli.d.ts +7 -3
- package/dist/preset-cli/cli.js +131 -5
- package/dist/preset-cli/heading-font-emitter.d.ts +1 -1
- package/dist/preset-cli/heading-font-emitter.js +3 -3
- package/dist/preset-cli/spacing-emitter.d.ts +132 -0
- package/dist/preset-cli/spacing-emitter.js +276 -0
- package/dist/preset-cli/write-path.d.ts +16 -0
- package/dist/preset-cli/write-path.js +22 -0
- package/dist/wp-cli-fs-validator.d.ts +1 -1
- package/dist/wp-cli-fs-validator.js +1 -1
- package/dist/wp-cli.js +1 -2
- package/dist/wp-client.js +17 -0
- package/package.json +3 -2
- package/dist/preset-cli/__tests__/button-emitter.test.d.ts +0 -8
- package/dist/preset-cli/__tests__/button-emitter.test.js +0 -188
- package/dist/preset-cli/__tests__/cli.test.d.ts +0 -9
- package/dist/preset-cli/__tests__/cli.test.js +0 -534
- package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +0 -12
- package/dist/preset-cli/__tests__/heading-font-emitter.test.js +0 -249
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.d.ts +0 -13
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.js +0 -64
- package/dist/preset-cli/__tests__/registry.test.d.ts +0 -5
- package/dist/preset-cli/__tests__/registry.test.js +0 -175
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.d.ts +0 -14
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.js +0 -191
- package/dist/preset-cli/__tests__/write-path.test.d.ts +0 -8
- package/dist/preset-cli/__tests__/write-path.test.js +0 -229
package/dist/preset-cli/cli.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `diviops-preset` — standalone preset-emitter CLI
|
|
2
|
+
* `diviops-preset` — standalone preset-emitter CLI.
|
|
3
3
|
*
|
|
4
4
|
* Emits byte-canonical Divi 5.5.x preset JSON, gated by the verified-attrs
|
|
5
|
-
* registry and routed through the existing storage-path contract.
|
|
6
|
-
*
|
|
5
|
+
* registry and routed through the existing storage-path contract. The
|
|
6
|
+
* initial slice shipped the `divi/button` group emitter; subsequent slices
|
|
7
|
+
* added the heading/font, color, and spacing emitters.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
10
|
* diviops-preset button [options] Emit a divi/button group preset
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
import { type ButtonEmitterInput } from "./button-emitter.js";
|
|
26
27
|
import { type HeadingFontEmitterInput } from "./heading-font-emitter.js";
|
|
27
28
|
import { type TextBodyFontEmitterInput } from "./text-body-font-emitter.js";
|
|
29
|
+
import { type SpacingEmitterInput } from "./spacing-emitter.js";
|
|
28
30
|
export declare const EXIT: {
|
|
29
31
|
readonly OK: 0;
|
|
30
32
|
readonly INVALID_INPUT: 1;
|
|
@@ -56,6 +58,8 @@ export declare function buildButtonInput(parsed: ParsedArgs): ButtonEmitterInput
|
|
|
56
58
|
export declare function buildHeadingFontInput(parsed: ParsedArgs): HeadingFontEmitterInput;
|
|
57
59
|
/** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
|
|
58
60
|
export declare function buildTextBodyFontInput(parsed: ParsedArgs): TextBodyFontEmitterInput;
|
|
61
|
+
/** Map parsed `spacing` options into the spacing emitter input shape. */
|
|
62
|
+
export declare function buildSpacingInput(parsed: ParsedArgs): SpacingEmitterInput;
|
|
59
63
|
/**
|
|
60
64
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
61
65
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
package/dist/preset-cli/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `diviops-preset` — standalone preset-emitter CLI
|
|
2
|
+
* `diviops-preset` — standalone preset-emitter CLI.
|
|
3
3
|
*
|
|
4
4
|
* Emits byte-canonical Divi 5.5.x preset JSON, gated by the verified-attrs
|
|
5
|
-
* registry and routed through the existing storage-path contract.
|
|
6
|
-
*
|
|
5
|
+
* registry and routed through the existing storage-path contract. The
|
|
6
|
+
* initial slice shipped the `divi/button` group emitter; subsequent slices
|
|
7
|
+
* added the heading/font, color, and spacing emitters.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
10
|
* diviops-preset button [options] Emit a divi/button group preset
|
|
@@ -25,8 +26,9 @@
|
|
|
25
26
|
import { emitButtonGroupPreset, buildPresetCreateBody, } from "./button-emitter.js";
|
|
26
27
|
import { emitHeadingFontGroupPreset, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, } from "./heading-font-emitter.js";
|
|
27
28
|
import { emitTextBodyFontGroupPreset, buildTextBodyFontPresetCreateBody, } from "./text-body-font-emitter.js";
|
|
29
|
+
import { emitSpacingGroupPreset, buildSpacingPresetCreateBody, } from "./spacing-emitter.js";
|
|
28
30
|
import { EvidenceGateError } from "./registry.js";
|
|
29
|
-
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
31
|
+
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, applySpacingPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
30
32
|
export const EXIT = {
|
|
31
33
|
OK: 0,
|
|
32
34
|
INVALID_INPUT: 1,
|
|
@@ -46,6 +48,8 @@ USAGE
|
|
|
46
48
|
divi/heading
|
|
47
49
|
diviops-preset text-body-font [options] Emit a divi/font-body group preset
|
|
48
50
|
for divi/text (Pattern A only)
|
|
51
|
+
diviops-preset spacing [options] Emit a divi/spacing group preset
|
|
52
|
+
(currently divi/section only)
|
|
49
53
|
diviops-preset --help Show this help
|
|
50
54
|
|
|
51
55
|
MODE
|
|
@@ -123,6 +127,38 @@ text-body-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
|
123
127
|
--font-size <v> Font size (e.g. "16px").
|
|
124
128
|
--font-line-height <v> Font line-height (e.g. "1.5").
|
|
125
129
|
|
|
130
|
+
spacing OPTIONS (sparse-emit per axis; paired sync flags per axis)
|
|
131
|
+
--name <string> Preset display name (required).
|
|
132
|
+
--module <name> Required. Currently only divi/section is wired
|
|
133
|
+
(the canonical capture verified only the
|
|
134
|
+
divi/section cell). Other modules (divi/heading,
|
|
135
|
+
divi/text, divi/button, etc.) resolve to the
|
|
136
|
+
registry gate and are refused with
|
|
137
|
+
EvidenceGateError — promoting them requires a
|
|
138
|
+
canonical-capture change plus a follow-up
|
|
139
|
+
implementation/docs change (NOT a free dispatch-
|
|
140
|
+
clear via the gate alone).
|
|
141
|
+
--padding-top <v> Desktop padding corners. Pass any subset; only
|
|
142
|
+
--padding-right <v> passed corners emit (sparse-emit per axis). v1
|
|
143
|
+
--padding-bottom <v> accepts literal CSS lengths only (px / rem /
|
|
144
|
+
--padding-left <v> em / % / vw / vh) — $variable(...) / gvid-*
|
|
145
|
+
tokens are refused (deferred until canonical
|
|
146
|
+
variable-token capture lands).
|
|
147
|
+
--margin-top <v> Desktop margin corners. Same shape rules as
|
|
148
|
+
--margin-right <v> padding; padding and margin are independent —
|
|
149
|
+
--margin-bottom <v> passing only padding flags omits the margin bag
|
|
150
|
+
--margin-left <v> from the output, and vice versa.
|
|
151
|
+
--padding-sync-vertical <on|off>
|
|
152
|
+
Explicit padding sync flag (default "off").
|
|
153
|
+
Both syncVertical AND syncHorizontal always
|
|
154
|
+
--padding-sync-horizontal <on|off>
|
|
155
|
+
emit as paired siblings when the padding axis
|
|
156
|
+
has any touched corner.
|
|
157
|
+
--margin-sync-vertical <on|off>
|
|
158
|
+
Explicit margin sync flag (default "off").
|
|
159
|
+
--margin-sync-horizontal <on|off>
|
|
160
|
+
Same paired-siblings rule as padding.
|
|
161
|
+
|
|
126
162
|
EXIT CODES
|
|
127
163
|
0 success 1 invalid input 2 evidence-gate refusal
|
|
128
164
|
3 capability missing 4 write error
|
|
@@ -145,6 +181,9 @@ EXAMPLES
|
|
|
145
181
|
diviops-preset text-body-font --name "Body Text" --pattern google \\
|
|
146
182
|
--font-family Inter --font-weight 400 \\
|
|
147
183
|
--font-color gcid-body-color --font-size 16px
|
|
184
|
+
|
|
185
|
+
diviops-preset spacing --name "Section Rhythm" --module divi/section \\
|
|
186
|
+
--padding-top 80px --padding-bottom 80px --margin-bottom 40px
|
|
148
187
|
`;
|
|
149
188
|
const VALUE_FLAGS = new Set([
|
|
150
189
|
"--name",
|
|
@@ -165,6 +204,19 @@ const VALUE_FLAGS = new Set([
|
|
|
165
204
|
"--font-size",
|
|
166
205
|
"--font-line-height",
|
|
167
206
|
"--pattern",
|
|
207
|
+
"--module",
|
|
208
|
+
"--padding-top",
|
|
209
|
+
"--padding-right",
|
|
210
|
+
"--padding-bottom",
|
|
211
|
+
"--padding-left",
|
|
212
|
+
"--margin-top",
|
|
213
|
+
"--margin-right",
|
|
214
|
+
"--margin-bottom",
|
|
215
|
+
"--margin-left",
|
|
216
|
+
"--padding-sync-vertical",
|
|
217
|
+
"--padding-sync-horizontal",
|
|
218
|
+
"--margin-sync-vertical",
|
|
219
|
+
"--margin-sync-horizontal",
|
|
168
220
|
]);
|
|
169
221
|
/** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
|
|
170
222
|
export function parseArgs(argv) {
|
|
@@ -229,7 +281,12 @@ export class UsageError extends Error {
|
|
|
229
281
|
}
|
|
230
282
|
}
|
|
231
283
|
/** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
|
|
232
|
-
const KNOWN_COMMANDS = new Set([
|
|
284
|
+
const KNOWN_COMMANDS = new Set([
|
|
285
|
+
"button",
|
|
286
|
+
"heading-font",
|
|
287
|
+
"text-body-font",
|
|
288
|
+
"spacing",
|
|
289
|
+
]);
|
|
233
290
|
/** Map parsed `button` options into the emitter input shape. */
|
|
234
291
|
export function buildButtonInput(parsed) {
|
|
235
292
|
const opt = (k) => {
|
|
@@ -394,6 +451,67 @@ export function buildTextBodyFontInput(parsed) {
|
|
|
394
451
|
input.lineHeight = lineHeight;
|
|
395
452
|
return input;
|
|
396
453
|
}
|
|
454
|
+
/** Parse an optional `on|off` flag value; throw a usage error otherwise. */
|
|
455
|
+
function parseOnOff(raw, flag) {
|
|
456
|
+
if (raw === undefined)
|
|
457
|
+
return undefined;
|
|
458
|
+
if (raw !== "on" && raw !== "off") {
|
|
459
|
+
throw new UsageError(`${flag} must be "on" or "off"; got ${JSON.stringify(raw)}.`);
|
|
460
|
+
}
|
|
461
|
+
return raw;
|
|
462
|
+
}
|
|
463
|
+
/** Map parsed `spacing` options into the spacing emitter input shape. */
|
|
464
|
+
export function buildSpacingInput(parsed) {
|
|
465
|
+
const opt = (k) => {
|
|
466
|
+
const v = parsed.options.get(k);
|
|
467
|
+
return typeof v === "string" ? v : undefined;
|
|
468
|
+
};
|
|
469
|
+
const name = opt("--name");
|
|
470
|
+
if (!name) {
|
|
471
|
+
throw new UsageError("spacing command requires --name <string>.");
|
|
472
|
+
}
|
|
473
|
+
const module = opt("--module");
|
|
474
|
+
if (!module) {
|
|
475
|
+
throw new UsageError("spacing command requires --module <name>. Currently only " +
|
|
476
|
+
"divi/section is wired; other modules are refused by the registry " +
|
|
477
|
+
"gate (heading/text/button cells are SCHEMA_OBSERVED).");
|
|
478
|
+
}
|
|
479
|
+
const input = { name, module };
|
|
480
|
+
// Sparse-emit at parse time too: only attach an axis bag when at least
|
|
481
|
+
// one corner OR a sync flag was passed. (Sync-flag-only input lands on
|
|
482
|
+
// the emitter's per-axis sync-without-corner refusal.) Padding and
|
|
483
|
+
// margin follow the identical shape rule, so the per-axis collection
|
|
484
|
+
// is hoisted into a single helper.
|
|
485
|
+
const buildAxis = (prefix) => {
|
|
486
|
+
const bag = {};
|
|
487
|
+
const t = opt(`--${prefix}-top`);
|
|
488
|
+
if (t !== undefined)
|
|
489
|
+
bag.top = t;
|
|
490
|
+
const r = opt(`--${prefix}-right`);
|
|
491
|
+
if (r !== undefined)
|
|
492
|
+
bag.right = r;
|
|
493
|
+
const b = opt(`--${prefix}-bottom`);
|
|
494
|
+
if (b !== undefined)
|
|
495
|
+
bag.bottom = b;
|
|
496
|
+
const l = opt(`--${prefix}-left`);
|
|
497
|
+
if (l !== undefined)
|
|
498
|
+
bag.left = l;
|
|
499
|
+
const sv = parseOnOff(opt(`--${prefix}-sync-vertical`), `--${prefix}-sync-vertical`);
|
|
500
|
+
if (sv !== undefined)
|
|
501
|
+
bag.syncVertical = sv;
|
|
502
|
+
const sh = parseOnOff(opt(`--${prefix}-sync-horizontal`), `--${prefix}-sync-horizontal`);
|
|
503
|
+
if (sh !== undefined)
|
|
504
|
+
bag.syncHorizontal = sh;
|
|
505
|
+
return Object.keys(bag).length > 0 ? bag : undefined;
|
|
506
|
+
};
|
|
507
|
+
const padding = buildAxis("padding");
|
|
508
|
+
if (padding)
|
|
509
|
+
input.padding = padding;
|
|
510
|
+
const margin = buildAxis("margin");
|
|
511
|
+
if (margin)
|
|
512
|
+
input.margin = margin;
|
|
513
|
+
return input;
|
|
514
|
+
}
|
|
397
515
|
/**
|
|
398
516
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
399
517
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
|
@@ -449,6 +567,14 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
449
567
|
dryRunBody = buildTextBodyFontPresetCreateBody(entry, { dry_run: true });
|
|
450
568
|
applyFn = (client, sv) => applyTextBodyFontPreset(client, entry, { serverVersion: sv });
|
|
451
569
|
}
|
|
570
|
+
else if (parsed.command === "spacing") {
|
|
571
|
+
// spacing (divi/section only — other modules land on the registry
|
|
572
|
+
// gate in emitSpacingGroupPreset and surface as EvidenceGateError).
|
|
573
|
+
const input = buildSpacingInput(parsed);
|
|
574
|
+
const entry = emitSpacingGroupPreset(input);
|
|
575
|
+
dryRunBody = buildSpacingPresetCreateBody(entry, { dry_run: true });
|
|
576
|
+
applyFn = (client, sv) => applySpacingPreset(client, entry, { serverVersion: sv });
|
|
577
|
+
}
|
|
452
578
|
else {
|
|
453
579
|
// Defensive: a new entry in KNOWN_COMMANDS without a dispatch
|
|
454
580
|
// branch here would silently break dry-run/apply. parseArgs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `divi/font` heading group-preset emitter
|
|
2
|
+
* `divi/font` heading group-preset emitter.
|
|
3
3
|
*
|
|
4
4
|
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font` preset
|
|
5
5
|
* targeting `divi/heading` at `attrs.title.decoration.font.font.desktop.value.*`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `divi/font` heading group-preset emitter
|
|
2
|
+
* `divi/font` heading group-preset emitter.
|
|
3
3
|
*
|
|
4
4
|
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font` preset
|
|
5
5
|
* targeting `divi/heading` at `attrs.title.decoration.font.font.desktop.value.*`,
|
|
@@ -109,8 +109,8 @@ export function emitHeadingFontGroupPreset(input, registry = loadRegistry()) {
|
|
|
109
109
|
throw new Error(`Heading-font emitter requires \`pattern\` to be "google" or "local"; got ${JSON.stringify(input.pattern)}. ` +
|
|
110
110
|
`There is no default — Pattern A (google) and Pattern B (local) are distinct registry/evidence variants.`);
|
|
111
111
|
}
|
|
112
|
-
// Pattern B + explicit weight is out-of-scope for
|
|
113
|
-
// Pattern B capture proves `weight` is absent; no registry entry
|
|
112
|
+
// Pattern B + explicit weight is out-of-scope for the current emitter. The
|
|
113
|
+
// verified Pattern B capture proves `weight` is absent; no registry entry
|
|
114
114
|
// currently authorizes a local-hosted-with-explicit-weight shape.
|
|
115
115
|
if (input.pattern === "local" && input.weight !== undefined) {
|
|
116
116
|
throw new UnsupportedVariantCombinationError(`Pattern B (local-hosted) does not support an explicit \`weight\` — the verified ` +
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/spacing` section group-preset emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.6.0 `type: "group"` `divi/spacing` preset
|
|
5
|
+
* targeting `divi/section` at
|
|
6
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape captured under
|
|
8
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json`.
|
|
9
|
+
*
|
|
10
|
+
* Scope: `divi/section` ONLY. Heading / text / button spacing cells remain
|
|
11
|
+
* `SCHEMA_OBSERVED` and resolve to `EvidenceGateError` here per
|
|
12
|
+
* `feedback_preset_map_per_module` (no cross-module shape carry-over).
|
|
13
|
+
*
|
|
14
|
+
* Shape rules enforced here (brief §4):
|
|
15
|
+
* - Sparse-emit per axis — only user-touched corners appear under
|
|
16
|
+
* `padding`/`margin`. Untouched corners are absent (not present-as-empty,
|
|
17
|
+
* not present-as-null).
|
|
18
|
+
* - Paired sync flags per axis — any spacing touch on an axis emits BOTH
|
|
19
|
+
* `syncVertical` AND `syncHorizontal` as siblings. Default `"off"` on
|
|
20
|
+
* both unless the caller explicitly toggled them.
|
|
21
|
+
* - Padding and margin are INDEPENDENT bags — passing only padding flags
|
|
22
|
+
* omits the margin bag entirely, and vice versa.
|
|
23
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
24
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
25
|
+
* VB save semantics); the CLI request body only carries `attrs`. The
|
|
26
|
+
* canonical capture fixture documents the post-write storage shape — do
|
|
27
|
+
* NOT add `styleAttrs` / `renderAttrs` keys to the emitter output.
|
|
28
|
+
* - `groupId: "designSpacing"` — the Composable Settings panel id, NOT a
|
|
29
|
+
* dotted attr path (the prior schema-inferred `module.decoration.spacing`
|
|
30
|
+
* note was a misread, now corrected).
|
|
31
|
+
* - `primaryAttrName: "module"` for section spacing.
|
|
32
|
+
* - Variable tokens (`$variable(...)` / bare `gvid-*`) in length flags are
|
|
33
|
+
* REFUSED — the canonical capture exercised literal CSS lengths only;
|
|
34
|
+
* variable-token shape needs its own capture before this emitter writes
|
|
35
|
+
* it.
|
|
36
|
+
*/
|
|
37
|
+
import { type VerifiedAttrsRegistry } from "./registry.js";
|
|
38
|
+
export declare const SPACING_GROUP_NAME = "divi/spacing";
|
|
39
|
+
export declare const SPACING_GROUP_ID = "designSpacing";
|
|
40
|
+
export declare const SPACING_PRIMARY_ATTR_NAME = "module";
|
|
41
|
+
export declare const SPACING_PATTERN_FAMILY = "divi/spacing";
|
|
42
|
+
/**
|
|
43
|
+
* The single currently-supported module cell. The `--module` flag accepts
|
|
44
|
+
* any string and routes through the registry gate (so the surface stays
|
|
45
|
+
* stable when heading/text/button cells eventually promote), but only
|
|
46
|
+
* `divi/section` is wired with fixtures + tests + canonical shape today.
|
|
47
|
+
*
|
|
48
|
+
* Promoting another module is NOT a free dispatch-clear via the registry
|
|
49
|
+
* gate — each new cell needs a canonical-capture change landing first AND
|
|
50
|
+
* a follow-up implementation/docs change.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SPACING_SUPPORTED_MODULES: readonly ["divi/section"];
|
|
53
|
+
export type SpacingCornerInput = {
|
|
54
|
+
top?: string;
|
|
55
|
+
right?: string;
|
|
56
|
+
bottom?: string;
|
|
57
|
+
left?: string;
|
|
58
|
+
syncVertical?: "on" | "off";
|
|
59
|
+
syncHorizontal?: "on" | "off";
|
|
60
|
+
};
|
|
61
|
+
export interface SpacingEmitterInput {
|
|
62
|
+
/** Required display name for the preset. */
|
|
63
|
+
name: string;
|
|
64
|
+
/**
|
|
65
|
+
* Required module selector. Forward-compat-shaped; only `divi/section` is
|
|
66
|
+
* wired today, everything else lands on `EvidenceGateError` via the
|
|
67
|
+
* registry gate (or a SCHEMA_OBSERVED cell evidence-gate refusal).
|
|
68
|
+
*/
|
|
69
|
+
module: string;
|
|
70
|
+
/** Desktop padding corner-and-sync bag. */
|
|
71
|
+
padding?: SpacingCornerInput;
|
|
72
|
+
/** Desktop margin corner-and-sync bag. */
|
|
73
|
+
margin?: SpacingCornerInput;
|
|
74
|
+
}
|
|
75
|
+
/** The composed canonical preset entry shape sent to `/preset/create`. */
|
|
76
|
+
export interface SpacingPresetEntry {
|
|
77
|
+
type: "group";
|
|
78
|
+
module_name: string;
|
|
79
|
+
group_name: string;
|
|
80
|
+
group_id: string;
|
|
81
|
+
primary_attr_name: string;
|
|
82
|
+
name: string;
|
|
83
|
+
attrs: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Compose the canonical
|
|
87
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}` bag.
|
|
88
|
+
* Sparse-emit per axis; padding and margin are independent.
|
|
89
|
+
*
|
|
90
|
+
* Returns BOTH the full `attrs` tree (what the emitter ships) AND the
|
|
91
|
+
* inner `value` bag — the caller does empty-preset validation against
|
|
92
|
+
* `value` without re-walking the nested attrs tree (no `as any` deep
|
|
93
|
+
* casts).
|
|
94
|
+
*/
|
|
95
|
+
export declare function composeSpacingAttrs(input: SpacingEmitterInput): {
|
|
96
|
+
attrs: Record<string, unknown>;
|
|
97
|
+
value: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Emit a canonical `divi/spacing` section group preset.
|
|
101
|
+
*
|
|
102
|
+
* 1. Validate input shape (name, module, at least one corner across either
|
|
103
|
+
* axis).
|
|
104
|
+
* 2. Reject variable-token values in any length flag (deferred until a
|
|
105
|
+
* canonical capture lands).
|
|
106
|
+
* 3. Compose sparse-emit `attrs.module.decoration.spacing.desktop.value.*`
|
|
107
|
+
* with paired sync flags.
|
|
108
|
+
* 4. Gate `(divi/spacing, <module>)` against the verified-attrs registry —
|
|
109
|
+
* throws `EvidenceGateError` when effective evidence is below
|
|
110
|
+
* `VB_PRESET_STORAGE_VERIFIED`.
|
|
111
|
+
*
|
|
112
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this entry:
|
|
113
|
+
* the plugin's `/preset/create` route mirrors the single `attrs` bag into
|
|
114
|
+
* all three buckets at write time. The canonical capture fixture documents
|
|
115
|
+
* the post-write storage shape (which is why both `attrs` and `styleAttrs`
|
|
116
|
+
* appear there byte-identical); the CLI emits the request shape only.
|
|
117
|
+
*/
|
|
118
|
+
export declare function emitSpacingGroupPreset(input: SpacingEmitterInput, registry?: VerifiedAttrsRegistry): SpacingPresetEntry;
|
|
119
|
+
/**
|
|
120
|
+
* Build the `POST /diviops/v1/preset/create` request body from a spacing
|
|
121
|
+
* preset entry. Matches the body shape the `diviops_preset_create` MCP
|
|
122
|
+
* tool posts — the CLI reuses the existing route, it does not add one.
|
|
123
|
+
*
|
|
124
|
+
* `primary_attr_name` IS sent on the wire (the plugin's `/preset/create`
|
|
125
|
+
* route accepts it as an optional snake_case param and stores it as
|
|
126
|
+
* `primaryAttrName` in the preset). The button / font / color emitters
|
|
127
|
+
* omit it because their preset types do not carry it; the divi/spacing
|
|
128
|
+
* cell does, per the canonical capture's load-bearing finding #2.
|
|
129
|
+
*/
|
|
130
|
+
export declare function buildSpacingPresetCreateBody(entry: SpacingPresetEntry, opts?: {
|
|
131
|
+
dry_run?: boolean;
|
|
132
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/spacing` section group-preset emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.6.0 `type: "group"` `divi/spacing` preset
|
|
5
|
+
* targeting `divi/section` at
|
|
6
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape captured under
|
|
8
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json`.
|
|
9
|
+
*
|
|
10
|
+
* Scope: `divi/section` ONLY. Heading / text / button spacing cells remain
|
|
11
|
+
* `SCHEMA_OBSERVED` and resolve to `EvidenceGateError` here per
|
|
12
|
+
* `feedback_preset_map_per_module` (no cross-module shape carry-over).
|
|
13
|
+
*
|
|
14
|
+
* Shape rules enforced here (brief §4):
|
|
15
|
+
* - Sparse-emit per axis — only user-touched corners appear under
|
|
16
|
+
* `padding`/`margin`. Untouched corners are absent (not present-as-empty,
|
|
17
|
+
* not present-as-null).
|
|
18
|
+
* - Paired sync flags per axis — any spacing touch on an axis emits BOTH
|
|
19
|
+
* `syncVertical` AND `syncHorizontal` as siblings. Default `"off"` on
|
|
20
|
+
* both unless the caller explicitly toggled them.
|
|
21
|
+
* - Padding and margin are INDEPENDENT bags — passing only padding flags
|
|
22
|
+
* omits the margin bag entirely, and vice versa.
|
|
23
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
24
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
25
|
+
* VB save semantics); the CLI request body only carries `attrs`. The
|
|
26
|
+
* canonical capture fixture documents the post-write storage shape — do
|
|
27
|
+
* NOT add `styleAttrs` / `renderAttrs` keys to the emitter output.
|
|
28
|
+
* - `groupId: "designSpacing"` — the Composable Settings panel id, NOT a
|
|
29
|
+
* dotted attr path (the prior schema-inferred `module.decoration.spacing`
|
|
30
|
+
* note was a misread, now corrected).
|
|
31
|
+
* - `primaryAttrName: "module"` for section spacing.
|
|
32
|
+
* - Variable tokens (`$variable(...)` / bare `gvid-*`) in length flags are
|
|
33
|
+
* REFUSED — the canonical capture exercised literal CSS lengths only;
|
|
34
|
+
* variable-token shape needs its own capture before this emitter writes
|
|
35
|
+
* it.
|
|
36
|
+
*/
|
|
37
|
+
import { loadRegistry, gateWriteAttr, } from "./registry.js";
|
|
38
|
+
export const SPACING_GROUP_NAME = "divi/spacing";
|
|
39
|
+
export const SPACING_GROUP_ID = "designSpacing";
|
|
40
|
+
export const SPACING_PRIMARY_ATTR_NAME = "module";
|
|
41
|
+
export const SPACING_PATTERN_FAMILY = "divi/spacing";
|
|
42
|
+
/**
|
|
43
|
+
* The single currently-supported module cell. The `--module` flag accepts
|
|
44
|
+
* any string and routes through the registry gate (so the surface stays
|
|
45
|
+
* stable when heading/text/button cells eventually promote), but only
|
|
46
|
+
* `divi/section` is wired with fixtures + tests + canonical shape today.
|
|
47
|
+
*
|
|
48
|
+
* Promoting another module is NOT a free dispatch-clear via the registry
|
|
49
|
+
* gate — each new cell needs a canonical-capture change landing first AND
|
|
50
|
+
* a follow-up implementation/docs change.
|
|
51
|
+
*/
|
|
52
|
+
export const SPACING_SUPPORTED_MODULES = ["divi/section"];
|
|
53
|
+
/** Detect bare `gvid-*` / `gcid-*` variable token names. */
|
|
54
|
+
const BARE_VARIABLE_TOKEN_RE = /^(gvid|gcid)-/;
|
|
55
|
+
/**
|
|
56
|
+
* Positive validator for CSS length values. Accepts only the v1 unit set
|
|
57
|
+
* from the brief: integer or decimal numbers (including a leading minus
|
|
58
|
+
* sign and bare `0`) followed by `px`, `rem`, `em`, `%`, `vw`, or `vh`.
|
|
59
|
+
* The canonical capture exercised literal values only — variable tokens,
|
|
60
|
+
* `var(...)`, `calc(...)`, and free-form strings like `banana` are all
|
|
61
|
+
* refused before emission. Adding any new unit requires a brief update
|
|
62
|
+
* (canonical-capture re-test if the unit changes server-side rendering).
|
|
63
|
+
*/
|
|
64
|
+
const LITERAL_CSS_LENGTH_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|%|vw|vh)$/;
|
|
65
|
+
/**
|
|
66
|
+
* Validate that `value` is a literal CSS length and not a deferred-shape
|
|
67
|
+
* token. The check runs on EVERY length flag before the emission path.
|
|
68
|
+
*
|
|
69
|
+
* Three refusal paths:
|
|
70
|
+
* 1. `$variable(...)` token — deferred shape, needs its own capture.
|
|
71
|
+
* 2. Bare `gvid-*` / `gcid-*` token name — same deferred shape.
|
|
72
|
+
* 3. Anything else that does NOT match the v1 literal CSS length grammar
|
|
73
|
+
* (`px`, `rem`, `em`, `%`, `vw`, `vh`). Catches `banana`, `var(--x)`,
|
|
74
|
+
* `calc(8px + 2vw)`, etc. — none of which the canonical capture verified.
|
|
75
|
+
*
|
|
76
|
+
* The variable-token branches fire BEFORE the generic literal check so
|
|
77
|
+
* the operator gets the precise "variable-token support deferred" hint
|
|
78
|
+
* rather than the generic "not a literal CSS length" message.
|
|
79
|
+
*/
|
|
80
|
+
function assertLiteralLength(value, flagLabel) {
|
|
81
|
+
// Trim before pattern-matching so leading / trailing whitespace can't
|
|
82
|
+
// bypass any prefix check (e.g. " gvid-space-1"). The raw value
|
|
83
|
+
// appears in the error message so the operator sees exactly what was
|
|
84
|
+
// passed.
|
|
85
|
+
const trimmed = value.trim();
|
|
86
|
+
if (trimmed.startsWith("$variable(")) {
|
|
87
|
+
throw new Error(`${flagLabel} value ${JSON.stringify(value)} is a $variable() token. ` +
|
|
88
|
+
`Variable-token support deferred — pending canonical capture. ` +
|
|
89
|
+
`Canonical capture exercised literal CSS length values only (px / rem / em / % / vw / vh).`);
|
|
90
|
+
}
|
|
91
|
+
if (BARE_VARIABLE_TOKEN_RE.test(trimmed)) {
|
|
92
|
+
throw new Error(`${flagLabel} value ${JSON.stringify(value)} is a bare ${trimmed.split("-")[0]}-* variable token. ` +
|
|
93
|
+
`Variable-token support deferred — pending canonical capture. ` +
|
|
94
|
+
`Canonical capture exercised literal CSS length values only (px / rem / em / % / vw / vh).`);
|
|
95
|
+
}
|
|
96
|
+
if (!LITERAL_CSS_LENGTH_RE.test(trimmed)) {
|
|
97
|
+
throw new Error(`${flagLabel} value ${JSON.stringify(value)} is not a literal CSS length. ` +
|
|
98
|
+
`v1 accepts only \`<number><unit>\` where unit is one of px / rem / em / % / vw / vh ` +
|
|
99
|
+
`(e.g. "40px", "1.5rem", "100%"). The canonical capture exercised literal ` +
|
|
100
|
+
`values only; broader value grammars (var(...), calc(...), etc.) need their own capture.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Compose a single axis bag (padding or margin) from a corner input.
|
|
105
|
+
*
|
|
106
|
+
* Returns `undefined` when the axis input is entirely absent (the axis is
|
|
107
|
+
* absent from the emitted attrs — sparse-emit at axis level). When ANY
|
|
108
|
+
* corner is passed, both sync flags are emitted as paired siblings
|
|
109
|
+
* (default `"off"` on each side unless the caller explicitly toggled
|
|
110
|
+
* them).
|
|
111
|
+
*
|
|
112
|
+
* Sync-flag-only input on an axis with no touched corner is REFUSED with
|
|
113
|
+
* an explicit error — silently no-op'ing a passed sync flag would be a
|
|
114
|
+
* surprising footgun. If the caller wants to toggle sync, they must also
|
|
115
|
+
* pass at least one corner on the same axis to anchor it.
|
|
116
|
+
*/
|
|
117
|
+
function composeAxis(input, flagPrefix) {
|
|
118
|
+
if (!input)
|
|
119
|
+
return undefined;
|
|
120
|
+
const corners = ["top", "right", "bottom", "left"].filter((c) => input[c] !== undefined);
|
|
121
|
+
if (corners.length === 0) {
|
|
122
|
+
// Sync flag passed without any corner on the same axis → explicit
|
|
123
|
+
// refusal. The paired sync flags only have meaning anchored to at
|
|
124
|
+
// least one corner; emitting them alone would silently drop them.
|
|
125
|
+
if (input.syncVertical !== undefined ||
|
|
126
|
+
input.syncHorizontal !== undefined) {
|
|
127
|
+
throw new Error(`--${flagPrefix}-sync-vertical / --${flagPrefix}-sync-horizontal require at ` +
|
|
128
|
+
`least one --${flagPrefix}-{top,right,bottom,left} corner on the same axis. ` +
|
|
129
|
+
`Sync flags are paired-sibling metadata anchored to corners; emitting them ` +
|
|
130
|
+
`alone would silently no-op.`);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const bag = {};
|
|
135
|
+
for (const c of corners) {
|
|
136
|
+
const v = input[c];
|
|
137
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
138
|
+
throw new Error(`--${flagPrefix}-${c} requires a non-empty CSS length value.`);
|
|
139
|
+
}
|
|
140
|
+
assertLiteralLength(v, `--${flagPrefix}-${c}`);
|
|
141
|
+
bag[c] = v;
|
|
142
|
+
}
|
|
143
|
+
bag.syncVertical = input.syncVertical ?? "off";
|
|
144
|
+
bag.syncHorizontal = input.syncHorizontal ?? "off";
|
|
145
|
+
return bag;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Compose the canonical
|
|
149
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}` bag.
|
|
150
|
+
* Sparse-emit per axis; padding and margin are independent.
|
|
151
|
+
*
|
|
152
|
+
* Returns BOTH the full `attrs` tree (what the emitter ships) AND the
|
|
153
|
+
* inner `value` bag — the caller does empty-preset validation against
|
|
154
|
+
* `value` without re-walking the nested attrs tree (no `as any` deep
|
|
155
|
+
* casts).
|
|
156
|
+
*/
|
|
157
|
+
export function composeSpacingAttrs(input) {
|
|
158
|
+
const padding = composeAxis(input.padding, "padding");
|
|
159
|
+
const margin = composeAxis(input.margin, "margin");
|
|
160
|
+
const value = {};
|
|
161
|
+
if (padding)
|
|
162
|
+
value.padding = padding;
|
|
163
|
+
if (margin)
|
|
164
|
+
value.margin = margin;
|
|
165
|
+
const attrs = {
|
|
166
|
+
[SPACING_PRIMARY_ATTR_NAME]: {
|
|
167
|
+
decoration: {
|
|
168
|
+
spacing: {
|
|
169
|
+
desktop: {
|
|
170
|
+
value,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return { attrs, value };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Emit a canonical `divi/spacing` section group preset.
|
|
180
|
+
*
|
|
181
|
+
* 1. Validate input shape (name, module, at least one corner across either
|
|
182
|
+
* axis).
|
|
183
|
+
* 2. Reject variable-token values in any length flag (deferred until a
|
|
184
|
+
* canonical capture lands).
|
|
185
|
+
* 3. Compose sparse-emit `attrs.module.decoration.spacing.desktop.value.*`
|
|
186
|
+
* with paired sync flags.
|
|
187
|
+
* 4. Gate `(divi/spacing, <module>)` against the verified-attrs registry —
|
|
188
|
+
* throws `EvidenceGateError` when effective evidence is below
|
|
189
|
+
* `VB_PRESET_STORAGE_VERIFIED`.
|
|
190
|
+
*
|
|
191
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this entry:
|
|
192
|
+
* the plugin's `/preset/create` route mirrors the single `attrs` bag into
|
|
193
|
+
* all three buckets at write time. The canonical capture fixture documents
|
|
194
|
+
* the post-write storage shape (which is why both `attrs` and `styleAttrs`
|
|
195
|
+
* appear there byte-identical); the CLI emits the request shape only.
|
|
196
|
+
*/
|
|
197
|
+
export function emitSpacingGroupPreset(input, registry = loadRegistry()) {
|
|
198
|
+
if (!input.name || typeof input.name !== "string") {
|
|
199
|
+
throw new Error("spacing emitter requires a non-empty `name`.");
|
|
200
|
+
}
|
|
201
|
+
if (!input.module || typeof input.module !== "string") {
|
|
202
|
+
throw new Error("spacing emitter requires `module` — currently only `divi/section` is " +
|
|
203
|
+
"wired (other module cells are SCHEMA_OBSERVED in the registry; " +
|
|
204
|
+
"promoting them requires a canonical-capture change + a follow-up " +
|
|
205
|
+
"implementation/docs change).");
|
|
206
|
+
}
|
|
207
|
+
const { attrs, value } = composeSpacingAttrs(input);
|
|
208
|
+
// Empty-input rejection: at least one corner across either axis MUST be
|
|
209
|
+
// specified. An empty value bag is a usage error — there is nothing to
|
|
210
|
+
// write, and the resulting preset would be the empty-shell VB authoring
|
|
211
|
+
// footgun documented in the canonical capture.
|
|
212
|
+
if (Object.keys(value).length === 0) {
|
|
213
|
+
throw new Error("spacing emitter produced an empty preset — pass at least one " +
|
|
214
|
+
"padding or margin corner (--padding-top, --margin-bottom, etc.).");
|
|
215
|
+
}
|
|
216
|
+
// Registry gate: dispatches off `(divi/spacing, <module>)`. Cells below
|
|
217
|
+
// VB_PRESET_STORAGE_VERIFIED (currently divi/heading, divi/text,
|
|
218
|
+
// divi/button — all SCHEMA_OBSERVED) throw EvidenceGateError natively
|
|
219
|
+
// here. Unknown modules with no applicability cell resolve to
|
|
220
|
+
// UNVERIFIED (0) and also throw.
|
|
221
|
+
gateWriteAttr(registry, input.module, SPACING_PATTERN_FAMILY);
|
|
222
|
+
// Implementation-supported-modules guard: even if a future registry-only
|
|
223
|
+
// PR promotes another cell to VB_PRESET_STORAGE_VERIFIED, this emitter
|
|
224
|
+
// must NOT silently start emitting it — heading/text/button cells need
|
|
225
|
+
// their own per-module wrapper (the section cell uses `module.*`, but a
|
|
226
|
+
// heading-spacing capture might surface `title.*` or a different
|
|
227
|
+
// primaryAttrName). Per `feedback_preset_map_per_module`, never assume
|
|
228
|
+
// cross-module shape carry-over. Promoting another module requires
|
|
229
|
+
// adding it to `SPACING_SUPPORTED_MODULES` alongside the fixture +
|
|
230
|
+
// tests + README updates that prove the per-module shape.
|
|
231
|
+
if (!SPACING_SUPPORTED_MODULES.includes(input.module)) {
|
|
232
|
+
throw new Error(`spacing emitter does not yet implement module "${input.module}". ` +
|
|
233
|
+
`Registry evidence cleared the cell, but the per-module wrapper + ` +
|
|
234
|
+
`attr shape have not been verified against a canonical capture ` +
|
|
235
|
+
`(this emitter hard-codes the divi/section wrapper "module" and ` +
|
|
236
|
+
`primaryAttrName "module"). Supported modules: ` +
|
|
237
|
+
`${SPACING_SUPPORTED_MODULES.join(", ")}. Adding a new module ` +
|
|
238
|
+
`requires a canonical-capture change landing first AND ` +
|
|
239
|
+
`a follow-up implementation change extending this constant alongside ` +
|
|
240
|
+
`fixtures + tests + README updates.`);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
type: "group",
|
|
244
|
+
module_name: input.module,
|
|
245
|
+
group_name: SPACING_GROUP_NAME,
|
|
246
|
+
group_id: SPACING_GROUP_ID,
|
|
247
|
+
primary_attr_name: SPACING_PRIMARY_ATTR_NAME,
|
|
248
|
+
name: input.name,
|
|
249
|
+
attrs,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Build the `POST /diviops/v1/preset/create` request body from a spacing
|
|
254
|
+
* preset entry. Matches the body shape the `diviops_preset_create` MCP
|
|
255
|
+
* tool posts — the CLI reuses the existing route, it does not add one.
|
|
256
|
+
*
|
|
257
|
+
* `primary_attr_name` IS sent on the wire (the plugin's `/preset/create`
|
|
258
|
+
* route accepts it as an optional snake_case param and stores it as
|
|
259
|
+
* `primaryAttrName` in the preset). The button / font / color emitters
|
|
260
|
+
* omit it because their preset types do not carry it; the divi/spacing
|
|
261
|
+
* cell does, per the canonical capture's load-bearing finding #2.
|
|
262
|
+
*/
|
|
263
|
+
export function buildSpacingPresetCreateBody(entry, opts = {}) {
|
|
264
|
+
const body = {
|
|
265
|
+
module_name: entry.module_name,
|
|
266
|
+
name: entry.name,
|
|
267
|
+
attrs: entry.attrs,
|
|
268
|
+
type: entry.type,
|
|
269
|
+
group_name: entry.group_name,
|
|
270
|
+
group_id: entry.group_id,
|
|
271
|
+
primary_attr_name: entry.primary_attr_name,
|
|
272
|
+
};
|
|
273
|
+
if (opts.dry_run)
|
|
274
|
+
body.dry_run = true;
|
|
275
|
+
return body;
|
|
276
|
+
}
|