@quillmark/svelte 0.5.1 → 0.6.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/README.md +6 -0
- package/THEMING.md +12 -16
- package/dist/core/address.d.ts +17 -3
- package/dist/core/address.js +20 -3
- package/dist/core/codec/blocks.d.ts +37 -0
- package/dist/core/codec/blocks.js +163 -0
- package/dist/core/codec/field.js +3 -5
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +24 -144
- package/dist/core/codec/prose.css +14 -25
- package/dist/core/codec/schema.d.ts +8 -0
- package/dist/core/codec/schema.js +33 -1
- package/dist/core/codec/slash.d.ts +18 -16
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.js +127 -157
- package/dist/core/codec/table.d.ts +0 -10
- package/dist/core/codec/table.js +0 -15
- package/dist/core/theme.css +350 -344
- package/dist/preset/recipes.css +10 -17
- package/dist/preview/Preview.svelte +4 -2
- package/dist/preview/geometry.d.ts +10 -9
- package/dist/preview/geometry.js +11 -13
- package/dist/visual/ArrayField.svelte +242 -63
- package/dist/visual/ArrayField.svelte.d.ts +5 -1
- package/dist/visual/Card.svelte +21 -2
- package/dist/visual/EnumField.svelte +33 -1
- package/dist/visual/Field.svelte +28 -4
- package/dist/visual/FieldLabel.svelte +2 -2
- package/dist/visual/FormatPopover.svelte +13 -1
- package/dist/visual/ObjectField.svelte +157 -35
- package/dist/visual/ObjectField.svelte.d.ts +13 -4
- package/dist/visual/ProseArrayElement.svelte +22 -2
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
- package/dist/visual/TipsCard.svelte +33 -13
- package/dist/visual/VariantField.svelte +98 -0
- package/dist/visual/VariantField.svelte.d.ts +21 -0
- package/dist/visual/VisualEditorInner.svelte +15 -10
- package/dist/visual/controls.css +282 -283
- package/dist/visual/diagnostics.d.ts +13 -11
- package/dist/visual/diagnostics.js +11 -9
- package/dist/visual/domid.d.ts +13 -0
- package/dist/visual/domid.js +16 -0
- package/dist/visual/leaves.d.ts +9 -7
- package/dist/visual/links.d.ts +5 -2
- package/dist/visual/links.js +9 -3
- package/dist/visual/strings.d.ts +15 -0
- package/dist/visual/strings.js +3 -0
- package/dist/visual/structure.d.ts +32 -6
- package/dist/visual/structure.js +49 -3
- package/package.json +2 -2
- package/dist/core/codec/clipboard.d.ts +0 -59
- package/dist/core/codec/clipboard.js +0 -202
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Diagnostic } from '@quillmark/wasm';
|
|
2
2
|
/** A field's routing address, and `/core`'s `Addr` structurally: an `Addr` is a
|
|
3
|
-
* positional `FieldKey`, so `
|
|
4
|
-
* carries no second copy of the grammar. `card` is `undefined` for the main
|
|
5
|
-
* composable card slot is a stable session id (the editor's own bookkeeping)
|
|
6
|
-
* resolved, or an absolute document-array index straight off a parsed `DocPath`
|
|
3
|
+
* positional `FieldKey`, so `nearestAddrForFieldPath` is the path→key walk and
|
|
4
|
+
* routing carries no second copy of the grammar. `card` is `undefined` for the main
|
|
5
|
+
* card; a composable card slot is a stable session id (the editor's own bookkeeping)
|
|
6
|
+
* once resolved, or an absolute document-array index straight off a parsed `DocPath`
|
|
7
7
|
* (resolve via {@link resolveCardKey} before merging with id-keyed sources). `field`
|
|
8
8
|
* `undefined` addresses the body (the field-less leaf). */
|
|
9
9
|
export interface FieldKey {
|
|
@@ -29,13 +29,15 @@ export interface RoutedDiagnostic {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
|
|
32
|
-
* editor's stable-id keying: the one door producers #1/#3 take,
|
|
33
|
-
* and `resolveCardKey` in a single pass. Warnings do not
|
|
34
|
-
* §Diagnostics): obligation, completeness, a render note — none
|
|
35
|
-
* the field cannot hold. An
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* editor's stable-id keying: the one door producers #1/#3 take,
|
|
33
|
+
* `nearestAddrForFieldPath` and `resolveCardKey` in a single pass. Warnings do not
|
|
34
|
+
* draw (VISUAL_EDITOR §Diagnostics): obligation, completeness, a render note — none
|
|
35
|
+
* of them is a value the field cannot hold. An anchor deeper than a commit address
|
|
36
|
+
* (`main.contact.email`, `main.keywords[0]`) draws at the nearest field holding it,
|
|
37
|
+
* whose subform or repeater draws that leaf. An entry drops rather than mis-routes
|
|
38
|
+
* when it carries no `path`, when no prefix of the path is addressable, or when its
|
|
39
|
+
* absolute card index is out of the live `cardIds`.
|
|
40
|
+
* Completeness stays a read the host makes on `quill.validate(doc)`.
|
|
39
41
|
*/
|
|
40
42
|
export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
|
|
41
43
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nearestAddrForFieldPath } from '../core/address.js';
|
|
2
2
|
/** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
|
|
3
3
|
export function fieldKeyToString(k) {
|
|
4
4
|
return `${k.card ?? 'main'}:${k.field ?? '$body'}`;
|
|
@@ -20,20 +20,22 @@ export function resolveCardKey(key, cardIds) {
|
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
|
|
23
|
-
* editor's stable-id keying: the one door producers #1/#3 take,
|
|
24
|
-
* and `resolveCardKey` in a single pass. Warnings do not
|
|
25
|
-
* §Diagnostics): obligation, completeness, a render note — none
|
|
26
|
-
* the field cannot hold. An
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
23
|
+
* editor's stable-id keying: the one door producers #1/#3 take,
|
|
24
|
+
* `nearestAddrForFieldPath` and `resolveCardKey` in a single pass. Warnings do not
|
|
25
|
+
* draw (VISUAL_EDITOR §Diagnostics): obligation, completeness, a render note — none
|
|
26
|
+
* of them is a value the field cannot hold. An anchor deeper than a commit address
|
|
27
|
+
* (`main.contact.email`, `main.keywords[0]`) draws at the nearest field holding it,
|
|
28
|
+
* whose subform or repeater draws that leaf. An entry drops rather than mis-routes
|
|
29
|
+
* when it carries no `path`, when no prefix of the path is addressable, or when its
|
|
30
|
+
* absolute card index is out of the live `cardIds`.
|
|
31
|
+
* Completeness stays a read the host makes on `quill.validate(doc)`.
|
|
30
32
|
*/
|
|
31
33
|
export function routeAndResolve(diagnostics, cardIds) {
|
|
32
34
|
const out = [];
|
|
33
35
|
for (const d of diagnostics ?? []) {
|
|
34
36
|
if (d.severity !== 'error' || !d.path)
|
|
35
37
|
continue;
|
|
36
|
-
const key =
|
|
38
|
+
const key = nearestAddrForFieldPath(d.path);
|
|
37
39
|
const resolved = key && resolveCardKey(key, cardIds);
|
|
38
40
|
if (resolved)
|
|
39
41
|
out.push({ key: resolved, diagnostic: d });
|
package/dist/visual/domid.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface FieldDomIds {
|
|
|
8
8
|
description: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function fieldDomIds(uid: string, leafKey: string): FieldDomIds;
|
|
11
|
+
/**
|
|
12
|
+
* A subform property's three names, derived from the field's own control id. A
|
|
13
|
+
* property is not a field — it has no leaf key, `Addr` reaching a root and one field
|
|
14
|
+
* and no deeper — so it borrows its parent's id space rather than minting a second:
|
|
15
|
+
* the same escape, one `-p-` segment down. That is what lets a property's label be a
|
|
16
|
+
* real `<label for>` like every other label on the surface, instead of a `<span>`
|
|
17
|
+
* folding its text into the control's composed name.
|
|
18
|
+
*
|
|
19
|
+
* The `-p-` infix cannot collide with a sibling field's id: an id is the escaped
|
|
20
|
+
* `card:field` and `-` only ever introduces an escape, so `-p-` decodes as nothing a
|
|
21
|
+
* key can spell.
|
|
22
|
+
*/
|
|
23
|
+
export declare function propertyDomIds(controlId: string, key: string): FieldDomIds;
|
|
11
24
|
/**
|
|
12
25
|
* A group panel's own id: what its header's `aria-controls` names, and the only
|
|
13
26
|
* name in the DOM that is a card's rather than a field's. `card` follows the leaf
|
package/dist/visual/domid.js
CHANGED
|
@@ -21,6 +21,22 @@ export function fieldDomIds(uid, leafKey) {
|
|
|
21
21
|
const base = `qm-${uid}-${escapeId(leafKey)}`;
|
|
22
22
|
return { control: base, label: `${base}-label`, description: `${base}-desc` };
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A subform property's three names, derived from the field's own control id. A
|
|
26
|
+
* property is not a field — it has no leaf key, `Addr` reaching a root and one field
|
|
27
|
+
* and no deeper — so it borrows its parent's id space rather than minting a second:
|
|
28
|
+
* the same escape, one `-p-` segment down. That is what lets a property's label be a
|
|
29
|
+
* real `<label for>` like every other label on the surface, instead of a `<span>`
|
|
30
|
+
* folding its text into the control's composed name.
|
|
31
|
+
*
|
|
32
|
+
* The `-p-` infix cannot collide with a sibling field's id: an id is the escaped
|
|
33
|
+
* `card:field` and `-` only ever introduces an escape, so `-p-` decodes as nothing a
|
|
34
|
+
* key can spell.
|
|
35
|
+
*/
|
|
36
|
+
export function propertyDomIds(controlId, key) {
|
|
37
|
+
const base = `${controlId}-p-${escapeId(key)}`;
|
|
38
|
+
return { control: base, label: `${base}-label`, description: `${base}-desc` };
|
|
39
|
+
}
|
|
24
40
|
/**
|
|
25
41
|
* A group panel's own id: what its header's `aria-controls` names, and the only
|
|
26
42
|
* name in the DOM that is a card's rather than a field's. `card` follows the leaf
|
package/dist/visual/leaves.d.ts
CHANGED
|
@@ -10,15 +10,17 @@ import type { FieldController } from '../core/codec/index.js';
|
|
|
10
10
|
export interface FieldControl {
|
|
11
11
|
focus(): void;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
13
|
+
* Land in element `index` of a control that has elements, at USV `pos` where the
|
|
14
|
+
* row's control can take one; resolving the index to the element's session id at
|
|
15
|
+
* the call, with {@link focus}'s own answer for an index past the live list. Absent
|
|
16
|
+
* on every control without elements.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* A landing, not a focus: an absent `pos` is the placement rung, exactly as on
|
|
19
|
+
* `Landing`, and what a `pos` means is the row control's, the same way focusing is.
|
|
20
|
+
* The place rides the call because the registry stays parent-keyed: a per-element
|
|
21
|
+
* key is positional, in a registry whose doctrine is dodging positional churn.
|
|
20
22
|
*/
|
|
21
|
-
focusElement?(index: number): void;
|
|
23
|
+
focusElement?(index: number, pos?: number): void;
|
|
22
24
|
readonly el: HTMLElement;
|
|
23
25
|
}
|
|
24
26
|
/**
|
package/dist/visual/links.d.ts
CHANGED
|
@@ -7,8 +7,11 @@ import type { Command, EditorState } from 'prosemirror-state';
|
|
|
7
7
|
* userinfo — a wrong destination this normalization would itself have minted.
|
|
8
8
|
*
|
|
9
9
|
* A rooted value is left alone, being the spelling that asks for the embedding
|
|
10
|
-
* page, and so is anything already carrying a scheme
|
|
11
|
-
*
|
|
10
|
+
* page, and so is anything already carrying a scheme the link mark renders.
|
|
11
|
+
*
|
|
12
|
+
* `''` for a blank value and for a scheme the mark renders inert, both being nothing
|
|
13
|
+
* to apply: `setLink` declines an empty href, so the refusal lands at the prompt's
|
|
14
|
+
* own exit rather than storing a link that draws as plain text.
|
|
12
15
|
*/
|
|
13
16
|
export declare function normalizeHref(raw: string): string;
|
|
14
17
|
/**
|
package/dist/visual/links.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rendersHref } from '../core/codec/index.js';
|
|
1
2
|
/** A scheme: a letter, then letters, digits, `+`, `-` or `.`, then `:` (RFC 3986). */
|
|
2
3
|
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
3
4
|
/** Rooted at the page rather than at a host: an absolute path, a query, a fragment,
|
|
@@ -13,12 +14,17 @@ const ADDRESS = /^[^\s/@]+@[^\s/@]+$/;
|
|
|
13
14
|
* userinfo — a wrong destination this normalization would itself have minted.
|
|
14
15
|
*
|
|
15
16
|
* A rooted value is left alone, being the spelling that asks for the embedding
|
|
16
|
-
* page, and so is anything already carrying a scheme
|
|
17
|
-
*
|
|
17
|
+
* page, and so is anything already carrying a scheme the link mark renders.
|
|
18
|
+
*
|
|
19
|
+
* `''` for a blank value and for a scheme the mark renders inert, both being nothing
|
|
20
|
+
* to apply: `setLink` declines an empty href, so the refusal lands at the prompt's
|
|
21
|
+
* own exit rather than storing a link that draws as plain text.
|
|
18
22
|
*/
|
|
19
23
|
export function normalizeHref(raw) {
|
|
20
24
|
const value = raw.trim();
|
|
21
|
-
if (!value ||
|
|
25
|
+
if (!value || !rendersHref(value))
|
|
26
|
+
return '';
|
|
27
|
+
if (SCHEME.test(value) || ROOTED.test(value))
|
|
22
28
|
return value;
|
|
23
29
|
return ADDRESS.test(value) ? `mailto:${value}` : `https://${value}`;
|
|
24
30
|
}
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -21,6 +21,21 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
21
21
|
addCard: string;
|
|
22
22
|
arrayAdd: string;
|
|
23
23
|
arrayRemove: string;
|
|
24
|
+
/** The tag on the unset sentinel's row, which is what tells it from the member it
|
|
25
|
+
* ghosts ({@link EnumField}). */
|
|
26
|
+
enumUnsetTag: string;
|
|
27
|
+
/**
|
|
28
|
+
* What stands where a subform declines to recurse (a nested prose / array /
|
|
29
|
+
* object property), and where to edit that property instead. The whole sentence,
|
|
30
|
+
* since a translator orders the kind within it.
|
|
31
|
+
*/
|
|
32
|
+
nestedUnsupported: (kind: string) => string;
|
|
33
|
+
/**
|
|
34
|
+
* A collapsed array element whose first text property is empty: the label and the
|
|
35
|
+
* 1-based index, which is what its accessible name already says. Parametric, so
|
|
36
|
+
* word order stays the translator's.
|
|
37
|
+
*/
|
|
38
|
+
elementUntitled: (label: string, index: number) => string;
|
|
24
39
|
/** The required marker's accessible name; the glyph itself is a `*`. */
|
|
25
40
|
fieldRequired: string;
|
|
26
41
|
formatGroup: string;
|
package/dist/visual/strings.js
CHANGED
|
@@ -30,6 +30,9 @@ export const DEFAULT_VISUAL_STRINGS = {
|
|
|
30
30
|
addCard: '+ Add Card',
|
|
31
31
|
arrayAdd: '+ Add',
|
|
32
32
|
arrayRemove: 'Remove',
|
|
33
|
+
enumUnsetTag: 'default',
|
|
34
|
+
nestedUnsupported: (kind) => `A nested ${kind} — edit this field in the source view.`,
|
|
35
|
+
elementUntitled: (label, index) => `${label} ${index}`,
|
|
33
36
|
fieldRequired: 'required',
|
|
34
37
|
formatGroup: 'Formatting',
|
|
35
38
|
formatBold: 'Bold (Mod-B)',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuillCardSchema, QuillFieldSchema, ResolvedField, Resolved } from '@quillmark/wasm';
|
|
2
2
|
/** The control a field type maps to (VISUAL_EDITOR §"Structure mirrors the schema"). */
|
|
3
|
-
export type ControlKind = 'prose' | 'text' | 'enum' | 'number' | 'boolean' | 'date' | 'array' | 'object';
|
|
3
|
+
export type ControlKind = 'prose' | 'text' | 'enum' | 'variant' | 'number' | 'boolean' | 'date' | 'array' | 'object';
|
|
4
4
|
/** One field, projected: its schema, the control it renders as, and its layout hints. */
|
|
5
5
|
export interface FieldModel {
|
|
6
6
|
name: string;
|
|
@@ -17,11 +17,10 @@ export interface FieldModel {
|
|
|
17
17
|
* undefined when the field declares none. Chrome-only; never gates. */
|
|
18
18
|
description: string | undefined;
|
|
19
19
|
/**
|
|
20
|
-
* Required-ness:
|
|
21
|
-
* `default
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* filling). Label chrome only; never gates.
|
|
20
|
+
* Required-ness: {@link obliged}. Drives a persistent label `*`; a field never
|
|
21
|
+
* carries both it and a ghosted `default:`, that default being the whole of what
|
|
22
|
+
* retires the obligation. Persistent (schema-derived, survives filling). Label
|
|
23
|
+
* chrome only; never gates.
|
|
25
24
|
*/
|
|
26
25
|
required: boolean;
|
|
27
26
|
/** Prose-leaf flags (only meaningful when `control === 'prose'` / array items). */
|
|
@@ -149,6 +148,33 @@ export declare function resolveBodyGhost(resolvedDefault: string | undefined, cu
|
|
|
149
148
|
* An array's element control is this over `items`: a missing `items` is a text
|
|
150
149
|
* element. */
|
|
151
150
|
export declare function controlKind(f: QuillFieldSchema): ControlKind;
|
|
151
|
+
/** The discriminant cell of a variant container (`VARIANT_DISCRIMINANT_KEY` upstream,
|
|
152
|
+
* which the boundary does not export). Reserved: no variant may declare it. */
|
|
153
|
+
export declare const VARIANT_DISCRIMINANT = "value";
|
|
154
|
+
/**
|
|
155
|
+
* Which world's cells to draw: the authored discriminant when the container carries
|
|
156
|
+
* one, else the ghosted default's member — an unset field renders its `default:`, so a
|
|
157
|
+
* defaulted world whose cells were hidden would print answers the form never asked
|
|
158
|
+
* for. The blank owns no world, and neither does a member declaring no cells.
|
|
159
|
+
*/
|
|
160
|
+
export declare function variantMember(value: Record<string, unknown> | undefined, ghostMember: string | undefined): string | undefined;
|
|
161
|
+
/** The cells a member declares, or `undefined` where that world has none. */
|
|
162
|
+
export declare function variantCells(schema: QuillFieldSchema, member: string | undefined): Record<string, QuillFieldSchema> | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* The container a discriminant pick commits, and the discriminant cell alone the unset
|
|
165
|
+
* sentinel clears: sibling cells are **kept** either way, since the boundary carries an
|
|
166
|
+
* answer its world does not select rather than dropping it (VISUAL_EDITOR §"Enum
|
|
167
|
+
* variants"). A container left holding nothing is an unset field, which commits
|
|
168
|
+
* `undefined`.
|
|
169
|
+
*/
|
|
170
|
+
export declare function commitDiscriminant(value: Record<string, unknown> | undefined, member: string | undefined): Record<string, unknown> | undefined;
|
|
171
|
+
/**
|
|
172
|
+
* Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
|
|
173
|
+
* obligation (DOCUMENT_MODEL). A typed dictionary is exempt — a namespace declares no
|
|
174
|
+
* `default:` at all, and `validate` anchors obligation on the leaves under it, so
|
|
175
|
+
* reading one off the container would mark every subform required.
|
|
176
|
+
*/
|
|
177
|
+
export declare function obliged(schema: QuillFieldSchema): boolean;
|
|
152
178
|
/** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
|
|
153
179
|
export declare function humanize(name: string): string;
|
|
154
180
|
/** Project a card schema's `fields` map (declaration = key order) into models. */
|
package/dist/visual/structure.js
CHANGED
|
@@ -55,7 +55,9 @@ export function controlKind(f) {
|
|
|
55
55
|
case 'plaintext':
|
|
56
56
|
return 'prose';
|
|
57
57
|
case 'enum':
|
|
58
|
-
|
|
58
|
+
// `variants:` is the one key that changes a field's resting shape (canon
|
|
59
|
+
// `SCHEMAS.md`): with it the field rests as a container, without it a bare string.
|
|
60
|
+
return f.variants ? 'variant' : 'enum';
|
|
59
61
|
case 'string':
|
|
60
62
|
return 'text';
|
|
61
63
|
case 'number':
|
|
@@ -74,6 +76,46 @@ export function controlKind(f) {
|
|
|
74
76
|
return 'text';
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
/** The discriminant cell of a variant container (`VARIANT_DISCRIMINANT_KEY` upstream,
|
|
80
|
+
* which the boundary does not export). Reserved: no variant may declare it. */
|
|
81
|
+
export const VARIANT_DISCRIMINANT = 'value';
|
|
82
|
+
/**
|
|
83
|
+
* Which world's cells to draw: the authored discriminant when the container carries
|
|
84
|
+
* one, else the ghosted default's member — an unset field renders its `default:`, so a
|
|
85
|
+
* defaulted world whose cells were hidden would print answers the form never asked
|
|
86
|
+
* for. The blank owns no world, and neither does a member declaring no cells.
|
|
87
|
+
*/
|
|
88
|
+
export function variantMember(value, ghostMember) {
|
|
89
|
+
const authored = value?.[VARIANT_DISCRIMINANT];
|
|
90
|
+
return (typeof authored === 'string' ? authored : ghostMember) || undefined;
|
|
91
|
+
}
|
|
92
|
+
/** The cells a member declares, or `undefined` where that world has none. */
|
|
93
|
+
export function variantCells(schema, member) {
|
|
94
|
+
return member ? schema.variants?.[member] : undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The container a discriminant pick commits, and the discriminant cell alone the unset
|
|
98
|
+
* sentinel clears: sibling cells are **kept** either way, since the boundary carries an
|
|
99
|
+
* answer its world does not select rather than dropping it (VISUAL_EDITOR §"Enum
|
|
100
|
+
* variants"). A container left holding nothing is an unset field, which commits
|
|
101
|
+
* `undefined`.
|
|
102
|
+
*/
|
|
103
|
+
export function commitDiscriminant(value, member) {
|
|
104
|
+
const next = { ...(value ?? {}) };
|
|
105
|
+
if (member !== undefined)
|
|
106
|
+
return { ...next, [VARIANT_DISCRIMINANT]: member };
|
|
107
|
+
delete next[VARIANT_DISCRIMINANT];
|
|
108
|
+
return Object.keys(next).length ? next : undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
|
|
112
|
+
* obligation (DOCUMENT_MODEL). A typed dictionary is exempt — a namespace declares no
|
|
113
|
+
* `default:` at all, and `validate` anchors obligation on the leaves under it, so
|
|
114
|
+
* reading one off the container would mark every subform required.
|
|
115
|
+
*/
|
|
116
|
+
export function obliged(schema) {
|
|
117
|
+
return schema.type !== 'object' && schema.default === undefined;
|
|
118
|
+
}
|
|
77
119
|
/** `foo_bar` → `Foo bar`: the label fallback when a field declares no `ui.title`. */
|
|
78
120
|
export function humanize(name) {
|
|
79
121
|
const spaced = name.replace(/_/g, ' ').trim();
|
|
@@ -89,7 +131,7 @@ export function fieldModels(cardSchema) {
|
|
|
89
131
|
compact: !!schema.ui?.compact,
|
|
90
132
|
label: schema.ui?.title ?? humanize(name),
|
|
91
133
|
description: schema.description,
|
|
92
|
-
required: schema
|
|
134
|
+
required: obliged(schema),
|
|
93
135
|
inline: !!schema.inline,
|
|
94
136
|
plaintext: schema.type === 'plaintext'
|
|
95
137
|
}));
|
|
@@ -174,6 +216,10 @@ export function initialExpandedGroup(sections) {
|
|
|
174
216
|
* document carries. Any of them stands its neighbour in a column of whitespace. An
|
|
175
217
|
* inline prose leaf is one line tall and packs like any scalar.
|
|
176
218
|
*
|
|
219
|
+
* A variant declines for the same reason one step further in: its height is the live
|
|
220
|
+
* world's cell count, so it is set by a pick rather than only by the document, and a
|
|
221
|
+
* packed neighbour would reflow every time the discriminant moves.
|
|
222
|
+
*
|
|
177
223
|
* An array declines whatever its items are: one-line elements make a one-line step,
|
|
178
224
|
* but nothing holds two arrays to the same number of them, so the shorter of a packed
|
|
179
225
|
* pair pays a cell of whitespace for every element the taller one has past it, and
|
|
@@ -182,7 +228,7 @@ export function initialExpandedGroup(sections) {
|
|
|
182
228
|
function packable(f) {
|
|
183
229
|
if (!f.compact)
|
|
184
230
|
return false;
|
|
185
|
-
if (f.control === 'object' || f.control === 'array')
|
|
231
|
+
if (f.control === 'object' || f.control === 'array' || f.control === 'variant')
|
|
186
232
|
return false;
|
|
187
233
|
return f.control !== 'prose' || f.inline;
|
|
188
234
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.108.0-0",
|
|
70
70
|
"svelte": "^5.0.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { DOMParser as PMDOMParser, Slice } from 'prosemirror-model';
|
|
2
|
-
import type { Node as PMNode, Schema } from 'prosemirror-model';
|
|
3
|
-
import { Plugin } from 'prosemirror-state';
|
|
4
|
-
import type { TableCell, TableProps } from '@quillmark/wasm';
|
|
5
|
-
import { type TableAlign } from './table.js';
|
|
6
|
-
/** What a clipboard event hands over, which is all either arm needs of it: jsdom has no
|
|
7
|
-
* `DataTransfer`, and the suite drives these paths with the two methods themselves. */
|
|
8
|
-
interface Clipboard {
|
|
9
|
-
getData: (type: string) => string;
|
|
10
|
-
setData: (type: string, data: string) => void;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* A `<table>` element as props. `rowspan` is not carried — a merged cell is not a
|
|
14
|
-
* rectangle, so a table with one arrives with the rows below it short and
|
|
15
|
-
* {@link normalizeTable} pads them at the end. `colspan` is, as the empty cells it
|
|
16
|
-
* covers, so the columns after it keep their index.
|
|
17
|
-
*
|
|
18
|
-
* Only the table's own rows: a `<tr>` inside a nested table belongs to a cell this
|
|
19
|
-
* reader is about to flatten, not to a rank of this one.
|
|
20
|
-
*/
|
|
21
|
-
export declare function tableFromDOM(dom: Element): TableProps;
|
|
22
|
-
/** The table a paste is carrying, or `undefined` for a paste that holds none. The first
|
|
23
|
-
* one: a clipboard holding a table among other blocks is a document paste, and only a
|
|
24
|
-
* surface that takes nothing but a table (a cell) reads it this way. */
|
|
25
|
-
export declare function tableFromClipboard(data: Clipboard | null | undefined): TableProps | undefined;
|
|
26
|
-
/**
|
|
27
|
-
* A rectangle as a `<table>`: the wire the HTML arm writes and reads. Its first row is
|
|
28
|
-
* a `<th>` row whatever row of the source it came from, that being the row index 0 a
|
|
29
|
-
* paste lands it at.
|
|
30
|
-
*/
|
|
31
|
-
export declare function tableToDOM(block: TableCell[][], aligns: TableAlign[], owner: Document): Element;
|
|
32
|
-
/**
|
|
33
|
-
* A rectangle as pipe rows, delimiter row included: what a plain-text target reads as a
|
|
34
|
-
* table and what markdown carries. The delimiter follows the first row for the reason
|
|
35
|
-
* that row serializes as `<th>` — a pasted rectangle's row 0 is a header.
|
|
36
|
-
*/
|
|
37
|
-
export declare function tableToPipe(block: TableCell[][], aligns: TableAlign[]): string;
|
|
38
|
-
/** Put a rectangle on the clipboard, both arms. The caller is a `copy` / `cut` handler
|
|
39
|
-
* over a selection its own surface holds, so the rectangle arrives already cut out and
|
|
40
|
-
* claiming the event is the caller's. */
|
|
41
|
-
export declare function writeRectangle(data: Clipboard, block: TableCell[][], aligns: TableAlign[], owner: Document): void;
|
|
42
|
-
/** The clipboard's parser: the schema's own rules with the table rule ahead of them.
|
|
43
|
-
* Ahead, because a `<table>` otherwise matches nothing and its cells' text falls
|
|
44
|
-
* through to the enclosing block. */
|
|
45
|
-
export declare function tableClipboardParser(schema: Schema): PMDOMParser;
|
|
46
|
-
/** Mint an id for every island a paste is carrying. An id is part of the document's
|
|
47
|
-
* canonical bytes and the channel that addresses an island (CODEC §Islands), so a
|
|
48
|
-
* pasted island with none reaches the store as an insert nothing can name afterwards.
|
|
49
|
-
* One minter for the whole slice, or a paste placing two tables hands them one id. */
|
|
50
|
-
export declare function stampIslandIds(slice: Slice, doc: PMNode): Slice;
|
|
51
|
-
/**
|
|
52
|
-
* The block leaf's clipboard plugin: the table rule on the way in, and the id mint over
|
|
53
|
-
* what it produced. A `clipboardParser` rather than the schema's own `parseDOM`,
|
|
54
|
-
* because what a `<table>` in the DOM means is a question only the clipboard asks — the
|
|
55
|
-
* schema's parse rules are also what a re-parse of the editor's own DOM runs through,
|
|
56
|
-
* where an island is already a node and a `<table>` is the chrome drawing one.
|
|
57
|
-
*/
|
|
58
|
-
export declare function tableClipboardPlugin(schema: Schema): Plugin;
|
|
59
|
-
export {};
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
// The table island's clipboard door, both directions: what a copy writes, a paste
|
|
2
|
-
// reads. The design and the two wire formats are CODEC §"The table island" and
|
|
3
|
-
// §"Markdown at the edges"; what is here is the grammar.
|
|
4
|
-
//
|
|
5
|
-
// What crosses is A rectangle, never a cell. A `<table>` read in is one; the held cell
|
|
6
|
-
// selection written out is one; a rectangle overlaid onto a table at a cell is one. The
|
|
7
|
-
// wire therefore carries `TableCell[][]` and no coordinates: each end names its own
|
|
8
|
-
// corner.
|
|
9
|
-
//
|
|
10
|
-
// A row 0 is A header, on both sides, which is the model's own rule (`table.ts`
|
|
11
|
-
// §`allRows`): whichever row holds index 0 is the header. So a `<table>` with no `<th>`
|
|
12
|
-
// promotes its first row, and a copied rectangle's first row is the header of what it
|
|
13
|
-
// pastes.
|
|
14
|
-
import { DOMParser as PMDOMParser, DOMSerializer, Fragment, Slice } from 'prosemirror-model';
|
|
15
|
-
import { Plugin } from 'prosemirror-state';
|
|
16
|
-
import { islandMinter } from './islands.js';
|
|
17
|
-
import { inlineSchema } from './schema.js';
|
|
18
|
-
import { ALIGNS, cellContent, cellFromDoc, emptyCell, normalizeTable } from './table.js';
|
|
19
|
-
import { decode } from './decode.js';
|
|
20
|
-
/** The two keys, spelled once: a reader and a writer that disagreed about which holds
|
|
21
|
-
* the table would each work alone and never round-trip. */
|
|
22
|
-
const HTML = 'text/html';
|
|
23
|
-
const PLAIN = 'text/plain';
|
|
24
|
-
// ── Reading a table in ──────────────────────────────────────────────────────
|
|
25
|
-
/**
|
|
26
|
-
* One cell from its `<td>` / `<th>`. The inline schema is the whole normalization: it
|
|
27
|
-
* declares one paragraph, no containers and no islands, so a cell holding blocks
|
|
28
|
-
* flattens to its text and an image in one drops out rather than leaving a slot char
|
|
29
|
-
* behind. Whitespace collapses and the ends trim, which is PM's own parse.
|
|
30
|
-
*/
|
|
31
|
-
function cellFromDOM(dom) {
|
|
32
|
-
const para = PMDOMParser.fromSchema(inlineSchema).parse(dom, {
|
|
33
|
-
topNode: inlineSchema.nodes.paragraph.create()
|
|
34
|
-
});
|
|
35
|
-
return cellFromDoc(inlineSchema.nodes.doc.create(null, para), emptyCell());
|
|
36
|
-
}
|
|
37
|
-
/** A column's alignment off its header cell, which is where a pipe table's delimiter
|
|
38
|
-
* row puts it. Anything else (`start`, `justify`, absent) is `none`. */
|
|
39
|
-
function alignFromDOM(dom) {
|
|
40
|
-
const raw = (dom.getAttribute('align') ??
|
|
41
|
-
dom.style?.textAlign ??
|
|
42
|
-
'').toLowerCase();
|
|
43
|
-
return ALIGNS.includes(raw) ? raw : 'none';
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* A `<table>` element as props. `rowspan` is not carried — a merged cell is not a
|
|
47
|
-
* rectangle, so a table with one arrives with the rows below it short and
|
|
48
|
-
* {@link normalizeTable} pads them at the end. `colspan` is, as the empty cells it
|
|
49
|
-
* covers, so the columns after it keep their index.
|
|
50
|
-
*
|
|
51
|
-
* Only the table's own rows: a `<tr>` inside a nested table belongs to a cell this
|
|
52
|
-
* reader is about to flatten, not to a rank of this one.
|
|
53
|
-
*/
|
|
54
|
-
export function tableFromDOM(dom) {
|
|
55
|
-
const rows = [];
|
|
56
|
-
const aligns = [];
|
|
57
|
-
for (const tr of dom.querySelectorAll('tr')) {
|
|
58
|
-
if (tr.closest('table') !== dom)
|
|
59
|
-
continue;
|
|
60
|
-
const cells = [];
|
|
61
|
-
for (const box of tr.children) {
|
|
62
|
-
if (box.tagName !== 'TD' && box.tagName !== 'TH')
|
|
63
|
-
continue;
|
|
64
|
-
if (!rows.length)
|
|
65
|
-
aligns.push(alignFromDOM(box));
|
|
66
|
-
cells.push(cellFromDOM(box));
|
|
67
|
-
const span = Number(box.getAttribute('colspan')) || 1;
|
|
68
|
-
for (let i = 1; i < span; i++) {
|
|
69
|
-
if (!rows.length)
|
|
70
|
-
aligns.push('none');
|
|
71
|
-
cells.push(emptyCell());
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
rows.push(cells);
|
|
75
|
-
}
|
|
76
|
-
const [header = [], ...body] = rows;
|
|
77
|
-
return normalizeTable({ header, rows: body, aligns });
|
|
78
|
-
}
|
|
79
|
-
/** The table a paste is carrying, or `undefined` for a paste that holds none. The first
|
|
80
|
-
* one: a clipboard holding a table among other blocks is a document paste, and only a
|
|
81
|
-
* surface that takes nothing but a table (a cell) reads it this way. */
|
|
82
|
-
export function tableFromClipboard(data) {
|
|
83
|
-
const html = data?.getData(HTML);
|
|
84
|
-
if (!html || !/<table[\s>]/i.test(html))
|
|
85
|
-
return undefined;
|
|
86
|
-
const dom = new DOMParser().parseFromString(html, HTML).querySelector('table');
|
|
87
|
-
return dom ? tableFromDOM(dom) : undefined;
|
|
88
|
-
}
|
|
89
|
-
// ── Writing a rectangle out ─────────────────────────────────────────────────
|
|
90
|
-
/** A cell's inline DOM, serialized by the schema that declares its marks, which is what
|
|
91
|
-
* makes {@link cellFromDOM} its exact inverse. */
|
|
92
|
-
function cellToDOM(cell, owner) {
|
|
93
|
-
const doc = decode(cellContent(cell), inlineSchema);
|
|
94
|
-
const line = doc.firstChild;
|
|
95
|
-
return DOMSerializer.fromSchema(inlineSchema).serializeFragment(line ? line.content : Fragment.empty, { document: owner });
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* A rectangle as a `<table>`: the wire the HTML arm writes and reads. Its first row is
|
|
99
|
-
* a `<th>` row whatever row of the source it came from, that being the row index 0 a
|
|
100
|
-
* paste lands it at.
|
|
101
|
-
*/
|
|
102
|
-
export function tableToDOM(block, aligns, owner) {
|
|
103
|
-
const table = owner.createElement('table');
|
|
104
|
-
block.forEach((cells, r) => {
|
|
105
|
-
const tr = owner.createElement('tr');
|
|
106
|
-
cells.forEach((cell, c) => {
|
|
107
|
-
const box = owner.createElement(r === 0 ? 'th' : 'td');
|
|
108
|
-
const align = aligns[c] ?? 'none';
|
|
109
|
-
if (align !== 'none')
|
|
110
|
-
box.style.textAlign = align;
|
|
111
|
-
box.appendChild(cellToDOM(cell, owner));
|
|
112
|
-
tr.appendChild(box);
|
|
113
|
-
});
|
|
114
|
-
table.appendChild(tr);
|
|
115
|
-
});
|
|
116
|
-
return table;
|
|
117
|
-
}
|
|
118
|
-
/** A cell's text as one pipe-table field: the delimiter escaped, since a `|` inside a
|
|
119
|
-
* cell would otherwise open a column that is not there. Marks are dropped, markdown
|
|
120
|
-
* being the lossy arm. */
|
|
121
|
-
const pipeCell = (cell) => cell.text.replaceAll('|', '\\|');
|
|
122
|
-
/**
|
|
123
|
-
* A rectangle as pipe rows, delimiter row included: what a plain-text target reads as a
|
|
124
|
-
* table and what markdown carries. The delimiter follows the first row for the reason
|
|
125
|
-
* that row serializes as `<th>` — a pasted rectangle's row 0 is a header.
|
|
126
|
-
*/
|
|
127
|
-
export function tableToPipe(block, aligns) {
|
|
128
|
-
const width = block[0]?.length ?? 0;
|
|
129
|
-
const rule = Array.from({ length: width }, (_, c) => {
|
|
130
|
-
const align = aligns[c] ?? 'none';
|
|
131
|
-
if (align === 'center')
|
|
132
|
-
return ':---:';
|
|
133
|
-
return align === 'left' ? ':---' : align === 'right' ? '---:' : '---';
|
|
134
|
-
});
|
|
135
|
-
const line = (cells) => `| ${cells.join(' | ')} |`;
|
|
136
|
-
const [head = [], ...body] = block;
|
|
137
|
-
return [line(head.map(pipeCell)), line(rule), ...body.map((row) => line(row.map(pipeCell)))].join('\n');
|
|
138
|
-
}
|
|
139
|
-
/** Put a rectangle on the clipboard, both arms. The caller is a `copy` / `cut` handler
|
|
140
|
-
* over a selection its own surface holds, so the rectangle arrives already cut out and
|
|
141
|
-
* claiming the event is the caller's. */
|
|
142
|
-
export function writeRectangle(data, block, aligns, owner) {
|
|
143
|
-
data.setData(HTML, tableToDOM(block, aligns, owner).outerHTML);
|
|
144
|
-
data.setData(PLAIN, tableToPipe(block, aligns));
|
|
145
|
-
}
|
|
146
|
-
// ── The paste door ──────────────────────────────────────────────────────────
|
|
147
|
-
/** The `<table>` rule: a clipboard table becomes the island the model already holds
|
|
148
|
-
* rather than the paragraph its text flattens to. `id` is left empty for
|
|
149
|
-
* {@link stampIslandIds}, a parse rule having no document to mint against. `loss` is
|
|
150
|
-
* authored: what this reader produces is a pipe table, so markdown carries it whole. */
|
|
151
|
-
function tableParseRule() {
|
|
152
|
-
return {
|
|
153
|
-
tag: 'table',
|
|
154
|
-
node: 'island_block',
|
|
155
|
-
getAttrs: (dom) => ({
|
|
156
|
-
id: '',
|
|
157
|
-
islandType: 'table',
|
|
158
|
-
props: tableFromDOM(dom),
|
|
159
|
-
loss: 'lossless'
|
|
160
|
-
})
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
/** The clipboard's parser: the schema's own rules with the table rule ahead of them.
|
|
164
|
-
* Ahead, because a `<table>` otherwise matches nothing and its cells' text falls
|
|
165
|
-
* through to the enclosing block. */
|
|
166
|
-
export function tableClipboardParser(schema) {
|
|
167
|
-
return new PMDOMParser(schema, [tableParseRule(), ...PMDOMParser.fromSchema(schema).rules]);
|
|
168
|
-
}
|
|
169
|
-
/** An island node a parse rule produced, which is the only kind carrying no id. */
|
|
170
|
-
const unstamped = (node) => (node.type.name === 'island_block' || node.type.name === 'island_inline') && !node.attrs.id;
|
|
171
|
-
/** Mint an id for every island a paste is carrying. An id is part of the document's
|
|
172
|
-
* canonical bytes and the channel that addresses an island (CODEC §Islands), so a
|
|
173
|
-
* pasted island with none reaches the store as an insert nothing can name afterwards.
|
|
174
|
-
* One minter for the whole slice, or a paste placing two tables hands them one id. */
|
|
175
|
-
export function stampIslandIds(slice, doc) {
|
|
176
|
-
const mint = islandMinter(doc);
|
|
177
|
-
const walk = (fragment) => {
|
|
178
|
-
const out = [];
|
|
179
|
-
fragment.forEach((node) => {
|
|
180
|
-
const next = node.content.size ? node.copy(walk(node.content)) : node;
|
|
181
|
-
out.push(unstamped(next) ? next.type.create({ ...next.attrs, id: mint() }, next.content) : next);
|
|
182
|
-
});
|
|
183
|
-
return Fragment.fromArray(out);
|
|
184
|
-
};
|
|
185
|
-
return new Slice(walk(slice.content), slice.openStart, slice.openEnd);
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* The block leaf's clipboard plugin: the table rule on the way in, and the id mint over
|
|
189
|
-
* what it produced. A `clipboardParser` rather than the schema's own `parseDOM`,
|
|
190
|
-
* because what a `<table>` in the DOM means is a question only the clipboard asks — the
|
|
191
|
-
* schema's parse rules are also what a re-parse of the editor's own DOM runs through,
|
|
192
|
-
* where an island is already a node and a `<table>` is the chrome drawing one.
|
|
193
|
-
*/
|
|
194
|
-
export function tableClipboardPlugin(schema) {
|
|
195
|
-
const parser = tableClipboardParser(schema);
|
|
196
|
-
return new Plugin({
|
|
197
|
-
props: {
|
|
198
|
-
clipboardParser: parser,
|
|
199
|
-
transformPasted: (slice, view) => stampIslandIds(slice, view.state.doc)
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|