@quillmark/svelte 0.12.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.
Files changed (42) hide show
  1. package/dist/core/codec/decode.d.ts +17 -0
  2. package/dist/core/codec/decode.js +57 -4
  3. package/dist/core/codec/field.d.ts +64 -15
  4. package/dist/core/codec/field.js +201 -65
  5. package/dist/core/codec/index.d.ts +3 -3
  6. package/dist/core/codec/index.js +3 -3
  7. package/dist/core/codec/keymap.d.ts +5 -4
  8. package/dist/core/codec/keymap.js +5 -4
  9. package/dist/core/codec/prose.css +42 -0
  10. package/dist/core/codec/schema.d.ts +24 -5
  11. package/dist/core/codec/schema.js +40 -9
  12. package/dist/core/errors.d.ts +3 -2
  13. package/dist/core/theme.css +9 -6
  14. package/dist/preview/paint.js +21 -17
  15. package/dist/visual/ArrayField.svelte +66 -22
  16. package/dist/visual/ArrayField.svelte.d.ts +2 -0
  17. package/dist/visual/BooleanField.svelte +28 -6
  18. package/dist/visual/BooleanField.svelte.d.ts +2 -0
  19. package/dist/visual/Card.svelte +3 -3
  20. package/dist/visual/DateField.svelte +66 -31
  21. package/dist/visual/EnumField.svelte +27 -9
  22. package/dist/visual/EnumField.svelte.d.ts +5 -0
  23. package/dist/visual/Field.svelte +45 -10
  24. package/dist/visual/FormatPopover.svelte +4 -3
  25. package/dist/visual/MatrixField.svelte +3 -2
  26. package/dist/visual/NumberField.svelte +31 -14
  27. package/dist/visual/NumberField.svelte.d.ts +5 -1
  28. package/dist/visual/ObjectField.svelte +62 -15
  29. package/dist/visual/ProseField.svelte +53 -23
  30. package/dist/visual/ProseField.svelte.d.ts +15 -3
  31. package/dist/visual/ProseValue.svelte +102 -46
  32. package/dist/visual/ProseValue.svelte.d.ts +17 -5
  33. package/dist/visual/TextField.svelte +35 -18
  34. package/dist/visual/TextField.svelte.d.ts +7 -0
  35. package/dist/visual/VariantField.svelte +1 -0
  36. package/dist/visual/VisualEditorInner.svelte +24 -24
  37. package/dist/visual/controls.css +16 -5
  38. package/dist/visual/strings.d.ts +8 -1
  39. package/dist/visual/strings.js +2 -0
  40. package/dist/visual/structure.d.ts +77 -43
  41. package/dist/visual/structure.js +132 -57
  42. package/package.json +2 -2
@@ -1,10 +1,11 @@
1
1
  // The schema × payload join, done by the editor (VISUAL_EDITOR §Structure). Pure
2
2
  // functions only (no runes, no Document reads) so the ordering, control
3
- // dispatch, group layout, title interpolation, and session-identity bookkeeping
3
+ // dispatch, group layout, titles, ghosts, and session-identity bookkeeping
4
4
  // are unit-testable in isolation (tests/visual/structure.test.ts). The reactive
5
5
  // orchestration (revision counter, live doc reads) lives in VisualEditor.svelte;
6
6
  // this module is the projection math it feeds.
7
7
  import { VARIANT_DISCRIMINANT_KEY } from '@quillmark/wasm';
8
+ import { core } from '../core/lifecycle.js';
8
9
  /** The container controls: the ones a nested address walks into and a deep
9
10
  * diagnostic routes through. */
10
11
  export function isContainer(kind) {
@@ -33,36 +34,112 @@ export const NO_RESOLVED_ROWS = { fields: [], body: null };
33
34
  export function resolvedByCardIndex(resolved) {
34
35
  return new Map((resolved?.cards ?? []).map((c) => [c.index, { fields: c.fields, body: c.body }]));
35
36
  }
36
- /** The ghost a field shows when unset: the resolved `default:` value the render
37
- * would use (`source === 'default'`), else undefined: an `authored` field shows
38
- * its value, a `zero` field has no default to ghost. */
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. */
39
40
  export function ghostDefault(row) {
40
41
  return row?.source === 'default' ? row.value : undefined;
41
42
  }
42
43
  /** A ghost value's string form, or undefined for null/object (only text ghosts
43
- * render a placeholder). The one text-ghost projection: a scalar field's
44
- * placeholder and a body leaf's both read `stringifyGhost ∘
45
- * ghostDefault`, since a richtext body resolves to a text render: the correct
46
- * 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}). */
47
47
  export function stringifyGhost(ghost) {
48
48
  return ghost != null && typeof ghost !== 'object' ? String(ghost) : undefined;
49
49
  }
50
50
  /**
51
- * The empty body's ghost: the resolved body `default:`, else the consumer's
52
- * wording, else the flat built-in. The `default:` wins because it is the only one
53
- * of the three that describes the render: it promises what prints if nothing is
54
- * written, and wording placed over it would make that promise unreadable. The
55
- * other two are invitations, and an invitation belongs only where there is no
56
- * 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.
57
58
  */
58
- export function resolveBodyGhost(resolvedDefault, custom, builtIn) {
59
- return resolvedDefault || custom || builtIn;
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.
122
+ */
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}`;
60
137
  }
61
138
  /** Map a field schema to its control (precedence: prose › enum › text › …).
62
139
  * An array's element control is this over `items`: a missing `items` is a text
63
140
  * element. */
64
141
  export function controlKind(f) {
65
- switch (f.type) {
142
+ switch (baseType(f)) {
66
143
  case 'richtext':
67
144
  case 'plaintext':
68
145
  return 'prose';
@@ -237,29 +314,22 @@ function stepInto(schema, step) {
237
314
  }
238
315
  // ── Row summaries and layouts ────────────────────────────────────────────────
239
316
  /** Whether a cell is one line tall by declaration: the shape a table column and a row
240
- * summary can hold. A `plaintext` leaf is inline at the codec whatever it declares. */
317
+ * summary can hold. A prose cell is one where it declares `inline`, `richtext` or
318
+ * `plaintext` alike. */
241
319
  export function shortCell(sub) {
242
320
  const kind = controlKind(sub);
243
321
  if (kind === 'prose')
244
- return sub.type === 'plaintext' || !!sub.inline;
322
+ return !!sub.inline;
245
323
  return (kind === 'text' || kind === 'enum' || kind === 'number' || kind === 'boolean' || kind === 'date');
246
324
  }
247
325
  /**
248
- * A collapsed row's own words: `items.ui.title` interpolated with the row's values
249
- * where the schema declares one, else the first short text cell in declaration order
250
- * — a `string`, or an inline `richtext` / `plaintext` — read through {@link titleText};
251
- * `undefined` while the row has nothing to say for itself. `title` sits on `items.ui`,
252
- * describing a row, where `layout` sits on the array's own `ui`, describing the array.
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`.
253
330
  */
254
- export function rowSummary(items, row) {
255
- const values = (row ?? {});
256
- const template = items?.ui?.title;
257
- if (template && template.trim()) {
258
- const shown = interpolateTitle(template, values).trim();
259
- if (shown)
260
- return shown;
261
- }
262
- for (const [k, sub] of Object.entries(items?.properties ?? {})) {
331
+ function firstShortText(cells, values) {
332
+ for (const [k, sub] of Object.entries(cells ?? {})) {
263
333
  const kind = controlKind(sub);
264
334
  if (kind !== 'text' && !(kind === 'prose' && shortCell(sub)))
265
335
  continue;
@@ -269,6 +339,11 @@ export function rowSummary(items, row) {
269
339
  }
270
340
  return undefined;
271
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
+ }
272
347
  /**
273
348
  * The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
274
349
  * and every cell of the row is short (a block prose cell declines it, at load width and
@@ -316,14 +391,18 @@ export function commitDiscriminant(value, member) {
316
391
  }
317
392
  /**
318
393
  * Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
319
- * obligation (DOCUMENT_MODEL). A typed dictionary and a matrix are exempt — a namespace
320
- * declares no `default:` at all, and `validate` anchors obligation on the leaves under
321
- * it, so reading one off the container would mark every subform required.
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`.
322
398
  */
323
399
  export function obliged(schema) {
324
- return schema.type !== 'object' && schema.type !== 'matrix' && schema.default === undefined;
400
+ return (schema.type !== 'object' &&
401
+ schema.type !== 'matrix' &&
402
+ !optionalCell(schema) &&
403
+ schema.default === undefined);
325
404
  }
326
- /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
405
+ /** `foo_bar` → `Foo bar`: the label fallback when a field declares no `title`. */
327
406
  export function humanize(name) {
328
407
  const spaced = name.replace(/_/g, ' ').trim();
329
408
  return spaced ? spaced[0].toUpperCase() + spaced.slice(1) : spaced;
@@ -336,11 +415,11 @@ export function fieldModels(cardSchema) {
336
415
  control: controlKind(schema),
337
416
  group: schema.ui?.group,
338
417
  compact: !!schema.ui?.compact,
339
- label: schema.ui?.title ?? humanize(name),
418
+ label: schema.title ?? humanize(name),
340
419
  description: schema.description,
341
420
  required: obliged(schema),
342
421
  inline: !!schema.inline,
343
- plaintext: schema.type === 'plaintext'
422
+ plaintext: baseType(schema) === 'plaintext'
344
423
  }));
345
424
  }
346
425
  /**
@@ -485,11 +564,7 @@ export function fieldValues(items) {
485
564
  values[p.key] = p.value;
486
565
  return values;
487
566
  }
488
- /** Interpolate a `{field}` card-title template against live field values. */
489
- export function interpolateTitle(template, values) {
490
- return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => titleText(ownValue(values, name)));
491
- }
492
- /** A value by own key: a template or a declaration can name `constructor`. */
567
+ /** A value by own key: a declaration can name `constructor`. */
493
568
  function ownValue(values, name) {
494
569
  return Object.hasOwn(values, name) ? values[name] : undefined;
495
570
  }
@@ -508,25 +583,25 @@ export function titleText(v) {
508
583
  const member = v[VARIANT_DISCRIMINANT];
509
584
  return typeof member === 'string' ? member : '';
510
585
  }
511
- /** The field names a `{field}` title reads. */
512
- export function titleFields(template) {
513
- if (!template)
514
- return [];
515
- return [...template.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
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);
516
591
  }
517
592
  /**
518
- * Resolve a card instance's header title: the per-instance `$ext.editor.title`
519
- * override wins; else the schema `ui.title` (literal or `{field}` template
520
- * interpolated); else the humanized kind. Empty overrides fall through so a
521
- * cleared rename reverts to the schema title.
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.
522
597
  */
523
598
  export function cardTitle(cardSchema, kind, values, extTitle) {
524
599
  if (extTitle && extTitle.trim())
525
600
  return extTitle;
526
- const t = cardSchema?.ui?.title;
601
+ const t = cardSchema?.title;
527
602
  if (t && t.trim())
528
- return interpolateTitle(t, values);
529
- return humanize(kind);
603
+ return t;
604
+ return firstShortText(cardSchema?.fields, values) ?? humanize(kind);
530
605
  }
531
606
  /** Whether a card kind renders a body leaf: gated by `body.enabled !== false`. */
532
607
  export function bodyEnabled(cardSchema) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.12.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.115.0-0",
69
+ "@quillmark/wasm": ">=0.116.0-0",
70
70
  "svelte": "^5.0.0"
71
71
  },
72
72
  "dependencies": {