@quillmark/svelte 0.23.0 → 0.24.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 +7 -8
- package/dist/core/codec/decode.d.ts +2 -1
- package/dist/core/codec/decode.js +19 -11
- package/dist/core/codec/field.d.ts +2 -1
- package/dist/core/codec/islands.d.ts +4 -1
- package/dist/core/codec/islands.js +43 -16
- package/dist/core/codec/markdown-paste.d.ts +4 -3
- package/dist/core/codec/markdown-paste.js +7 -4
- package/dist/core/codec/prose.css +15 -0
- package/dist/index.js +1 -1
- package/dist/visual/Card.svelte +5 -4
- package/dist/visual/Field.svelte +2 -2
- package/dist/visual/ObjectField.svelte +4 -7
- package/dist/visual/ObjectField.svelte.d.ts +2 -2
- package/dist/visual/VisualEditorInner.svelte +5 -11
- package/dist/visual/strings.d.ts +2 -2
- package/dist/visual/structure.d.ts +22 -10
- package/dist/visual/structure.js +35 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,12 +14,12 @@ npm install @quillmark/svelte
|
|
|
14
14
|
|
|
15
15
|
Each subpath is its own module root; a bundler pulls only what the entry you import reaches.
|
|
16
16
|
|
|
17
|
-
| Import | Surface
|
|
18
|
-
| --------------------------- |
|
|
19
|
-
| `@quillmark/svelte/core` | What the surfaces share: the `DocPath`/`Place` address vocabulary, the `EditorError` channel, `init`.
|
|
20
|
-
| `@quillmark/svelte/preview` | The live preview: `createPreview` + `<Preview>`. Reaches nothing editor-side.
|
|
21
|
-
| `@quillmark/svelte/visual` | The federated WYSIWYG: `<VisualEditor>`, the codec's `createField` prose leaf.
|
|
22
|
-
| `@quillmark/svelte` | Re-exports `/core`.
|
|
17
|
+
| Import | Surface |
|
|
18
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `@quillmark/svelte/core` | What the surfaces share: the `DocPath`/`Place` address vocabulary, the `EditorError` channel, `init`. No UI framework. |
|
|
20
|
+
| `@quillmark/svelte/preview` | The live preview: `createPreview` + `<Preview>`. Reaches nothing editor-side. |
|
|
21
|
+
| `@quillmark/svelte/visual` | The federated WYSIWYG: `<VisualEditor>`, the codec's `createField` prose leaf. |
|
|
22
|
+
| `@quillmark/svelte` | Re-exports `/core`. |
|
|
23
23
|
|
|
24
24
|
## Open a session
|
|
25
25
|
|
|
@@ -29,8 +29,7 @@ The **consumer** owns the session and the handles, and drives every edit; the su
|
|
|
29
29
|
import { Engine } from '@quillmark/wasm';
|
|
30
30
|
import { init } from '@quillmark/svelte/core';
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
// neither statically.
|
|
32
|
+
// Quill and Document are exported only through init(), which loads the wasm once.
|
|
34
33
|
const { Quill, Document } = await init();
|
|
35
34
|
|
|
36
35
|
const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
|
|
@@ -29,7 +29,8 @@ export declare function fitsInline(rt: Content): boolean;
|
|
|
29
29
|
export declare function fitsPlain(rt: Content): boolean;
|
|
30
30
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
31
31
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
32
|
-
* store; on the block schema anything but an element
|
|
32
|
+
* store; on the block schema anything but a construct read and not edited — an element,
|
|
33
|
+
* a definition list ({@link editable}), a footnote or an equation (`isEditedIsland`) —
|
|
33
34
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
34
35
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
35
36
|
* reads. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// applied here; they are decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
|
|
9
9
|
// code point so an astral char is one unit, never a surrogate half.
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
|
-
import { ISLAND_SLOT, isBlockIsland } from './islands.js';
|
|
11
|
+
import { ISLAND_SLOT, isBlockIsland, isEditedIsland } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
13
|
import { core } from '../lifecycle.js';
|
|
14
14
|
import { hasMarks, isBlockSchema, isInlineSchema, takesLineBreak } from './schema.js';
|
|
@@ -96,7 +96,8 @@ export function fitsPlain(rt) {
|
|
|
96
96
|
}
|
|
97
97
|
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
98
98
|
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
99
|
-
* store; on the block schema anything but an element
|
|
99
|
+
* store; on the block schema anything but a construct read and not edited — an element,
|
|
100
|
+
* a definition list ({@link editable}), a footnote or an equation (`isEditedIsland`) —
|
|
100
101
|
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
101
102
|
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
102
103
|
* reads. */
|
|
@@ -105,11 +106,16 @@ export function fitsLeaf(rt, leaf) {
|
|
|
105
106
|
return fitsInline(rt);
|
|
106
107
|
if (leaf.plaintext)
|
|
107
108
|
return fitsPlain(rt);
|
|
108
|
-
|
|
109
|
+
if (!leaf.islands && rt.islands.length)
|
|
110
|
+
return false;
|
|
111
|
+
return rt.islands.every((isl) => isEditedIsland(isl.type)) && rt.lines.every(editable);
|
|
109
112
|
}
|
|
110
|
-
/** Whether
|
|
111
|
-
*
|
|
113
|
+
/** Whether the block schema edits `line`'s kind and every container on it: a term draws
|
|
114
|
+
* as a paragraph and an element and a description as the blocks they wrap, so the first
|
|
115
|
+
* commit would drop each. */
|
|
112
116
|
function editable(line) {
|
|
117
|
+
if (line.kind === 'term')
|
|
118
|
+
return false;
|
|
113
119
|
return line.containers.every((c) => c.container === 'quote' || c.container === 'list_item');
|
|
114
120
|
}
|
|
115
121
|
/** Inline decode: one paragraph, containers and islands stripped. */
|
|
@@ -192,9 +198,9 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
192
198
|
i = j;
|
|
193
199
|
continue;
|
|
194
200
|
}
|
|
195
|
-
if (here.container === 'element') {
|
|
196
|
-
// No node holds an element, and a leaf holding one is held
|
|
197
|
-
// what it wraps draws in place, read-only.
|
|
201
|
+
if (here.container === 'element' || here.container === 'description') {
|
|
202
|
+
// No node holds an element or a description, and a leaf holding one is held
|
|
203
|
+
// (`fitsLeaf`), so what it wraps draws in place, read-only.
|
|
198
204
|
const key = containerKey(here);
|
|
199
205
|
let j = i + 1;
|
|
200
206
|
while (j < leaves.length) {
|
|
@@ -249,7 +255,7 @@ function instanceOf(c) {
|
|
|
249
255
|
}
|
|
250
256
|
/** Identity of a container for run gathering: its name, its `instance` and an element's
|
|
251
257
|
* attributes, NUL-joined as `markKey` joins a mark's. `list_item` carries its own run
|
|
252
|
-
* rule, so the
|
|
258
|
+
* rule, so the rest are what key here. */
|
|
253
259
|
function containerKey(c) {
|
|
254
260
|
const attrs = c.container === 'element' ? JSON.stringify(c.attrs) : '';
|
|
255
261
|
return `${c.container}\u0000${instanceOf(c)}\u0000${attrs}`;
|
|
@@ -269,13 +275,15 @@ function makeLeaf(schema, leaf, marks, cursor) {
|
|
|
269
275
|
return schema.nodes.code_block.create({ lang: line.attrs?.lang ?? null }, content);
|
|
270
276
|
}
|
|
271
277
|
case 'para':
|
|
278
|
+
case 'term':
|
|
272
279
|
case 'heading': {
|
|
273
|
-
if (line.kind
|
|
280
|
+
if (line.kind !== 'heading' && blockIslandLine(leaf, cursor))
|
|
274
281
|
return schema.nodes.island_block.create(islandAttrs(cursor));
|
|
275
282
|
// Inline content, the segment boundary a `hard_break` in a paragraph and a space
|
|
276
283
|
// in a heading, which takes none (`schema.ts` §`takesLineBreak`). A stored heading
|
|
277
284
|
// carries no continuation — the store clears one after a block of a single line —
|
|
278
|
-
// so the space is what keeps a hand-built content decodable.
|
|
285
|
+
// so the space is what keeps a hand-built content decodable. A term draws as a
|
|
286
|
+
// paragraph, read-only (`fitsLeaf`).
|
|
279
287
|
const type = line.kind === 'heading' ? schema.nodes.heading : schema.nodes.paragraph;
|
|
280
288
|
const inline = [];
|
|
281
289
|
leaf.segments.forEach((seg, idx) => {
|
|
@@ -28,7 +28,8 @@ export interface CreateFieldOpts {
|
|
|
28
28
|
* leaf, or a `text` one, is held over a value its schema cannot
|
|
29
29
|
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
30
30
|
* and drop containers and islands for the first commit to store. Any leaf is held
|
|
31
|
-
* over an element,
|
|
31
|
+
* over what is read and not edited: an element, a definition list, a footnote and an
|
|
32
|
+
* equation (CODEC §Decode). A held leaf draws
|
|
32
33
|
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
33
34
|
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
34
35
|
* re-evaluates it.
|
|
@@ -5,9 +5,12 @@ import type { ContentIsland, TableProps } from '@quillmark/wasm';
|
|
|
5
5
|
export declare const ISLAND_SLOT = "\uFFFC";
|
|
6
6
|
/** Whether an island of `type` is block markup: a `table` stands alone on its line. */
|
|
7
7
|
export declare function isBlockIsland(type: ContentIsland['type']): boolean;
|
|
8
|
+
/** Whether a leaf edits an island of `type`; one holding any other is held (`fitsLeaf`). */
|
|
9
|
+
export declare function isEditedIsland(type: ContentIsland['type']): boolean;
|
|
8
10
|
/** Block island node (a table): a `U+FFFC` slot alone on its `para` line. Atom, unselectable content. */
|
|
9
11
|
export declare const islandBlockSpec: NodeSpec;
|
|
10
|
-
/** Inline island node (an image): a `U+FFFC` slot inside a
|
|
12
|
+
/** Inline island node (an image, a footnote, an equation): a `U+FFFC` slot inside a
|
|
13
|
+
* `para` line. */
|
|
11
14
|
export declare const islandInlineSpec: NodeSpec;
|
|
12
15
|
/** A PM island node's attributes: the content entry, spelled the way a node spec
|
|
13
16
|
* spells it (`islandType`, because `type` on a PM node is the node type). `islandType`
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. Every slot sits
|
|
2
2
|
// in a `para` line; the island's type says which node it is, a `table` a block alone on
|
|
3
|
-
// its line and
|
|
3
|
+
// its line and every other type inline. The node carries `id`, `islandType` and `props`
|
|
4
|
+
// verbatim.
|
|
4
5
|
//
|
|
5
6
|
// A props-aware consumer reads the shape straight off the boundary:
|
|
6
|
-
// `ContentIsland.props` is the typed union (`TableProps` for `table`,
|
|
7
|
-
// `
|
|
7
|
+
// `ContentIsland.props` is the typed union (`TableProps` for `table`, `ImageProps` for
|
|
8
|
+
// `image`, …; DOCUMENT_MODEL §Stability seams), so the codec
|
|
8
9
|
// reads it rather than hand-rolling a shape. `type` is a closed set, so a
|
|
9
10
|
// discriminant check narrows it; `props` still crosses the DOM as opaque JSON,
|
|
10
11
|
// which is what §`tablePropsOfNode` guards.
|
|
@@ -38,25 +39,35 @@ function islandDOM(node) {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
/** The island types the content model names, each with whether its markup is a block,
|
|
41
|
-
* which decides the node its slot decodes to. A `Record`
|
|
42
|
-
* so a member added upstream is a missing key here
|
|
43
|
-
* one door with no decode behind it.
|
|
44
|
-
|
|
42
|
+
* which decides the node its slot decodes to, and whether a leaf edits it. A `Record`
|
|
43
|
+
* over the boundary's own union, so a member added upstream is a missing key here
|
|
44
|
+
* rather than a silent refusal at the one door with no decode behind it. A footnote and
|
|
45
|
+
* an equation are read, not edited (CODEC §Decode): a leaf holding one is held. */
|
|
46
|
+
const ISLANDS = {
|
|
47
|
+
table: { block: true, edited: true },
|
|
48
|
+
image: { block: false, edited: true },
|
|
49
|
+
footnote: { block: false, edited: false },
|
|
50
|
+
math: { block: false, edited: false }
|
|
51
|
+
};
|
|
45
52
|
/** Whether an island of `type` is block markup: a `table` stands alone on its line. */
|
|
46
53
|
export function isBlockIsland(type) {
|
|
47
|
-
return
|
|
54
|
+
return ISLANDS[type].block;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a leaf edits an island of `type`; one holding any other is held (`fitsLeaf`). */
|
|
57
|
+
export function isEditedIsland(type) {
|
|
58
|
+
return ISLANDS[type].edited;
|
|
48
59
|
}
|
|
49
60
|
/** An island node's attributes off that DOM, or `false` where the id is absent or the
|
|
50
|
-
* type is one the
|
|
51
|
-
*
|
|
52
|
-
* written above reads as `null`, and one of the wrong shape is
|
|
53
|
-
* reader (§`tablePropsOfNode`): the entry survives as an island of
|
|
54
|
-
* than taking the paste down with it. */
|
|
61
|
+
* type is one no leaf edits — a name the vocabulary does not hold, or a footnote or an
|
|
62
|
+
* equation, which no gesture mints — so the element's text arrives as text. A `props`
|
|
63
|
+
* that is not the JSON written above reads as `null`, and one of the wrong shape is
|
|
64
|
+
* `undefined` at its reader (§`tablePropsOfNode`): the entry survives as an island of
|
|
65
|
+
* its type rather than taking the paste down with it. */
|
|
55
66
|
function islandAttrsFromDOM(el) {
|
|
56
67
|
const id = el.getAttribute('data-qm-island-id');
|
|
57
68
|
// Asserted off an untrusted attribute and checked against the set on the next line.
|
|
58
69
|
const type = el.getAttribute('data-qm-island');
|
|
59
|
-
if (id == null || type == null || !Object.hasOwn(
|
|
70
|
+
if (id == null || type == null || !Object.hasOwn(ISLANDS, type) || !isEditedIsland(type))
|
|
60
71
|
return false;
|
|
61
72
|
const raw = el.getAttribute('data-qm-island-props');
|
|
62
73
|
let props = null;
|
|
@@ -86,7 +97,8 @@ export const islandBlockSpec = {
|
|
|
86
97
|
parseDOM: [{ tag: 'div[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
|
|
87
98
|
toDOM: (node) => ['div', islandDOM(node), `[${node.attrs.islandType || 'island'}]`]
|
|
88
99
|
};
|
|
89
|
-
/** Inline island node (an image): a `U+FFFC` slot inside a
|
|
100
|
+
/** Inline island node (an image, a footnote, an equation): a `U+FFFC` slot inside a
|
|
101
|
+
* `para` line. */
|
|
90
102
|
export const islandInlineSpec = {
|
|
91
103
|
group: 'inline',
|
|
92
104
|
inline: true,
|
|
@@ -94,8 +106,23 @@ export const islandInlineSpec = {
|
|
|
94
106
|
selectable: true,
|
|
95
107
|
attrs: islandAttrs,
|
|
96
108
|
parseDOM: [{ tag: 'span[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
|
|
97
|
-
toDOM:
|
|
109
|
+
toDOM: inlineIslandDOM
|
|
98
110
|
};
|
|
111
|
+
/** An inline island's DOM: a footnote its reference mark, whose number `prose.css` draws
|
|
112
|
+
* in reading order and whose note is its title, an equation its TeX between the dollars
|
|
113
|
+
* that write it, and an image its tag. */
|
|
114
|
+
function inlineIslandDOM(node) {
|
|
115
|
+
const attrs = node.attrs;
|
|
116
|
+
const dom = islandDOM(node);
|
|
117
|
+
const props = attrs.props;
|
|
118
|
+
if (attrs.islandType === 'footnote')
|
|
119
|
+
return ['sup', typeof props?.text === 'string' ? { ...dom, title: props.text } : dom];
|
|
120
|
+
if (attrs.islandType === 'math' && typeof props?.tex === 'string') {
|
|
121
|
+
const fence = props.display ? '$$' : '$';
|
|
122
|
+
return ['span', dom, `${fence}${props.tex}${fence}`];
|
|
123
|
+
}
|
|
124
|
+
return ['span', dom, `[${attrs.islandType || 'island'}]`];
|
|
125
|
+
}
|
|
99
126
|
/** Build a content island entry from a PM island node's attrs (block or inline). The
|
|
100
127
|
* cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
|
|
101
128
|
* store is lenient about a table's shape — it fills what it can and keeps the rest —
|
|
@@ -2,8 +2,9 @@ import { Plugin } from 'prosemirror-state';
|
|
|
2
2
|
/**
|
|
3
3
|
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
4
4
|
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
5
|
-
* structure, so HTML wins. A paste as plain text keeps the text
|
|
6
|
-
*
|
|
7
|
-
* re-mints where the
|
|
5
|
+
* structure, so HTML wins. A paste as plain text keeps the text, and so does a parse
|
|
6
|
+
* building a construct no leaf edits (`fitsLeaf`). An island the parse builds arrives
|
|
7
|
+
* under the id `importMarkdown` minted, which the paste's island pass re-mints where the
|
|
8
|
+
* field holds it (`islands.ts`).
|
|
8
9
|
*/
|
|
9
10
|
export declare function markdownPastePlugin(): Plugin;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Slice } from 'prosemirror-model';
|
|
8
8
|
import { Plugin } from 'prosemirror-state';
|
|
9
9
|
import { core } from '../lifecycle.js';
|
|
10
|
-
import { decode } from './decode.js';
|
|
10
|
+
import { decode, fitsLeaf } from './decode.js';
|
|
11
11
|
import { blockSchema } from './schema.js';
|
|
12
12
|
/** A line opening a block markdown spells and prose does not: a bullet or an ordered item at
|
|
13
13
|
* any indent, and within CommonMark's three spaces an ATX heading, a quote, a fence, or a
|
|
@@ -21,9 +21,10 @@ const BLOCK_HEAD = new RegExp([
|
|
|
21
21
|
/**
|
|
22
22
|
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
23
23
|
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
24
|
-
* structure, so HTML wins. A paste as plain text keeps the text
|
|
25
|
-
*
|
|
26
|
-
* re-mints where the
|
|
24
|
+
* structure, so HTML wins. A paste as plain text keeps the text, and so does a parse
|
|
25
|
+
* building a construct no leaf edits (`fitsLeaf`). An island the parse builds arrives
|
|
26
|
+
* under the id `importMarkdown` minted, which the paste's island pass re-mints where the
|
|
27
|
+
* field holds it (`islands.ts`).
|
|
27
28
|
*/
|
|
28
29
|
export function markdownPastePlugin() {
|
|
29
30
|
return new Plugin({
|
|
@@ -42,6 +43,8 @@ export function markdownPastePlugin() {
|
|
|
42
43
|
// Nesting past the store's depth is refused; the literal text holds it.
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
46
|
+
if (!fitsLeaf(rt, { inline: false, plaintext: false, islands: true }))
|
|
47
|
+
return null;
|
|
45
48
|
return new Slice(decode(rt, blockSchema).content, 0, 0);
|
|
46
49
|
})
|
|
47
50
|
}
|
|
@@ -162,6 +162,21 @@ li.ProseMirror-selectednode::after {
|
|
|
162
162
|
pointer-events: none;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/* A footnote and an equation are read, not edited (CODEC §Decode), and draw as what
|
|
166
|
+
their markdown writes: a footnote its reference mark, numbered in reading order off a
|
|
167
|
+
counter each leaf resets, its note the marker's title (`islands.ts`); an equation its
|
|
168
|
+
TeX between its dollars, in the code face. */
|
|
169
|
+
.ProseMirror {
|
|
170
|
+
counter-reset: qm-footnote;
|
|
171
|
+
}
|
|
172
|
+
.ProseMirror sup[data-qm-island='footnote']::after {
|
|
173
|
+
counter-increment: qm-footnote;
|
|
174
|
+
content: counter(qm-footnote);
|
|
175
|
+
}
|
|
176
|
+
.ProseMirror [data-qm-island='math'] {
|
|
177
|
+
font-family: var(--_qm-font-mono);
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
/* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
|
|
166
181
|
dotted underline. Dotted rather than solid because `underline` is a mark in the
|
|
167
182
|
same schema, and a solid rule renders an authored `<u>` and a link identically in a
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `@quillmark/svelte` root: the
|
|
1
|
+
// `@quillmark/svelte` root: the substrate, free of any UI framework.
|
|
2
2
|
//
|
|
3
3
|
// The headline surfaces mount from their own subpaths (`@quillmark/svelte/preview`,
|
|
4
4
|
// `/visual`) so a bundler pulls only what an entry reaches; the root
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
One card block, its
|
|
2
|
+
One card block, its top-level objects (or legacy `ui.group`s) drawn as panels and its
|
|
3
|
+
fields packed by `ui.compact`. `main` is headerless
|
|
3
4
|
and takes no controls. Every prose leaf takes a parent-built live address, so a card
|
|
4
5
|
reorder re-targets its commits without a remount.
|
|
5
6
|
-->
|
|
@@ -273,9 +274,9 @@
|
|
|
273
274
|
{@const headIds = section.head ? ops.domIds(section.head.name) : undefined}
|
|
274
275
|
{@const headHint = section.head?.description}
|
|
275
276
|
<div class={['qm-group', { 'qm-open': isOpen }]}>
|
|
276
|
-
<!-- A
|
|
277
|
-
takes its label id and parks its description: the field
|
|
278
|
-
label of its own to say the section's name twice. -->
|
|
277
|
+
<!-- A panel's object, or an object alone in its section, is named by the
|
|
278
|
+
header, which takes its label id and parks its description: the field
|
|
279
|
+
draws no label of its own to say the section's name twice. -->
|
|
279
280
|
<div class="qm-group-head">
|
|
280
281
|
<button
|
|
281
282
|
type="button"
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
VARIANT_DISCRIMINANT,
|
|
34
34
|
arrayLayout,
|
|
35
35
|
baseType,
|
|
36
|
-
flatObject,
|
|
37
36
|
ghostDefault,
|
|
38
37
|
isContainer,
|
|
39
38
|
optionalCell,
|
|
@@ -174,7 +173,8 @@
|
|
|
174
173
|
);
|
|
175
174
|
// Two controls own their label row, the add chip and the held count sharing it.
|
|
176
175
|
const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
|
|
177
|
-
|
|
176
|
+
// A headed object is a panel's, and the panel is its frame.
|
|
177
|
+
const flat = $derived(headed);
|
|
178
178
|
|
|
179
179
|
// A container walks its diagnostics down to the cell each names and draws the rest
|
|
180
180
|
// at its own foot (`diagnostics.ts`); every other control draws them here, under
|
|
@@ -30,9 +30,8 @@
|
|
|
30
30
|
member's id. So a cell reads at whatever depth it sits, which nothing here knows.
|
|
31
31
|
|
|
32
32
|
`bare` is the table's row: the same cells with no labels and no figure of their own,
|
|
33
|
-
laid onto the tracks the table's header names ({@link ArrayField}). `flat` is
|
|
34
|
-
|
|
35
|
-
depth of whatever holds the subform.
|
|
33
|
+
laid onto the tracks the table's header names ({@link ArrayField}). `flat` is a
|
|
34
|
+
panel's object: the cells keep their labels and lose the figure, the panel being it.
|
|
36
35
|
-->
|
|
37
36
|
<script lang="ts">
|
|
38
37
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
@@ -42,7 +41,6 @@
|
|
|
42
41
|
controlKind,
|
|
43
42
|
declaredContent,
|
|
44
43
|
fieldModels,
|
|
45
|
-
flatObject,
|
|
46
44
|
humanize,
|
|
47
45
|
isContainer,
|
|
48
46
|
literalText,
|
|
@@ -103,8 +101,8 @@
|
|
|
103
101
|
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
104
102
|
* no labels and no vertical of their own. */
|
|
105
103
|
bare?: boolean;
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
104
|
+
/** A panel's object: no vertical and no rung, the properties at the depth of the
|
|
105
|
+
* panel's field list. */
|
|
108
106
|
flat?: boolean;
|
|
109
107
|
/** A card-level object, or one nested in it: its properties are placed as a
|
|
110
108
|
* card's fields are (`placeFields`), so moving a field into a group leaves its row
|
|
@@ -426,7 +424,6 @@
|
|
|
426
424
|
bind:this={nestedEls[key]}
|
|
427
425
|
value={obj[key] as Record<string, unknown> | undefined}
|
|
428
426
|
properties={sub.properties}
|
|
429
|
-
flat={flatObject(sub)}
|
|
430
427
|
{placed}
|
|
431
428
|
label={ids ? undefined : fallbackName(key, sub)}
|
|
432
429
|
idBase={ids?.control}
|
|
@@ -32,8 +32,8 @@ interface Props {
|
|
|
32
32
|
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
33
33
|
* no labels and no vertical of their own. */
|
|
34
34
|
bare?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
35
|
+
/** A panel's object: no vertical and no rung, the properties at the depth of the
|
|
36
|
+
* panel's field list. */
|
|
37
37
|
flat?: boolean;
|
|
38
38
|
/** A card-level object, or one nested in it: its properties are placed as a
|
|
39
39
|
* card's fields are (`placeFields`), so moving a field into a group leaves its row
|
|
@@ -54,10 +54,7 @@
|
|
|
54
54
|
isContainer,
|
|
55
55
|
shortCell,
|
|
56
56
|
schemaAt,
|
|
57
|
-
|
|
58
|
-
groupOrder,
|
|
59
|
-
groupSections,
|
|
60
|
-
groupLabel,
|
|
57
|
+
cardSections,
|
|
61
58
|
cardTitle,
|
|
62
59
|
kindTitle,
|
|
63
60
|
fieldValues,
|
|
@@ -325,9 +322,9 @@
|
|
|
325
322
|
try {
|
|
326
323
|
if (value === undefined) {
|
|
327
324
|
// The unset rung (VISUAL_EDITOR §"The commitment ladder"): removing the field
|
|
328
|
-
// leaves
|
|
325
|
+
// leaves Quillmark's authored › `default:` › blank-fill resolve to render the
|
|
329
326
|
// default, where writing one bakes a snapshot the schema cannot track (canon
|
|
330
|
-
// SCHEMAS.md:
|
|
327
|
+
// SCHEMAS.md: Quillmark never writes a default; the editor writes one only
|
|
331
328
|
// as the edit that takes it). Removal writes no value, so there is
|
|
332
329
|
// nothing for a schema to conform and the lane is the quill-free one.
|
|
333
330
|
if (isMain) {
|
|
@@ -632,14 +629,11 @@
|
|
|
632
629
|
payloadItems: readonly PayloadItem[];
|
|
633
630
|
ext?: Record<string, unknown>;
|
|
634
631
|
},
|
|
635
|
-
cardSchema: Parameters<typeof
|
|
632
|
+
cardSchema: Parameters<typeof cardSections>[0] | undefined,
|
|
636
633
|
rows: ResolvedCardRows
|
|
637
634
|
): CardModel {
|
|
638
635
|
const values = fieldValues(card.payloadItems);
|
|
639
|
-
const
|
|
640
|
-
const sections = cardSchema
|
|
641
|
-
? groupSections(fields, groupOrder(cardSchema), (g) => groupLabel(cardSchema, g))
|
|
642
|
-
: [];
|
|
636
|
+
const sections = cardSchema ? cardSections(cardSchema) : [];
|
|
643
637
|
const extEditor = card.ext?.editor as { title?: string } | undefined;
|
|
644
638
|
const hasBody = bodyEnabled(cardSchema);
|
|
645
639
|
return {
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
122
122
|
*
|
|
123
123
|
* - **`validation::enum_violation`, `type_mismatch`, `format_violation`**: the
|
|
124
124
|
* constraint re-words from the quill's schema at `path`, the offending value from
|
|
125
|
-
* `args` (`value` / `sourceToken` / `actual`,
|
|
125
|
+
* `args` (`value` / `sourceToken` / `actual`, Quillmark testifying to what it
|
|
126
126
|
* saw) and never from the document at `path`: validation runs post-coercion, so
|
|
127
127
|
* the validator read a value the document does not hold and a sentence built from
|
|
128
128
|
* `path` names a spelling the user never typed.
|
|
@@ -130,7 +130,7 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
130
130
|
* refused value being in neither document (unchanged on throw) nor schema.
|
|
131
131
|
* - **`parse::yaml_error_with_location`, `invalid_structure`**: does not re-word. No
|
|
132
132
|
* `path`; `args` names the block (`blockIndex`) and `location` places the failure in
|
|
133
|
-
* the document's own coordinates,
|
|
133
|
+
* the document's own coordinates, Quillmark prose riding under no key: the parser's own
|
|
134
134
|
* text, its column and its caret snippet exist only inside `message`.
|
|
135
135
|
* - **`LiveSession.warnings`**: does not re-word. Backend text, an external feed.
|
|
136
136
|
*
|
|
@@ -26,18 +26,17 @@ export interface FieldModel {
|
|
|
26
26
|
inline: boolean;
|
|
27
27
|
plaintext: boolean;
|
|
28
28
|
}
|
|
29
|
-
/** A `ui.group`
|
|
29
|
+
/** A panel section, a top-level `object`'s or a legacy `ui.group`'s: its label and the
|
|
30
|
+
* fields (declaration order) inside it. */
|
|
30
31
|
export interface GroupSection {
|
|
31
32
|
group: string | undefined;
|
|
32
33
|
label: string;
|
|
33
34
|
fields: FieldModel[];
|
|
34
|
-
/** The
|
|
35
|
-
*
|
|
35
|
+
/** The object the section's header names in place of a label of its own: the
|
|
36
|
+
* object a panel is, or a group section's one field where that field is an
|
|
37
|
+
* `object`. Its properties draw flat, the panel being their frame. */
|
|
36
38
|
head?: FieldModel;
|
|
37
39
|
}
|
|
38
|
-
/** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
|
|
39
|
-
* standing at the depth of the fields around it rather than a rung in. */
|
|
40
|
-
export declare function flatObject(schema: QuillFieldSchema): boolean;
|
|
41
40
|
/**
|
|
42
41
|
* One card instance projected for rendering: the schema × payload join result
|
|
43
42
|
* the VisualEditor's `$derived` produces and hands to `<Card>`. Positional
|
|
@@ -112,7 +111,7 @@ export declare function resolvedByCardIndex(resolved: Resolved | undefined): Map
|
|
|
112
111
|
export declare function ghostDefault(row: ResolvedField | undefined): unknown;
|
|
113
112
|
/** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
|
|
114
113
|
* case, or a number by whether it is non-zero; undefined where the render refuses the
|
|
115
|
-
* value.
|
|
114
|
+
* value. Quillmark's coercion, restated, so the suite holds it to `reader.resolve()`. */
|
|
116
115
|
export declare function renderedBoolean(value: unknown): boolean | undefined;
|
|
117
116
|
/** A ghost value's string form, or undefined for null/object (only text ghosts
|
|
118
117
|
* render a placeholder). A scalar's resolved default reads through it; a content
|
|
@@ -207,9 +206,9 @@ export declare function matrixMembers(roster: Record<string, string> | undefined
|
|
|
207
206
|
export declare function memberValue(map: Record<string, unknown> | undefined, id: string): unknown;
|
|
208
207
|
/**
|
|
209
208
|
* Whether a stored member is held: by being present. A mapping is held whatever it
|
|
210
|
-
* carries, `{}` included, and a bare scalar is the tick itself, read as
|
|
209
|
+
* carries, `{}` included, and a bare scalar is the tick itself, read as Quillmark's
|
|
211
210
|
* coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
|
|
212
|
-
* scalar held, so a spelling
|
|
211
|
+
* scalar held, so a spelling Quillmark would refuse still draws the tick it claims.
|
|
213
212
|
*/
|
|
214
213
|
export declare function matrixHeld(stored: unknown): boolean;
|
|
215
214
|
/** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
|
|
@@ -245,7 +244,7 @@ export declare function matrixDeclares(matrix: QuillFieldSchema, id: string): bo
|
|
|
245
244
|
/**
|
|
246
245
|
* The added items a stored map holds, sorted by id as the plate orders them: on an
|
|
247
246
|
* open matrix, each key off the roster spelled as an id whose mapping carries a
|
|
248
|
-
* `title`. Any other key off the roster is
|
|
247
|
+
* `title`. Any other key off the roster is Quillmark's refusal, drawn nowhere and
|
|
249
248
|
* ridden through.
|
|
250
249
|
*/
|
|
251
250
|
export declare function addedItems(matrix: QuillFieldSchema, map: Record<string, unknown> | undefined): MatrixMember[];
|
|
@@ -329,6 +328,19 @@ export declare function groupLabel(cardSchema: QuillCardSchema, group: string):
|
|
|
329
328
|
* the card schema, which falls back to {@link humanize} itself.
|
|
330
329
|
*/
|
|
331
330
|
export declare function groupSections(fields: FieldModel[], order: string[], labelFor: (group: string) => string): GroupSection[];
|
|
331
|
+
/**
|
|
332
|
+
* Object-panel sections (VISUAL_EDITOR §"Structure mirrors the schema"): every
|
|
333
|
+
* top-level `object` is a panel of its own, headed by the object, in declaration
|
|
334
|
+
* order; everything else stands in one unframed section above them. The section id
|
|
335
|
+
* is the field name, which a card's keys hold unique.
|
|
336
|
+
*/
|
|
337
|
+
export declare function objectSections(fields: FieldModel[]): GroupSection[];
|
|
338
|
+
/**
|
|
339
|
+
* A card's sections: its ungrouped fields' {@link objectSections}, then each legacy
|
|
340
|
+
* `ui.group` a panel in registry order, the order the loader settles the fields in.
|
|
341
|
+
* A panel named as a group is keyed off the group's id space, which `.` never joins.
|
|
342
|
+
*/
|
|
343
|
+
export declare function cardSections(cardSchema: QuillCardSchema): GroupSection[];
|
|
332
344
|
/**
|
|
333
345
|
* The group section a card's accordion opens on first mount: the first in
|
|
334
346
|
* order, or `null` when the card declares none. Ungrouped fields render outside
|
package/dist/visual/structure.js
CHANGED
|
@@ -11,11 +11,6 @@ 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
|
-
}
|
|
19
14
|
/**
|
|
20
15
|
* A card's resolved rows keyed by field name: the provenance channel parallel to
|
|
21
16
|
* {@link CardModel.values}. `resolve` returns rows in declaration order; the
|
|
@@ -47,7 +42,7 @@ export function ghostDefault(row) {
|
|
|
47
42
|
}
|
|
48
43
|
/** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
|
|
49
44
|
* case, or a number by whether it is non-zero; undefined where the render refuses the
|
|
50
|
-
* value.
|
|
45
|
+
* value. Quillmark's coercion, restated, so the suite holds it to `reader.resolve()`. */
|
|
51
46
|
export function renderedBoolean(value) {
|
|
52
47
|
if (typeof value === 'boolean')
|
|
53
48
|
return value;
|
|
@@ -197,16 +192,16 @@ export function memberValue(map, id) {
|
|
|
197
192
|
}
|
|
198
193
|
/**
|
|
199
194
|
* Whether a stored member is held: by being present. A mapping is held whatever it
|
|
200
|
-
* carries, `{}` included, and a bare scalar is the tick itself, read as
|
|
195
|
+
* carries, `{}` included, and a bare scalar is the tick itself, read as Quillmark's
|
|
201
196
|
* coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
|
|
202
|
-
* scalar held, so a spelling
|
|
197
|
+
* scalar held, so a spelling Quillmark would refuse still draws the tick it claims.
|
|
203
198
|
*/
|
|
204
199
|
export function matrixHeld(stored) {
|
|
205
200
|
if (typeof stored === 'object' && stored !== null)
|
|
206
201
|
return true;
|
|
207
202
|
return tickOf(stored);
|
|
208
203
|
}
|
|
209
|
-
/** A tick's scalar reading: absent and
|
|
204
|
+
/** A tick's scalar reading: absent and Quillmark's false spellings unheld, the rest held. */
|
|
210
205
|
function tickOf(v) {
|
|
211
206
|
if (v === undefined || v === null)
|
|
212
207
|
return false;
|
|
@@ -280,7 +275,7 @@ export function matrixDeclares(matrix, id) {
|
|
|
280
275
|
/**
|
|
281
276
|
* The added items a stored map holds, sorted by id as the plate orders them: on an
|
|
282
277
|
* open matrix, each key off the roster spelled as an id whose mapping carries a
|
|
283
|
-
* `title`. Any other key off the roster is
|
|
278
|
+
* `title`. Any other key off the roster is Quillmark's refusal, drawn nowhere and
|
|
284
279
|
* ridden through.
|
|
285
280
|
*/
|
|
286
281
|
export function addedItems(matrix, map) {
|
|
@@ -526,7 +521,7 @@ export function groupSections(fields, order, labelFor) {
|
|
|
526
521
|
for (const g of order) {
|
|
527
522
|
const fs = fields.filter((f) => f.group === g);
|
|
528
523
|
if (fs.length) {
|
|
529
|
-
const head = fs.length === 1 &&
|
|
524
|
+
const head = fs.length === 1 && fs[0].control === 'object' ? fs[0] : undefined;
|
|
530
525
|
sections.push({ group: g, label: labelFor(g), fields: fs, head });
|
|
531
526
|
emitted.add(g);
|
|
532
527
|
}
|
|
@@ -543,6 +538,35 @@ export function groupSections(fields, order, labelFor) {
|
|
|
543
538
|
}
|
|
544
539
|
return sections;
|
|
545
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Object-panel sections (VISUAL_EDITOR §"Structure mirrors the schema"): every
|
|
543
|
+
* top-level `object` is a panel of its own, headed by the object, in declaration
|
|
544
|
+
* order; everything else stands in one unframed section above them. The section id
|
|
545
|
+
* is the field name, which a card's keys hold unique.
|
|
546
|
+
*/
|
|
547
|
+
export function objectSections(fields) {
|
|
548
|
+
const panel = (f) => f.control === 'object';
|
|
549
|
+
const loose = fields.filter((f) => !panel(f));
|
|
550
|
+
const sections = loose.length
|
|
551
|
+
? [{ group: undefined, label: '', fields: loose }]
|
|
552
|
+
: [];
|
|
553
|
+
for (const f of fields)
|
|
554
|
+
if (panel(f))
|
|
555
|
+
sections.push({ group: f.name, label: f.label, fields: [f], head: f });
|
|
556
|
+
return sections;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* A card's sections: its ungrouped fields' {@link objectSections}, then each legacy
|
|
560
|
+
* `ui.group` a panel in registry order, the order the loader settles the fields in.
|
|
561
|
+
* A panel named as a group is keyed off the group's id space, which `.` never joins.
|
|
562
|
+
*/
|
|
563
|
+
export function cardSections(cardSchema) {
|
|
564
|
+
const fields = fieldModels(cardSchema);
|
|
565
|
+
const order = groupOrder(cardSchema);
|
|
566
|
+
const panels = objectSections(fields.filter((f) => f.group === undefined)).map((s) => s.head && order.includes(s.head.name) ? { ...s, group: `${s.head.name}.` } : s);
|
|
567
|
+
const grouped = fields.filter((f) => f.group !== undefined);
|
|
568
|
+
return [...panels, ...groupSections(grouped, order, (g) => groupLabel(cardSchema, g))];
|
|
569
|
+
}
|
|
546
570
|
/**
|
|
547
571
|
* The group section a card's accordion opens on first mount: the first in
|
|
548
572
|
* order, or `null` when the card declares none. Ungrouped fields render outside
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@quillmark/wasm": ">=0.
|
|
72
|
+
"@quillmark/wasm": ">=0.129.0-0",
|
|
73
73
|
"svelte": "^5.33.0"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|