@diviops/mcp-server 1.5.12 → 1.5.13
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 +5 -0
- package/dist/preset-cli/__tests__/cli.test.js +204 -0
- package/dist/preset-cli/__tests__/registry.test.js +26 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.d.ts +14 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.js +191 -0
- package/dist/preset-cli/__tests__/write-path.test.js +56 -1
- package/dist/preset-cli/cli.d.ts +3 -0
- package/dist/preset-cli/cli.js +92 -8
- package/dist/preset-cli/text-body-font-emitter.d.ts +127 -0
- package/dist/preset-cli/text-body-font-emitter.js +169 -0
- package/dist/preset-cli/write-path.d.ts +16 -0
- package/dist/preset-cli/write-path.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ run it directly. Current commands:
|
|
|
91
91
|
|---|---|
|
|
92
92
|
| `diviops-preset button [options]` | `divi/button` group preset |
|
|
93
93
|
| `diviops-preset heading-font [options]` | `divi/font` group preset for `divi/heading` (Pattern A — Google Fonts — or Pattern B — local-hosted) |
|
|
94
|
+
| `diviops-preset text-body-font [options]` | `divi/font-body` group preset for `divi/text` — **Pattern A (Google Fonts) only**; Pattern B for body-text has no registered canonical shape and is refused |
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
97
|
diviops-preset button --name "Primary" --bg-color gcid-primary-color \
|
|
@@ -100,6 +101,10 @@ diviops-preset button --name "Primary" --bg-color gcid-primary-color \
|
|
|
100
101
|
diviops-preset heading-font --name "Heading H1" --pattern google \
|
|
101
102
|
--font-family Inter --font-weight 700 \
|
|
102
103
|
--font-color gcid-heading-color --font-size 48px
|
|
104
|
+
|
|
105
|
+
diviops-preset text-body-font --name "Body Text" --pattern google \
|
|
106
|
+
--font-family Inter --font-weight 400 \
|
|
107
|
+
--font-color gcid-body-color --font-size 16px
|
|
103
108
|
```
|
|
104
109
|
|
|
105
110
|
`--dry-run` (the default) composes and prints the canonical JSON with no
|
|
@@ -319,6 +319,210 @@ test("heading-font --apply without credentials exits 1 with a credentials hint",
|
|
|
319
319
|
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
|
+
// ------------------------------------------------------------------
|
|
323
|
+
// text-body-font command — CLI integration (parse → emit → dry-run JSON)
|
|
324
|
+
// Track 6: Pattern A only; Pattern B refused via registry-absence.
|
|
325
|
+
// ------------------------------------------------------------------
|
|
326
|
+
test("--help advertises the text-body-font command (Pattern A only)", async () => {
|
|
327
|
+
const io = capture();
|
|
328
|
+
const code = await run(["--help"], io);
|
|
329
|
+
assert.equal(code, EXIT.OK);
|
|
330
|
+
const help = io.stdout.join("\n");
|
|
331
|
+
assert.match(help, /text-body-font/);
|
|
332
|
+
assert.match(help, /Pattern A/);
|
|
333
|
+
});
|
|
334
|
+
test("text-body-font dry-run (Pattern A) emits canonical JSON, exit 0", async () => {
|
|
335
|
+
const io = capture();
|
|
336
|
+
const code = await run([
|
|
337
|
+
"text-body-font",
|
|
338
|
+
"--name",
|
|
339
|
+
"Body",
|
|
340
|
+
"--pattern",
|
|
341
|
+
"google",
|
|
342
|
+
"--font-family",
|
|
343
|
+
"Inter",
|
|
344
|
+
"--font-weight",
|
|
345
|
+
"400",
|
|
346
|
+
"--font-color",
|
|
347
|
+
"gcid-body-color",
|
|
348
|
+
"--font-size",
|
|
349
|
+
"16px",
|
|
350
|
+
], io);
|
|
351
|
+
assert.equal(code, EXIT.OK);
|
|
352
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
353
|
+
assert.equal(parsed.type, "group");
|
|
354
|
+
assert.equal(parsed.dry_run, true);
|
|
355
|
+
assert.equal(parsed.module_name, "divi/text");
|
|
356
|
+
assert.equal(parsed.group_name, "divi/font-body");
|
|
357
|
+
assert.equal(parsed.group_id, "designText");
|
|
358
|
+
const value = parsed.attrs.content.decoration.bodyFont.body.font.desktop.value;
|
|
359
|
+
assert.equal(value.family, "Inter");
|
|
360
|
+
assert.equal(value.weight, "400");
|
|
361
|
+
assert.equal(value.size, "16px");
|
|
362
|
+
assert.equal(value.color, '$variable({"type":"color","value":{"name":"gcid-body-color","settings":{}}})$');
|
|
363
|
+
});
|
|
364
|
+
test("text-body-font dry-run omits weight/size/lineHeight keys when not specified", async () => {
|
|
365
|
+
const io = capture();
|
|
366
|
+
const code = await run([
|
|
367
|
+
"text-body-font",
|
|
368
|
+
"--name",
|
|
369
|
+
"Body",
|
|
370
|
+
"--pattern",
|
|
371
|
+
"google",
|
|
372
|
+
"--font-family",
|
|
373
|
+
"Inter",
|
|
374
|
+
], io);
|
|
375
|
+
assert.equal(code, EXIT.OK);
|
|
376
|
+
const parsed = JSON.parse(io.stdout.join("\n"));
|
|
377
|
+
const value = parsed.attrs.content.decoration.bodyFont.body.font.desktop.value;
|
|
378
|
+
assert.deepEqual(Object.keys(value), ["family"]);
|
|
379
|
+
});
|
|
380
|
+
test("text-body-font without --pattern exits 1 (invalid input)", async () => {
|
|
381
|
+
const io = capture();
|
|
382
|
+
const code = await run([
|
|
383
|
+
"text-body-font",
|
|
384
|
+
"--name",
|
|
385
|
+
"Body",
|
|
386
|
+
"--font-family",
|
|
387
|
+
"Inter",
|
|
388
|
+
"--font-weight",
|
|
389
|
+
"400",
|
|
390
|
+
], io);
|
|
391
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
392
|
+
assert.match(io.stderr.join("\n"), /--pattern/);
|
|
393
|
+
assert.match(io.stderr.join("\n"), /google\|local/);
|
|
394
|
+
});
|
|
395
|
+
test("text-body-font without --name exits 1", async () => {
|
|
396
|
+
const io = capture();
|
|
397
|
+
const code = await run(["text-body-font", "--pattern", "google", "--font-family", "Inter"], io);
|
|
398
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
399
|
+
assert.match(io.stderr.join("\n"), /requires --name/);
|
|
400
|
+
});
|
|
401
|
+
test("text-body-font --pattern local is refused (registry-absence), exit 2", async () => {
|
|
402
|
+
// Track 6 contract: Pattern B has NO registry entry for `divi/font-body`.
|
|
403
|
+
// The CLI must exit non-zero. The registry-absence throw is NOT an
|
|
404
|
+
// EvidenceGateError (no resolution constructed) — it surfaces as a
|
|
405
|
+
// plain Error and the CLI's fallback branch returns INVALID_INPUT (1).
|
|
406
|
+
// What matters here is: (a) non-zero exit; (b) the error message names
|
|
407
|
+
// both family and variant so the operator can file the gap.
|
|
408
|
+
const io = capture();
|
|
409
|
+
const code = await run([
|
|
410
|
+
"text-body-font",
|
|
411
|
+
"--name",
|
|
412
|
+
"Body",
|
|
413
|
+
"--pattern",
|
|
414
|
+
"local",
|
|
415
|
+
"--font-family",
|
|
416
|
+
"Inter",
|
|
417
|
+
"--font-color",
|
|
418
|
+
"#666666",
|
|
419
|
+
], io);
|
|
420
|
+
assert.notEqual(code, EXIT.OK, "refusal MUST exit non-zero");
|
|
421
|
+
const err = io.stderr.join("\n");
|
|
422
|
+
assert.match(err, /absent from verified-attrs\.json/);
|
|
423
|
+
assert.match(err, /divi\/font-body/);
|
|
424
|
+
assert.match(err, /local_hosted_pattern_b/);
|
|
425
|
+
});
|
|
426
|
+
test("text-body-font --pattern local --font-weight is ALSO refused (compound input)", async () => {
|
|
427
|
+
// Compound-input parity: adding --font-weight (or any other field) does
|
|
428
|
+
// NOT change the refusal — the gate fires on the missing variant entry
|
|
429
|
+
// regardless of which other fields are set.
|
|
430
|
+
const io = capture();
|
|
431
|
+
const code = await run([
|
|
432
|
+
"text-body-font",
|
|
433
|
+
"--name",
|
|
434
|
+
"Body",
|
|
435
|
+
"--pattern",
|
|
436
|
+
"local",
|
|
437
|
+
"--font-family",
|
|
438
|
+
"Inter",
|
|
439
|
+
"--font-weight",
|
|
440
|
+
"700",
|
|
441
|
+
], io);
|
|
442
|
+
assert.notEqual(code, EXIT.OK);
|
|
443
|
+
const err = io.stderr.join("\n");
|
|
444
|
+
assert.match(err, /absent from verified-attrs\.json/);
|
|
445
|
+
assert.match(err, /local_hosted_pattern_b/);
|
|
446
|
+
});
|
|
447
|
+
test("text-body-font --pattern with an invalid value exits 1", async () => {
|
|
448
|
+
const io = capture();
|
|
449
|
+
const code = await run([
|
|
450
|
+
"text-body-font",
|
|
451
|
+
"--name",
|
|
452
|
+
"Body",
|
|
453
|
+
"--pattern",
|
|
454
|
+
"auto",
|
|
455
|
+
"--font-family",
|
|
456
|
+
"Inter",
|
|
457
|
+
], io);
|
|
458
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
459
|
+
assert.match(io.stderr.join("\n"), /--pattern must be/);
|
|
460
|
+
});
|
|
461
|
+
test("text-body-font dry-run requires no credentials and no network", async () => {
|
|
462
|
+
const saved = {
|
|
463
|
+
WP_URL: process.env.WP_URL,
|
|
464
|
+
WP_USER: process.env.WP_USER,
|
|
465
|
+
WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
|
|
466
|
+
};
|
|
467
|
+
delete process.env.WP_URL;
|
|
468
|
+
delete process.env.WP_USER;
|
|
469
|
+
delete process.env.WP_APP_PASSWORD;
|
|
470
|
+
try {
|
|
471
|
+
const io = capture();
|
|
472
|
+
const code = await run([
|
|
473
|
+
"text-body-font",
|
|
474
|
+
"--name",
|
|
475
|
+
"Body",
|
|
476
|
+
"--pattern",
|
|
477
|
+
"google",
|
|
478
|
+
"--font-family",
|
|
479
|
+
"Inter",
|
|
480
|
+
], io);
|
|
481
|
+
assert.equal(code, EXIT.OK);
|
|
482
|
+
assert.equal(io.stderr.length, 0, "no error output on credential-free dry-run");
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
if (saved.WP_URL !== undefined)
|
|
486
|
+
process.env.WP_URL = saved.WP_URL;
|
|
487
|
+
if (saved.WP_USER !== undefined)
|
|
488
|
+
process.env.WP_USER = saved.WP_USER;
|
|
489
|
+
if (saved.WP_APP_PASSWORD !== undefined)
|
|
490
|
+
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
test("text-body-font --apply without credentials exits 1 with a credentials hint", async () => {
|
|
494
|
+
const saved = {
|
|
495
|
+
WP_URL: process.env.WP_URL,
|
|
496
|
+
WP_USER: process.env.WP_USER,
|
|
497
|
+
WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
|
|
498
|
+
};
|
|
499
|
+
delete process.env.WP_URL;
|
|
500
|
+
delete process.env.WP_USER;
|
|
501
|
+
delete process.env.WP_APP_PASSWORD;
|
|
502
|
+
try {
|
|
503
|
+
const io = capture();
|
|
504
|
+
const code = await run([
|
|
505
|
+
"text-body-font",
|
|
506
|
+
"--name",
|
|
507
|
+
"Body",
|
|
508
|
+
"--pattern",
|
|
509
|
+
"google",
|
|
510
|
+
"--font-family",
|
|
511
|
+
"Inter",
|
|
512
|
+
"--apply",
|
|
513
|
+
], io);
|
|
514
|
+
assert.equal(code, EXIT.INVALID_INPUT);
|
|
515
|
+
assert.match(io.stderr.join("\n"), /requires WordPress credentials/);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
if (saved.WP_URL !== undefined)
|
|
519
|
+
process.env.WP_URL = saved.WP_URL;
|
|
520
|
+
if (saved.WP_USER !== undefined)
|
|
521
|
+
process.env.WP_USER = saved.WP_USER;
|
|
522
|
+
if (saved.WP_APP_PASSWORD !== undefined)
|
|
523
|
+
process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
322
526
|
test("dry-run output includes the bypass corner when requested", async () => {
|
|
323
527
|
const io = capture();
|
|
324
528
|
const code = await run(["button", "--name", "P", "--bg-color", "#111", "--bypass-hover-padding-gate"], io);
|
|
@@ -147,3 +147,29 @@ test("variant-aware lookup selects the correct entry by pattern_variant", () =>
|
|
|
147
147
|
// A bogus variant under a known family throws — no silent fallback.
|
|
148
148
|
assert.throws(() => resolveEvidence(reg, "divi/heading", "divi/font", "no_such_variant"), /absent from verified-attrs\.json/);
|
|
149
149
|
});
|
|
150
|
+
test("variant-aware lookup: divi/font-body Pattern A on divi/text clears the write threshold", () => {
|
|
151
|
+
// Track 6 contract: the shipped registry MUST carry one cleared
|
|
152
|
+
// `divi/font-body` + `google_fonts_pattern_a` entry on `divi/text`
|
|
153
|
+
// (effective level = VB_PRESET_STORAGE_VERIFIED). If a registry edit
|
|
154
|
+
// ever drops it, this canary fires.
|
|
155
|
+
const reg = loadRegistry();
|
|
156
|
+
const a = resolveEvidence(reg, "divi/text", "divi/font-body", "google_fonts_pattern_a");
|
|
157
|
+
assert.equal(a.patternVariant, "google_fonts_pattern_a");
|
|
158
|
+
assert.equal(a.effectiveLevel, 4, "divi/font-body Pattern A on divi/text must clear VB_PRESET_STORAGE_VERIFIED");
|
|
159
|
+
// gateWriteAttr must not throw at this cell.
|
|
160
|
+
gateWriteAttr(reg, "divi/text", "divi/font-body", "google_fonts_pattern_a");
|
|
161
|
+
});
|
|
162
|
+
test("variant-aware lookup: divi/font-body Pattern B is absent from the registry (refused)", () => {
|
|
163
|
+
// Track 6 contract: the shipped registry MUST NOT carry a
|
|
164
|
+
// `divi/font-body` + `local_hosted_pattern_b` entry. Pattern B for
|
|
165
|
+
// body-text was deliberately deferred per round-2 _meta — there is no
|
|
166
|
+
// captured evidence. resolveEvidence MUST throw the standard
|
|
167
|
+
// registry-absence error.
|
|
168
|
+
const reg = loadRegistry();
|
|
169
|
+
assert.throws(() => resolveEvidence(reg, "divi/text", "divi/font-body", "local_hosted_pattern_b"), (err) => {
|
|
170
|
+
assert.match(err.message, /absent from verified-attrs\.json/);
|
|
171
|
+
assert.match(err.message, /divi\/font-body/);
|
|
172
|
+
assert.match(err.message, /local_hosted_pattern_b/);
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font-body` text-body-font emitter shape + gating coverage:
|
|
3
|
+
* - fixture-based shape assertion against round-2 (Pattern A) canonical
|
|
4
|
+
* capture for the captured fields;
|
|
5
|
+
* - emit-on-specification: omitted size / lineHeight / weight produce no
|
|
6
|
+
* key in the output;
|
|
7
|
+
* - no renderAttrs on the in-memory entry (mirror happens at route layer);
|
|
8
|
+
* - Pattern B refusal: selecting `--pattern local` resolves to a
|
|
9
|
+
* registry-absence refusal (no `divi/font-body` + `local_hosted_pattern_b`
|
|
10
|
+
* entry exists in verified-attrs.json);
|
|
11
|
+
* - variant-aware registry gating: Pattern A evidence does NOT vouch for
|
|
12
|
+
* Pattern B and vice versa.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font-body` text-body-font emitter shape + gating coverage:
|
|
3
|
+
* - fixture-based shape assertion against round-2 (Pattern A) canonical
|
|
4
|
+
* capture for the captured fields;
|
|
5
|
+
* - emit-on-specification: omitted size / lineHeight / weight produce no
|
|
6
|
+
* key in the output;
|
|
7
|
+
* - no renderAttrs on the in-memory entry (mirror happens at route layer);
|
|
8
|
+
* - Pattern B refusal: selecting `--pattern local` resolves to a
|
|
9
|
+
* registry-absence refusal (no `divi/font-body` + `local_hosted_pattern_b`
|
|
10
|
+
* entry exists in verified-attrs.json);
|
|
11
|
+
* - variant-aware registry gating: Pattern A evidence does NOT vouch for
|
|
12
|
+
* Pattern B and vice versa.
|
|
13
|
+
*/
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { emitTextBodyFontGroupPreset, composeTextBodyFontAttrs, buildTextBodyFontPresetCreateBody, TEXT_BODY_FONT_MODULE, TEXT_BODY_FONT_GROUP_NAME, TEXT_BODY_FONT_GROUP_ID, TEXT_BODY_FONT_PATTERN_VARIANTS, } from "../text-body-font-emitter.js";
|
|
20
|
+
import { loadRegistry } from "../registry.js";
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const REPO_ROOT = join(__dirname, "..", "..", "..", "..");
|
|
23
|
+
const FIXTURE_DIR = join(REPO_ROOT, "docs/verification/evidence/canonical-shape-dumps-2026-05-18");
|
|
24
|
+
const FIXTURE_ROUND_2 = join(FIXTURE_DIR, "round-2-body-text-pattern-a.json");
|
|
25
|
+
const registry = loadRegistry();
|
|
26
|
+
test("emitter byte-matches round-2 (Pattern A — Google Fonts) fixture attrs", () => {
|
|
27
|
+
const fixture = JSON.parse(readFileSync(FIXTURE_ROUND_2, "utf8"));
|
|
28
|
+
const canonicalAttrs = fixture.preset_entry.attrs;
|
|
29
|
+
const entry = emitTextBodyFontGroupPreset({
|
|
30
|
+
name: "canonical-body-text-vb-2026-05-18",
|
|
31
|
+
pattern: "google",
|
|
32
|
+
family: "Inter",
|
|
33
|
+
weight: "400",
|
|
34
|
+
color: '$variable({"type":"color","value":{"name":"gcid-body-color","settings":{}}})$',
|
|
35
|
+
size: "16px",
|
|
36
|
+
}, registry);
|
|
37
|
+
assert.deepEqual(entry.attrs, canonicalAttrs, "emitted attrs must byte-match the Pattern A canonical capture");
|
|
38
|
+
assert.equal(entry.type, "group");
|
|
39
|
+
assert.equal(entry.group_name, TEXT_BODY_FONT_GROUP_NAME);
|
|
40
|
+
assert.equal(entry.group_id, TEXT_BODY_FONT_GROUP_ID);
|
|
41
|
+
assert.equal(entry.module_name, TEXT_BODY_FONT_MODULE);
|
|
42
|
+
assert.equal(entry.pattern_variant, TEXT_BODY_FONT_PATTERN_VARIANTS.google);
|
|
43
|
+
});
|
|
44
|
+
test("emit-on-specification: only specified keys produce keys (no defaults)", () => {
|
|
45
|
+
const attrs = composeTextBodyFontAttrs({
|
|
46
|
+
name: "minimal",
|
|
47
|
+
pattern: "google",
|
|
48
|
+
family: "Inter",
|
|
49
|
+
});
|
|
50
|
+
const value = attrs.content.decoration.bodyFont.body.font.desktop
|
|
51
|
+
.value;
|
|
52
|
+
assert.deepEqual(Object.keys(value), ["family"], "ONLY the touched key emitted — no defaulted weight/color/size/lineHeight");
|
|
53
|
+
});
|
|
54
|
+
test("omitted weight / size / lineHeight produce no keys", () => {
|
|
55
|
+
const attrs = composeTextBodyFontAttrs({
|
|
56
|
+
name: "fc",
|
|
57
|
+
pattern: "google",
|
|
58
|
+
family: "Inter",
|
|
59
|
+
color: "#666666",
|
|
60
|
+
});
|
|
61
|
+
const value = attrs.content.decoration.bodyFont.body.font.desktop
|
|
62
|
+
.value;
|
|
63
|
+
assert.equal("weight" in value, false, "weight omitted → no key");
|
|
64
|
+
assert.equal("size" in value, false, "size omitted → no key");
|
|
65
|
+
assert.equal("lineHeight" in value, false, "lineHeight omitted → no key");
|
|
66
|
+
assert.deepEqual(Object.keys(value).sort(), ["color", "family"]);
|
|
67
|
+
});
|
|
68
|
+
test("entry carries no renderAttrs key (mirror happens at route layer)", () => {
|
|
69
|
+
const entry = emitTextBodyFontGroupPreset({
|
|
70
|
+
name: "no-renderattrs",
|
|
71
|
+
pattern: "google",
|
|
72
|
+
family: "Inter",
|
|
73
|
+
weight: "400",
|
|
74
|
+
}, registry);
|
|
75
|
+
assert.equal("renderAttrs" in entry, false, "renderAttrs must NOT appear on the in-memory entry — the plugin's " +
|
|
76
|
+
"/preset/create route mirrors attrs into styleAttrs/renderAttrs at " +
|
|
77
|
+
"the write layer");
|
|
78
|
+
assert.equal("styleAttrs" in entry, false, "same for styleAttrs");
|
|
79
|
+
});
|
|
80
|
+
test("variable color tokens get the {name,settings} object shape + trailing )$", () => {
|
|
81
|
+
const attrs = composeTextBodyFontAttrs({
|
|
82
|
+
name: "v",
|
|
83
|
+
pattern: "google",
|
|
84
|
+
color: "gcid-body-color",
|
|
85
|
+
});
|
|
86
|
+
const color = attrs.content.decoration.bodyFont.body.font.desktop
|
|
87
|
+
.value.color;
|
|
88
|
+
assert.ok(color.endsWith(")$"));
|
|
89
|
+
assert.match(color, /^\$variable\(/);
|
|
90
|
+
const payload = JSON.parse(color.slice("$variable(".length, -2));
|
|
91
|
+
assert.deepEqual(payload, {
|
|
92
|
+
type: "color",
|
|
93
|
+
value: { name: "gcid-body-color", settings: {} },
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
test("literal hex color is emitted verbatim, not wrapped", () => {
|
|
97
|
+
const attrs = composeTextBodyFontAttrs({
|
|
98
|
+
name: "h",
|
|
99
|
+
pattern: "google",
|
|
100
|
+
color: "#666666",
|
|
101
|
+
});
|
|
102
|
+
assert.equal(attrs.content.decoration.bodyFont.body.font.desktop.value.color, "#666666");
|
|
103
|
+
});
|
|
104
|
+
test("lineHeight is emitted only when specified", () => {
|
|
105
|
+
const withLH = composeTextBodyFontAttrs({
|
|
106
|
+
name: "lh+",
|
|
107
|
+
pattern: "google",
|
|
108
|
+
family: "Inter",
|
|
109
|
+
lineHeight: "1.5",
|
|
110
|
+
});
|
|
111
|
+
assert.equal(withLH.content.decoration.bodyFont.body.font.desktop.value
|
|
112
|
+
.lineHeight, "1.5");
|
|
113
|
+
const without = composeTextBodyFontAttrs({
|
|
114
|
+
name: "lh-",
|
|
115
|
+
pattern: "google",
|
|
116
|
+
family: "Inter",
|
|
117
|
+
});
|
|
118
|
+
assert.equal("lineHeight" in
|
|
119
|
+
without.content.decoration.bodyFont.body.font.desktop.value, false);
|
|
120
|
+
});
|
|
121
|
+
test("emitter rejects an invalid `pattern` value", () => {
|
|
122
|
+
assert.throws(() => emitTextBodyFontGroupPreset(
|
|
123
|
+
// @ts-expect-error — exercising runtime validation
|
|
124
|
+
{ name: "x", pattern: "auto", family: "Inter" }, registry), /pattern.*google.*local/i);
|
|
125
|
+
});
|
|
126
|
+
test("emitter rejects an empty preset (no styling specified)", () => {
|
|
127
|
+
assert.throws(() => emitTextBodyFontGroupPreset({ name: "empty", pattern: "google" }, registry), /empty preset/);
|
|
128
|
+
});
|
|
129
|
+
test("emitter rejects a missing name", () => {
|
|
130
|
+
assert.throws(() => emitTextBodyFontGroupPreset({ name: "", pattern: "google", family: "Inter" }, registry), /requires a non-empty `name`/);
|
|
131
|
+
});
|
|
132
|
+
test("Pattern B (local) is refused — registry-absence for divi/font-body", () => {
|
|
133
|
+
// Track 6 contract: NO `divi/font-body` + `local_hosted_pattern_b` entry
|
|
134
|
+
// exists in verified-attrs.json. `resolveEvidence`'s standard "absent
|
|
135
|
+
// from verified-attrs.json" throw fires natively — no special-case branch
|
|
136
|
+
// in the emitter. The error message MUST name the family AND the variant
|
|
137
|
+
// so operators can file the gap correctly.
|
|
138
|
+
assert.throws(() => emitTextBodyFontGroupPreset({
|
|
139
|
+
name: "body-local",
|
|
140
|
+
pattern: "local",
|
|
141
|
+
family: "Inter",
|
|
142
|
+
color: "#666666",
|
|
143
|
+
}, registry), (err) => {
|
|
144
|
+
assert.match(err.message, /absent from verified-attrs\.json/, "missing variant must fail with the canonical registry-absence message");
|
|
145
|
+
assert.match(err.message, /divi\/font-body/, "error names the pattern family");
|
|
146
|
+
assert.match(err.message, /local_hosted_pattern_b/, "error names the missing variant");
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
test("Pattern B refusal fires even with compound input (e.g. --font-weight)", () => {
|
|
151
|
+
// Compound-input parity: passing additional fields alongside
|
|
152
|
+
// --pattern local does NOT change the refusal — the gate fires on the
|
|
153
|
+
// missing registry entry regardless of which other fields are set.
|
|
154
|
+
assert.throws(() => emitTextBodyFontGroupPreset({
|
|
155
|
+
name: "body-local-weight",
|
|
156
|
+
pattern: "local",
|
|
157
|
+
family: "Inter",
|
|
158
|
+
weight: "700",
|
|
159
|
+
color: "#666666",
|
|
160
|
+
size: "16px",
|
|
161
|
+
}, registry), /absent from verified-attrs\.json/);
|
|
162
|
+
});
|
|
163
|
+
test("real registry: Pattern A on divi/text clears the write threshold", () => {
|
|
164
|
+
// Sanity guard: if a registry edit ever drops the (divi/font-body,
|
|
165
|
+
// google_fonts_pattern_a, divi/text) cell below VB_PRESET_STORAGE_VERIFIED
|
|
166
|
+
// (4), this test is the canary.
|
|
167
|
+
const entry = emitTextBodyFontGroupPreset({
|
|
168
|
+
name: "canary",
|
|
169
|
+
pattern: "google",
|
|
170
|
+
family: "Inter",
|
|
171
|
+
weight: "400",
|
|
172
|
+
}, registry);
|
|
173
|
+
assert.equal(entry.pattern_variant, TEXT_BODY_FONT_PATTERN_VARIANTS.google);
|
|
174
|
+
});
|
|
175
|
+
test("buildTextBodyFontPresetCreateBody mirrors the diviops_preset_create body shape", () => {
|
|
176
|
+
const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter", weight: "400" }, registry);
|
|
177
|
+
const body = buildTextBodyFontPresetCreateBody(entry, { dry_run: true });
|
|
178
|
+
assert.deepEqual(body, {
|
|
179
|
+
module_name: TEXT_BODY_FONT_MODULE,
|
|
180
|
+
name: "Body",
|
|
181
|
+
attrs: entry.attrs,
|
|
182
|
+
type: "group",
|
|
183
|
+
group_name: TEXT_BODY_FONT_GROUP_NAME,
|
|
184
|
+
group_id: TEXT_BODY_FONT_GROUP_ID,
|
|
185
|
+
dry_run: true,
|
|
186
|
+
});
|
|
187
|
+
// pattern_variant is in-memory metadata; it must NOT leak into the wire body.
|
|
188
|
+
assert.equal("pattern_variant" in body, false, "pattern_variant is client-side gating metadata; it must not be sent over the wire");
|
|
189
|
+
const noDry = buildTextBodyFontPresetCreateBody(entry);
|
|
190
|
+
assert.equal("dry_run" in noDry, false);
|
|
191
|
+
});
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { test } from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { applyButtonPreset, applyHeadingFontPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
|
|
10
|
+
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
|
|
11
11
|
import { emitButtonGroupPreset } from "../button-emitter.js";
|
|
12
12
|
import { emitHeadingFontGroupPreset } from "../heading-font-emitter.js";
|
|
13
|
+
import { emitTextBodyFontGroupPreset } from "../text-body-font-emitter.js";
|
|
13
14
|
import { loadRegistry } from "../registry.js";
|
|
14
15
|
const registry = loadRegistry();
|
|
15
16
|
function handshake(capabilities) {
|
|
@@ -155,6 +156,60 @@ test("applyHeadingFontPreset threads dry_run into the body when requested", asyn
|
|
|
155
156
|
const options = client.calls[0].options;
|
|
156
157
|
assert.equal(options.body.dry_run, true);
|
|
157
158
|
});
|
|
159
|
+
// ------------------------------------------------------------------
|
|
160
|
+
// text-body-font apply-mode — mocked only (Track 6).
|
|
161
|
+
// Mirrors the heading-font apply-mode coverage. pattern_variant is
|
|
162
|
+
// in-memory metadata and must NOT appear in the wire body. Pattern B is
|
|
163
|
+
// refused at the emitter level (registry-absence) — apply-mode is never
|
|
164
|
+
// reached for `--pattern local`, so it has no mocked test here.
|
|
165
|
+
// ------------------------------------------------------------------
|
|
166
|
+
test("applyTextBodyFontPreset gates capability BEFORE issuing the write", async () => {
|
|
167
|
+
const client = mockClient({ capabilities: {} });
|
|
168
|
+
const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter", weight: "400" }, registry);
|
|
169
|
+
await assert.rejects(() => applyTextBodyFontPreset(client, entry, {
|
|
170
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
171
|
+
}), CapabilityMissingError);
|
|
172
|
+
assert.equal(client.calls.length, 0, "no write issued when capability is absent");
|
|
173
|
+
});
|
|
174
|
+
test("applyTextBodyFontPreset posts to /preset/create with the canonical body", async () => {
|
|
175
|
+
const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
|
|
176
|
+
const entry = emitTextBodyFontGroupPreset({
|
|
177
|
+
name: "Body",
|
|
178
|
+
pattern: "google",
|
|
179
|
+
family: "Inter",
|
|
180
|
+
weight: "400",
|
|
181
|
+
color: "gcid-body-color",
|
|
182
|
+
size: "16px",
|
|
183
|
+
}, registry);
|
|
184
|
+
const result = await applyTextBodyFontPreset(client, entry, {
|
|
185
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
186
|
+
});
|
|
187
|
+
assert.equal(client.handshakeVersions[0], TEST_SERVER_VERSION);
|
|
188
|
+
assert.equal(client.calls.length, 1);
|
|
189
|
+
const call = client.calls[0];
|
|
190
|
+
assert.equal(call.endpoint, PRESET_CREATE_ROUTE);
|
|
191
|
+
const options = call.options;
|
|
192
|
+
assert.equal(options.method, "POST");
|
|
193
|
+
assert.equal(options.body.type, "group");
|
|
194
|
+
assert.equal(options.body.module_name, "divi/text");
|
|
195
|
+
assert.equal(options.body.group_name, "divi/font-body");
|
|
196
|
+
assert.equal(options.body.group_id, "designText");
|
|
197
|
+
assert.equal(options.body.name, "Body");
|
|
198
|
+
assert.deepEqual(options.body.attrs, entry.attrs);
|
|
199
|
+
assert.equal("pattern_variant" in options.body, false, "pattern_variant is client-side gating metadata; it must not be on the wire");
|
|
200
|
+
assert.equal("dry_run" in options.body, false);
|
|
201
|
+
assert.deepEqual(result, { ok: true, data: { preset_id: "mocked123" } });
|
|
202
|
+
});
|
|
203
|
+
test("applyTextBodyFontPreset threads dry_run into the body when requested", async () => {
|
|
204
|
+
const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
|
|
205
|
+
const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter" }, registry);
|
|
206
|
+
await applyTextBodyFontPreset(client, entry, {
|
|
207
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
208
|
+
dry_run: true,
|
|
209
|
+
});
|
|
210
|
+
const options = client.calls[0].options;
|
|
211
|
+
assert.equal(options.body.dry_run, true);
|
|
212
|
+
});
|
|
158
213
|
test("buildClientFromEnv throws CredentialsMissingError when env vars are absent", () => {
|
|
159
214
|
assert.throws(() => buildClientFromEnv({}), (err) => {
|
|
160
215
|
assert.ok(err instanceof CredentialsMissingError);
|
package/dist/preset-cli/cli.d.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type ButtonEmitterInput } from "./button-emitter.js";
|
|
26
26
|
import { type HeadingFontEmitterInput } from "./heading-font-emitter.js";
|
|
27
|
+
import { type TextBodyFontEmitterInput } from "./text-body-font-emitter.js";
|
|
27
28
|
export declare const EXIT: {
|
|
28
29
|
readonly OK: 0;
|
|
29
30
|
readonly INVALID_INPUT: 1;
|
|
@@ -53,6 +54,8 @@ export declare class UsageError extends Error {
|
|
|
53
54
|
export declare function buildButtonInput(parsed: ParsedArgs): ButtonEmitterInput;
|
|
54
55
|
/** Map parsed `heading-font` options into the heading-font emitter input shape. */
|
|
55
56
|
export declare function buildHeadingFontInput(parsed: ParsedArgs): HeadingFontEmitterInput;
|
|
57
|
+
/** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
|
|
58
|
+
export declare function buildTextBodyFontInput(parsed: ParsedArgs): TextBodyFontEmitterInput;
|
|
56
59
|
/**
|
|
57
60
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
58
61
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
package/dist/preset-cli/cli.js
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { emitButtonGroupPreset, buildPresetCreateBody, } from "./button-emitter.js";
|
|
26
26
|
import { emitHeadingFontGroupPreset, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, } from "./heading-font-emitter.js";
|
|
27
|
+
import { emitTextBodyFontGroupPreset, buildTextBodyFontPresetCreateBody, } from "./text-body-font-emitter.js";
|
|
27
28
|
import { EvidenceGateError } from "./registry.js";
|
|
28
|
-
import { applyButtonPreset, applyHeadingFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
29
|
+
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
29
30
|
export const EXIT = {
|
|
30
31
|
OK: 0,
|
|
31
32
|
INVALID_INPUT: 1,
|
|
@@ -40,10 +41,12 @@ const realIO = {
|
|
|
40
41
|
const HELP = `diviops-preset — Divi 5.5.x canonical preset-emitter CLI
|
|
41
42
|
|
|
42
43
|
USAGE
|
|
43
|
-
diviops-preset button [options]
|
|
44
|
-
diviops-preset heading-font [options]
|
|
45
|
-
|
|
46
|
-
diviops-preset
|
|
44
|
+
diviops-preset button [options] Emit a divi/button group preset
|
|
45
|
+
diviops-preset heading-font [options] Emit a divi/font group preset for
|
|
46
|
+
divi/heading
|
|
47
|
+
diviops-preset text-body-font [options] Emit a divi/font-body group preset
|
|
48
|
+
for divi/text (Pattern A only)
|
|
49
|
+
diviops-preset --help Show this help
|
|
47
50
|
|
|
48
51
|
MODE
|
|
49
52
|
--dry-run Compose + print canonical JSON only (DEFAULT).
|
|
@@ -99,6 +102,27 @@ heading-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
|
99
102
|
--font-size <v> Font size (e.g. "48px").
|
|
100
103
|
--font-line-height <v> Font line-height (e.g. "1.1").
|
|
101
104
|
|
|
105
|
+
text-body-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
106
|
+
--name <string> Preset display name (required).
|
|
107
|
+
--pattern <google|local> Required. Example: --pattern google.
|
|
108
|
+
Pattern A is the only supported variant for now:
|
|
109
|
+
google — Pattern A: plain family + optional
|
|
110
|
+
numeric weight (e.g. family "Inter",
|
|
111
|
+
weight "400"). Verified against
|
|
112
|
+
round-2 body-text fixture.
|
|
113
|
+
local — Pattern B for divi/font-body is NOT
|
|
114
|
+
registered (no canonical-shape
|
|
115
|
+
capture exists yet). Selecting it
|
|
116
|
+
lands on a registry-absence refusal.
|
|
117
|
+
There is NO default; omitting --pattern is
|
|
118
|
+
invalid input.
|
|
119
|
+
--font-family <string> Font family (plain name, e.g. "Inter").
|
|
120
|
+
--font-weight <string> Font weight (e.g. "400").
|
|
121
|
+
--font-color <value> Font color. Hex literal, bare gcid-*/gvid-*
|
|
122
|
+
token, or already-formed $variable(...)$ token.
|
|
123
|
+
--font-size <v> Font size (e.g. "16px").
|
|
124
|
+
--font-line-height <v> Font line-height (e.g. "1.5").
|
|
125
|
+
|
|
102
126
|
EXIT CODES
|
|
103
127
|
0 success 1 invalid input 2 evidence-gate refusal
|
|
104
128
|
3 capability missing 4 write error
|
|
@@ -117,6 +141,10 @@ EXAMPLES
|
|
|
117
141
|
diviops-preset heading-font --name "Heading H1 (local)" --pattern local \\
|
|
118
142
|
--font-family "Sora 700" \\
|
|
119
143
|
--font-color gcid-heading-color --font-size 48px
|
|
144
|
+
|
|
145
|
+
diviops-preset text-body-font --name "Body Text" --pattern google \\
|
|
146
|
+
--font-family Inter --font-weight 400 \\
|
|
147
|
+
--font-color gcid-body-color --font-size 16px
|
|
120
148
|
`;
|
|
121
149
|
const VALUE_FLAGS = new Set([
|
|
122
150
|
"--name",
|
|
@@ -201,7 +229,7 @@ export class UsageError extends Error {
|
|
|
201
229
|
}
|
|
202
230
|
}
|
|
203
231
|
/** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
|
|
204
|
-
const KNOWN_COMMANDS = new Set(["button", "heading-font"]);
|
|
232
|
+
const KNOWN_COMMANDS = new Set(["button", "heading-font", "text-body-font"]);
|
|
205
233
|
/** Map parsed `button` options into the emitter input shape. */
|
|
206
234
|
export function buildButtonInput(parsed) {
|
|
207
235
|
const opt = (k) => {
|
|
@@ -324,6 +352,48 @@ export function buildHeadingFontInput(parsed) {
|
|
|
324
352
|
input.lineHeight = lineHeight;
|
|
325
353
|
return input;
|
|
326
354
|
}
|
|
355
|
+
/** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
|
|
356
|
+
export function buildTextBodyFontInput(parsed) {
|
|
357
|
+
const opt = (k) => {
|
|
358
|
+
const v = parsed.options.get(k);
|
|
359
|
+
return typeof v === "string" ? v : undefined;
|
|
360
|
+
};
|
|
361
|
+
const name = opt("--name");
|
|
362
|
+
if (!name) {
|
|
363
|
+
throw new UsageError("text-body-font command requires --name <string>.");
|
|
364
|
+
}
|
|
365
|
+
// --pattern is REQUIRED — there is no safe default. Pattern A only;
|
|
366
|
+
// passing "local" lands on the registry-absence refusal downstream
|
|
367
|
+
// (no Pattern B entry exists for `divi/font-body`).
|
|
368
|
+
const patternRaw = opt("--pattern");
|
|
369
|
+
if (patternRaw === undefined) {
|
|
370
|
+
throw new UsageError("text-body-font command requires --pattern <google|local>. " +
|
|
371
|
+
"Example: --pattern google. Pattern A (google) is the only " +
|
|
372
|
+
"supported variant for now; Pattern B (local) has no registry " +
|
|
373
|
+
"entry for `divi/font-body` and will be refused.");
|
|
374
|
+
}
|
|
375
|
+
if (patternRaw !== "google" && patternRaw !== "local") {
|
|
376
|
+
throw new UsageError(`--pattern must be "google" or "local"; got ${JSON.stringify(patternRaw)}.`);
|
|
377
|
+
}
|
|
378
|
+
const pattern = patternRaw;
|
|
379
|
+
const input = { name, pattern };
|
|
380
|
+
const family = opt("--font-family");
|
|
381
|
+
if (family !== undefined)
|
|
382
|
+
input.family = family;
|
|
383
|
+
const weight = opt("--font-weight");
|
|
384
|
+
if (weight !== undefined)
|
|
385
|
+
input.weight = weight;
|
|
386
|
+
const color = opt("--font-color");
|
|
387
|
+
if (color !== undefined)
|
|
388
|
+
input.color = color;
|
|
389
|
+
const size = opt("--font-size");
|
|
390
|
+
if (size !== undefined)
|
|
391
|
+
input.size = size;
|
|
392
|
+
const lineHeight = opt("--font-line-height");
|
|
393
|
+
if (lineHeight !== undefined)
|
|
394
|
+
input.lineHeight = lineHeight;
|
|
395
|
+
return input;
|
|
396
|
+
}
|
|
327
397
|
/**
|
|
328
398
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
329
399
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
|
@@ -365,13 +435,27 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
365
435
|
dryRunBody = buildPresetCreateBody(entry, { dry_run: true });
|
|
366
436
|
applyFn = (client, sv) => applyButtonPreset(client, entry, { serverVersion: sv });
|
|
367
437
|
}
|
|
368
|
-
else {
|
|
369
|
-
// heading-font
|
|
438
|
+
else if (parsed.command === "heading-font") {
|
|
370
439
|
const input = buildHeadingFontInput(parsed);
|
|
371
440
|
const entry = emitHeadingFontGroupPreset(input);
|
|
372
441
|
dryRunBody = buildHeadingFontPresetCreateBody(entry, { dry_run: true });
|
|
373
442
|
applyFn = (client, sv) => applyHeadingFontPreset(client, entry, { serverVersion: sv });
|
|
374
443
|
}
|
|
444
|
+
else if (parsed.command === "text-body-font") {
|
|
445
|
+
// text-body-font (Pattern A only; Pattern B lands on the
|
|
446
|
+
// registry-absence refusal in emitTextBodyFontGroupPreset).
|
|
447
|
+
const input = buildTextBodyFontInput(parsed);
|
|
448
|
+
const entry = emitTextBodyFontGroupPreset(input);
|
|
449
|
+
dryRunBody = buildTextBodyFontPresetCreateBody(entry, { dry_run: true });
|
|
450
|
+
applyFn = (client, sv) => applyTextBodyFontPreset(client, entry, { serverVersion: sv });
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Defensive: a new entry in KNOWN_COMMANDS without a dispatch
|
|
454
|
+
// branch here would silently break dry-run/apply. parseArgs
|
|
455
|
+
// already gates on KNOWN_COMMANDS upstream, so reaching this is a
|
|
456
|
+
// programmer error in the dispatch wiring.
|
|
457
|
+
throw new UsageError(`Unhandled command: ${parsed.command}`);
|
|
458
|
+
}
|
|
375
459
|
}
|
|
376
460
|
catch (err) {
|
|
377
461
|
if (err instanceof EvidenceGateError) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font-body` body-text group-preset emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font-body` preset
|
|
5
|
+
* targeting `divi/text` at
|
|
6
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
8
|
+
* `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
|
|
9
|
+
* (`divi/font-body` section), cross-checked against
|
|
10
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-2-body-text-pattern-a.json`.
|
|
11
|
+
*
|
|
12
|
+
* Scope: Pattern A (Google Fonts) ONLY. Pattern B (local-hosted) has NO
|
|
13
|
+
* registry entry for `divi/font-body` — the heading-vs-body shape
|
|
14
|
+
* divergence question deferred per round-2 _meta. Selecting
|
|
15
|
+
* `pattern: "local"` therefore resolves to a missing-variant registry-
|
|
16
|
+
* absence error from `resolveEvidence` (the standard
|
|
17
|
+
* `findPatternEntry → undefined → throw` shape). No special-cased
|
|
18
|
+
* Pattern-B branch lives in this file — the registry IS the gate.
|
|
19
|
+
*
|
|
20
|
+
* Shape rules enforced here:
|
|
21
|
+
* - Emit-on-specification only — omitted params produce NO keys.
|
|
22
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
23
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
24
|
+
* VB save semantics); the CLI request body only carries `attrs`.
|
|
25
|
+
* - Wrapper chain: `content.decoration.bodyFont.body.font.desktop.value.*` —
|
|
26
|
+
* note the per-bucket divergence vs `divi/font` (which uses
|
|
27
|
+
* `title.decoration.font.font.desktop.value.*`). Sibling buckets do NOT
|
|
28
|
+
* share canonical keys (per `feedback_preset_map_per_module`).
|
|
29
|
+
* - Link color (`bodyFont.link.font.desktop.value.color`) is OUT OF SCOPE
|
|
30
|
+
* for this emitter — no `--link-color` option, no link emission. Needs
|
|
31
|
+
* a dedicated capture-backed follow-up before the CLI writes it.
|
|
32
|
+
* - Body-text emission on modules OTHER than `divi/text` is OUT OF SCOPE
|
|
33
|
+
* — Testimonial / Accordion-item / Slide / Blurb cells are
|
|
34
|
+
* `SCHEMA_OBSERVED` in the registry today (under the write threshold)
|
|
35
|
+
* and the gate will refuse them.
|
|
36
|
+
*/
|
|
37
|
+
import { type VerifiedAttrsRegistry } from "./registry.js";
|
|
38
|
+
export declare const TEXT_BODY_FONT_MODULE = "divi/text";
|
|
39
|
+
export declare const TEXT_BODY_FONT_GROUP_NAME = "divi/font-body";
|
|
40
|
+
export declare const TEXT_BODY_FONT_GROUP_ID = "designText";
|
|
41
|
+
export declare const TEXT_BODY_FONT_PATTERN_FAMILY = "divi/font-body";
|
|
42
|
+
/**
|
|
43
|
+
* The verified pattern variants the CLI honors for `divi/font-body`.
|
|
44
|
+
*
|
|
45
|
+
* Only Pattern A is supported. Pattern B is included in the type so the
|
|
46
|
+
* CLI surface mirrors `heading-font` (and so `--pattern local` parses
|
|
47
|
+
* cleanly enough to reach the registry-gate refusal), but its registry
|
|
48
|
+
* entry is intentionally absent — `resolveEvidence` will throw a
|
|
49
|
+
* registry-absence error when the local variant is selected. There is NO
|
|
50
|
+
* shape-level Pattern B encoding here (no weight-in-family heuristic, no
|
|
51
|
+
* `weight`-refusal branch beyond what the missing-variant gate produces).
|
|
52
|
+
*/
|
|
53
|
+
export declare const TEXT_BODY_FONT_PATTERN_VARIANTS: {
|
|
54
|
+
readonly google: "google_fonts_pattern_a";
|
|
55
|
+
readonly local: "local_hosted_pattern_b";
|
|
56
|
+
};
|
|
57
|
+
export type TextBodyFontPattern = "google" | "local";
|
|
58
|
+
export interface TextBodyFontEmitterInput {
|
|
59
|
+
/** Required display name for the preset. */
|
|
60
|
+
name: string;
|
|
61
|
+
/**
|
|
62
|
+
* Required pattern selector. There is no default. Only Pattern A is
|
|
63
|
+
* supported; passing `"local"` resolves to a registry-absence refusal
|
|
64
|
+
* (no Pattern B entry exists for `divi/font-body` today).
|
|
65
|
+
*/
|
|
66
|
+
pattern: TextBodyFontPattern;
|
|
67
|
+
/** Font family (Pattern A: plain family name, e.g. `"Inter"`). */
|
|
68
|
+
family?: string;
|
|
69
|
+
/** Font weight (numeric-string, e.g. `"400"`); emit-on-specification. */
|
|
70
|
+
weight?: string;
|
|
71
|
+
/** Font color — literal hex or bare/formed variable token. */
|
|
72
|
+
color?: string;
|
|
73
|
+
/** Font size — literal (e.g. `"16px"`) or already-formed `$variable(...)$`. */
|
|
74
|
+
size?: string;
|
|
75
|
+
/** Line height — optional, emit-on-specification only. */
|
|
76
|
+
lineHeight?: string;
|
|
77
|
+
}
|
|
78
|
+
/** The composed canonical preset entry shape sent to `/preset/create`. */
|
|
79
|
+
export interface TextBodyFontPresetEntry {
|
|
80
|
+
type: "group";
|
|
81
|
+
module_name: string;
|
|
82
|
+
group_name: string;
|
|
83
|
+
group_id: string;
|
|
84
|
+
pattern_variant: string;
|
|
85
|
+
name: string;
|
|
86
|
+
attrs: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compose the canonical
|
|
90
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value` bag from the
|
|
91
|
+
* input. Emit-on-specification: only specified sub-fields produce keys.
|
|
92
|
+
*/
|
|
93
|
+
export declare function composeTextBodyFontAttrs(input: TextBodyFontEmitterInput): Record<string, unknown>;
|
|
94
|
+
/**
|
|
95
|
+
* Emit a canonical `divi/font-body` body-text group preset for `divi/text`.
|
|
96
|
+
*
|
|
97
|
+
* 1. Validate input shape (name, pattern).
|
|
98
|
+
* 2. Compose `attrs.content.decoration.bodyFont.body.font.desktop.value.*`
|
|
99
|
+
* (emit-on-specification).
|
|
100
|
+
* 3. Gate the chosen `(divi/font-body, pattern_variant)` cell on
|
|
101
|
+
* `divi/text` against the verified-attrs registry — throws an Error
|
|
102
|
+
* (registry-absence) when the variant is missing (Pattern B is
|
|
103
|
+
* missing by design), or `EvidenceGateError` when effective evidence
|
|
104
|
+
* is below `VB_PRESET_STORAGE_VERIFIED`.
|
|
105
|
+
*
|
|
106
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this
|
|
107
|
+
* entry: the plugin's `/preset/create` route mirrors the single `attrs`
|
|
108
|
+
* bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
|
|
109
|
+
* to match VB save semantics. The Round 2 fixture captures the post-write
|
|
110
|
+
* storage shape (note: round-2 _meta records `renderAttrs_present: false`
|
|
111
|
+
* for font-styling group presets, which is consistent with rounds 1a/1b —
|
|
112
|
+
* the mirror happens at the route layer regardless).
|
|
113
|
+
*/
|
|
114
|
+
export declare function emitTextBodyFontGroupPreset(input: TextBodyFontEmitterInput, registry?: VerifiedAttrsRegistry): TextBodyFontPresetEntry;
|
|
115
|
+
/**
|
|
116
|
+
* Build the `POST /diviops/v1/preset/create` request body from a
|
|
117
|
+
* text-body-font preset entry. Matches the body shape the
|
|
118
|
+
* `diviops_preset_create` MCP tool posts — the CLI reuses the existing
|
|
119
|
+
* route, it does not add one.
|
|
120
|
+
*
|
|
121
|
+
* `pattern_variant` is informational metadata on the in-memory entry and
|
|
122
|
+
* is NOT sent over the wire — the registry-gate decision happens
|
|
123
|
+
* client-side before the write.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildTextBodyFontPresetCreateBody(entry: TextBodyFontPresetEntry, opts?: {
|
|
126
|
+
dry_run?: boolean;
|
|
127
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font-body` body-text group-preset emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font-body` preset
|
|
5
|
+
* targeting `divi/text` at
|
|
6
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
8
|
+
* `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
|
|
9
|
+
* (`divi/font-body` section), cross-checked against
|
|
10
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-2-body-text-pattern-a.json`.
|
|
11
|
+
*
|
|
12
|
+
* Scope: Pattern A (Google Fonts) ONLY. Pattern B (local-hosted) has NO
|
|
13
|
+
* registry entry for `divi/font-body` — the heading-vs-body shape
|
|
14
|
+
* divergence question deferred per round-2 _meta. Selecting
|
|
15
|
+
* `pattern: "local"` therefore resolves to a missing-variant registry-
|
|
16
|
+
* absence error from `resolveEvidence` (the standard
|
|
17
|
+
* `findPatternEntry → undefined → throw` shape). No special-cased
|
|
18
|
+
* Pattern-B branch lives in this file — the registry IS the gate.
|
|
19
|
+
*
|
|
20
|
+
* Shape rules enforced here:
|
|
21
|
+
* - Emit-on-specification only — omitted params produce NO keys.
|
|
22
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
23
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
24
|
+
* VB save semantics); the CLI request body only carries `attrs`.
|
|
25
|
+
* - Wrapper chain: `content.decoration.bodyFont.body.font.desktop.value.*` —
|
|
26
|
+
* note the per-bucket divergence vs `divi/font` (which uses
|
|
27
|
+
* `title.decoration.font.font.desktop.value.*`). Sibling buckets do NOT
|
|
28
|
+
* share canonical keys (per `feedback_preset_map_per_module`).
|
|
29
|
+
* - Link color (`bodyFont.link.font.desktop.value.color`) is OUT OF SCOPE
|
|
30
|
+
* for this emitter — no `--link-color` option, no link emission. Needs
|
|
31
|
+
* a dedicated capture-backed follow-up before the CLI writes it.
|
|
32
|
+
* - Body-text emission on modules OTHER than `divi/text` is OUT OF SCOPE
|
|
33
|
+
* — Testimonial / Accordion-item / Slide / Blurb cells are
|
|
34
|
+
* `SCHEMA_OBSERVED` in the registry today (under the write threshold)
|
|
35
|
+
* and the gate will refuse them.
|
|
36
|
+
*/
|
|
37
|
+
import { loadRegistry, gateWriteAttr, } from "./registry.js";
|
|
38
|
+
import { normalizeColorValue } from "./variable-token.js";
|
|
39
|
+
export const TEXT_BODY_FONT_MODULE = "divi/text";
|
|
40
|
+
export const TEXT_BODY_FONT_GROUP_NAME = "divi/font-body";
|
|
41
|
+
export const TEXT_BODY_FONT_GROUP_ID = "designText";
|
|
42
|
+
export const TEXT_BODY_FONT_PATTERN_FAMILY = "divi/font-body";
|
|
43
|
+
/**
|
|
44
|
+
* The verified pattern variants the CLI honors for `divi/font-body`.
|
|
45
|
+
*
|
|
46
|
+
* Only Pattern A is supported. Pattern B is included in the type so the
|
|
47
|
+
* CLI surface mirrors `heading-font` (and so `--pattern local` parses
|
|
48
|
+
* cleanly enough to reach the registry-gate refusal), but its registry
|
|
49
|
+
* entry is intentionally absent — `resolveEvidence` will throw a
|
|
50
|
+
* registry-absence error when the local variant is selected. There is NO
|
|
51
|
+
* shape-level Pattern B encoding here (no weight-in-family heuristic, no
|
|
52
|
+
* `weight`-refusal branch beyond what the missing-variant gate produces).
|
|
53
|
+
*/
|
|
54
|
+
export const TEXT_BODY_FONT_PATTERN_VARIANTS = {
|
|
55
|
+
google: "google_fonts_pattern_a",
|
|
56
|
+
local: "local_hosted_pattern_b",
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Compose the canonical
|
|
60
|
+
* `attrs.content.decoration.bodyFont.body.font.desktop.value` bag from the
|
|
61
|
+
* input. Emit-on-specification: only specified sub-fields produce keys.
|
|
62
|
+
*/
|
|
63
|
+
export function composeTextBodyFontAttrs(input) {
|
|
64
|
+
const value = {};
|
|
65
|
+
if (input.family !== undefined)
|
|
66
|
+
value.family = input.family;
|
|
67
|
+
if (input.weight !== undefined)
|
|
68
|
+
value.weight = input.weight;
|
|
69
|
+
if (input.color !== undefined)
|
|
70
|
+
value.color = normalizeColorValue(input.color);
|
|
71
|
+
if (input.size !== undefined)
|
|
72
|
+
value.size = input.size;
|
|
73
|
+
if (input.lineHeight !== undefined)
|
|
74
|
+
value.lineHeight = input.lineHeight;
|
|
75
|
+
return {
|
|
76
|
+
content: {
|
|
77
|
+
decoration: {
|
|
78
|
+
bodyFont: {
|
|
79
|
+
body: {
|
|
80
|
+
font: {
|
|
81
|
+
desktop: {
|
|
82
|
+
value,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Emit a canonical `divi/font-body` body-text group preset for `divi/text`.
|
|
93
|
+
*
|
|
94
|
+
* 1. Validate input shape (name, pattern).
|
|
95
|
+
* 2. Compose `attrs.content.decoration.bodyFont.body.font.desktop.value.*`
|
|
96
|
+
* (emit-on-specification).
|
|
97
|
+
* 3. Gate the chosen `(divi/font-body, pattern_variant)` cell on
|
|
98
|
+
* `divi/text` against the verified-attrs registry — throws an Error
|
|
99
|
+
* (registry-absence) when the variant is missing (Pattern B is
|
|
100
|
+
* missing by design), or `EvidenceGateError` when effective evidence
|
|
101
|
+
* is below `VB_PRESET_STORAGE_VERIFIED`.
|
|
102
|
+
*
|
|
103
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this
|
|
104
|
+
* entry: the plugin's `/preset/create` route mirrors the single `attrs`
|
|
105
|
+
* bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
|
|
106
|
+
* to match VB save semantics. The Round 2 fixture captures the post-write
|
|
107
|
+
* storage shape (note: round-2 _meta records `renderAttrs_present: false`
|
|
108
|
+
* for font-styling group presets, which is consistent with rounds 1a/1b —
|
|
109
|
+
* the mirror happens at the route layer regardless).
|
|
110
|
+
*/
|
|
111
|
+
export function emitTextBodyFontGroupPreset(input, registry = loadRegistry()) {
|
|
112
|
+
if (!input.name || typeof input.name !== "string") {
|
|
113
|
+
throw new Error("text-body-font emitter requires a non-empty `name`.");
|
|
114
|
+
}
|
|
115
|
+
if (input.pattern !== "google" && input.pattern !== "local") {
|
|
116
|
+
throw new Error(`text-body-font emitter requires \`pattern\` to be "google" or "local"; got ${JSON.stringify(input.pattern)}. ` +
|
|
117
|
+
`There is no default — Pattern A (google) is the only registry-verified variant for ` +
|
|
118
|
+
`\`divi/font-body\` today; Pattern B (local) resolves to a registry-absence refusal.`);
|
|
119
|
+
}
|
|
120
|
+
const attrs = composeTextBodyFontAttrs(input);
|
|
121
|
+
// Sanity check: at least one styling field was specified. An empty
|
|
122
|
+
// value bag is a usage error — there is nothing to write.
|
|
123
|
+
const value = attrs.content.decoration.bodyFont.body.font.desktop
|
|
124
|
+
.value;
|
|
125
|
+
if (!value || Object.keys(value).length === 0) {
|
|
126
|
+
throw new Error("text-body-font emitter produced an empty preset — pass at least one of " +
|
|
127
|
+
"family, weight, color, size, or lineHeight.");
|
|
128
|
+
}
|
|
129
|
+
// Registry gate: variant-aware. Pattern A evidence must NOT vouch for
|
|
130
|
+
// Pattern B and vice versa — gateWriteAttr resolves by both family AND
|
|
131
|
+
// variant, so a missing/under-verified variant entry throws here.
|
|
132
|
+
// Pattern B has NO registry entry for `divi/font-body`, so
|
|
133
|
+
// `--pattern local` lands on `resolveEvidence`'s "absent from
|
|
134
|
+
// verified-attrs.json" throw natively — no special-cased branch needed.
|
|
135
|
+
const patternVariant = TEXT_BODY_FONT_PATTERN_VARIANTS[input.pattern];
|
|
136
|
+
gateWriteAttr(registry, TEXT_BODY_FONT_MODULE, TEXT_BODY_FONT_PATTERN_FAMILY, patternVariant);
|
|
137
|
+
return {
|
|
138
|
+
type: "group",
|
|
139
|
+
module_name: TEXT_BODY_FONT_MODULE,
|
|
140
|
+
group_name: TEXT_BODY_FONT_GROUP_NAME,
|
|
141
|
+
group_id: TEXT_BODY_FONT_GROUP_ID,
|
|
142
|
+
pattern_variant: patternVariant,
|
|
143
|
+
name: input.name,
|
|
144
|
+
attrs,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build the `POST /diviops/v1/preset/create` request body from a
|
|
149
|
+
* text-body-font preset entry. Matches the body shape the
|
|
150
|
+
* `diviops_preset_create` MCP tool posts — the CLI reuses the existing
|
|
151
|
+
* route, it does not add one.
|
|
152
|
+
*
|
|
153
|
+
* `pattern_variant` is informational metadata on the in-memory entry and
|
|
154
|
+
* is NOT sent over the wire — the registry-gate decision happens
|
|
155
|
+
* client-side before the write.
|
|
156
|
+
*/
|
|
157
|
+
export function buildTextBodyFontPresetCreateBody(entry, opts = {}) {
|
|
158
|
+
const body = {
|
|
159
|
+
module_name: entry.module_name,
|
|
160
|
+
name: entry.name,
|
|
161
|
+
attrs: entry.attrs,
|
|
162
|
+
type: entry.type,
|
|
163
|
+
group_name: entry.group_name,
|
|
164
|
+
group_id: entry.group_id,
|
|
165
|
+
};
|
|
166
|
+
if (opts.dry_run)
|
|
167
|
+
body.dry_run = true;
|
|
168
|
+
return body;
|
|
169
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { HandshakeResult } from "../compatibility.js";
|
|
|
19
19
|
import type { DiviopsResponse } from "../envelope.js";
|
|
20
20
|
import type { ButtonPresetEntry } from "./button-emitter.js";
|
|
21
21
|
import type { HeadingFontPresetEntry } from "./heading-font-emitter.js";
|
|
22
|
+
import type { TextBodyFontPresetEntry } from "./text-body-font-emitter.js";
|
|
22
23
|
/** The plugin capability the storage-path capability contract ships. */
|
|
23
24
|
export declare const STORAGE_CAPABILITY = "storage_multipath_probe_v1";
|
|
24
25
|
/** The REST route the CLI posts to — same route `diviops_preset_create` uses. */
|
|
@@ -72,3 +73,18 @@ export declare function applyHeadingFontPreset(client: PresetWriteClient, entry:
|
|
|
72
73
|
serverVersion: string;
|
|
73
74
|
dry_run?: boolean;
|
|
74
75
|
}): Promise<DiviopsResponse<unknown>>;
|
|
76
|
+
/**
|
|
77
|
+
* Apply a `divi/font-body` body-text group preset for `divi/text`:
|
|
78
|
+
* capability-gate, then POST to `/preset/create`. Mirrors
|
|
79
|
+
* `applyHeadingFontPreset`'s sequence — the capability check runs BEFORE
|
|
80
|
+
* the write, and the write reuses the existing storage-routed route (no
|
|
81
|
+
* plugin route is added).
|
|
82
|
+
*
|
|
83
|
+
* The `pattern_variant` metadata is intentionally NOT in the wire body —
|
|
84
|
+
* variant selection is a client-side registry-gate decision and the
|
|
85
|
+
* server route accepts only the standard preset-create fields.
|
|
86
|
+
*/
|
|
87
|
+
export declare function applyTextBodyFontPreset(client: PresetWriteClient, entry: TextBodyFontPresetEntry, opts: {
|
|
88
|
+
serverVersion: string;
|
|
89
|
+
dry_run?: boolean;
|
|
90
|
+
}): Promise<DiviopsResponse<unknown>>;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { WPClient } from "../wp-client.js";
|
|
18
18
|
import { buildPresetCreateBody } from "./button-emitter.js";
|
|
19
19
|
import { buildHeadingFontPresetCreateBody } from "./heading-font-emitter.js";
|
|
20
|
+
import { buildTextBodyFontPresetCreateBody } from "./text-body-font-emitter.js";
|
|
20
21
|
/** The plugin capability the storage-path capability contract ships. */
|
|
21
22
|
export const STORAGE_CAPABILITY = "storage_multipath_probe_v1";
|
|
22
23
|
/** The REST route the CLI posts to — same route `diviops_preset_create` uses. */
|
|
@@ -108,3 +109,24 @@ export async function applyHeadingFontPreset(client, entry, opts) {
|
|
|
108
109
|
body,
|
|
109
110
|
});
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Apply a `divi/font-body` body-text group preset for `divi/text`:
|
|
114
|
+
* capability-gate, then POST to `/preset/create`. Mirrors
|
|
115
|
+
* `applyHeadingFontPreset`'s sequence — the capability check runs BEFORE
|
|
116
|
+
* the write, and the write reuses the existing storage-routed route (no
|
|
117
|
+
* plugin route is added).
|
|
118
|
+
*
|
|
119
|
+
* The `pattern_variant` metadata is intentionally NOT in the wire body —
|
|
120
|
+
* variant selection is a client-side registry-gate decision and the
|
|
121
|
+
* server route accepts only the standard preset-create fields.
|
|
122
|
+
*/
|
|
123
|
+
export async function applyTextBodyFontPreset(client, entry, opts) {
|
|
124
|
+
await assertStorageCapability(client, opts.serverVersion);
|
|
125
|
+
const body = buildTextBodyFontPresetCreateBody(entry, {
|
|
126
|
+
dry_run: opts.dry_run,
|
|
127
|
+
});
|
|
128
|
+
return client.requestEnveloped(PRESET_CREATE_ROUTE, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body,
|
|
131
|
+
});
|
|
132
|
+
}
|