@quillmark/svelte 0.10.1 → 0.11.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 (58) hide show
  1. package/README.md +1 -1
  2. package/THEMING.md +2 -2
  3. package/dist/core/codec/blocks.js +21 -1
  4. package/dist/core/codec/breaks.d.ts +10 -2
  5. package/dist/core/codec/breaks.js +65 -9
  6. package/dist/core/codec/code.d.ts +6 -4
  7. package/dist/core/codec/code.js +7 -4
  8. package/dist/core/codec/decode.js +91 -62
  9. package/dist/core/codec/encode.d.ts +5 -8
  10. package/dist/core/codec/encode.js +55 -85
  11. package/dist/core/codec/field.js +43 -24
  12. package/dist/core/codec/index.d.ts +2 -1
  13. package/dist/core/codec/index.js +6 -3
  14. package/dist/core/codec/islands.d.ts +13 -9
  15. package/dist/core/codec/islands.js +75 -49
  16. package/dist/core/codec/keymap.js +2 -1
  17. package/dist/core/codec/marks.d.ts +17 -13
  18. package/dist/core/codec/marks.js +39 -42
  19. package/dist/core/codec/positions.d.ts +4 -1
  20. package/dist/core/codec/positions.js +5 -1
  21. package/dist/core/codec/reconcile.d.ts +6 -0
  22. package/dist/core/codec/reconcile.js +19 -0
  23. package/dist/core/codec/schema.d.ts +5 -8
  24. package/dist/core/codec/schema.js +30 -156
  25. package/dist/core/codec/table-view.js +77 -26
  26. package/dist/core/codec/table.d.ts +6 -5
  27. package/dist/core/codec/table.js +14 -31
  28. package/dist/core/codec/urls.d.ts +20 -0
  29. package/dist/core/codec/urls.js +45 -0
  30. package/dist/preset/recipes.css +1 -1
  31. package/dist/preset/scale.css +7 -4
  32. package/dist/preview/bridge.d.ts +8 -1
  33. package/dist/preview/bridge.js +31 -25
  34. package/dist/preview/controller.js +13 -13
  35. package/dist/preview/geometry.d.ts +12 -1
  36. package/dist/preview/geometry.js +14 -1
  37. package/dist/preview/paint.js +6 -4
  38. package/dist/preview/strings.d.ts +0 -2
  39. package/dist/preview/strings.js +5 -6
  40. package/dist/visual/ArrayField.svelte +11 -13
  41. package/dist/visual/Card.svelte +5 -8
  42. package/dist/visual/DateField.svelte +2 -2
  43. package/dist/visual/Field.svelte +7 -3
  44. package/dist/visual/FieldHint.svelte +1 -1
  45. package/dist/visual/ObjectField.svelte +6 -1
  46. package/dist/visual/VisualEditor.svelte +1 -1
  47. package/dist/visual/VisualEditorInner.svelte +65 -18
  48. package/dist/visual/controls.css +8 -9
  49. package/dist/visual/ext.d.ts +12 -13
  50. package/dist/visual/ext.js +22 -19
  51. package/dist/visual/index.js +2 -3
  52. package/dist/visual/links.d.ts +4 -0
  53. package/dist/visual/links.js +15 -4
  54. package/dist/visual/strings.d.ts +7 -4
  55. package/dist/visual/strings.js +1 -0
  56. package/dist/visual/structure.d.ts +9 -1
  57. package/dist/visual/structure.js +26 -4
  58. package/package.json +2 -2
@@ -1,32 +1,31 @@
1
1
  // Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. A slot
2
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.
3
+ // block island (a table). 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
16
  // The four island node attributes carry the content entry verbatim: `id` is the
18
17
  // 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.
18
+ // the payload the codec preserves byte-for-byte across a round-trip, and `loss` how
19
+ // faithfully markdown can carry it. `islandType` and `props` declare no default: a
20
+ // node standing for an island holds one, so the pair is supplied at every `create`.
21
+ // `loss` is authored, not derived: `applyChange` stores the class an island op gives
22
+ // it and re-derives nothing, so an entry that did not carry its own would promote a
23
+ // degraded table to lossless on the first cell edit. Its default is the weakest
24
+ // claim; decode always supplies the stored value.
26
25
  const islandAttrs = {
27
26
  id: { default: '' },
28
- islandType: { default: '' },
29
- props: { default: null },
27
+ islandType: {},
28
+ props: {},
30
29
  loss: { default: 'unrepresentable' }
31
30
  };
32
31
  // All four attributes cross the DOM: a copy and a paste inside one body run the document
@@ -44,13 +43,28 @@ function islandDOM(node) {
44
43
  'data-qm-island-props': JSON.stringify(node.attrs.props ?? null)
45
44
  };
46
45
  }
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. */
46
+ // The two closed sets an element claims a value from. A `Record` over the boundary's own
47
+ // union rather than a literal check, so a member added upstream is a missing key here
48
+ // rather than a silent refusal at the one door with no decode behind it.
49
+ /** The island types the content model names. */
50
+ const ISLAND_TYPES = { table: true, image: true };
51
+ /** The loss classes it names, weakest last: an unrecognized claim takes that one. */
52
+ const LOSS_CLASSES = {
53
+ lossless: true,
54
+ degraded: true,
55
+ unrepresentable: true
56
+ };
57
+ /** An island node's attributes off that DOM, or `false` where the id is absent or the
58
+ * type is one the content vocabulary does not name — a name no read produces and no
59
+ * write takes, so the element's text arrives as text. A `props` that is not the JSON
60
+ * written above reads as `null`, and one of the wrong shape is `undefined` at its
61
+ * reader (§`tablePropsOfNode`): the entry survives as an island of its type rather
62
+ * than taking the paste down with it. */
51
63
  function islandAttrsFromDOM(el) {
52
64
  const id = el.getAttribute('data-qm-island-id');
53
- if (id == null)
65
+ // Asserted off an untrusted attribute and checked against the set on the next line.
66
+ const type = el.getAttribute('data-qm-island');
67
+ if (id == null || type == null || !ISLAND_TYPES[type])
54
68
  return false;
55
69
  const raw = el.getAttribute('data-qm-island-props');
56
70
  let props = null;
@@ -60,12 +74,22 @@ function islandAttrsFromDOM(el) {
60
74
  catch {
61
75
  props = null;
62
76
  }
63
- return {
64
- id,
65
- islandType: el.getAttribute('data-qm-island') ?? '',
66
- props,
67
- loss: (el.getAttribute('data-qm-island-loss') ?? 'unrepresentable')
68
- };
77
+ return { id, islandType: type, props: storableProps(type, props), loss: lossFromDOM(el) };
78
+ }
79
+ /** The `loss` an element claims, or the weakest class for a claim outside the set:
80
+ * an under-claim keeps a degraded island from re-crossing as a lossless one. */
81
+ function lossFromDOM(el) {
82
+ const raw = el.getAttribute('data-qm-island-loss');
83
+ return raw != null && LOSS_CLASSES[raw] ? raw : 'unrepresentable';
84
+ }
85
+ /** `props` with the one key the authored lane refuses made storable: an `image`'s `url`
86
+ * carrying a line ending, which `IslandOp` and `overwrite` throw on. The rest crosses
87
+ * opaque, as it did on the way out. */
88
+ function storableProps(type, props) {
89
+ if (type !== 'image' || typeof props !== 'object' || props === null)
90
+ return props;
91
+ const url = props.url;
92
+ return typeof url === 'string' ? { ...props, url: storableUrl(url) } : props;
69
93
  }
70
94
  /** Block island node (a table): one `island`-kind content line. Atom, unselectable content. */
71
95
  export const islandBlockSpec = {
@@ -86,7 +110,11 @@ export const islandInlineSpec = {
86
110
  parseDOM: [{ tag: 'span[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
87
111
  toDOM: (node) => ['span', islandDOM(node), `[${node.attrs.islandType || 'island'}]`]
88
112
  };
89
- /** Build a content island entry from a PM island node's attrs (block or inline). */
113
+ /** Build a content island entry from a PM island node's attrs (block or inline). The
114
+ * cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
115
+ * store is lenient about a table's shape — it fills what it can and keeps the rest —
116
+ * so a payload the arm does not hold is stored rather than refused, and `undefined` at
117
+ * its reader is what the surface draws the placeholder for (§`tablePropsOfNode`). */
90
118
  export function islandEntryFromNode(attrs) {
91
119
  return {
92
120
  id: attrs.id,
@@ -107,13 +135,13 @@ function isTableProps(props) {
107
135
  return (cells(header) && Array.isArray(aligns) && Array.isArray(rows) && rows.every((row) => cells(row)));
108
136
  }
109
137
  /** 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`). */
138
+ * wrong shape: the boundary's own guard over the entry the node carries, so a `props`
139
+ * that crossed the DOM is read once here rather than at each reader. The type says
140
+ * which reader, the shape says whether it can read; every caller draws the placeholder
141
+ * for `undefined` (`table-view.ts` §`render`). */
114
142
  export function tablePropsOfNode(node) {
115
- const entry = islandEntryFromNode(node.attrs);
116
- return isTableIsland(entry) && isTableProps(entry.props) ? entry.props : undefined;
143
+ const attrs = node.attrs;
144
+ return attrs.islandType === 'table' && isTableProps(attrs.props) ? attrs.props : undefined;
117
145
  }
118
146
  /**
119
147
  * A minter over one document: the positional `isl-{n}` sequence continued past the
@@ -128,38 +156,35 @@ export function tablePropsOfNode(node) {
128
156
  * a stale content could collide with one the same transaction places.
129
157
  */
130
158
  export function islandMinter(doc) {
159
+ return islandState(doc).mint;
160
+ }
161
+ /** The ids a document holds and the minter continuing past them, off one sweep: both
162
+ * are the same walk under the same predicate, and the paste pass wants both. */
163
+ function islandState(doc) {
164
+ const held = new Set();
131
165
  let next = 0;
132
166
  doc.descendants((node) => {
133
167
  if (!isIsland(node))
134
168
  return true;
135
- const m = /^isl-(\d+)$/.exec(node.attrs.id);
169
+ const id = node.attrs.id;
170
+ held.add(id);
171
+ const m = /^isl-(\d+)$/.exec(id);
136
172
  if (m)
137
173
  next = Math.max(next, Number(m[1]) + 1);
138
174
  return false;
139
175
  });
140
- return () => `isl-${next++}`;
176
+ return { held, mint: () => `isl-${next++}` };
141
177
  }
142
178
  /** The next island id for a field: one draw from {@link islandMinter}, which is what a
143
179
  * command placing exactly one island wants. */
144
180
  export function mintIslandId(doc) {
145
181
  return islandMinter(doc)();
146
182
  }
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. */
183
+ /** Either island node: the test the sweep and the paste pass share, so a third island
184
+ * shape is one arm rather than two. */
149
185
  function isIsland(node) {
150
186
  return node.type.name === 'island_block' || node.type.name === 'island_inline';
151
187
  }
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
188
  /** `fragment` with every island whose id is already `taken` re-minted, or `null` where
164
189
  * none was: an untouched paste stays the slice it arrived as. Every surviving id joins
165
190
  * the set, so a slice carrying one id twice comes apart in the same pass. */
@@ -205,7 +230,8 @@ export function islandPastePlugin() {
205
230
  return new Plugin({
206
231
  props: {
207
232
  transformPasted: (slice, view) => {
208
- const content = remintIslands(slice.content, heldIds(view.state.doc), islandMinter(view.state.doc));
233
+ const { held, mint } = islandState(view.state.doc);
234
+ const content = remintIslands(slice.content, held, mint);
209
235
  return content ? new Slice(content, slice.openStart, slice.openEnd) : slice;
210
236
  }
211
237
  }
@@ -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;