@quillmark/svelte 0.10.1 → 0.12.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 (80) hide show
  1. package/README.md +1 -1
  2. package/THEMING.md +2 -2
  3. package/dist/core/address.d.ts +17 -10
  4. package/dist/core/address.js +26 -12
  5. package/dist/core/bloom.js +4 -3
  6. package/dist/core/codec/blocks.js +21 -1
  7. package/dist/core/codec/breaks.d.ts +10 -2
  8. package/dist/core/codec/breaks.js +65 -9
  9. package/dist/core/codec/code.d.ts +6 -4
  10. package/dist/core/codec/code.js +7 -4
  11. package/dist/core/codec/decode.js +103 -68
  12. package/dist/core/codec/encode.d.ts +5 -8
  13. package/dist/core/codec/encode.js +56 -86
  14. package/dist/core/codec/field.js +43 -24
  15. package/dist/core/codec/index.d.ts +2 -1
  16. package/dist/core/codec/index.js +6 -3
  17. package/dist/core/codec/islands.d.ts +16 -11
  18. package/dist/core/codec/islands.js +70 -59
  19. package/dist/core/codec/keymap.js +2 -1
  20. package/dist/core/codec/marks.d.ts +17 -13
  21. package/dist/core/codec/marks.js +39 -42
  22. package/dist/core/codec/positions.d.ts +4 -1
  23. package/dist/core/codec/positions.js +5 -1
  24. package/dist/core/codec/reconcile.d.ts +6 -0
  25. package/dist/core/codec/reconcile.js +19 -0
  26. package/dist/core/codec/schema.d.ts +5 -8
  27. package/dist/core/codec/schema.js +30 -156
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-view.js +77 -26
  30. package/dist/core/codec/table.d.ts +6 -5
  31. package/dist/core/codec/table.js +14 -31
  32. package/dist/core/codec/urls.d.ts +20 -0
  33. package/dist/core/codec/urls.js +45 -0
  34. package/dist/core/rungs.d.ts +8 -0
  35. package/dist/core/rungs.js +24 -0
  36. package/dist/core/theme.css +10 -0
  37. package/dist/preset/recipes.css +1 -1
  38. package/dist/preset/scale.css +7 -4
  39. package/dist/preview/bridge.d.ts +8 -1
  40. package/dist/preview/bridge.js +31 -25
  41. package/dist/preview/controller.js +13 -13
  42. package/dist/preview/geometry.d.ts +12 -1
  43. package/dist/preview/geometry.js +14 -1
  44. package/dist/preview/paint.js +6 -4
  45. package/dist/preview/strings.d.ts +0 -2
  46. package/dist/preview/strings.js +5 -6
  47. package/dist/visual/ArrayField.svelte +600 -173
  48. package/dist/visual/ArrayField.svelte.d.ts +14 -1
  49. package/dist/visual/BooleanField.svelte +10 -7
  50. package/dist/visual/Card.svelte +8 -30
  51. package/dist/visual/DateField.svelte +2 -2
  52. package/dist/visual/Field.svelte +76 -32
  53. package/dist/visual/FieldHint.svelte +1 -1
  54. package/dist/visual/MatrixField.svelte +352 -0
  55. package/dist/visual/MatrixField.svelte.d.ts +36 -0
  56. package/dist/visual/ObjectField.svelte +356 -133
  57. package/dist/visual/ObjectField.svelte.d.ts +17 -2
  58. package/dist/visual/VariantField.svelte +55 -2
  59. package/dist/visual/VariantField.svelte.d.ts +10 -1
  60. package/dist/visual/VisualEditor.svelte +1 -1
  61. package/dist/visual/VisualEditorInner.svelte +118 -67
  62. package/dist/visual/controls.css +39 -9
  63. package/dist/visual/diagnostics.d.ts +26 -2
  64. package/dist/visual/diagnostics.js +52 -5
  65. package/dist/visual/ext.d.ts +12 -13
  66. package/dist/visual/ext.js +22 -19
  67. package/dist/visual/hold.js +2 -2
  68. package/dist/visual/icons/nodes.d.ts +3 -0
  69. package/dist/visual/icons/nodes.js +2 -1
  70. package/dist/visual/index.js +2 -3
  71. package/dist/visual/leaves.d.ts +25 -12
  72. package/dist/visual/links.d.ts +4 -0
  73. package/dist/visual/links.js +15 -4
  74. package/dist/visual/motion.d.ts +14 -5
  75. package/dist/visual/motion.js +13 -11
  76. package/dist/visual/strings.d.ts +26 -13
  77. package/dist/visual/strings.js +7 -2
  78. package/dist/visual/structure.d.ts +101 -8
  79. package/dist/visual/structure.js +252 -13
  80. package/package.json +2 -2
@@ -1,35 +1,29 @@
1
- // Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. A slot
2
- // inside a `para` line is an inline island (an image); an `island`-kind line is a
3
- // block island (a table). The node carries `id`, `islandType`, and the opaque
4
- // `props` verbatim, so an unknown island type round-trips untouched.
1
+ // Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. Every slot sits
2
+ // in a `para` line; the island's type says which node it is, a `table` a block alone on
3
+ // its line and an `image` inline. The node carries `id`, `islandType` and `props` verbatim.
5
4
  //
6
5
  // A props-aware consumer reads the shape straight off the boundary:
7
6
  // `ContentIsland.props` is the typed union (`TableProps` for `table`,
8
7
  // `ImageProps` for `image`; DOCUMENT_MODEL §Stability seams), so the codec
9
- // reads it rather than hand-rolling a shape or guard. The open `type` arm means
10
- // a discriminant check does not auto-narrow `props`; key off `type` and read
11
- // `props` as the matching upstream type.
8
+ // reads it rather than hand-rolling a shape. `type` is a closed set, so a
9
+ // discriminant check narrows it; `props` still crosses the DOM as opaque JSON,
10
+ // which is what §`tablePropsOfNode` guards.
12
11
  import { Fragment, Slice } from 'prosemirror-model';
13
12
  import { Plugin } from 'prosemirror-state';
14
- import { isTableIsland } from '@quillmark/wasm';
13
+ import { storableUrl } from './urls.js';
15
14
  /** The `U+FFFC` object-replacement char that occupies one island slot in `text`. */
16
15
  export const ISLAND_SLOT = '';
17
- // The four island node attributes carry the content entry verbatim: `id` is the
18
- // stable identity (rebases like an anchor), `islandType` the discriminator, `props`
19
- // the opaque payload the codec preserves byte-for-byte across a round-trip, and
20
- // `loss` how faithfully markdown can carry it. `loss` is authored, not derived:
21
- // `applyChange` stores the class an island op gives it and re-derives nothing, so
22
- // an entry that did not carry its own would promote a degraded table to lossless
23
- // on the first cell edit. The default is the class an unknown one reads as, so a
24
- // node no decode produced under-claims rather than over-claims; decode always
25
- // supplies the stored value.
16
+ // The three island node attributes carry the content entry verbatim: `id` is the
17
+ // stable identity (rebases like an anchor), `islandType` the discriminator, and `props`
18
+ // the payload the codec preserves byte-for-byte across a round-trip. `islandType` and
19
+ // `props` declare no default: a node standing for an island holds one, so the pair is
20
+ // supplied at every `create`.
26
21
  const islandAttrs = {
27
22
  id: { default: '' },
28
- islandType: { default: '' },
29
- props: { default: null },
30
- loss: { default: 'unrepresentable' }
23
+ islandType: {},
24
+ props: {}
31
25
  };
32
- // All four attributes cross the DOM: a copy and a paste inside one body run the document
26
+ // All three attributes cross the DOM: a copy and a paste inside one body run the document
33
27
  // through `toDOM`/`parseDOM` (CODEC §"Markdown at the edges"), and an island is a leaf
34
28
  // whose tags alone say only `[table]`. `props` crosses as JSON, the form the payload has
35
29
  // at the boundary. The names are `data-qm-*` like every other carrier's, and the id is
@@ -40,17 +34,29 @@ function islandDOM(node) {
40
34
  return {
41
35
  'data-qm-island': node.attrs.islandType,
42
36
  'data-qm-island-id': node.attrs.id,
43
- 'data-qm-island-loss': node.attrs.loss,
44
37
  'data-qm-island-props': JSON.stringify(node.attrs.props ?? null)
45
38
  };
46
39
  }
47
- /** An island node's attributes off that DOM, or `false` where the id is absent. A
48
- * `props` that is not the JSON written above reads as `null`, and one of the wrong
49
- * shape is `undefined` at its reader (§`tablePropsOfNode`): the entry survives as an
50
- * island of its type rather than taking the paste down with it. */
40
+ /** 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` over the boundary's own union,
42
+ * so a member added upstream is a missing key here rather than a silent refusal at the
43
+ * one door with no decode behind it. */
44
+ const BLOCK_ISLAND = { table: true, image: false };
45
+ /** Whether an island of `type` is block markup: a `table` stands alone on its line. */
46
+ export function isBlockIsland(type) {
47
+ return BLOCK_ISLAND[type];
48
+ }
49
+ /** An island node's attributes off that DOM, or `false` where the id is absent or the
50
+ * type is one the content vocabulary does not name — a name no read produces and no
51
+ * write takes, so the element's text arrives as text. A `props` that is not the JSON
52
+ * written above reads as `null`, and one of the wrong shape is `undefined` at its
53
+ * reader (§`tablePropsOfNode`): the entry survives as an island of its type rather
54
+ * than taking the paste down with it. */
51
55
  function islandAttrsFromDOM(el) {
52
56
  const id = el.getAttribute('data-qm-island-id');
53
- if (id == null)
57
+ // Asserted off an untrusted attribute and checked against the set on the next line.
58
+ const type = el.getAttribute('data-qm-island');
59
+ if (id == null || type == null || !Object.hasOwn(BLOCK_ISLAND, type))
54
60
  return false;
55
61
  const raw = el.getAttribute('data-qm-island-props');
56
62
  let props = null;
@@ -60,14 +66,18 @@ function islandAttrsFromDOM(el) {
60
66
  catch {
61
67
  props = null;
62
68
  }
63
- return {
64
- id,
65
- islandType: el.getAttribute('data-qm-island') ?? '',
66
- props,
67
- loss: (el.getAttribute('data-qm-island-loss') ?? 'unrepresentable')
68
- };
69
+ return { id, islandType: type, props: storableProps(type, props) };
70
+ }
71
+ /** `props` with the one key the authored lane refuses made storable: an `image`'s `url`
72
+ * carrying a line ending, which `IslandOp` and `overwrite` throw on. The rest crosses
73
+ * opaque, as it did on the way out. */
74
+ function storableProps(type, props) {
75
+ if (type !== 'image' || typeof props !== 'object' || props === null)
76
+ return props;
77
+ const url = props.url;
78
+ return typeof url === 'string' ? { ...props, url: storableUrl(url) } : props;
69
79
  }
70
- /** Block island node (a table): one `island`-kind content line. Atom, unselectable content. */
80
+ /** Block island node (a table): a `U+FFFC` slot alone on its `para` line. Atom, unselectable content. */
71
81
  export const islandBlockSpec = {
72
82
  group: 'block',
73
83
  atom: true,
@@ -86,13 +96,16 @@ export const islandInlineSpec = {
86
96
  parseDOM: [{ tag: 'span[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
87
97
  toDOM: (node) => ['span', islandDOM(node), `[${node.attrs.islandType || 'island'}]`]
88
98
  };
89
- /** Build a content island entry from a PM island node's attrs (block or inline). */
99
+ /** Build a content island entry from a PM island node's attrs (block or inline). The
100
+ * cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
101
+ * store is lenient about a table's shape — it fills what it can and keeps the rest —
102
+ * so a payload the arm does not hold is stored rather than refused, and `undefined` at
103
+ * its reader is what the surface draws the placeholder for (§`tablePropsOfNode`). */
90
104
  export function islandEntryFromNode(attrs) {
91
105
  return {
92
106
  id: attrs.id,
93
107
  type: attrs.islandType,
94
- props: attrs.props,
95
- loss: attrs.loss
108
+ props: attrs.props
96
109
  };
97
110
  }
98
111
  /** The `TableProps` shape, over a value that reached the node off the DOM rather than
@@ -107,13 +120,13 @@ function isTableProps(props) {
107
120
  return (cells(header) && Array.isArray(aligns) && Array.isArray(rows) && rows.every((row) => cells(row)));
108
121
  }
109
122
  /** A node's `TableProps`, or `undefined` for any other island and for a payload of the
110
- * wrong shape: the boundary's own guard over the entry the node carries, so the open
111
- * `type` arm narrows once here rather than at each reader. The type says which reader,
112
- * the shape says whether it can read; every caller draws the placeholder for
113
- * `undefined` (`table-view.ts` §`render`). */
123
+ * wrong shape: the boundary's own guard over the entry the node carries, so a `props`
124
+ * that crossed the DOM is read once here rather than at each reader. The type says
125
+ * which reader, the shape says whether it can read; every caller draws the placeholder
126
+ * for `undefined` (`table-view.ts` §`render`). */
114
127
  export function tablePropsOfNode(node) {
115
- const entry = islandEntryFromNode(node.attrs);
116
- return isTableIsland(entry) && isTableProps(entry.props) ? entry.props : undefined;
128
+ const attrs = node.attrs;
129
+ return attrs.islandType === 'table' && isTableProps(attrs.props) ? attrs.props : undefined;
117
130
  }
118
131
  /**
119
132
  * A minter over one document: the positional `isl-{n}` sequence continued past the
@@ -128,38 +141,35 @@ export function tablePropsOfNode(node) {
128
141
  * a stale content could collide with one the same transaction places.
129
142
  */
130
143
  export function islandMinter(doc) {
144
+ return islandState(doc).mint;
145
+ }
146
+ /** The ids a document holds and the minter continuing past them, off one sweep: both
147
+ * are the same walk under the same predicate, and the paste pass wants both. */
148
+ function islandState(doc) {
149
+ const held = new Set();
131
150
  let next = 0;
132
151
  doc.descendants((node) => {
133
152
  if (!isIsland(node))
134
153
  return true;
135
- const m = /^isl-(\d+)$/.exec(node.attrs.id);
154
+ const id = node.attrs.id;
155
+ held.add(id);
156
+ const m = /^isl-(\d+)$/.exec(id);
136
157
  if (m)
137
158
  next = Math.max(next, Number(m[1]) + 1);
138
159
  return false;
139
160
  });
140
- return () => `isl-${next++}`;
161
+ return { held, mint: () => `isl-${next++}` };
141
162
  }
142
163
  /** The next island id for a field: one draw from {@link islandMinter}, which is what a
143
164
  * command placing exactly one island wants. */
144
165
  export function mintIslandId(doc) {
145
166
  return islandMinter(doc)();
146
167
  }
147
- /** Either island node: the test the minter, the id sweep and the paste pass share, so a
148
- * third island shape is one arm rather than three. */
168
+ /** Either island node: the test the sweep and the paste pass share, so a third island
169
+ * shape is one arm rather than two. */
149
170
  function isIsland(node) {
150
171
  return node.type.name === 'island_block' || node.type.name === 'island_inline';
151
172
  }
152
- /** The ids a document already holds. */
153
- function heldIds(doc) {
154
- const held = new Set();
155
- doc.descendants((node) => {
156
- if (!isIsland(node))
157
- return true;
158
- held.add(node.attrs.id);
159
- return false;
160
- });
161
- return held;
162
- }
163
173
  /** `fragment` with every island whose id is already `taken` re-minted, or `null` where
164
174
  * none was: an untouched paste stays the slice it arrived as. Every surviving id joins
165
175
  * the set, so a slice carrying one id twice comes apart in the same pass. */
@@ -205,7 +215,8 @@ export function islandPastePlugin() {
205
215
  return new Plugin({
206
216
  props: {
207
217
  transformPasted: (slice, view) => {
208
- const content = remintIslands(slice.content, heldIds(view.state.doc), islandMinter(view.state.doc));
218
+ const { held, mint } = islandState(view.state.doc);
219
+ const content = remintIslands(slice.content, held, mint);
209
220
  return content ? new Slice(content, slice.openStart, slice.openEnd) : slice;
210
221
  }
211
222
  }
@@ -1,6 +1,7 @@
1
1
  import { chainCommands } from 'prosemirror-commands';
2
2
  import { atomKeymap } from './atoms.js';
3
3
  import { blockKeymap } from './blocks.js';
4
+ import { breakKeymap } from './breaks.js';
4
5
  import { codeKeymap } from './code.js';
5
6
  import { listKeymap } from './lists.js';
6
7
  import { slashKeymap } from './slash.js';
@@ -27,5 +28,5 @@ function chainKeymaps(...maps) {
27
28
  * no visible reason. */
28
29
  export function bodyKeymap(schema, slash) {
29
30
  const links = slash ? [slashKeymap()] : [];
30
- return chainKeymaps(...links, blockKeymap(schema), codeKeymap(schema), listKeymap(schema), atomKeymap(schema));
31
+ return chainKeymaps(...links, blockKeymap(schema), codeKeymap(schema), breakKeymap(schema), listKeymap(schema), atomKeymap(schema));
31
32
  }
@@ -1,27 +1,31 @@
1
1
  import type { Mark, Schema } from 'prosemirror-model';
2
- import type { ContentMark } from '@quillmark/wasm';
3
- /** A PM mark from a content formatting/unknown mark, or `null` for an anchor. */
2
+ import type { ContentMark, ContentMarkKind } from '@quillmark/wasm';
3
+ /** A PM mark from a content formatting mark, or `null` for an anchor. Exhaustive over
4
+ * the closed mark vocabulary, so a member added upstream is a compile error here rather
5
+ * than an index into a PM mark this schema does not declare. */
4
6
  export declare function pmMarkFromContent(schema: Schema, m: ContentMark): Mark | null;
5
7
  /**
6
8
  * A content mark descriptor from a PM mark (range-free): the `{ type, … }` half
7
- * of a `ContentMark` / `MarkOp`. `strong`/`emph`/… collapse to their content
8
- * name; the `unknown` mark re-emits its stored `type`/`attrs` verbatim.
9
+ * of a `ContentMark` / `MarkOp`. `em` collapses to `emph` and a link's `href` rides
10
+ * out as the `url` its payload spells; the other four are their own content name,
11
+ * which is what the cast claims.
9
12
  */
10
- export declare function contentDescriptorFromPM(mark: Mark): Record<string, unknown>;
13
+ export declare function contentDescriptorFromPM(mark: Mark): ContentMarkKind;
11
14
  /**
12
15
  * The range-free `{ type, … }` half of a content mark: `contentDescriptorFromPM`'s
13
- * content-side twin, and what a `MarkOp` carries beside its range. An anchor keys
14
- * on its type alone: its `id` is identity, not a formatting family, and the diff
15
- * routes anchors by id on a separate channel.
16
+ * content-side twin, and what a `MarkOp` carries beside its range. Lifted off the
17
+ * range envelope rather than rebuilt arm by arm, so a payload crosses whole and an
18
+ * arm added upstream reaches this by construction.
16
19
  */
17
- export declare function descriptorOf(m: ContentMark): Record<string, unknown>;
20
+ export declare function descriptorOf(m: ContentMark): ContentMarkKind;
18
21
  /**
19
22
  * A stable grouping key for the mark diff: marks sharing a key union into one
20
- * coverage set. Formatting keys on its type; a link keys on type+url and an
21
- * unknown on type+attrs, because `applyChange`'s `remove` matches type and attrs
22
- * (verified), so different urls / attrs are independent mark families.
23
+ * coverage set. Payload-free formatting keys on its type, everything else on
24
+ * type+attrs, because `applyChange`'s `remove` matches type and attrs (verified),
25
+ * so two links differing in url are independent mark families. This is the seam's
26
+ * own `(type, attrs)` tie-break, which is why no arm needs a case of its own.
23
27
  */
24
- export declare function markKey(descriptor: Record<string, unknown>): string;
28
+ export declare function markKey(descriptor: ContentMarkKind): string;
25
29
  /** A held anchor position: an identity id at a USV content offset (zero-width). */
26
30
  export interface AnchorPos {
27
31
  id: string;
@@ -1,64 +1,61 @@
1
- import { isAnchorMark, isLinkMark } from '@quillmark/wasm';
2
- /** Content formatting types that map 1:1 to a same-named PM mark. */
3
- const PLAIN_FORMATTING = new Set(['strong', 'underline', 'strike', 'code']);
4
- /** A PM mark from a content formatting/unknown mark, or `null` for an anchor. */
1
+ import { canonicalJson } from './reconcile.js';
2
+ /** A PM mark from a content formatting mark, or `null` for an anchor. Exhaustive over
3
+ * the closed mark vocabulary, so a member added upstream is a compile error here rather
4
+ * than an index into a PM mark this schema does not declare. */
5
5
  export function pmMarkFromContent(schema, m) {
6
- if (isAnchorMark(m))
7
- return null;
8
- if (m.type === 'emph')
9
- return schema.marks.em.create();
10
- if (isLinkMark(m))
11
- return schema.marks.link.create({ href: m.url });
12
- if (PLAIN_FORMATTING.has(m.type))
13
- return schema.marks[m.type].create();
14
- // Anything else is an unknown mark: inert, renders nothing, round-trips verbatim.
15
- return schema.marks.unknown.create({
16
- type: m.type,
17
- attrs: m.attrs ?? null
18
- });
6
+ switch (m.type) {
7
+ case 'anchor':
8
+ return null;
9
+ case 'emph':
10
+ return schema.marks.em.create();
11
+ case 'link':
12
+ return schema.marks.link.create({ href: m.attrs.url });
13
+ case 'strong':
14
+ case 'underline':
15
+ case 'strike':
16
+ case 'code':
17
+ return schema.marks[m.type].create();
18
+ default: {
19
+ const unreached = m;
20
+ return unreached;
21
+ }
22
+ }
19
23
  }
20
24
  /**
21
25
  * A content mark descriptor from a PM mark (range-free): the `{ type, … }` half
22
- * of a `ContentMark` / `MarkOp`. `strong`/`emph`/… collapse to their content
23
- * name; the `unknown` mark re-emits its stored `type`/`attrs` verbatim.
26
+ * of a `ContentMark` / `MarkOp`. `em` collapses to `emph` and a link's `href` rides
27
+ * out as the `url` its payload spells; the other four are their own content name,
28
+ * which is what the cast claims.
24
29
  */
25
30
  export function contentDescriptorFromPM(mark) {
26
31
  const name = mark.type.name;
27
32
  if (name === 'em')
28
33
  return { type: 'emph' };
29
34
  if (name === 'link')
30
- return { type: 'link', url: mark.attrs.href };
31
- if (name === 'unknown')
32
- return { type: mark.attrs.type, attrs: mark.attrs.attrs };
33
- // strong / underline / strike / code
35
+ return { type: 'link', attrs: { url: mark.attrs.href } };
34
36
  return { type: name };
35
37
  }
36
38
  /**
37
39
  * The range-free `{ type, … }` half of a content mark: `contentDescriptorFromPM`'s
38
- * content-side twin, and what a `MarkOp` carries beside its range. An anchor keys
39
- * on its type alone: its `id` is identity, not a formatting family, and the diff
40
- * routes anchors by id on a separate channel.
40
+ * content-side twin, and what a `MarkOp` carries beside its range. Lifted off the
41
+ * range envelope rather than rebuilt arm by arm, so a payload crosses whole and an
42
+ * arm added upstream reaches this by construction.
41
43
  */
42
44
  export function descriptorOf(m) {
43
- if (isLinkMark(m))
44
- return { type: 'link', url: m.url };
45
- if ('attrs' in m && !isAnchorMark(m))
46
- return { type: m.type, attrs: m.attrs };
47
- return { type: m.type };
45
+ const { start: _start, end: _end, ...kind } = m;
46
+ return kind;
48
47
  }
49
48
  /**
50
49
  * A stable grouping key for the mark diff: marks sharing a key union into one
51
- * coverage set. Formatting keys on its type; a link keys on type+url and an
52
- * unknown on type+attrs, because `applyChange`'s `remove` matches type and attrs
53
- * (verified), so different urls / attrs are independent mark families.
50
+ * coverage set. Payload-free formatting keys on its type, everything else on
51
+ * type+attrs, because `applyChange`'s `remove` matches type and attrs (verified),
52
+ * so two links differing in url are independent mark families. This is the seam's
53
+ * own `(type, attrs)` tie-break, which is why no arm needs a case of its own.
54
54
  */
55
55
  export function markKey(descriptor) {
56
- const type = descriptor.type;
57
- if (type === 'link')
58
- return `link\u0000${String(descriptor.url)}`;
59
- if (descriptor.attrs !== undefined)
60
- return `${type}\u0000${JSON.stringify(descriptor.attrs)}`;
61
- return type;
56
+ if ('attrs' in descriptor)
57
+ return `${descriptor.type}\u0000${canonicalJson(descriptor.attrs)}`;
58
+ return descriptor.type;
62
59
  }
63
60
  /**
64
61
  * The identity anchors of a `Content` as `{ id, pos }` in USV: the seed for the
@@ -68,7 +65,7 @@ export function markKey(descriptor) {
68
65
  export function anchorsFromContent(rt) {
69
66
  const out = [];
70
67
  for (const m of rt.marks)
71
- if (isAnchorMark(m))
72
- out.push({ id: m.id, pos: m.start });
68
+ if (m.type === 'anchor')
69
+ out.push({ id: m.attrs.id, pos: m.start });
73
70
  return out;
74
71
  }
@@ -1,5 +1,5 @@
1
1
  import type { Node as PMNode } from 'prosemirror-model';
2
- import { type PosRun } from './encode.js';
2
+ import { type PosRun, type Scan } from './encode.js';
3
3
  /** A prebuilt line→position index for a PM doc, rebuilt on structural change. */
4
4
  export interface LineIndex {
5
5
  runs: PosRun[];
@@ -11,6 +11,9 @@ export interface LineIndex {
11
11
  }
12
12
  /** Build the position index for `doc` (walks it once via the shared scanner). */
13
13
  export declare function buildLineIndex(doc: PMNode): LineIndex;
14
+ /** The index a scan's runs make. The commit path takes this one: it wants the
15
+ * content projection off the same walk (`encode.ts`, `scanContent`). */
16
+ export declare function lineIndexOf(s: Scan): LineIndex;
14
17
  /** USV content offset → PM position. */
15
18
  export declare function usvToPM(index: LineIndex, usvPos: number): number;
16
19
  /** PM position → USV content offset. */
@@ -1,7 +1,11 @@
1
1
  import { scanDoc } from './encode.js';
2
2
  /** Build the position index for `doc` (walks it once via the shared scanner). */
3
3
  export function buildLineIndex(doc) {
4
- const s = scanDoc(doc);
4
+ return lineIndexOf(scanDoc(doc));
5
+ }
6
+ /** The index a scan's runs make. The commit path takes this one: it wants the
7
+ * content projection off the same walk (`encode.ts`, `scanContent`). */
8
+ export function lineIndexOf(s) {
5
9
  const runs = s.runs;
6
10
  let pmEndContent = 1;
7
11
  let firstContentStart = 1;
@@ -24,3 +24,9 @@ export declare function contentEqual(a: Content, b: Content): boolean;
24
24
  * on one side and a PM node attribute on the other.
25
25
  */
26
26
  export declare function valueEqual(a: unknown, b: unknown): boolean;
27
+ /**
28
+ * A value's canonical string: key-sorted at every depth, so two spellings of one value
29
+ * mint one key whatever order each side's producer wrote them in. What the key-minting
30
+ * half of the seam stringifies with, for the reason the equalities deep-compare.
31
+ */
32
+ export declare function canonicalJson(value: unknown): string;
@@ -62,3 +62,22 @@ function deepEqual(a, b) {
62
62
  }
63
63
  return false;
64
64
  }
65
+ /**
66
+ * A value's canonical string: key-sorted at every depth, so two spellings of one value
67
+ * mint one key whatever order each side's producer wrote them in. What the key-minting
68
+ * half of the seam stringifies with, for the reason the equalities deep-compare.
69
+ */
70
+ export function canonicalJson(value) {
71
+ return JSON.stringify(canonical(value));
72
+ }
73
+ function canonical(v) {
74
+ if (Array.isArray(v))
75
+ return v.map(canonical);
76
+ if (typeof v !== 'object' || v === null)
77
+ return v;
78
+ const o = v;
79
+ const out = {};
80
+ for (const k of Object.keys(o).sort())
81
+ out[k] = canonical(o[k]);
82
+ return out;
83
+ }
@@ -1,12 +1,5 @@
1
1
  import { Schema } from 'prosemirror-model';
2
- /**
3
- * Whether a link carrying `href` renders as one. A value with no scheme is relative
4
- * to the embedding page and has none to refuse.
5
- *
6
- * A refused href is unchanged: it stays on the mark and round-trips, so a document
7
- * survives an editor that declines to make it clickable.
8
- */
9
- export declare function rendersHref(href: string): boolean;
2
+ import type { NodeType } from 'prosemirror-model';
10
3
  /** The full field schema: every block kind, container, mark, and island. */
11
4
  export declare const blockSchema: Schema<string, string>;
12
5
  /** The constrained inline schema: one paragraph, no block splitting, no
@@ -28,3 +21,7 @@ export declare const plaintextSchema: Schema<string, never>;
28
21
  export declare function hasMarks(schema: Schema): boolean;
29
22
  /** True for the constrained inline schema (no block containers); decode branches on it. */
30
23
  export declare function isInlineSchema(schema: Schema): boolean;
24
+ /** Whether `type` can hold a within-block line break, which a heading cannot (§`heading`).
25
+ * What a `\n` landing in a textblock is rewritten to turns on it (`breaks.ts`), and so
26
+ * does what decode puts between a block's continued segments. */
27
+ export declare function takesLineBreak(type: NodeType): boolean;