@quillmark/svelte 0.11.0 → 0.13.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/dist/core/address.d.ts +17 -10
- package/dist/core/address.js +26 -12
- package/dist/core/bloom.js +4 -3
- package/dist/core/codec/decode.d.ts +17 -0
- package/dist/core/codec/decode.js +78 -19
- package/dist/core/codec/encode.js +1 -1
- package/dist/core/codec/field.d.ts +64 -15
- package/dist/core/codec/field.js +201 -65
- package/dist/core/codec/index.d.ts +3 -3
- package/dist/core/codec/index.js +3 -3
- package/dist/core/codec/islands.d.ts +5 -4
- package/dist/core/codec/islands.js +23 -38
- package/dist/core/codec/keymap.d.ts +5 -4
- package/dist/core/codec/keymap.js +5 -4
- package/dist/core/codec/prose.css +42 -0
- package/dist/core/codec/schema.d.ts +24 -5
- package/dist/core/codec/schema.js +40 -9
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +19 -6
- package/dist/preview/paint.js +21 -17
- package/dist/visual/ArrayField.svelte +649 -176
- package/dist/visual/ArrayField.svelte.d.ts +16 -1
- package/dist/visual/BooleanField.svelte +36 -11
- package/dist/visual/BooleanField.svelte.d.ts +2 -0
- package/dist/visual/Card.svelte +6 -25
- package/dist/visual/DateField.svelte +66 -31
- package/dist/visual/EnumField.svelte +27 -9
- package/dist/visual/EnumField.svelte.d.ts +5 -0
- package/dist/visual/Field.svelte +113 -38
- package/dist/visual/FormatPopover.svelte +4 -3
- package/dist/visual/MatrixField.svelte +353 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/NumberField.svelte +31 -14
- package/dist/visual/NumberField.svelte.d.ts +5 -1
- package/dist/visual/ObjectField.svelte +404 -139
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/ProseField.svelte +53 -23
- package/dist/visual/ProseField.svelte.d.ts +15 -3
- package/dist/visual/ProseValue.svelte +102 -46
- package/dist/visual/ProseValue.svelte.d.ts +17 -5
- package/dist/visual/TextField.svelte +35 -18
- package/dist/visual/TextField.svelte.d.ts +7 -0
- package/dist/visual/VariantField.svelte +56 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditorInner.svelte +77 -73
- package/dist/visual/controls.css +47 -5
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/hold.js +2 -2
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +27 -10
- package/dist/visual/strings.js +8 -2
- package/dist/visual/structure.d.ts +159 -40
- package/dist/visual/structure.js +337 -45
- package/package.json +2 -2
package/dist/visual/structure.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
// The schema × payload join, done by the editor (VISUAL_EDITOR §Structure). Pure
|
|
2
|
+
// functions only (no runes, no Document reads) so the ordering, control
|
|
3
|
+
// dispatch, group layout, titles, ghosts, and session-identity bookkeeping
|
|
4
|
+
// are unit-testable in isolation (tests/visual/structure.test.ts). The reactive
|
|
5
|
+
// orchestration (revision counter, live doc reads) lives in VisualEditor.svelte;
|
|
6
|
+
// this module is the projection math it feeds.
|
|
7
|
+
import { VARIANT_DISCRIMINANT_KEY } from '@quillmark/wasm';
|
|
8
|
+
import { core } from '../core/lifecycle.js';
|
|
9
|
+
/** The container controls: the ones a nested address walks into and a deep
|
|
10
|
+
* diagnostic routes through. */
|
|
11
|
+
export function isContainer(kind) {
|
|
12
|
+
return kind === 'array' || kind === 'object' || kind === 'variant' || kind === 'matrix';
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* A card's resolved rows keyed by field name: the provenance channel parallel to
|
|
3
16
|
* {@link CardModel.values}. `resolve` returns rows in declaration order; the
|
|
@@ -21,36 +34,112 @@ export const NO_RESOLVED_ROWS = { fields: [], body: null };
|
|
|
21
34
|
export function resolvedByCardIndex(resolved) {
|
|
22
35
|
return new Map((resolved?.cards ?? []).map((c) => [c.index, { fields: c.fields, body: c.body }]));
|
|
23
36
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
37
|
+
/** What an unset field shows of its `default:`: the resolved value the render would
|
|
38
|
+
* use (`source === 'default'`), else undefined: an `authored` field shows its value,
|
|
39
|
+
* a `blank` field has no default to show. */
|
|
27
40
|
export function ghostDefault(row) {
|
|
28
41
|
return row?.source === 'default' ? row.value : undefined;
|
|
29
42
|
}
|
|
30
43
|
/** A ghost value's string form, or undefined for null/object (only text ghosts
|
|
31
|
-
* render a placeholder).
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* thing to display as a placeholder (FIELD_PROVENANCE). */
|
|
44
|
+
* render a placeholder). A scalar's resolved default reads through it; a content
|
|
45
|
+
* field's resolves as `Content`, which a prose leaf reads by its text instead
|
|
46
|
+
* ({@link titleText}). */
|
|
35
47
|
export function stringifyGhost(ghost) {
|
|
36
48
|
return ghost != null && typeof ghost !== 'object' ? String(ghost) : undefined;
|
|
37
49
|
}
|
|
38
50
|
/**
|
|
39
|
-
* The empty body's ghost: the resolved body `default:`, else the
|
|
40
|
-
* wording, else the flat built-in. The
|
|
41
|
-
*
|
|
42
|
-
* written, and wording placed over it would make that
|
|
43
|
-
*
|
|
44
|
-
* promise
|
|
51
|
+
* The empty body's ghost: the resolved body `default:`, else the kind's
|
|
52
|
+
* `body.example`, else the consumer's wording, else the flat built-in. The
|
|
53
|
+
* `default:` wins because it is the only one that describes the render: it promises
|
|
54
|
+
* what prints if nothing is written, and wording placed over it would make that
|
|
55
|
+
* promise unreadable. The rest are invitations, and an invitation belongs only where
|
|
56
|
+
* there is no promise; the quill's own comes first, being the one an agent working
|
|
57
|
+
* from the blueprint sees too.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveBodyGhost(resolvedDefault, example, custom, builtIn) {
|
|
60
|
+
return resolvedDefault || example || custom || builtIn;
|
|
61
|
+
}
|
|
62
|
+
/** Whether a cell is optional: a `t?` type, which renders `none` unanswered and so
|
|
63
|
+
* never declares a `default:` (canon `SCHEMAS.md` §"Optional cells"). */
|
|
64
|
+
export function optionalCell(f) {
|
|
65
|
+
return f.type.endsWith('?');
|
|
66
|
+
}
|
|
67
|
+
/** The type a cell's control reads: the declared one, an optional cell's `?` stripped.
|
|
68
|
+
* The `?` moves the render floor and nothing else, so every read of a type is this. */
|
|
69
|
+
export function baseType(f) {
|
|
70
|
+
return (optionalCell(f) ? f.type.slice(0, -1) : f.type);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A value the schema declares, as ghost text: a `default:`, an `example:` or a
|
|
74
|
+
* `body.example`. Markdown — a `richtext` value — ghosts as the text it renders, one
|
|
75
|
+
* block to a line, as a resolved default does ({@link titleText}); anything else as
|
|
76
|
+
* itself. `undefined` for a blank or a non-scalar.
|
|
77
|
+
*/
|
|
78
|
+
export function declaredGhost(v, markdown) {
|
|
79
|
+
const text = stringifyGhost(v)?.trim();
|
|
80
|
+
if (!text)
|
|
81
|
+
return undefined;
|
|
82
|
+
return (markdown ? core().importMarkdown(text).text.trim() : text) || undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
86
|
+
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
87
|
+
* nothing a reader sees, so a control holds none and draws its example instead.
|
|
88
|
+
*/
|
|
89
|
+
export function printedText(v) {
|
|
90
|
+
const text = stringifyGhost(v);
|
|
91
|
+
return text?.trim() ? text : undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A declared content literal as the `Content` it holds: a `richtext` one's markdown
|
|
95
|
+
* imported, a `plaintext` one's text taken literally, one paragraph whose later lines
|
|
96
|
+
* continue it, as the literal codec reads a stored one. What a cell's static
|
|
97
|
+
* `default:` stands in its leaf as, where no resolved row reaches it. `undefined` for
|
|
98
|
+
* a blank or a non-scalar.
|
|
99
|
+
*/
|
|
100
|
+
export function declaredContent(v, markdown) {
|
|
101
|
+
const text = printedText(v);
|
|
102
|
+
if (text === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (markdown)
|
|
105
|
+
return core().importMarkdown(text);
|
|
106
|
+
return {
|
|
107
|
+
text,
|
|
108
|
+
lines: text
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map((_, i) => i === 0
|
|
111
|
+
? { containers: [], kind: 'para' }
|
|
112
|
+
: { containers: [], kind: 'para', continues: true }),
|
|
113
|
+
marks: [],
|
|
114
|
+
islands: []
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The `example:` an unset free-text cell ghosts: a `string`, `plaintext` or `richtext`
|
|
119
|
+
* whose `default:` prints nothing. Every other type takes none. A default that prints
|
|
120
|
+
* dominates, standing in the control as the value it is; a type-empty one, the
|
|
121
|
+
* skippable marker, prints nothing and leaves the example the only thing to draw.
|
|
45
122
|
*/
|
|
46
|
-
export function
|
|
47
|
-
|
|
123
|
+
export function exampleGhost(f) {
|
|
124
|
+
const kind = controlKind(f);
|
|
125
|
+
if (kind !== 'text' && kind !== 'prose')
|
|
126
|
+
return undefined;
|
|
127
|
+
const markdown = baseType(f) === 'richtext';
|
|
128
|
+
if (declaredGhost(f.default, markdown) !== undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
return declaredGhost(f.example, markdown);
|
|
131
|
+
}
|
|
132
|
+
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
|
|
133
|
+
* `inline` and its elements'. A retype or a quill swap that changes any of them under a
|
|
134
|
+
* name remounts the field, where a kept leaf would commit on the old schema. */
|
|
135
|
+
export function mountKey(name, f) {
|
|
136
|
+
return `${name}\u0000${f.type}\u0000${f.inline === true}\u0000${f.items?.type ?? ''}\u0000${f.items?.inline === true}`;
|
|
48
137
|
}
|
|
49
138
|
/** Map a field schema to its control (precedence: prose › enum › text › …).
|
|
50
139
|
* An array's element control is this over `items`: a missing `items` is a text
|
|
51
140
|
* element. */
|
|
52
141
|
export function controlKind(f) {
|
|
53
|
-
switch (f
|
|
142
|
+
switch (baseType(f)) {
|
|
54
143
|
case 'richtext':
|
|
55
144
|
case 'plaintext':
|
|
56
145
|
return 'prose';
|
|
@@ -72,13 +161,206 @@ export function controlKind(f) {
|
|
|
72
161
|
return 'array';
|
|
73
162
|
case 'object':
|
|
74
163
|
return 'object';
|
|
164
|
+
case 'matrix':
|
|
165
|
+
return 'matrix';
|
|
75
166
|
default:
|
|
76
167
|
return 'text';
|
|
77
168
|
}
|
|
78
169
|
}
|
|
79
|
-
/** The discriminant cell of a variant container
|
|
80
|
-
*
|
|
81
|
-
|
|
170
|
+
/** The discriminant cell of a variant container, off the boundary's own constant: it
|
|
171
|
+
* crosses inside untyped container data with no type to read it off, so a literal here
|
|
172
|
+
* would be a second spelling of a reserved key. No variant may declare it. */
|
|
173
|
+
export const VARIANT_DISCRIMINANT = VARIANT_DISCRIMINANT_KEY;
|
|
174
|
+
/** The tick cell of a matrix member, the second reserved key beside the discriminant.
|
|
175
|
+
* The boundary synthesizes it on every member and exports no constant for it, so this
|
|
176
|
+
* is where the package spells it once (canon `SCHEMAS.md` §Matrix). */
|
|
177
|
+
export const MATRIX_HELD = 'held';
|
|
178
|
+
/** The roster's members, in declaration order. Own keys only: a schema map is indexed
|
|
179
|
+
* by a document-supplied id, and `'toString' in members` is true of every roster. */
|
|
180
|
+
export function matrixMembers(roster) {
|
|
181
|
+
return roster ? Object.keys(roster).map((id) => ({ id, title: roster[id] })) : [];
|
|
182
|
+
}
|
|
183
|
+
/** A member's stored payload, off the sparse map by own key: a document key can be
|
|
184
|
+
* spelled `constructor`. */
|
|
185
|
+
export function memberValue(map, id) {
|
|
186
|
+
return map && Object.hasOwn(map, id) ? map[id] : undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Whether a stored member is held. A bare scalar is the tick and a mapping is the member
|
|
190
|
+
* object, whose `held` cell resolves as every absent cell does, to its `default:` of
|
|
191
|
+
* `false` (canon `SCHEMAS.md` §Matrix): an absent key is unheld, a bare `true` is held,
|
|
192
|
+
* and a member object is held only where its `held` cell says so. The spellings the
|
|
193
|
+
* engine coerces to false — `false`, `0`, `"false"`, `null` — read false here; every other
|
|
194
|
+
* present scalar reads held, so a document the engine would refuse still draws the tick
|
|
195
|
+
* its spelling claims.
|
|
196
|
+
*/
|
|
197
|
+
export function matrixHeld(stored) {
|
|
198
|
+
if (typeof stored === 'object' && stored !== null)
|
|
199
|
+
return (Object.hasOwn(stored, MATRIX_HELD) && tickOf(stored[MATRIX_HELD]));
|
|
200
|
+
return tickOf(stored);
|
|
201
|
+
}
|
|
202
|
+
/** A tick's scalar reading: absent and the engine's false spellings unheld, the rest held. */
|
|
203
|
+
function tickOf(v) {
|
|
204
|
+
if (v === undefined || v === null)
|
|
205
|
+
return false;
|
|
206
|
+
if (typeof v === 'boolean')
|
|
207
|
+
return v;
|
|
208
|
+
if (typeof v === 'number')
|
|
209
|
+
return v !== 0;
|
|
210
|
+
if (typeof v === 'string')
|
|
211
|
+
return v !== 'false';
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
/** A member's columns: the member object with its `held` cell taken out, `{}` for the
|
|
215
|
+
* bare and absent spellings. What the columns subform takes as its value. */
|
|
216
|
+
export function matrixColumns(stored) {
|
|
217
|
+
if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
|
|
218
|
+
return {};
|
|
219
|
+
const rest = { ...stored };
|
|
220
|
+
delete rest[MATRIX_HELD];
|
|
221
|
+
return rest;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The map after one member moves: `next` written under `id`, or the key dropped where
|
|
225
|
+
* `next` is `undefined`; a map left holding nothing is an unset field. Every other
|
|
226
|
+
* member rides through in the spelling the document had, so a stored `cyber_200: true`
|
|
227
|
+
* stays `true` while its neighbour is edited.
|
|
228
|
+
*/
|
|
229
|
+
export function commitMember(map, id, next) {
|
|
230
|
+
const out = { ...(map ?? {}) };
|
|
231
|
+
if (next === undefined)
|
|
232
|
+
delete out[id];
|
|
233
|
+
else
|
|
234
|
+
out[id] = next;
|
|
235
|
+
return Object.keys(out).length ? out : undefined;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The member a tick or an edit writes. Held with columns is the member object with an
|
|
239
|
+
* explicit `held: true`; unheld keeps the columns under `held: false`, so tick, type,
|
|
240
|
+
* untick, retick loses nothing; unheld with no columns is `undefined`, the key dropped.
|
|
241
|
+
* Which columns are blank is the engine's rule and not restated here: a column control
|
|
242
|
+
* that clears drops its key (`ObjectField`), so a member emptied by hand arrives with no
|
|
243
|
+
* columns and only then leaves the map.
|
|
244
|
+
*/
|
|
245
|
+
export function memberWrite(held, columns) {
|
|
246
|
+
if (!held && Object.keys(columns).length === 0)
|
|
247
|
+
return undefined;
|
|
248
|
+
return { [MATRIX_HELD]: held, ...columns };
|
|
249
|
+
}
|
|
250
|
+
/** The `object` a matrix member desugars to, for the schema walk: `held` beside the
|
|
251
|
+
* declared columns. Composed here because the boundary serializes the roster alone. */
|
|
252
|
+
export function matrixMemberSchema(matrix) {
|
|
253
|
+
return {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
[MATRIX_HELD]: { type: 'boolean', default: false },
|
|
257
|
+
...(matrix.properties ?? {})
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Whether `id` is on the roster. */
|
|
262
|
+
export function matrixDeclares(matrix, id) {
|
|
263
|
+
return matrix.members != null && Object.hasOwn(matrix.members, id);
|
|
264
|
+
}
|
|
265
|
+
// ── The schema walk ──────────────────────────────────────────────────────────
|
|
266
|
+
/**
|
|
267
|
+
* The schema at `steps` under `field`, or `undefined` where the schema declares no such
|
|
268
|
+
* place: an index under an `array`, a key under an `object`'s properties, a member id
|
|
269
|
+
* under a `matrix` and a key under that member, a cell of any world (or the
|
|
270
|
+
* discriminant) under a variant-bearing `enum`. The editor's copy of the boundary's
|
|
271
|
+
* `schema_at`, held here because the landing lane checks an address against the schema
|
|
272
|
+
* before it asks the mounted tree for it (VISUAL_EDITOR §Surface).
|
|
273
|
+
*/
|
|
274
|
+
export function schemaAt(field, steps) {
|
|
275
|
+
let cursor = field;
|
|
276
|
+
for (const step of steps) {
|
|
277
|
+
if (!cursor)
|
|
278
|
+
return undefined;
|
|
279
|
+
cursor = stepInto(cursor, step);
|
|
280
|
+
}
|
|
281
|
+
return cursor;
|
|
282
|
+
}
|
|
283
|
+
/** What an array declaring no `items` holds, the walk's copy of the element control
|
|
284
|
+
* {@link controlKind} names for one: a text element, so an index into such an array is
|
|
285
|
+
* a place and not a refusal. */
|
|
286
|
+
const UNDECLARED_ELEMENT = { type: 'string' };
|
|
287
|
+
function stepInto(schema, step) {
|
|
288
|
+
switch (controlKind(schema)) {
|
|
289
|
+
case 'array':
|
|
290
|
+
return typeof step === 'number' ? (schema.items ?? UNDECLARED_ELEMENT) : undefined;
|
|
291
|
+
case 'object':
|
|
292
|
+
return typeof step === 'string' && Object.hasOwn(schema.properties ?? {}, step)
|
|
293
|
+
? schema.properties[step]
|
|
294
|
+
: undefined;
|
|
295
|
+
case 'matrix':
|
|
296
|
+
return typeof step === 'string' && matrixDeclares(schema, step)
|
|
297
|
+
? matrixMemberSchema(schema)
|
|
298
|
+
: undefined;
|
|
299
|
+
case 'variant': {
|
|
300
|
+
if (typeof step !== 'string')
|
|
301
|
+
return undefined;
|
|
302
|
+
if (step === VARIANT_DISCRIMINANT)
|
|
303
|
+
return { type: 'enum', values: schema.values };
|
|
304
|
+
// Which world is live is a value-time fact, so the walk unions the worlds, as
|
|
305
|
+
// the boundary's does.
|
|
306
|
+
for (const cells of Object.values(schema.variants ?? {}))
|
|
307
|
+
if (Object.hasOwn(cells, step))
|
|
308
|
+
return cells[step];
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
default:
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ── Row summaries and layouts ────────────────────────────────────────────────
|
|
316
|
+
/** Whether a cell is one line tall by declaration: the shape a table column and a row
|
|
317
|
+
* summary can hold. A prose cell is one where it declares `inline`, `richtext` or
|
|
318
|
+
* `plaintext` alike. */
|
|
319
|
+
export function shortCell(sub) {
|
|
320
|
+
const kind = controlKind(sub);
|
|
321
|
+
if (kind === 'prose')
|
|
322
|
+
return !!sub.inline;
|
|
323
|
+
return (kind === 'text' || kind === 'enum' || kind === 'number' || kind === 'boolean' || kind === 'date');
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The first short text cell's words, in declaration order — a `string`, or an inline
|
|
327
|
+
* `richtext` / `plaintext` — read through {@link titleText}; `undefined` while none of
|
|
328
|
+
* them has any. What names an instance from its own values: a collapsed row, and a
|
|
329
|
+
* card whose kind declares no `title`.
|
|
330
|
+
*/
|
|
331
|
+
function firstShortText(cells, values) {
|
|
332
|
+
for (const [k, sub] of Object.entries(cells ?? {})) {
|
|
333
|
+
const kind = controlKind(sub);
|
|
334
|
+
if (kind !== 'text' && !(kind === 'prose' && shortCell(sub)))
|
|
335
|
+
continue;
|
|
336
|
+
const text = titleText(ownValue(values, k)).trim();
|
|
337
|
+
if (text)
|
|
338
|
+
return text;
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
/** A collapsed row's own words ({@link firstShortText}); `undefined` while the row has
|
|
343
|
+
* nothing to say for itself. */
|
|
344
|
+
export function rowSummary(items, row) {
|
|
345
|
+
return firstShortText(items?.properties, (row ?? {}));
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
|
|
349
|
+
* and every cell of the row is short (a block prose cell declines it, at load width and
|
|
350
|
+
* every width after), else `'list'`. That every column is a leaf is the loader's
|
|
351
|
+
* contract (`quill::table_column_not_flat`); whether a leaf fits a cell is the surface's
|
|
352
|
+
* answer (canon `SCHEMAS.md`): a table composes by position, so a row that would stack
|
|
353
|
+
* inside a cell is not one.
|
|
354
|
+
*/
|
|
355
|
+
export function arrayLayout(field) {
|
|
356
|
+
if (field.ui?.layout !== 'table')
|
|
357
|
+
return 'list';
|
|
358
|
+
const items = field.items;
|
|
359
|
+
if (!items || controlKind(items) !== 'object')
|
|
360
|
+
return 'list';
|
|
361
|
+
const cells = Object.values(items.properties ?? {});
|
|
362
|
+
return cells.length > 0 && cells.every(shortCell) ? 'table' : 'list';
|
|
363
|
+
}
|
|
82
364
|
/**
|
|
83
365
|
* Which world's cells to draw: the authored discriminant when the container carries
|
|
84
366
|
* one, else the ghosted default's member — an unset field renders its `default:`, so a
|
|
@@ -109,14 +391,18 @@ export function commitDiscriminant(value, member) {
|
|
|
109
391
|
}
|
|
110
392
|
/**
|
|
111
393
|
* Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
|
|
112
|
-
* obligation (DOCUMENT_MODEL).
|
|
113
|
-
*
|
|
114
|
-
*
|
|
394
|
+
* obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
|
|
395
|
+
* a typed dictionary and a matrix, since a namespace declares no `default:` at all and
|
|
396
|
+
* `validate` anchors obligation on the leaves under it; and an optional cell, which can
|
|
397
|
+
* declare none, its unanswered render being `none`.
|
|
115
398
|
*/
|
|
116
399
|
export function obliged(schema) {
|
|
117
|
-
return schema.type !== 'object' &&
|
|
400
|
+
return (schema.type !== 'object' &&
|
|
401
|
+
schema.type !== 'matrix' &&
|
|
402
|
+
!optionalCell(schema) &&
|
|
403
|
+
schema.default === undefined);
|
|
118
404
|
}
|
|
119
|
-
/** `foo_bar` → `Foo bar`: the label fallback when a field declares no `
|
|
405
|
+
/** `foo_bar` → `Foo bar`: the label fallback when a field declares no `title`. */
|
|
120
406
|
export function humanize(name) {
|
|
121
407
|
const spaced = name.replace(/_/g, ' ').trim();
|
|
122
408
|
return spaced ? spaced[0].toUpperCase() + spaced.slice(1) : spaced;
|
|
@@ -129,11 +415,11 @@ export function fieldModels(cardSchema) {
|
|
|
129
415
|
control: controlKind(schema),
|
|
130
416
|
group: schema.ui?.group,
|
|
131
417
|
compact: !!schema.ui?.compact,
|
|
132
|
-
label: schema.
|
|
418
|
+
label: schema.title ?? humanize(name),
|
|
133
419
|
description: schema.description,
|
|
134
420
|
required: obliged(schema),
|
|
135
421
|
inline: !!schema.inline,
|
|
136
|
-
plaintext: schema
|
|
422
|
+
plaintext: baseType(schema) === 'plaintext'
|
|
137
423
|
}));
|
|
138
424
|
}
|
|
139
425
|
/**
|
|
@@ -225,12 +511,14 @@ export function initialExpandedGroup(sections) {
|
|
|
225
511
|
* An array declines whatever its items are: one-line elements make a one-line step,
|
|
226
512
|
* but nothing holds two arrays to the same number of them, so the shorter of a packed
|
|
227
513
|
* pair pays a cell of whitespace for every element the taller one has past it, and
|
|
228
|
-
* pays more of it as the document is filled.
|
|
514
|
+
* pays more of it as the document is filled. A matrix declines as a roster does: its
|
|
515
|
+
* height is the roster's, and grows again under every member ticked open. Its hint
|
|
516
|
+
* reaches the roster instead, whose members stand abreast (`MatrixField`).
|
|
229
517
|
*/
|
|
230
518
|
function packable(f) {
|
|
231
519
|
if (!f.compact)
|
|
232
520
|
return false;
|
|
233
|
-
if (f.control
|
|
521
|
+
if (isContainer(f.control))
|
|
234
522
|
return false;
|
|
235
523
|
return f.control !== 'prose' || f.inline;
|
|
236
524
|
}
|
|
@@ -276,40 +564,44 @@ export function fieldValues(items) {
|
|
|
276
564
|
values[p.key] = p.value;
|
|
277
565
|
return values;
|
|
278
566
|
}
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
return
|
|
567
|
+
/** A value by own key: a declaration can name `constructor`. */
|
|
568
|
+
function ownValue(values, name) {
|
|
569
|
+
return Object.hasOwn(values, name) ? values[name] : undefined;
|
|
282
570
|
}
|
|
283
571
|
/** A field value as title text. A parsed field rests as the authored string and a
|
|
284
572
|
* committed one as `Content`, whose `text` is the same words; a scalar reads as
|
|
285
|
-
* itself
|
|
573
|
+
* itself; a variant container reads as its discriminant member, the one cell of it
|
|
574
|
+
* that names the world; any other container as nothing. */
|
|
286
575
|
export function titleText(v) {
|
|
287
576
|
if (v == null)
|
|
288
577
|
return '';
|
|
289
578
|
if (typeof v !== 'object')
|
|
290
579
|
return String(v);
|
|
291
580
|
const text = v.text;
|
|
292
|
-
|
|
581
|
+
if (typeof text === 'string')
|
|
582
|
+
return text;
|
|
583
|
+
const member = v[VARIANT_DISCRIMINANT];
|
|
584
|
+
return typeof member === 'string' ? member : '';
|
|
293
585
|
}
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return
|
|
586
|
+
/** A card kind's name: its schema `title`, else the humanized kind. What names a kind
|
|
587
|
+
* before there is an instance of it: the add menu, the retype select. */
|
|
588
|
+
export function kindTitle(cardSchema, kind) {
|
|
589
|
+
const t = cardSchema?.title;
|
|
590
|
+
return t && t.trim() ? t : humanize(kind);
|
|
299
591
|
}
|
|
300
592
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* cleared rename reverts to the schema
|
|
593
|
+
* A card instance's header: the per-instance `$ext.editor.title` rename; else the
|
|
594
|
+
* kind's schema `title`, which names every instance alike; else the instance's own
|
|
595
|
+
* first short text cell ({@link firstShortText}); else the humanized kind. Empty
|
|
596
|
+
* renames fall through, so a cleared rename reverts to what the schema says.
|
|
305
597
|
*/
|
|
306
598
|
export function cardTitle(cardSchema, kind, values, extTitle) {
|
|
307
599
|
if (extTitle && extTitle.trim())
|
|
308
600
|
return extTitle;
|
|
309
|
-
const t = cardSchema?.
|
|
601
|
+
const t = cardSchema?.title;
|
|
310
602
|
if (t && t.trim())
|
|
311
|
-
return
|
|
312
|
-
return humanize(kind);
|
|
603
|
+
return t;
|
|
604
|
+
return firstShortText(cardSchema?.fields, values) ?? humanize(kind);
|
|
313
605
|
}
|
|
314
606
|
/** Whether a card kind renders a body leaf: gated by `body.enabled !== false`. */
|
|
315
607
|
export function bodyEnabled(cardSchema) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.
|
|
69
|
+
"@quillmark/wasm": ">=0.116.0-0",
|
|
70
70
|
"svelte": "^5.0.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|