@adobe/helix-md2jcr 1.3.0 → 1.5.0
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/CHANGELOG.md +21 -0
- package/README.md +5 -0
- package/docs/block-options.md +3 -2
- package/docs/phrasing.md +54 -0
- package/docs/section-styles.md +65 -0
- package/docs/superpowers/plans/2026-06-08-section-style-grouping.md +969 -0
- package/package.json +1 -1
- package/src/mdast2jcr/domain/GroupOptions.js +112 -0
- package/src/mdast2jcr/domain/Models.js +18 -4
- package/src/mdast2jcr/hb/helpers/section-helper.js +23 -0
- package/src/mdast2jcr/hb/partials/grid-table.js +6 -88
- package/src/mdast2jcr/hb/partials/paragraph.js +2 -1
- package/src/mdast2jcr/hb/partials/supports/hast-handlers.js +40 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
# Section Style Options (Element Grouping for `style`) Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Bring PR #215's `classes`/`classes_*` element-grouping for block options to sections, so a section model's `style`/`style_*` group is reverse-mapped from the single Section Metadata `style` cell into the correct JCR attributes.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Generalize the routing helpers from PR #215 (currently hardcoded to the `classes` group and living inside `grid-table.js`) into a shared, group-name-parameterized module. Blocks keep calling them with `'classes'`; the section helper calls them with `'style'`, fed by the tokenized value of the single well-known `style` Section Metadata cell. The block path already alphabetically sorts its output attributes, so generalizing the helper's result ordering is safe for blocks; sections emit attributes in model-field order for deterministic, readable XML.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node ESM, Handlebars helpers/partials, mdast (remark gridtables), Mocha + Chai/assert golden-file fixtures (`*.md` -> `*.xml`), c8.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Background / Why
|
|
14
|
+
|
|
15
|
+
Per the AEM spec (markup-sections-blocks): *"the only well-known section metadata property is **Style** which will be turned into additional CSS classes added to the containing section element."* `style` is therefore the section analog of a block's `classes`. PR #215 implemented element grouping for `classes`; this plan implements the identical concept for the `style` group on sections.
|
|
16
|
+
|
|
17
|
+
**Current gap** (`src/mdast2jcr/hb/helpers/section-helper.js`): `processMetadataRows` maps each metadata row to a model field by exact name (`getField(model, key)`) and silently drops keys with no field. A `style_*` field never matches the single `style` row, so grouped section style options cannot be reverse-mapped. The distribution logic (`collectOptionValues`, `distributeBlockOptions`, `isClassesField`) lives entirely in `grid-table.js`/`Models.js` and is hardcoded to `classes`; the section helper does not use it.
|
|
18
|
+
|
|
19
|
+
## Design decisions (locked)
|
|
20
|
+
|
|
21
|
+
1. **Reuse strategy:** Generalize the helpers by group name (one shared implementation) — not a section-local duplicate.
|
|
22
|
+
2. **Token source:** The single, well-known `style` Section Metadata cell (comma-separated), routed across `style` + `style_*` fields. Per-field rows are not the canonical authored form (a directly authored `style_x` row would still be set by the normal row path, but fixtures use only the collapsed `style` cell).
|
|
23
|
+
3. **`modelFields` asymmetry (intentional):** For blocks, the `classes` group is excluded from `modelFields` (block options, not content). For sections, the `style` group **stays in** `modelFields` — the Universal Editor needs those fields to render the section form, and existing section fixtures already include `style`. `getModelFieldNames` is therefore left unchanged (it excludes only the `classes` group).
|
|
24
|
+
4. **Trigger gate:** Section style distribution runs only when the model declares at least one grouped field whose name starts with `style_`. A section model with only a base `style` field keeps the existing per-row behavior (no change to existing fixtures, including the `multiselect` `[a,b]` formatting).
|
|
25
|
+
5. **Case sensitivity:** Row keys are matched case-sensitively against lowercase field names (`style`), consistent with all existing fixtures and `getField`. Not changing this here.
|
|
26
|
+
|
|
27
|
+
## File Structure
|
|
28
|
+
|
|
29
|
+
- **Create** `src/mdast2jcr/domain/GroupOptions.js` — pure routing helpers `collectOptionValues` and `distributeGroupOptions(model, tokens, group)`. One responsibility: turn a flat token list into a `{ fieldName: value }` map for one element-grouping group. No mdast/Handlebars deps; imports only `isGroupField` from `Models.js`.
|
|
30
|
+
- **Modify** `src/mdast2jcr/domain/Models.js` — add `isGroupField(name, group)`; redefine `isClassesField` in terms of it; export `isGroupField`.
|
|
31
|
+
- **Modify** `src/mdast2jcr/hb/partials/grid-table.js` — delete the local `collectOptionValues`/`distributeBlockOptions`; import `distributeGroupOptions` from `GroupOptions.js`; update the two call sites to pass `'classes'`.
|
|
32
|
+
- **Modify** `src/mdast2jcr/hb/helpers/section-helper.js` — import `distributeGroupOptions`; in `processMetadataRows`, when the model has a `style_*` field, distribute the `style` cell's tokens across the style group.
|
|
33
|
+
- **Create** fixtures under `test/fixtures/blocks/core/section-style-options/` — four scenarios mirroring the block-options suite.
|
|
34
|
+
- **Modify** `test/block.test.js` — add a `section style options` describe block with four `it`s.
|
|
35
|
+
- **Modify** `test/models.test.js` — unit tests for `isGroupField`.
|
|
36
|
+
- **Create** `test/group.options.test.js` — unit tests for `distributeGroupOptions`.
|
|
37
|
+
- **Create** `docs/section-styles.md` and **modify** `README.md` — document the feature.
|
|
38
|
+
|
|
39
|
+
## How to run tests
|
|
40
|
+
|
|
41
|
+
- Full suite (144+ tests): `npm test`
|
|
42
|
+
- A single file: `npx mocha test/<file>.test.js` (mocha config auto-loads `test/setup-env.js`)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Task 1: Add `isGroupField` to `Models.js`
|
|
47
|
+
|
|
48
|
+
**Files:**
|
|
49
|
+
- Modify: `src/mdast2jcr/domain/Models.js`
|
|
50
|
+
- Test: `test/models.test.js`
|
|
51
|
+
|
|
52
|
+
- [ ] **Step 1: Write the failing test**
|
|
53
|
+
|
|
54
|
+
Add to `test/models.test.js` a new `describe` inside the top-level `describe('Models Utility Test', ...)` block, and add `isGroupField` to the import from `../src/mdast2jcr/domain/Models.js`.
|
|
55
|
+
|
|
56
|
+
Update the import:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import {
|
|
60
|
+
findModelById,
|
|
61
|
+
getField,
|
|
62
|
+
getModelFieldNames,
|
|
63
|
+
isGroupField,
|
|
64
|
+
} from '../src/mdast2jcr/domain/Models.js';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add the test:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
describe('isGroupField', () => {
|
|
71
|
+
it('matches the base group name and its underscored options', () => {
|
|
72
|
+
assert.strictEqual(isGroupField('classes', 'classes'), true);
|
|
73
|
+
assert.strictEqual(isGroupField('classes_background', 'classes'), true);
|
|
74
|
+
assert.strictEqual(isGroupField('style', 'style'), true);
|
|
75
|
+
assert.strictEqual(isGroupField('style_fullwidth', 'style'), true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not match other fields or a different group', () => {
|
|
79
|
+
assert.strictEqual(isGroupField('title', 'classes'), false);
|
|
80
|
+
assert.strictEqual(isGroupField('style', 'classes'), false);
|
|
81
|
+
assert.strictEqual(isGroupField('stylebox', 'style'), false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
87
|
+
|
|
88
|
+
Run: `npx mocha test/models.test.js`
|
|
89
|
+
Expected: FAIL — `isGroupField is not a function` (import is `undefined`).
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 3: Implement `isGroupField` and rebase `isClassesField` on it**
|
|
92
|
+
|
|
93
|
+
In `src/mdast2jcr/domain/Models.js`, replace the existing `isClassesField` definition (currently lines 44-53) with:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
/**
|
|
97
|
+
* Returns true if the field name belongs to an element-grouping group — either
|
|
98
|
+
* the base field named exactly `group`, or a grouped option field `group_<name>`
|
|
99
|
+
* (element grouping for block options / section styles).
|
|
100
|
+
* @param {string} name The field name.
|
|
101
|
+
* @param {string} group The group name (e.g. `classes` or `style`).
|
|
102
|
+
* @return {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function isGroupField(name, group) {
|
|
105
|
+
return name === group || name.startsWith(`${group}_`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns true if the field name belongs to the `classes` block-options group —
|
|
110
|
+
* either the base `classes` field or a grouped `classes_*` option field
|
|
111
|
+
* (element grouping for block options).
|
|
112
|
+
* @param {string} name The field name.
|
|
113
|
+
* @return {boolean}
|
|
114
|
+
*/
|
|
115
|
+
function isClassesField(name) {
|
|
116
|
+
return isGroupField(name, 'classes');
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then add `isGroupField` to the export block at the bottom of the file:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
export {
|
|
124
|
+
getField,
|
|
125
|
+
findModelById,
|
|
126
|
+
getModelFieldNames,
|
|
127
|
+
isClassesField,
|
|
128
|
+
isGroupField,
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
133
|
+
|
|
134
|
+
Run: `npx mocha test/models.test.js`
|
|
135
|
+
Expected: PASS (all existing Models tests + the two new `isGroupField` tests).
|
|
136
|
+
|
|
137
|
+
- [ ] **Step 5: Commit**
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
git add src/mdast2jcr/domain/Models.js test/models.test.js
|
|
141
|
+
git commit -m "feat(models): add group-name-parameterized isGroupField helper
|
|
142
|
+
|
|
143
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Task 2: Create the shared `GroupOptions.js` module
|
|
149
|
+
|
|
150
|
+
**Files:**
|
|
151
|
+
- Create: `src/mdast2jcr/domain/GroupOptions.js`
|
|
152
|
+
- Test: `test/group.options.test.js`
|
|
153
|
+
|
|
154
|
+
This moves `collectOptionValues` and the generalized `distributeGroupOptions` out of `grid-table.js`. Two behavior notes vs. the original `distributeBlockOptions`: (a) it takes a `group` parameter (default `'classes'`); (b) it emits the result in **model-field order** (the original emitted in match order). Block output is alphabetically sorted downstream by `sortJcrProperties`, so (b) does not change any block fixture; it gives sections clean, deterministic ordering.
|
|
155
|
+
|
|
156
|
+
- [ ] **Step 1: Write the failing test**
|
|
157
|
+
|
|
158
|
+
Create `test/group.options.test.js`:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
/*
|
|
162
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
163
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
164
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
165
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
166
|
+
*
|
|
167
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
168
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
169
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
170
|
+
* governing permissions and limitations under the License.
|
|
171
|
+
*/
|
|
172
|
+
/* eslint-env mocha */
|
|
173
|
+
|
|
174
|
+
import assert from 'assert';
|
|
175
|
+
import { distributeGroupOptions } from '../src/mdast2jcr/domain/GroupOptions.js';
|
|
176
|
+
|
|
177
|
+
describe('GroupOptions Utility Test', () => {
|
|
178
|
+
describe('distributeGroupOptions', () => {
|
|
179
|
+
it('routes select + select + boolean for the style group', () => {
|
|
180
|
+
const model = {
|
|
181
|
+
fields: [
|
|
182
|
+
{ component: 'text', name: 'name' },
|
|
183
|
+
{ component: 'select', name: 'style', options: [{ value: 'variant-a' }, { value: 'variant-b' }] },
|
|
184
|
+
{ component: 'select', name: 'style_background', options: [{ value: 'light' }, { value: 'dark' }] },
|
|
185
|
+
{ component: 'boolean', name: 'style_fullwidth' },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
const result = distributeGroupOptions(model, ['variant-a', 'light', 'fullwidth'], 'style');
|
|
189
|
+
assert.deepStrictEqual(result, {
|
|
190
|
+
style: 'variant-a',
|
|
191
|
+
style_background: 'light',
|
|
192
|
+
style_fullwidth: 'true',
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('lets a multi field claim every matching token (space after comma)', () => {
|
|
197
|
+
const model = {
|
|
198
|
+
fields: [
|
|
199
|
+
{ component: 'select', name: 'style', options: [{ value: 'variant-a' }, { value: 'variant-b' }] },
|
|
200
|
+
{ component: 'multiselect', name: 'style_background', multi: true, options: [{ value: 'light' }, { value: 'dark' }, { value: 'muted' }] },
|
|
201
|
+
{ component: 'boolean', name: 'style_fullwidth' },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
const result = distributeGroupOptions(model, ['variant-b', 'light', 'muted'], 'style');
|
|
205
|
+
assert.deepStrictEqual(result, {
|
|
206
|
+
style: 'variant-b',
|
|
207
|
+
style_background: '[light, muted]',
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('falls back leftover tokens to the free-form base field', () => {
|
|
212
|
+
const model = {
|
|
213
|
+
fields: [
|
|
214
|
+
{ component: 'text', name: 'style', multi: true },
|
|
215
|
+
{ component: 'boolean', name: 'style_sticky' },
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
const result = distributeGroupOptions(model, ['highlight', 'featured', 'sticky'], 'style');
|
|
219
|
+
assert.deepStrictEqual(result, {
|
|
220
|
+
style: '[highlight, featured]',
|
|
221
|
+
style_sticky: 'true',
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('emits only matched booleans, in model-field order', () => {
|
|
226
|
+
const model = {
|
|
227
|
+
fields: [
|
|
228
|
+
{ component: 'boolean', name: 'style_dark' },
|
|
229
|
+
{ component: 'boolean', name: 'style_fullwidth' },
|
|
230
|
+
{ component: 'boolean', name: 'style_narrow' },
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
const result = distributeGroupOptions(model, ['dark', 'narrow'], 'style');
|
|
234
|
+
assert.deepStrictEqual(result, {
|
|
235
|
+
style_dark: 'true',
|
|
236
|
+
style_narrow: 'true',
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('defaults to the classes group and returns {} when nothing matches', () => {
|
|
241
|
+
const model = { fields: [{ component: 'select', name: 'classes', options: [{ value: 'a' }] }] };
|
|
242
|
+
assert.deepStrictEqual(distributeGroupOptions(model, ['a']), { classes: 'a' });
|
|
243
|
+
assert.deepStrictEqual(distributeGroupOptions(model, []), {});
|
|
244
|
+
assert.deepStrictEqual(distributeGroupOptions({ fields: [] }, ['a'], 'style'), {});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
251
|
+
|
|
252
|
+
Run: `npx mocha test/group.options.test.js`
|
|
253
|
+
Expected: FAIL — cannot find module `../src/mdast2jcr/domain/GroupOptions.js`.
|
|
254
|
+
|
|
255
|
+
- [ ] **Step 3: Create the module**
|
|
256
|
+
|
|
257
|
+
Create `src/mdast2jcr/domain/GroupOptions.js`:
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
/*
|
|
261
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
262
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
263
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
264
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
265
|
+
*
|
|
266
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
267
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
268
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
269
|
+
* governing permissions and limitations under the License.
|
|
270
|
+
*/
|
|
271
|
+
import { isGroupField } from './Models.js';
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @typedef {import('../index.d.ts').ModelDef} Model
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Recursively collect every option `value` from a field's options tree
|
|
279
|
+
* (options may be nested under `children`).
|
|
280
|
+
* @param {Array<object>} options - The field's options array.
|
|
281
|
+
* @return {Array<string>} The flat list of option values.
|
|
282
|
+
*/
|
|
283
|
+
function collectOptionValues(options) {
|
|
284
|
+
return (options || []).flatMap((option) => {
|
|
285
|
+
if (option.children) {
|
|
286
|
+
return collectOptionValues(option.children);
|
|
287
|
+
}
|
|
288
|
+
return option.value !== undefined ? [option.value] : [];
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Distribute element-grouping tokens (a block header's classes, a container
|
|
294
|
+
* child's leading cell, or a section's `style` cell) across the model's group
|
|
295
|
+
* fields, per the AEM "element grouping for block options" rules:
|
|
296
|
+
*
|
|
297
|
+
* - a boolean field receives "true" when its name suffix (after `${group}_`)
|
|
298
|
+
* appears as a token (e.g. token "fullwidth" -> `${group}_fullwidth`="true");
|
|
299
|
+
* - a text/select field with options claims the tokens that match its option
|
|
300
|
+
* values (e.g. token "light" -> `${group}_background`="light");
|
|
301
|
+
* - any remaining tokens fall back to the base `${group}` field, which also
|
|
302
|
+
* covers the common single-field case (a free-form `${group}` field).
|
|
303
|
+
*
|
|
304
|
+
* Multi fields are written as `[a, b]`; single fields as a comma-separated list.
|
|
305
|
+
* The result is emitted in model-field order so callers that do not sort
|
|
306
|
+
* (e.g. sections) get deterministic, readable output.
|
|
307
|
+
* @param {Model} model - The block, item, or section model.
|
|
308
|
+
* @param {Array<string>} tokens - The option tokens.
|
|
309
|
+
* @param {string} [group] - The group name (defaults to `classes`).
|
|
310
|
+
* @return {Object<string, string>} A map of group field name to property value.
|
|
311
|
+
*/
|
|
312
|
+
function distributeGroupOptions(model, tokens, group = 'classes') {
|
|
313
|
+
const groupFields = (model?.fields || []).filter((f) => isGroupField(f.name, group));
|
|
314
|
+
if (groupFields.length === 0 || tokens.length === 0) {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const suffixRegExp = new RegExp(`^${group}_`);
|
|
319
|
+
const remaining = [...tokens];
|
|
320
|
+
const assigned = new Map();
|
|
321
|
+
|
|
322
|
+
// boolean fields: the suffix after `${group}_` becomes the option when present
|
|
323
|
+
groupFields
|
|
324
|
+
.filter((field) => field.component === 'boolean')
|
|
325
|
+
.forEach((field) => {
|
|
326
|
+
const suffix = field.name.replace(suffixRegExp, '');
|
|
327
|
+
const idx = remaining.indexOf(suffix);
|
|
328
|
+
if (idx !== -1) {
|
|
329
|
+
assigned.set(field.name, true);
|
|
330
|
+
remaining.splice(idx, 1);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// text/select fields with options: claim the tokens that match their values
|
|
335
|
+
groupFields
|
|
336
|
+
.filter((field) => field.component !== 'boolean' && field.options)
|
|
337
|
+
.forEach((field) => {
|
|
338
|
+
const values = collectOptionValues(field.options);
|
|
339
|
+
const matched = remaining.filter((t) => values.includes(t));
|
|
340
|
+
if (matched.length > 0) {
|
|
341
|
+
assigned.set(field.name, matched);
|
|
342
|
+
matched.forEach((m) => remaining.splice(remaining.indexOf(m), 1));
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// leftover tokens fall back to the base `${group}` field (and the single-field case)
|
|
347
|
+
const base = groupFields.find((f) => f.name === group && f.component !== 'boolean');
|
|
348
|
+
if (remaining.length > 0 && base) {
|
|
349
|
+
assigned.set(group, [...(assigned.get(group) || []), ...remaining]);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// emit in model-field order for deterministic, readable output
|
|
353
|
+
const result = {};
|
|
354
|
+
groupFields.forEach((field) => {
|
|
355
|
+
if (!assigned.has(field.name)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const value = assigned.get(field.name);
|
|
359
|
+
if (value === true) {
|
|
360
|
+
result[field.name] = 'true';
|
|
361
|
+
} else {
|
|
362
|
+
result[field.name] = field.multi === true ? `[${value.join(', ')}]` : value.join(', ');
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export {
|
|
369
|
+
collectOptionValues,
|
|
370
|
+
distributeGroupOptions,
|
|
371
|
+
};
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
375
|
+
|
|
376
|
+
Run: `npx mocha test/group.options.test.js`
|
|
377
|
+
Expected: PASS (all five `distributeGroupOptions` tests).
|
|
378
|
+
|
|
379
|
+
- [ ] **Step 5: Commit**
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
git add src/mdast2jcr/domain/GroupOptions.js test/group.options.test.js
|
|
383
|
+
git commit -m "feat(group-options): extract group-parameterized distribution helper
|
|
384
|
+
|
|
385
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### Task 3: Wire `grid-table.js` to the shared module (no behavior change for blocks)
|
|
391
|
+
|
|
392
|
+
**Files:**
|
|
393
|
+
- Modify: `src/mdast2jcr/hb/partials/grid-table.js`
|
|
394
|
+
- Test (regression): `test/block.test.js`
|
|
395
|
+
|
|
396
|
+
- [ ] **Step 1: Confirm the block-options baseline is green**
|
|
397
|
+
|
|
398
|
+
Run: `npx mocha test/block.test.js`
|
|
399
|
+
Expected: PASS. This is the regression guard for this task — output must be identical after the refactor.
|
|
400
|
+
|
|
401
|
+
- [ ] **Step 2: Delete the local helpers from `grid-table.js`**
|
|
402
|
+
|
|
403
|
+
Remove the entire block from line 41 through line 124 inclusive — the `// -- Block Options (classes) ...` comment, the `collectOptionValues` function, and the `distributeBlockOptions` function. Leave the following `// -- Block Header ...` section intact.
|
|
404
|
+
|
|
405
|
+
- [ ] **Step 3: Import `distributeGroupOptions` from the shared module**
|
|
406
|
+
|
|
407
|
+
Add the import after the existing `Models.js` import (currently lines 20-23):
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
import {
|
|
411
|
+
findModelById,
|
|
412
|
+
getModelFieldNames, isClassesField,
|
|
413
|
+
} from '../../domain/Models.js';
|
|
414
|
+
import { distributeGroupOptions } from '../../domain/GroupOptions.js';
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
(`isClassesField` is still imported — it is used at the container child-item gate.)
|
|
418
|
+
|
|
419
|
+
- [ ] **Step 4: Update the two call sites to pass `'classes'`**
|
|
420
|
+
|
|
421
|
+
In `extractBlockHeaderProperties` (was line 196):
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
if (model) {
|
|
425
|
+
Object.assign(props, distributeGroupOptions(model, blockDetails.classes, 'classes'));
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
In the `blockItem` branch (was line 449):
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
// distribute the options across the model's classes group fields
|
|
433
|
+
Object.assign(properties, distributeGroupOptions(model, classes, 'classes'));
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
- [ ] **Step 5: Run the block tests + lint to verify no regression**
|
|
437
|
+
|
|
438
|
+
Run: `npx mocha test/block.test.js && npx eslint src/mdast2jcr/hb/partials/grid-table.js src/mdast2jcr/domain/GroupOptions.js`
|
|
439
|
+
Expected: PASS, no lint errors. All block-options fixtures (`block-options-grouping`, `-multi`, `-fallback`, `-booleans`) and container-block fixtures unchanged.
|
|
440
|
+
|
|
441
|
+
- [ ] **Step 6: Commit**
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
git add src/mdast2jcr/hb/partials/grid-table.js
|
|
445
|
+
git commit -m "refactor(grid-table): use shared distributeGroupOptions for classes
|
|
446
|
+
|
|
447
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
### Task 4: Distribute the section `style` group in `section-helper.js`
|
|
453
|
+
|
|
454
|
+
**Files:**
|
|
455
|
+
- Modify: `src/mdast2jcr/hb/helpers/section-helper.js`
|
|
456
|
+
- Test: deferred to Task 5 (golden-file fixtures exercise this end-to-end)
|
|
457
|
+
|
|
458
|
+
- [ ] **Step 1: Import the shared helper**
|
|
459
|
+
|
|
460
|
+
In `src/mdast2jcr/hb/helpers/section-helper.js`, add after the existing `Models.js` import (currently lines 16-20):
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
import {
|
|
464
|
+
findModelById,
|
|
465
|
+
getField,
|
|
466
|
+
getModelFieldNames,
|
|
467
|
+
} from '../../domain/Models.js';
|
|
468
|
+
import { distributeGroupOptions } from '../../domain/GroupOptions.js';
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
- [ ] **Step 2: Distribute the `style` cell in `processMetadataRows`**
|
|
472
|
+
|
|
473
|
+
Replace the body of `processMetadataRows` (currently lines 58-97) with:
|
|
474
|
+
|
|
475
|
+
```js
|
|
476
|
+
function processMetadataRows(rows, model) {
|
|
477
|
+
const attributes = {};
|
|
478
|
+
|
|
479
|
+
// always push the model fields so UE can use them, even if no attributes are found
|
|
480
|
+
const modelFields = getModelFieldNames(model);
|
|
481
|
+
|
|
482
|
+
// a section uses element grouping when it declares a `style_*` field; in that
|
|
483
|
+
// case the single well-known `style` cell collapses the whole style group and
|
|
484
|
+
// must be distributed back across style / style_* fields. A model with only a
|
|
485
|
+
// base `style` field keeps the simple per-row mapping below.
|
|
486
|
+
const hasStyleGroup = (model?.fields || []).some((f) => f.name.startsWith('style_'));
|
|
487
|
+
|
|
488
|
+
for (const row of rows) {
|
|
489
|
+
const cells = findAll(row, (n) => n.type === 'gtCell', true);
|
|
490
|
+
if (cells.length < 2) {
|
|
491
|
+
// eslint-disable-next-line no-continue
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const key = toString(cells[0])?.trim();
|
|
496
|
+
const value = toString(cells[1])?.trim();
|
|
497
|
+
|
|
498
|
+
// Skip empty keys/values and the special blockModelId field
|
|
499
|
+
if (!key || !value || key === 'blockModelId') {
|
|
500
|
+
// eslint-disable-next-line no-continue
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// element grouping for section styles: route the flat `style` cell across
|
|
505
|
+
// the model's style group fields (boolean by name suffix, select/multiselect
|
|
506
|
+
// by declared options, leftovers to the base `style` field)
|
|
507
|
+
if (hasStyleGroup && key === 'style') {
|
|
508
|
+
Object.assign(
|
|
509
|
+
attributes,
|
|
510
|
+
distributeGroupOptions(
|
|
511
|
+
model,
|
|
512
|
+
value.split(',').map((s) => s.trim()).filter(Boolean),
|
|
513
|
+
'style',
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
// eslint-disable-next-line no-continue
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const field = getField(model, key);
|
|
521
|
+
if (!field) {
|
|
522
|
+
// eslint-disable-next-line no-continue
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (field.component === 'multiselect') {
|
|
527
|
+
const multiValue = value.split(',')
|
|
528
|
+
.map((s) => s.trim())
|
|
529
|
+
.filter(Boolean);
|
|
530
|
+
attributes[key] = `[${multiValue.join(',')}]`;
|
|
531
|
+
} else {
|
|
532
|
+
attributes[key] = value;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return { attributes, modelFields };
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
- [ ] **Step 3: Sanity-check existing section tests still pass**
|
|
541
|
+
|
|
542
|
+
Run: `npx mocha test/block.test.js --grep "section-metadata"`
|
|
543
|
+
Expected: PASS. The existing `section-metadata` and `section-metadata-custom` fixtures use models with no `style_*` field, so `hasStyleGroup` is `false` and their output is unchanged.
|
|
544
|
+
|
|
545
|
+
- [ ] **Step 4: Lint**
|
|
546
|
+
|
|
547
|
+
Run: `npx eslint src/mdast2jcr/hb/helpers/section-helper.js`
|
|
548
|
+
Expected: no errors.
|
|
549
|
+
|
|
550
|
+
- [ ] **Step 5: Commit**
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
git add src/mdast2jcr/hb/helpers/section-helper.js
|
|
554
|
+
git commit -m "feat(section-metadata): distribute style group via element grouping
|
|
555
|
+
|
|
556
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
### Task 5: Golden-file fixtures + `section style options` test suite
|
|
562
|
+
|
|
563
|
+
**Files:**
|
|
564
|
+
- Create: `test/fixtures/blocks/core/section-style-options/section-style-grouping/{_section-style-grouping.json,section-style-grouping.md,section-style-grouping.xml}`
|
|
565
|
+
- Create: `test/fixtures/blocks/core/section-style-options/section-style-grouping-multi/{_...json,...md,...xml}`
|
|
566
|
+
- Create: `test/fixtures/blocks/core/section-style-options/section-style-grouping-fallback/{_...json,...md,...xml}`
|
|
567
|
+
- Create: `test/fixtures/blocks/core/section-style-options/section-style-grouping-booleans/{_...json,...md,...xml}`
|
|
568
|
+
- Modify: `test/block.test.js`
|
|
569
|
+
|
|
570
|
+
Each fixture defines the global `section` model (no `blockModelId` needed) plus a single section containing a heading and a Section Metadata table. The `_<spec>.json` consolidated format (`{ definitions, models, filters }`) is loaded by `loadBlockResources`.
|
|
571
|
+
|
|
572
|
+
- [ ] **Step 1: Write the failing test wiring**
|
|
573
|
+
|
|
574
|
+
In `test/block.test.js`, add a new top-level describe immediately after the `describe('block options', ...)` block (which ends at line 337, just before `describe('cust-x', ...)`):
|
|
575
|
+
|
|
576
|
+
```js
|
|
577
|
+
/**
|
|
578
|
+
* Section style options via element grouping. A section model exposes its CSS
|
|
579
|
+
* classes through a `style` group (the base `style` field plus any `style_*`
|
|
580
|
+
* field). The single well-known Section Metadata `style` cell collapses the
|
|
581
|
+
* whole group; md2jcr routes each token back to its field — select/multiselect
|
|
582
|
+
* by declared options, boolean by name suffix — with leftovers falling back to
|
|
583
|
+
* the base `style` field. Unlike a block's `classes` group, the section `style`
|
|
584
|
+
* group stays in modelFields (the UE renders those fields).
|
|
585
|
+
*/
|
|
586
|
+
describe('section style options', () => {
|
|
587
|
+
const folder = 'blocks/core/section-style-options';
|
|
588
|
+
|
|
589
|
+
// Single selects + a boolean that is on.
|
|
590
|
+
it('section-style-grouping', async () => {
|
|
591
|
+
await testBlock('section-style-grouping', `${folder}/section-style-grouping`);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// A multiselect claims several values; a boolean that is off contributes nothing.
|
|
595
|
+
it('section-style-grouping-multi', async () => {
|
|
596
|
+
await testBlock('section-style-grouping-multi', `${folder}/section-style-grouping-multi`);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Free-form base `style` catches leftover tokens alongside a grouped boolean.
|
|
600
|
+
it('section-style-grouping-fallback', async () => {
|
|
601
|
+
await testBlock('section-style-grouping-fallback', `${folder}/section-style-grouping-fallback`);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Several independent booleans: each is "true" only when its suffix appears.
|
|
605
|
+
it('section-style-grouping-booleans', async () => {
|
|
606
|
+
await testBlock('section-style-grouping-booleans', `${folder}/section-style-grouping-booleans`);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
- [ ] **Step 2: Run to verify it fails**
|
|
612
|
+
|
|
613
|
+
Run: `npx mocha test/block.test.js --grep "section style options"`
|
|
614
|
+
Expected: FAIL — fixtures do not exist (ENOENT reading the `.md`/`.json`).
|
|
615
|
+
|
|
616
|
+
- [ ] **Step 3: Create fixture 1 — `section-style-grouping` (select + select + boolean)**
|
|
617
|
+
|
|
618
|
+
`test/fixtures/blocks/core/section-style-options/section-style-grouping/_section-style-grouping.json`:
|
|
619
|
+
|
|
620
|
+
```json
|
|
621
|
+
{
|
|
622
|
+
"definitions": [],
|
|
623
|
+
"models": [
|
|
624
|
+
{
|
|
625
|
+
"id": "section",
|
|
626
|
+
"fields": [
|
|
627
|
+
{ "component": "text", "name": "name", "label": "Section Name" },
|
|
628
|
+
{ "component": "select", "name": "style", "label": "Variant", "options": [ { "name": "Variant A", "value": "variant-a" }, { "name": "Variant B", "value": "variant-b" } ] },
|
|
629
|
+
{ "component": "select", "name": "style_background", "label": "Background", "options": [ { "name": "Light", "value": "light" }, { "name": "Dark", "value": "dark" } ] },
|
|
630
|
+
{ "component": "boolean", "name": "style_fullwidth", "label": "Full width" }
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
],
|
|
634
|
+
"filters": []
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`test/fixtures/blocks/core/section-style-options/section-style-grouping/section-style-grouping.md`:
|
|
639
|
+
|
|
640
|
+
```markdown
|
|
641
|
+
# Section Title
|
|
642
|
+
|
|
643
|
+
+-------------------------------------------------------+
|
|
644
|
+
| Section Metadata |
|
|
645
|
+
+================+======================================+
|
|
646
|
+
| style | variant-a, light, fullwidth |
|
|
647
|
+
+----------------+--------------------------------------+
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`test/fixtures/blocks/core/section-style-options/section-style-grouping/section-style-grouping.xml`:
|
|
651
|
+
|
|
652
|
+
```xml
|
|
653
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
654
|
+
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" jcr:primaryType="cq:Page">
|
|
655
|
+
<jcr:content cq:template="/libs/core/franklin/templates/page" sling:resourceType="core/franklin/components/page/v1/page" jcr:primaryType="cq:PageContent">
|
|
656
|
+
<root jcr:primaryType="nt:unstructured" sling:resourceType="core/franklin/components/root/v1/root">
|
|
657
|
+
<section sling:resourceType="core/franklin/components/section/v1/section" jcr:primaryType="nt:unstructured" style="variant-a" style_background="light" style_fullwidth="true" model="section" modelFields="[name,style,style_background,style_fullwidth]">
|
|
658
|
+
<title sling:resourceType="core/franklin/components/title/v1/title" jcr:primaryType="nt:unstructured" title="Section Title" titleType="h1"/>
|
|
659
|
+
</section>
|
|
660
|
+
</root>
|
|
661
|
+
</jcr:content>
|
|
662
|
+
</jcr:root>
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
- [ ] **Step 4: Create fixture 2 — `section-style-grouping-multi` (multiselect claims several, boolean off)**
|
|
666
|
+
|
|
667
|
+
`_section-style-grouping-multi.json`:
|
|
668
|
+
|
|
669
|
+
```json
|
|
670
|
+
{
|
|
671
|
+
"definitions": [],
|
|
672
|
+
"models": [
|
|
673
|
+
{
|
|
674
|
+
"id": "section",
|
|
675
|
+
"fields": [
|
|
676
|
+
{ "component": "text", "name": "name" },
|
|
677
|
+
{ "component": "select", "name": "style", "options": [ { "value": "variant-a" }, { "value": "variant-b" } ] },
|
|
678
|
+
{ "component": "multiselect", "name": "style_background", "multi": true, "options": [ { "value": "light" }, { "value": "dark" }, { "value": "muted" } ] },
|
|
679
|
+
{ "component": "boolean", "name": "style_fullwidth" }
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
],
|
|
683
|
+
"filters": []
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
`section-style-grouping-multi.md`:
|
|
688
|
+
|
|
689
|
+
```markdown
|
|
690
|
+
# Section Title
|
|
691
|
+
|
|
692
|
+
+-------------------------------------------------------+
|
|
693
|
+
| Section Metadata |
|
|
694
|
+
+================+======================================+
|
|
695
|
+
| style | variant-b, light, muted |
|
|
696
|
+
+----------------+--------------------------------------+
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`section-style-grouping-multi.xml`:
|
|
700
|
+
|
|
701
|
+
```xml
|
|
702
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
703
|
+
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" jcr:primaryType="cq:Page">
|
|
704
|
+
<jcr:content cq:template="/libs/core/franklin/templates/page" sling:resourceType="core/franklin/components/page/v1/page" jcr:primaryType="cq:PageContent">
|
|
705
|
+
<root jcr:primaryType="nt:unstructured" sling:resourceType="core/franklin/components/root/v1/root">
|
|
706
|
+
<section sling:resourceType="core/franklin/components/section/v1/section" jcr:primaryType="nt:unstructured" style="variant-b" style_background="[light, muted]" model="section" modelFields="[name,style,style_background,style_fullwidth]">
|
|
707
|
+
<title sling:resourceType="core/franklin/components/title/v1/title" jcr:primaryType="nt:unstructured" title="Section Title" titleType="h1"/>
|
|
708
|
+
</section>
|
|
709
|
+
</root>
|
|
710
|
+
</jcr:content>
|
|
711
|
+
</jcr:root>
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
- [ ] **Step 5: Create fixture 3 — `section-style-grouping-fallback` (free-form base + boolean)**
|
|
715
|
+
|
|
716
|
+
`_section-style-grouping-fallback.json`:
|
|
717
|
+
|
|
718
|
+
```json
|
|
719
|
+
{
|
|
720
|
+
"definitions": [],
|
|
721
|
+
"models": [
|
|
722
|
+
{
|
|
723
|
+
"id": "section",
|
|
724
|
+
"fields": [
|
|
725
|
+
{ "component": "text", "name": "name" },
|
|
726
|
+
{ "component": "text", "name": "style", "multi": true },
|
|
727
|
+
{ "component": "boolean", "name": "style_sticky" }
|
|
728
|
+
]
|
|
729
|
+
}
|
|
730
|
+
],
|
|
731
|
+
"filters": []
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
`section-style-grouping-fallback.md`:
|
|
736
|
+
|
|
737
|
+
```markdown
|
|
738
|
+
# Section Title
|
|
739
|
+
|
|
740
|
+
+-------------------------------------------------------+
|
|
741
|
+
| Section Metadata |
|
|
742
|
+
+================+======================================+
|
|
743
|
+
| style | highlight, featured, sticky |
|
|
744
|
+
+----------------+--------------------------------------+
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
`section-style-grouping-fallback.xml`:
|
|
748
|
+
|
|
749
|
+
```xml
|
|
750
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
751
|
+
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" jcr:primaryType="cq:Page">
|
|
752
|
+
<jcr:content cq:template="/libs/core/franklin/templates/page" sling:resourceType="core/franklin/components/page/v1/page" jcr:primaryType="cq:PageContent">
|
|
753
|
+
<root jcr:primaryType="nt:unstructured" sling:resourceType="core/franklin/components/root/v1/root">
|
|
754
|
+
<section sling:resourceType="core/franklin/components/section/v1/section" jcr:primaryType="nt:unstructured" style="[highlight, featured]" style_sticky="true" model="section" modelFields="[name,style,style_sticky]">
|
|
755
|
+
<title sling:resourceType="core/franklin/components/title/v1/title" jcr:primaryType="nt:unstructured" title="Section Title" titleType="h1"/>
|
|
756
|
+
</section>
|
|
757
|
+
</root>
|
|
758
|
+
</jcr:content>
|
|
759
|
+
</jcr:root>
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
- [ ] **Step 6: Create fixture 4 — `section-style-grouping-booleans` (independent booleans, mixed on/off)**
|
|
763
|
+
|
|
764
|
+
`_section-style-grouping-booleans.json`:
|
|
765
|
+
|
|
766
|
+
```json
|
|
767
|
+
{
|
|
768
|
+
"definitions": [],
|
|
769
|
+
"models": [
|
|
770
|
+
{
|
|
771
|
+
"id": "section",
|
|
772
|
+
"fields": [
|
|
773
|
+
{ "component": "text", "name": "name" },
|
|
774
|
+
{ "component": "boolean", "name": "style_dark" },
|
|
775
|
+
{ "component": "boolean", "name": "style_fullwidth" },
|
|
776
|
+
{ "component": "boolean", "name": "style_narrow" }
|
|
777
|
+
]
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
"filters": []
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
`section-style-grouping-booleans.md`:
|
|
785
|
+
|
|
786
|
+
```markdown
|
|
787
|
+
# Section Title
|
|
788
|
+
|
|
789
|
+
+-------------------------------------------------------+
|
|
790
|
+
| Section Metadata |
|
|
791
|
+
+================+======================================+
|
|
792
|
+
| style | dark, narrow |
|
|
793
|
+
+----------------+--------------------------------------+
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
`section-style-grouping-booleans.xml`:
|
|
797
|
+
|
|
798
|
+
```xml
|
|
799
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
800
|
+
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" jcr:primaryType="cq:Page">
|
|
801
|
+
<jcr:content cq:template="/libs/core/franklin/templates/page" sling:resourceType="core/franklin/components/page/v1/page" jcr:primaryType="cq:PageContent">
|
|
802
|
+
<root jcr:primaryType="nt:unstructured" sling:resourceType="core/franklin/components/root/v1/root">
|
|
803
|
+
<section sling:resourceType="core/franklin/components/section/v1/section" jcr:primaryType="nt:unstructured" style_dark="true" style_narrow="true" model="section" modelFields="[name,style_dark,style_fullwidth,style_narrow]">
|
|
804
|
+
<title sling:resourceType="core/franklin/components/title/v1/title" jcr:primaryType="nt:unstructured" title="Section Title" titleType="h1"/>
|
|
805
|
+
</section>
|
|
806
|
+
</root>
|
|
807
|
+
</jcr:content>
|
|
808
|
+
</jcr:root>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
- [ ] **Step 7: Run the section style suite**
|
|
812
|
+
|
|
813
|
+
Run: `npx mocha test/block.test.js --grep "section style options"`
|
|
814
|
+
Expected: PASS (4 passing).
|
|
815
|
+
|
|
816
|
+
> If a test fails on attribute formatting only (e.g. a space difference or attribute order), the implementation in `GroupOptions.js`/`section-helper.js` is the source of truth for serialization — re-read the actual output from the assertion diff and reconcile the `.xml` to it, confirming the *intent* (which token routed to which field) is correct. Do not loosen the assertion.
|
|
817
|
+
|
|
818
|
+
- [ ] **Step 8: Commit**
|
|
819
|
+
|
|
820
|
+
```bash
|
|
821
|
+
git add test/fixtures/blocks/core/section-style-options test/block.test.js
|
|
822
|
+
git commit -m "test(section-metadata): cover style element-grouping scenarios
|
|
823
|
+
|
|
824
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
### Task 6: Documentation
|
|
830
|
+
|
|
831
|
+
**Files:**
|
|
832
|
+
- Create: `docs/section-styles.md`
|
|
833
|
+
- Modify: `README.md`
|
|
834
|
+
|
|
835
|
+
- [ ] **Step 1: Write the docs page**
|
|
836
|
+
|
|
837
|
+
Create `docs/section-styles.md`:
|
|
838
|
+
|
|
839
|
+
```markdown
|
|
840
|
+
# Section Styles (Element Grouping for `style`)
|
|
841
|
+
|
|
842
|
+
A section's appearance is controlled by the **Style** property of its Section
|
|
843
|
+
Metadata, which AEM turns into CSS classes on the containing section element.
|
|
844
|
+
`style` is the section counterpart of a block's [`classes`](./block-options.md)
|
|
845
|
+
property.
|
|
846
|
+
|
|
847
|
+
When a section model exposes several style options — especially mutually
|
|
848
|
+
exclusive ones — it can use the
|
|
849
|
+
[element grouping](https://www.aem.live/developer/component-model-definitions#element-grouping-for-block-options)
|
|
850
|
+
naming convention: a **style group** is the base `style` field plus any
|
|
851
|
+
`style_<name>` field.
|
|
852
|
+
|
|
853
|
+
```jsonc
|
|
854
|
+
{ "component": "select", "name": "style", "options": [ "variant-a", "variant-b" ] }
|
|
855
|
+
{ "component": "multiselect", "name": "style_background", "options": [ "light", "dark" ] }
|
|
856
|
+
{ "component": "boolean", "name": "style_fullwidth" }
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
AEM collapses the whole style group into the single, well-known `style` cell of
|
|
860
|
+
the Section Metadata table (a flat, comma-separated list). Going from markdown to
|
|
861
|
+
JCR, md2jcr routes each token back to its field — exactly as it does for block
|
|
862
|
+
`classes`:
|
|
863
|
+
|
|
864
|
+
- **boolean field** — matched by its name suffix (after `style_`); token
|
|
865
|
+
`fullwidth` sets `style_fullwidth="true"`, absent leaves it unset;
|
|
866
|
+
- **select / multiselect field** — matched by its declared option values; a
|
|
867
|
+
multiselect claims all of its matching tokens (`style_background="[light, dark]"`);
|
|
868
|
+
- **base `style` field** — receives any leftover tokens (also the common single,
|
|
869
|
+
free-form `style` field case).
|
|
870
|
+
|
|
871
|
+
### Example
|
|
872
|
+
|
|
873
|
+
```markdown
|
|
874
|
+
+-------------------------------------------------------+
|
|
875
|
+
| Section Metadata |
|
|
876
|
+
+================+======================================+
|
|
877
|
+
| style | variant-a, light, fullwidth |
|
|
878
|
+
+----------------+--------------------------------------+
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
becomes:
|
|
882
|
+
|
|
883
|
+
```text
|
|
884
|
+
style="variant-a" style_background="light" style_fullwidth="true"
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Notes
|
|
888
|
+
|
|
889
|
+
- The routing rules and the underlying implementation are shared with block
|
|
890
|
+
options — see [Block Options](./block-options.md).
|
|
891
|
+
- Distribution only kicks in when the section model declares at least one
|
|
892
|
+
`style_<name>` field. A model with only a base `style` field keeps the simple
|
|
893
|
+
one-to-one mapping.
|
|
894
|
+
- Unlike a block's `classes` group (which is excluded from `modelFields`), the
|
|
895
|
+
section `style` group remains in `modelFields` so the Universal Editor can
|
|
896
|
+
render those fields.
|
|
897
|
+
|
|
898
|
+
### Worked fixtures
|
|
899
|
+
|
|
900
|
+
See [`test/fixtures/blocks/core/section-style-options`](../test/fixtures/blocks/core/section-style-options):
|
|
901
|
+
|
|
902
|
+
- `section-style-grouping` — single selects + a boolean that is on
|
|
903
|
+
- `section-style-grouping-multi` — a multiselect claiming several values, boolean off
|
|
904
|
+
- `section-style-grouping-fallback` — free-form base `style` catch-all + a boolean
|
|
905
|
+
- `section-style-grouping-booleans` — several independent boolean options, mixed on/off
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
- [ ] **Step 2: Link it from the README**
|
|
909
|
+
|
|
910
|
+
Find the line in `README.md` that links to `docs/block-options.md` (added by PR #215) and add a sibling link directly after it. Run to locate it:
|
|
911
|
+
|
|
912
|
+
```bash
|
|
913
|
+
grep -n "block-options.md" README.md
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
Then add, immediately after that matched line, a parallel bullet/link in the same style, e.g.:
|
|
917
|
+
|
|
918
|
+
```markdown
|
|
919
|
+
- [Section Styles](docs/section-styles.md) — element grouping for section `style`
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
(Match the exact list/link formatting used on the adjacent `block-options.md` line.)
|
|
923
|
+
|
|
924
|
+
- [ ] **Step 3: Commit**
|
|
925
|
+
|
|
926
|
+
```bash
|
|
927
|
+
git add docs/section-styles.md README.md
|
|
928
|
+
git commit -m "docs(section-styles): document element grouping for section style
|
|
929
|
+
|
|
930
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
### Task 7: Full verification
|
|
936
|
+
|
|
937
|
+
**Files:** none (verification only)
|
|
938
|
+
|
|
939
|
+
- [ ] **Step 1: Run the whole suite**
|
|
940
|
+
|
|
941
|
+
Run: `npm test`
|
|
942
|
+
Expected: PASS — previous count (144) + 4 new section-style fixtures + new `isGroupField` and `distributeGroupOptions` unit tests; zero failures.
|
|
943
|
+
|
|
944
|
+
- [ ] **Step 2: Lint the repo**
|
|
945
|
+
|
|
946
|
+
Run: `npm run lint`
|
|
947
|
+
Expected: no errors.
|
|
948
|
+
|
|
949
|
+
- [ ] **Step 3: Confirm no unintended changes**
|
|
950
|
+
|
|
951
|
+
Run: `git status` and `git diff --stat main`
|
|
952
|
+
Expected: only the files named in Tasks 1-6 appear.
|
|
953
|
+
|
|
954
|
+
---
|
|
955
|
+
|
|
956
|
+
## Self-Review
|
|
957
|
+
|
|
958
|
+
**1. Spec coverage**
|
|
959
|
+
- Generalize PR #215 helpers by group name → Tasks 1 (`isGroupField`), 2 (`distributeGroupOptions`), 3 (block path rewired).
|
|
960
|
+
- Section `style`/`style_*` distribution from the single Style cell → Task 4.
|
|
961
|
+
- Boolean (suffix), select/multiselect (options), base fallback routing → covered by unit tests (Task 2) and fixtures (Task 5).
|
|
962
|
+
- `style` group stays in section `modelFields` (asymmetry vs. blocks) → preserved by leaving `getModelFieldNames` unchanged; asserted by every fixture's `modelFields=[...]`; documented in Task 6.
|
|
963
|
+
- No regression to existing block options / section metadata → Task 3 Step 5 and Task 4 Step 3 guards; Task 7 full run.
|
|
964
|
+
|
|
965
|
+
**2. Placeholder scan** — No TBD/TODO/"handle edge cases"; every code step shows complete code; every fixture shows full `.md`/`.json`/`.xml`.
|
|
966
|
+
|
|
967
|
+
**3. Type/name consistency** — `isGroupField(name, group)` (Task 1) is consumed by `GroupOptions.js` (Task 2); `distributeGroupOptions(model, tokens, group='classes')` is the single name used in `grid-table.js` (Task 3) and `section-helper.js` (Task 4). The local `distributeBlockOptions`/`collectOptionValues` are fully removed from `grid-table.js` in Task 3 (no dangling references). Fixture spec names match the `_<spec>.json` / `<spec>.md` / `<spec>.xml` triple and the `testBlock(spec, folder)` calls in Task 5.
|
|
968
|
+
|
|
969
|
+
> **Known fragility:** the four `.xml` files are hand-computed golden files. Their attribute ordering assumes (a) `distributeGroupOptions` emits in model-field order and (b) `section-helper` serializes section attributes in insertion order (resourceType, primaryType, distributed style attrs, `model`, `modelFields`) without sorting — both true in the current code. Task 5 Step 7 has an explicit reconciliation note if serialization differs.
|