@quillmark/svelte 0.15.0 → 0.17.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.
@@ -11,6 +11,11 @@ import { core } from '../core/lifecycle.js';
11
11
  export function isContainer(kind) {
12
12
  return kind === 'array' || kind === 'object' || kind === 'variant' || kind === 'matrix';
13
13
  }
14
+ /** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
15
+ * standing at the depth of the fields around it rather than a rung in. */
16
+ export function flatObject(schema) {
17
+ return controlKind(schema) === 'object' && schema.ui?.layout === 'flat';
18
+ }
14
19
  /**
15
20
  * A card's resolved rows keyed by field name: the provenance channel parallel to
16
21
  * {@link CardModel.values}. `resolve` returns rows in declaration order; the
@@ -142,10 +147,10 @@ export function controlKind(f) {
142
147
  * crosses inside untyped container data with no type to read it off, so a literal here
143
148
  * would be a second spelling of a reserved key. No variant may declare it. */
144
149
  export const VARIANT_DISCRIMINANT = VARIANT_DISCRIMINANT_KEY;
145
- /** The tick cell of a matrix member, the second reserved key beside the discriminant.
146
- * The boundary synthesizes it on every member and exports no constant for it, so this
147
- * is where the package spells it once (canon `SCHEMAS.md` §Matrix). */
148
- export const MATRIX_HELD = 'held';
150
+ /** The label key of an open matrix's added item, the one cell a roster member never
151
+ * stores. The boundary exports no constant for it, so the package spells it once
152
+ * (canon `SCHEMAS.md` §Matrix). */
153
+ export const MATRIX_TITLE = 'title';
149
154
  /** The roster's members, in declaration order. Own keys only: a schema map is indexed
150
155
  * by a document-supplied id, and `'toString' in members` is true of every roster. */
151
156
  export function matrixMembers(roster) {
@@ -157,17 +162,14 @@ export function memberValue(map, id) {
157
162
  return map && Object.hasOwn(map, id) ? map[id] : undefined;
158
163
  }
159
164
  /**
160
- * Whether a stored member is held. A bare scalar is the tick and a mapping is the member
161
- * object, whose `held` cell resolves as every absent cell does, to its `default:` of
162
- * `false` (canon `SCHEMAS.md` §Matrix): an absent key is unheld, a bare `true` is held,
163
- * and a member object is held only where its `held` cell says so. The spellings the
164
- * engine coerces to false — `false`, `0`, `"false"`, `null` — read false here; every other
165
- * present scalar reads held, so a document the engine would refuse still draws the tick
166
- * its spelling claims.
165
+ * Whether a stored member is held: by being present. A mapping is held whatever it
166
+ * carries, `{}` included, and a bare scalar is the tick itself, read as the engine's
167
+ * coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
168
+ * scalar held, so a spelling the engine would refuse still draws the tick it claims.
167
169
  */
168
170
  export function matrixHeld(stored) {
169
171
  if (typeof stored === 'object' && stored !== null)
170
- return (Object.hasOwn(stored, MATRIX_HELD) && tickOf(stored[MATRIX_HELD]));
172
+ return true;
171
173
  return tickOf(stored);
172
174
  }
173
175
  /** A tick's scalar reading: absent and the engine's false spellings unheld, the rest held. */
@@ -182,20 +184,29 @@ function tickOf(v) {
182
184
  return v !== 'false';
183
185
  return true;
184
186
  }
185
- /** A member's columns: the member object with its `held` cell taken out, `{}` for the
186
- * bare and absent spellings. What the columns subform takes as its value. */
187
+ /** The keys no column may take (`quill::matrix_reserved_column`): an added item's
188
+ * label, and the `held` a member never stores. */
189
+ const MATRIX_RESERVED = [MATRIX_TITLE, 'held'];
190
+ /** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
191
+ * absent member. What the columns subform takes as its value, so an edit writes back
192
+ * no `held` a document stored (`validation::held_stored`). */
187
193
  export function matrixColumns(stored) {
188
194
  if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
189
195
  return {};
190
196
  const rest = { ...stored };
191
- delete rest[MATRIX_HELD];
197
+ for (const key of MATRIX_RESERVED)
198
+ delete rest[key];
192
199
  return rest;
193
200
  }
201
+ /** What a matrix holding nothing commits: the unset field where unset prints nothing
202
+ * held, and the authored `{}` where it would print a `default:` or `none` instead. */
203
+ export function matrixEmpty(matrix) {
204
+ return matrix.default !== undefined || optionalCell(matrix) ? {} : undefined;
205
+ }
194
206
  /**
195
207
  * The map after one member moves: `next` written under `id`, or the key dropped where
196
208
  * `next` is `undefined`; a map left holding nothing is an unset field. Every other
197
- * member rides through in the spelling the document had, so a stored `cyber_200: true`
198
- * stays `true` while its neighbour is edited.
209
+ * member rides through in the spelling the document had.
199
210
  */
200
211
  export function commitMember(map, id, next) {
201
212
  const out = { ...(map ?? {}) };
@@ -206,33 +217,86 @@ export function commitMember(map, id, next) {
206
217
  return Object.keys(out).length ? out : undefined;
207
218
  }
208
219
  /**
209
- * The member a tick or an edit writes. Held with columns is the member object with an
210
- * explicit `held: true`; unheld keeps the columns under `held: false`, so tick, type,
211
- * untick, retick loses nothing; unheld with no columns is `undefined`, the key dropped.
212
- * Which columns are blank is the engine's rule and not restated here: a column control
213
- * that clears drops its key (`ObjectField`), so a member emptied by hand arrives with no
214
- * columns and only then leaves the map.
220
+ * The held member a tick or an edit writes: its columns, the bare tick `true` where
221
+ * it answers none, and an added item's `title` beside them. Unticking is removal
222
+ * ({@link commitMember} with `undefined`), never a write.
215
223
  */
216
- export function memberWrite(held, columns) {
217
- if (!held && Object.keys(columns).length === 0)
218
- return undefined;
219
- return { [MATRIX_HELD]: held, ...columns };
224
+ export function memberWrite(columns, title) {
225
+ if (title !== undefined)
226
+ return { [MATRIX_TITLE]: title, ...columns };
227
+ return Object.keys(columns).length ? columns : true;
220
228
  }
221
- /** The `object` a matrix member desugars to, for the schema walk: `held` beside the
222
- * declared columns. Composed here because the boundary serializes the roster alone. */
223
- export function matrixMemberSchema(matrix) {
224
- return {
225
- type: 'object',
226
- properties: {
227
- [MATRIX_HELD]: { type: 'boolean', default: false },
228
- ...(matrix.properties ?? {})
229
- }
230
- };
229
+ /** Whether `id` is spelled as a matrix id: snake_case, the rule the roster's ids and
230
+ * an added item's key share. */
231
+ export function matrixId(id) {
232
+ return /^[a-z][a-z0-9_]*$/.test(id);
231
233
  }
232
234
  /** Whether `id` is on the roster. */
233
- export function matrixDeclares(matrix, id) {
235
+ export function onRoster(matrix, id) {
234
236
  return matrix.members != null && Object.hasOwn(matrix.members, id);
235
237
  }
238
+ /**
239
+ * Whether a step under the matrix names a member, judged on the schema alone as an
240
+ * index step is: a roster id, or on an open matrix any key spelled as an id, the added
241
+ * item it would be. So an unheld member's address resolves.
242
+ */
243
+ export function matrixDeclares(matrix, id) {
244
+ return onRoster(matrix, id) || (!!matrix.open && matrixId(id));
245
+ }
246
+ /**
247
+ * The added items a stored map holds, sorted by id as the plate orders them: on an
248
+ * open matrix, each key off the roster spelled as an id whose mapping carries a
249
+ * `title`. Any other key off the roster is the engine's refusal, drawn nowhere and
250
+ * ridden through.
251
+ */
252
+ export function addedItems(matrix, map) {
253
+ if (!matrix.open || !map)
254
+ return [];
255
+ const out = [];
256
+ for (const id of Object.keys(map)) {
257
+ if (onRoster(matrix, id) || !matrixId(id))
258
+ continue;
259
+ const stored = map[id];
260
+ if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
261
+ continue;
262
+ const title = stored[MATRIX_TITLE];
263
+ if (title == null)
264
+ continue;
265
+ out.push({ id, title: String(title) });
266
+ }
267
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
268
+ }
269
+ /**
270
+ * The id an item added under `title` takes: the title folded to snake_case, prefixed
271
+ * where it opens on no letter, and suffixed past every id `taken` names, the roster's
272
+ * and the document's alike. The id is the wire's and never drawn, so a later retitle
273
+ * leaves it where it is.
274
+ */
275
+ export function addedId(title, taken) {
276
+ const folded = title
277
+ .normalize('NFKD')
278
+ .replace(/[\u0300-\u036f]/g, '')
279
+ .toLowerCase()
280
+ .replace(/[^a-z0-9]+/g, '_')
281
+ .replace(/^_+|_+$/g, '');
282
+ const base = /^[a-z]/.test(folded) ? folded : `item${folded ? `_${folded}` : ''}`;
283
+ if (!taken(base))
284
+ return base;
285
+ let n = 2;
286
+ while (taken(`${base}_${n}`))
287
+ n++;
288
+ return `${base}_${n}`;
289
+ }
290
+ /** The `object` a matrix member is, for the schema walk: the declared columns, and an
291
+ * added item's `title` beside them. Composed here because the boundary serializes the
292
+ * roster and the columns alone. */
293
+ export function matrixMemberSchema(matrix, id) {
294
+ const columns = matrix.properties ?? {};
295
+ return {
296
+ type: 'object',
297
+ properties: onRoster(matrix, id) ? columns : { [MATRIX_TITLE]: { type: 'string' }, ...columns }
298
+ };
299
+ }
236
300
  // ── The schema walk ──────────────────────────────────────────────────────────
237
301
  /**
238
302
  * The schema at `steps` under `field`, or `undefined` where the schema declares no such
@@ -265,7 +329,7 @@ function stepInto(schema, step) {
265
329
  : undefined;
266
330
  case 'matrix':
267
331
  return typeof step === 'string' && matrixDeclares(schema, step)
268
- ? matrixMemberSchema(schema)
332
+ ? matrixMemberSchema(schema, step)
269
333
  : undefined;
270
334
  case 'variant': {
271
335
  if (typeof step !== 'string')
@@ -318,7 +382,7 @@ export function rowSummary(items, row) {
318
382
  * The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
319
383
  * and every cell of the row is short (a block prose cell declines it, at load width and
320
384
  * every width after), else `'list'`. That every column is a leaf is the loader's
321
- * contract (`quill::table_column_not_flat`); whether a leaf fits a cell is the surface's
385
+ * contract (`quill::table_column_not_leaf`); whether a leaf fits a cell is the surface's
322
386
  * answer (canon `SCHEMAS.md`): a table composes by position, so a row that would stack
323
387
  * inside a cell is not one.
324
388
  */
@@ -362,9 +426,10 @@ export function commitDiscriminant(value, member) {
362
426
  /**
363
427
  * Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
364
428
  * obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
365
- * a typed dictionary and a matrix, since a namespace declares no `default:` at all and
366
- * `validate` anchors obligation on the leaves under it; and an optional cell, which can
367
- * declare none, its unanswered render being `none`.
429
+ * a typed dictionary, since a namespace declares no `default:` at all and `validate`
430
+ * anchors obligation on the leaves under it; a matrix, whose blank is nothing held, a
431
+ * whole answer; and an optional cell, which can declare none, its unanswered render
432
+ * being `none`.
368
433
  */
369
434
  export function obliged(schema) {
370
435
  return (schema.type !== 'object' &&
@@ -431,7 +496,8 @@ export function groupSections(fields, order, labelFor) {
431
496
  for (const g of order) {
432
497
  const fs = fields.filter((f) => f.group === g);
433
498
  if (fs.length) {
434
- sections.push({ group: g, label: labelFor(g), fields: fs });
499
+ const head = fs.length === 1 && flatObject(fs[0].schema) ? fs[0] : undefined;
500
+ sections.push({ group: g, label: labelFor(g), fields: fs, head });
435
501
  emitted.add(g);
436
502
  }
437
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -66,7 +66,7 @@
66
66
  "check": "svelte-check --tsconfig ./tsconfig.json"
67
67
  },
68
68
  "peerDependencies": {
69
- "@quillmark/wasm": ">=0.117.0-0",
69
+ "@quillmark/wasm": ">=0.121.0-0",
70
70
  "svelte": "^5.0.0"
71
71
  },
72
72
  "dependencies": {