@quillmark/svelte 0.0.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +58 -0
  3. package/README.md +241 -0
  4. package/THEMING.md +153 -0
  5. package/dist/core/address.d.ts +56 -0
  6. package/dist/core/address.js +95 -0
  7. package/dist/core/bloom.d.ts +37 -0
  8. package/dist/core/bloom.js +116 -0
  9. package/dist/core/codec/anchor.d.ts +19 -0
  10. package/dist/core/codec/anchor.js +36 -0
  11. package/dist/core/codec/code.d.ts +12 -0
  12. package/dist/core/codec/code.js +118 -0
  13. package/dist/core/codec/decode.d.ts +24 -0
  14. package/dist/core/codec/decode.js +270 -0
  15. package/dist/core/codec/encode.d.ts +71 -0
  16. package/dist/core/codec/encode.js +480 -0
  17. package/dist/core/codec/field.d.ts +171 -0
  18. package/dist/core/codec/field.js +493 -0
  19. package/dist/core/codec/index.d.ts +22 -0
  20. package/dist/core/codec/index.js +43 -0
  21. package/dist/core/codec/inputrules.d.ts +7 -0
  22. package/dist/core/codec/inputrules.js +168 -0
  23. package/dist/core/codec/islands.d.ts +36 -0
  24. package/dist/core/codec/islands.js +82 -0
  25. package/dist/core/codec/keymap.d.ts +13 -0
  26. package/dist/core/codec/keymap.js +29 -0
  27. package/dist/core/codec/lists.d.ts +22 -0
  28. package/dist/core/codec/lists.js +104 -0
  29. package/dist/core/codec/marks.d.ts +37 -0
  30. package/dist/core/codec/marks.js +74 -0
  31. package/dist/core/codec/positions.d.ts +17 -0
  32. package/dist/core/codec/positions.js +108 -0
  33. package/dist/core/codec/prose.css +458 -0
  34. package/dist/core/codec/reconcile.d.ts +26 -0
  35. package/dist/core/codec/reconcile.js +64 -0
  36. package/dist/core/codec/schema.d.ts +13 -0
  37. package/dist/core/codec/schema.js +155 -0
  38. package/dist/core/codec/slash.d.ts +79 -0
  39. package/dist/core/codec/slash.js +0 -0
  40. package/dist/core/codec/table-view.d.ts +50 -0
  41. package/dist/core/codec/table-view.js +928 -0
  42. package/dist/core/codec/table.d.ts +85 -0
  43. package/dist/core/codec/table.js +224 -0
  44. package/dist/core/errors.d.ts +73 -0
  45. package/dist/core/errors.js +24 -0
  46. package/dist/core/index.d.ts +5 -0
  47. package/dist/core/index.js +19 -0
  48. package/dist/core/lifecycle.d.ts +7 -0
  49. package/dist/core/lifecycle.js +21 -0
  50. package/dist/core/rebind.svelte.d.ts +18 -0
  51. package/dist/core/rebind.svelte.js +44 -0
  52. package/dist/core/teardown.d.ts +20 -0
  53. package/dist/core/teardown.js +37 -0
  54. package/dist/core/theme.css +276 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +9 -0
  57. package/dist/preset/index.css +10 -0
  58. package/dist/preset/recipes.css +188 -0
  59. package/dist/preset/scale.css +97 -0
  60. package/dist/preview/Preview.svelte +118 -0
  61. package/dist/preview/Preview.svelte.d.ts +39 -0
  62. package/dist/preview/bridge.d.ts +13 -0
  63. package/dist/preview/bridge.js +88 -0
  64. package/dist/preview/controller.d.ts +40 -0
  65. package/dist/preview/controller.js +141 -0
  66. package/dist/preview/geometry.d.ts +37 -0
  67. package/dist/preview/geometry.js +45 -0
  68. package/dist/preview/index.d.ts +5 -0
  69. package/dist/preview/index.js +14 -0
  70. package/dist/preview/overlay.d.ts +10 -0
  71. package/dist/preview/overlay.js +82 -0
  72. package/dist/preview/paint.d.ts +17 -0
  73. package/dist/preview/paint.js +262 -0
  74. package/dist/preview/strings.d.ts +14 -0
  75. package/dist/preview/strings.js +22 -0
  76. package/dist/visual/ArrayField.svelte +362 -0
  77. package/dist/visual/ArrayField.svelte.d.ts +24 -0
  78. package/dist/visual/BooleanField.svelte +88 -0
  79. package/dist/visual/BooleanField.svelte.d.ts +20 -0
  80. package/dist/visual/Card.svelte +849 -0
  81. package/dist/visual/Card.svelte.d.ts +43 -0
  82. package/dist/visual/CardControls.svelte +92 -0
  83. package/dist/visual/CardControls.svelte.d.ts +11 -0
  84. package/dist/visual/DateField.svelte +203 -0
  85. package/dist/visual/DateField.svelte.d.ts +28 -0
  86. package/dist/visual/DiagnosticList.svelte +50 -0
  87. package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
  88. package/dist/visual/EnumField.svelte +169 -0
  89. package/dist/visual/EnumField.svelte.d.ts +25 -0
  90. package/dist/visual/Field.svelte +270 -0
  91. package/dist/visual/Field.svelte.d.ts +37 -0
  92. package/dist/visual/FieldHint.svelte +185 -0
  93. package/dist/visual/FieldHint.svelte.d.ts +13 -0
  94. package/dist/visual/FieldLabel.svelte +141 -0
  95. package/dist/visual/FieldLabel.svelte.d.ts +19 -0
  96. package/dist/visual/FormatPopover.svelte +396 -0
  97. package/dist/visual/FormatPopover.svelte.d.ts +9 -0
  98. package/dist/visual/NumberField.svelte +72 -0
  99. package/dist/visual/NumberField.svelte.d.ts +19 -0
  100. package/dist/visual/ObjectField.svelte +131 -0
  101. package/dist/visual/ObjectField.svelte.d.ts +17 -0
  102. package/dist/visual/ProseArrayElement.svelte +98 -0
  103. package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
  104. package/dist/visual/ProseField.svelte +213 -0
  105. package/dist/visual/ProseField.svelte.d.ts +50 -0
  106. package/dist/visual/SlashMenu.svelte +126 -0
  107. package/dist/visual/SlashMenu.svelte.d.ts +14 -0
  108. package/dist/visual/TextField.svelte +63 -0
  109. package/dist/visual/TextField.svelte.d.ts +23 -0
  110. package/dist/visual/TipsCard.svelte +191 -0
  111. package/dist/visual/TipsCard.svelte.d.ts +13 -0
  112. package/dist/visual/VisualEditor.svelte +96 -0
  113. package/dist/visual/VisualEditor.svelte.d.ts +14 -0
  114. package/dist/visual/VisualEditorInner.svelte +1032 -0
  115. package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
  116. package/dist/visual/controls.css +272 -0
  117. package/dist/visual/diagnostics.d.ts +52 -0
  118. package/dist/visual/diagnostics.js +83 -0
  119. package/dist/visual/domid.d.ts +21 -0
  120. package/dist/visual/domid.js +36 -0
  121. package/dist/visual/ext.d.ts +19 -0
  122. package/dist/visual/ext.js +29 -0
  123. package/dist/visual/icons/Icon.svelte +39 -0
  124. package/dist/visual/icons/Icon.svelte.d.ts +12 -0
  125. package/dist/visual/icons/nodes.d.ts +103 -0
  126. package/dist/visual/icons/nodes.js +55 -0
  127. package/dist/visual/index.d.ts +9 -0
  128. package/dist/visual/index.js +24 -0
  129. package/dist/visual/motion.d.ts +18 -0
  130. package/dist/visual/motion.js +53 -0
  131. package/dist/visual/props.d.ts +77 -0
  132. package/dist/visual/props.js +1 -0
  133. package/dist/visual/signals.d.ts +59 -0
  134. package/dist/visual/signals.js +1 -0
  135. package/dist/visual/strings.d.ts +123 -0
  136. package/dist/visual/strings.js +80 -0
  137. package/dist/visual/structure.d.ts +224 -0
  138. package/dist/visual/structure.js +270 -0
  139. package/dist/visual/synced.svelte.d.ts +15 -0
  140. package/dist/visual/synced.svelte.js +34 -0
  141. package/dist/visual/tips.d.ts +24 -0
  142. package/dist/visual/tips.js +50 -0
  143. package/package.json +102 -0
@@ -0,0 +1,168 @@
1
+ // Input rules: the markdown-shorthand plugin the codec owns (`**`→strong, `*`→em,
2
+ // `~~`→strike, `` ` ``→code, `# `→heading, `- `/`1. `→lists, `> `→blockquote,
3
+ // `---`→divider).
4
+ // Mark rules are hand-rolled (prosemirror-inputrules ships only node rules); block
5
+ // rules reuse `wrappingInputRule` / `textblockTypeInputRule`. These produce
6
+ // transactions the codec already lowers: they add no new lowering surface.
7
+ import { InputRule, inputRules, textblockTypeInputRule, wrappingInputRule } from 'prosemirror-inputrules';
8
+ import { canJoin, findWrapping } from 'prosemirror-transform';
9
+ import { Selection } from 'prosemirror-state';
10
+ /**
11
+ * A mark input rule: when `regexp` (ending in the closing delimiter) matches
12
+ * before the caret, wrap capture group 1 in `markType` and drop the delimiters.
13
+ * `delimLen` is the delimiter length (opening == closing for every rule here).
14
+ * `match[0]` anatomy is `prefix? open captured close`: the em rule's
15
+ * `(?:^|[^*])` guard consumes ONE non-delimiter prefix char into the match, so
16
+ * positions are derived from the KNOWN delimiter/capture lengths, never from
17
+ * `indexOf` (which both deletes that prefix char and mis-anchors when it
18
+ * happens to equal the captured text). `[start, end)` spans `match[0]` minus
19
+ * the just-typed text (not yet inserted when the rule fires), so the trailing
20
+ * delete covers only the closing-delimiter chars actually in the doc.
21
+ */
22
+ function markInputRule(regexp, markType, delimLen) {
23
+ return new InputRule(regexp, (state, match, start, end) => {
24
+ const captured = match[1];
25
+ if (captured == null)
26
+ return null;
27
+ const tr = state.tr;
28
+ const prefixLen = match[0].length - (captured.length + 2 * delimLen);
29
+ const openStart = start + prefixLen;
30
+ const contentStart = openStart + delimLen;
31
+ const contentEnd = contentStart + captured.length;
32
+ // Delete trailing delimiter, then leading, then mark the surviving content.
33
+ if (end > contentEnd)
34
+ tr.delete(contentEnd, end);
35
+ tr.delete(openStart, contentStart);
36
+ tr.addMark(openStart, openStart + captured.length, markType.create());
37
+ tr.removeStoredMark(markType); // don't let the mark bleed into the next char
38
+ return tr;
39
+ });
40
+ }
41
+ /**
42
+ * `wrappingInputRule` (whose body this mirrors), plus one normalization: a
43
+ * `heading` being wrapped becomes a `paragraph` first.
44
+ *
45
+ * `list_item` is `block+`, so `list_item > heading` is *representable* and a bare
46
+ * wrap mints it (a shape no quill renders): the reference quill derives an item's
47
+ * numbering and indent from the container path and typesets the item's blocks as
48
+ * body paragraphs (`usaf_memo`'s `render-body`), where a heading resolves to
49
+ * nothing. This is the wrap-side route into that shape; the `# ` rule guards the
50
+ * other, in {@link markdownInputRules}.
51
+ */
52
+ function listWrappingRule(regexp, listType, paragraph, getAttrs, joinPredicate) {
53
+ return new InputRule(regexp, (state, match, start, end) => {
54
+ const attrs = getAttrs ? getAttrs(match) : null;
55
+ const tr = state.tr.delete(start, end);
56
+ // Positions survive the retype: same content, same size.
57
+ if (tr.doc.resolve(start).parent.type !== paragraph) {
58
+ tr.setBlockType(start, start, paragraph);
59
+ }
60
+ const $start = tr.doc.resolve(start);
61
+ const range = $start.blockRange();
62
+ const wrapping = range && findWrapping(range, listType, attrs);
63
+ if (!range || !wrapping)
64
+ return null;
65
+ tr.wrap(range, wrapping);
66
+ // Join a preceding list of the same type: the one boundary this rule
67
+ // itself opens (`lists.ts` §cleanup: command-local, never a global pass).
68
+ const before = tr.doc.resolve(start - 1).nodeBefore;
69
+ if (before &&
70
+ before.type === listType &&
71
+ canJoin(tr.doc, start - 1) &&
72
+ (!joinPredicate || joinPredicate(match, before))) {
73
+ tr.join(start - 1);
74
+ }
75
+ return tr;
76
+ });
77
+ }
78
+ /** The full markdown-shorthand rule set for a block schema. */
79
+ export function markdownInputRules(schema) {
80
+ const rules = [];
81
+ const m = schema.marks;
82
+ // Inline marks. Non-greedy capture between matching delimiters.
83
+ if (m.strong)
84
+ rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
85
+ if (m.em)
86
+ rules.push(markInputRule(/(?:^|[^*])\*([^*]+)\*$/, m.em, 1));
87
+ if (m.strike)
88
+ rules.push(markInputRule(/~~([^~]+)~~$/, m.strike, 2));
89
+ if (m.code)
90
+ rules.push(markInputRule(/`([^`]+)`$/, m.code, 1));
91
+ // Block shorthands (only where the schema has the node; the inline schema
92
+ // omits them, so this stays a no-op there).
93
+ if (schema.nodes.heading) {
94
+ // `textblockTypeInputRule`'s body plus one guard: inside a list item, `# `
95
+ // declines (null) and stays literal text, rather than minting the unrenderable
96
+ // `list_item > heading` {@link listWrappingRule} normalizes away on the wrap side.
97
+ const heading = schema.nodes.heading;
98
+ const item = schema.nodes.list_item;
99
+ rules.push(new InputRule(/^(#{1,6})\s$/, (state, match, start, end) => {
100
+ const $start = state.doc.resolve(start);
101
+ for (let d = $start.depth; d > 0; d--) {
102
+ if (item && $start.node(d).type === item)
103
+ return null;
104
+ }
105
+ if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), heading)) {
106
+ return null;
107
+ }
108
+ return state.tr
109
+ .delete(start, end)
110
+ .setBlockType(start, start, heading, { level: match[1].length });
111
+ }));
112
+ }
113
+ if (schema.nodes.code_block) {
114
+ rules.push(textblockTypeInputRule(/^```$/, schema.nodes.code_block));
115
+ }
116
+ if (schema.nodes.horizontal_rule && schema.nodes.paragraph) {
117
+ // A divider is a whole block, so this rule REPLACES its textblock rather than
118
+ // retyping one: `horizontal_rule` holds no content to retype into.
119
+ const rule = schema.nodes.horizontal_rule;
120
+ const paragraph = schema.nodes.paragraph;
121
+ const item = schema.nodes.list_item;
122
+ rules.push(new InputRule(/^---$/, (state, _match, start, end) => {
123
+ const $start = state.doc.resolve(start);
124
+ // The block ENTIRELY, not a prefix of one. `[start, end)` is the match minus
125
+ // the just-typed `-`, so the block holds `--` when this fires; anything else
126
+ // in it is a paragraph being edited, which a replace would eat.
127
+ if ($start.parentOffset !== 0 || $start.parent.content.size !== end - start) {
128
+ return null;
129
+ }
130
+ // The `# ` rule's guard, for its reason: `list_item` is `block+`, so a
131
+ // divider inside an item is representable and renders to nothing.
132
+ for (let d = $start.depth; d > 0; d--) {
133
+ if (item && $start.node(d).type === item)
134
+ return null;
135
+ }
136
+ if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), rule)) {
137
+ return null;
138
+ }
139
+ const from = $start.before();
140
+ const tr = state.tr.replaceWith(from, $start.after(), rule.create());
141
+ // A divider at the end of a body leaves nowhere to type; the paragraph after
142
+ // it is the exit, and it is where the caret lands (`slash.ts` §insertBlock
143
+ // opens the same block for the same reason).
144
+ const at = from + 1;
145
+ if (!tr.doc.nodeAt(at))
146
+ tr.insert(at, paragraph.create());
147
+ tr.setSelection(Selection.near(tr.doc.resolve(at), 1));
148
+ return tr;
149
+ }));
150
+ }
151
+ // `- ` / `1. ` at the start of a block, here or nested inside an item. These are
152
+ // the one pair of shorthands the slash menu ALSO carries: a list opens an item the
153
+ // caret then lives in, which is what that menu is the set of (`slash.ts`).
154
+ if (schema.nodes.bullet_list && schema.nodes.paragraph) {
155
+ rules.push(listWrappingRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list, schema.nodes.paragraph));
156
+ }
157
+ if (schema.nodes.ordered_list && schema.nodes.paragraph) {
158
+ rules.push(listWrappingRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, schema.nodes.paragraph, (match) => ({ start: +match[1] }), (match, node) => node.childCount + node.attrs.start === +match[1]));
159
+ }
160
+ if (schema.nodes.blockquote) {
161
+ rules.push(wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote));
162
+ }
163
+ return rules;
164
+ }
165
+ /** The input-rules plugin the field mounts by default (opt-out via createField). */
166
+ export function inputRulesPlugin(schema) {
167
+ return inputRules({ rules: markdownInputRules(schema) });
168
+ }
@@ -0,0 +1,36 @@
1
+ import type { Node as PMNode, NodeSpec } from 'prosemirror-model';
2
+ import type { ContentIsland, TableProps } from '@quillmark/wasm';
3
+ /** The `U+FFFC` object-replacement char that occupies one island slot in `text`. */
4
+ export declare const ISLAND_SLOT = "\uFFFC";
5
+ /** Block island node (a table): one `island`-kind content line. Atom, unselectable content. */
6
+ export declare const islandBlockSpec: NodeSpec;
7
+ /** Inline island node (an image): a `U+FFFC` slot inside a `para` line. */
8
+ export declare const islandInlineSpec: NodeSpec;
9
+ /** A PM island node's attributes: the content entry, spelled the way a node spec
10
+ * spells it (`islandType`, because `type` on a PM node is the node type). `loss`
11
+ * reads its class off the entry rather than restating the union, which the public
12
+ * entry point does not name (DOCUMENT_MODEL §Stability seams). */
13
+ export interface IslandNodeAttrs {
14
+ id: string;
15
+ islandType: string;
16
+ props: unknown;
17
+ loss: ContentIsland['loss'];
18
+ }
19
+ /** Build a content island entry from a PM island node's attrs (block or inline). */
20
+ export declare function islandEntryFromNode(attrs: IslandNodeAttrs): ContentIsland;
21
+ /** A node's `TableProps`, or `undefined` for any other island: the boundary's own
22
+ * guard over the entry the node carries, so the open `type` arm narrows once here
23
+ * rather than at each reader. */
24
+ export declare function tablePropsOfNode(node: PMNode): TableProps | undefined;
25
+ /**
26
+ * The next island id for a field: the positional `isl-{n}` sequence continued past
27
+ * the highest one the field already holds. A minted id is this tier's to produce
28
+ * and it is part of the document's canonical bytes (CODEC §Islands), so it is a
29
+ * counter over the projection in hand — never a UUID, never a clock reading, and
30
+ * never a re-numbering of the ids already there, which are identities.
31
+ *
32
+ * Reads the PM doc rather than the stored content because that is what the caller
33
+ * (an insert command) holds and what the commit will project: an id minted against
34
+ * a stale content could collide with one the same transaction places.
35
+ */
36
+ export declare function mintIslandId(doc: PMNode): string;
@@ -0,0 +1,82 @@
1
+ import { isTableIsland } from '@quillmark/wasm';
2
+ /** The `U+FFFC` object-replacement char that occupies one island slot in `text`. */
3
+ export const ISLAND_SLOT = '';
4
+ // The four island node attributes carry the content entry verbatim: `id` is the
5
+ // stable identity (rebases like an anchor), `islandType` the discriminator, `props`
6
+ // the opaque payload the codec preserves byte-for-byte across a round-trip, and
7
+ // `loss` how faithfully markdown can carry it. `loss` is AUTHORED, not derived:
8
+ // `applyChange` stores the class an island op gives it and re-derives nothing, so
9
+ // an entry that did not carry its own would promote a degraded table to lossless
10
+ // on the first cell edit. The default is the class an unknown one reads as, so a
11
+ // node no decode produced under-claims rather than over-claims; decode always
12
+ // supplies the stored value.
13
+ const islandAttrs = {
14
+ id: { default: '' },
15
+ islandType: { default: '' },
16
+ props: { default: null },
17
+ loss: { default: 'unrepresentable' }
18
+ };
19
+ /** Block island node (a table): one `island`-kind content line. Atom, unselectable content. */
20
+ export const islandBlockSpec = {
21
+ group: 'block',
22
+ atom: true,
23
+ selectable: true,
24
+ attrs: islandAttrs,
25
+ toDOM: (node) => [
26
+ 'div',
27
+ { 'data-island': node.attrs.islandType, 'data-island-id': node.attrs.id },
28
+ `[${node.attrs.islandType || 'island'}]`
29
+ ]
30
+ };
31
+ /** Inline island node (an image): a `U+FFFC` slot inside a `para` line. */
32
+ export const islandInlineSpec = {
33
+ group: 'inline',
34
+ inline: true,
35
+ atom: true,
36
+ selectable: true,
37
+ attrs: islandAttrs,
38
+ toDOM: (node) => [
39
+ 'span',
40
+ { 'data-island': node.attrs.islandType, 'data-island-id': node.attrs.id },
41
+ `[${node.attrs.islandType || 'island'}]`
42
+ ]
43
+ };
44
+ /** Build a content island entry from a PM island node's attrs (block or inline). */
45
+ export function islandEntryFromNode(attrs) {
46
+ return {
47
+ id: attrs.id,
48
+ type: attrs.islandType,
49
+ props: attrs.props,
50
+ loss: attrs.loss
51
+ };
52
+ }
53
+ /** A node's `TableProps`, or `undefined` for any other island: the boundary's own
54
+ * guard over the entry the node carries, so the open `type` arm narrows once here
55
+ * rather than at each reader. */
56
+ export function tablePropsOfNode(node) {
57
+ const entry = islandEntryFromNode(node.attrs);
58
+ return isTableIsland(entry) ? entry.props : undefined;
59
+ }
60
+ /**
61
+ * The next island id for a field: the positional `isl-{n}` sequence continued past
62
+ * the highest one the field already holds. A minted id is this tier's to produce
63
+ * and it is part of the document's canonical bytes (CODEC §Islands), so it is a
64
+ * counter over the projection in hand — never a UUID, never a clock reading, and
65
+ * never a re-numbering of the ids already there, which are identities.
66
+ *
67
+ * Reads the PM doc rather than the stored content because that is what the caller
68
+ * (an insert command) holds and what the commit will project: an id minted against
69
+ * a stale content could collide with one the same transaction places.
70
+ */
71
+ export function mintIslandId(doc) {
72
+ let next = 0;
73
+ doc.descendants((node) => {
74
+ if (node.type.name !== 'island_block' && node.type.name !== 'island_inline')
75
+ return true;
76
+ const m = /^isl-(\d+)$/.exec(node.attrs.id);
77
+ if (m)
78
+ next = Math.max(next, Number(m[1]) + 1);
79
+ return false;
80
+ });
81
+ return `isl-${next}`;
82
+ }
@@ -0,0 +1,13 @@
1
+ import type { Schema } from 'prosemirror-model';
2
+ import type { Command } from 'prosemirror-state';
3
+ import { type SlashItem } from './slash.js';
4
+ /** The block-schema body's structural keys: `{}` for the inline/plaintext schemas,
5
+ * whose leaves declare neither lists nor code blocks. The argument list IS the
6
+ * precedence order VISUAL_EDITOR §Chrome states, so a new link is one more argument
7
+ * at its place in that order rather than a nesting to read inside out.
8
+ *
9
+ * The slash menu is innermost of the three, and it is conditional rather than
10
+ * schema-gated: the keys exist only where a menu can be DRAWN (`slashItems` present),
11
+ * because Enter and Escape claimed by a surface nobody can see is a body that
12
+ * swallows two keys for no visible reason. */
13
+ export declare function bodyKeymap(schema: Schema, slashItems?: () => SlashItem[]): Record<string, Command>;
@@ -0,0 +1,29 @@
1
+ import { chainCommands } from 'prosemirror-commands';
2
+ import { codeKeymap } from './code.js';
3
+ import { listKeymap } from './lists.js';
4
+ import { slashKeymap } from './slash.js';
5
+ /** One chain per key, the maps taken in precedence order: on a key several bind, an
6
+ * earlier map's command runs first and the next runs only if it declines. A key one
7
+ * map alone binds is its own. No input is mutated. */
8
+ function chainKeymaps(...maps) {
9
+ const out = {};
10
+ for (const map of maps) {
11
+ for (const [key, cmd] of Object.entries(map)) {
12
+ out[key] = out[key] ? chainCommands(out[key], cmd) : cmd;
13
+ }
14
+ }
15
+ return out;
16
+ }
17
+ /** The block-schema body's structural keys: `{}` for the inline/plaintext schemas,
18
+ * whose leaves declare neither lists nor code blocks. The argument list IS the
19
+ * precedence order VISUAL_EDITOR §Chrome states, so a new link is one more argument
20
+ * at its place in that order rather than a nesting to read inside out.
21
+ *
22
+ * The slash menu is innermost of the three, and it is conditional rather than
23
+ * schema-gated: the keys exist only where a menu can be DRAWN (`slashItems` present),
24
+ * because Enter and Escape claimed by a surface nobody can see is a body that
25
+ * swallows two keys for no visible reason. */
26
+ export function bodyKeymap(schema, slashItems) {
27
+ const links = slashItems ? [slashKeymap(slashItems)] : [];
28
+ return chainKeymaps(...links, codeKeymap(schema), listKeymap(schema));
29
+ }
@@ -0,0 +1,22 @@
1
+ import type { Schema } from 'prosemirror-model';
2
+ import type { Command } from 'prosemirror-state';
3
+ /**
4
+ * The list bindings for a block-schema leaf: `{}` for the inline/plaintext
5
+ * schemas, which declare no list nodes.
6
+ *
7
+ * `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
8
+ * Tab more locally under the same rule (`prose/canon/VISUAL_EDITOR.md` §Chrome)
9
+ * prepend a link ahead of these in `keymap.ts`: a `code_block` takes literal
10
+ * indentation (`code.ts`), an island takes cell traversal once added. Outside all
11
+ * of them every link returns false and the key is NOT swallowed: Tab keeps its
12
+ * default meaning, which is the body's only keyboard exit and the open seam for a
13
+ * shell structural keymap (VISUAL_EDITOR §Settled and open).
14
+ *
15
+ * `Enter` is a chain in precedence order: the paragraph-above gesture, then
16
+ * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
17
+ * its own: it splits a non-empty item, and on an empty item it either splits the
18
+ * WRAPPING item (a nested empty item, so Enter lifts exactly one level) or bails
19
+ * so the third link lifts a top-level empty item out to a paragraph. `Backspace`
20
+ * forks on the item's index: the first item lifts, any later one merges.
21
+ */
22
+ export declare function listKeymap(schema: Schema): Record<string, Command>;
@@ -0,0 +1,104 @@
1
+ import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
2
+ import { chainCommands, joinTextblockBackward } from 'prosemirror-commands';
3
+ /** The caret's enclosing `list_item` when it sits at the very start of that item's
4
+ * FIRST block: the position both structural keys branch on. `null` otherwise
5
+ * (mid-text, a non-first block of a multi-paragraph item, a non-empty selection),
6
+ * which is where the key keeps its ordinary meaning. */
7
+ function atItemStart(state, itemType) {
8
+ const { $from, empty } = state.selection;
9
+ if (!empty || $from.parentOffset !== 0)
10
+ return null;
11
+ if ($from.depth < 3)
12
+ return null; // doc > list > item > block is the shallowest
13
+ if ($from.node(-1).type !== itemType)
14
+ return null;
15
+ if ($from.index(-1) !== 0)
16
+ return null; // a later block of the item
17
+ return {
18
+ itemIndex: $from.index(-2),
19
+ nested: $from.node(-3).type === itemType,
20
+ listPos: $from.before(-2)
21
+ };
22
+ }
23
+ /**
24
+ * Enter at the very start of a top-level list's first item → an empty paragraph
25
+ * above the list, caret staying with the text it pushed down.
26
+ *
27
+ * Only where the list itself is not inside an item: in a NESTED list this pushes
28
+ * an empty paragraph into the parent item (`list_item` is `block+`, so the shape
29
+ * is representable and wrong), where the conventional split (an empty item above)
30
+ * is what the next link does. An empty first item falls through to that same
31
+ * link, which exits the list instead.
32
+ */
33
+ function paragraphAboveList(itemType, paragraph) {
34
+ return (state, dispatch) => {
35
+ const at = atItemStart(state, itemType);
36
+ if (!at || at.nested || at.itemIndex !== 0)
37
+ return false;
38
+ if (state.selection.$from.parent.content.size === 0)
39
+ return false;
40
+ if (dispatch)
41
+ dispatch(state.tr.insert(at.listPos, paragraph.create()).scrollIntoView());
42
+ return true;
43
+ };
44
+ }
45
+ /**
46
+ * Backspace at the start of a list's FIRST item → lift it: one level out for a
47
+ * nested item, out of the list entirely (to a paragraph) at the top.
48
+ */
49
+ function liftAtListStart(itemType) {
50
+ return (state, dispatch) => {
51
+ const at = atItemStart(state, itemType);
52
+ if (!at || at.itemIndex !== 0)
53
+ return false;
54
+ return liftListItem(itemType)(state, dispatch);
55
+ };
56
+ }
57
+ /**
58
+ * Backspace at the start of any LATER item → join its text into the previous
59
+ * item's last block, so the two items become one line.
60
+ *
61
+ * Not the base keymap's `joinBackward`, which merges the BLOCK and leaves
62
+ * `list_item(paragraph, paragraph)`: the item's marker gone while its text stays
63
+ * on its own line, which the reference quill typesets as an unnumbered
64
+ * continuation paragraph.
65
+ */
66
+ function mergeIntoPreviousItem(itemType) {
67
+ return (state, dispatch, view) => {
68
+ const at = atItemStart(state, itemType);
69
+ if (!at || at.itemIndex === 0)
70
+ return false;
71
+ return joinTextblockBackward(state, dispatch, view);
72
+ };
73
+ }
74
+ /**
75
+ * The list bindings for a block-schema leaf: `{}` for the inline/plaintext
76
+ * schemas, which declare no list nodes.
77
+ *
78
+ * `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
79
+ * Tab more locally under the same rule (`prose/canon/VISUAL_EDITOR.md` §Chrome)
80
+ * prepend a link ahead of these in `keymap.ts`: a `code_block` takes literal
81
+ * indentation (`code.ts`), an island takes cell traversal once added. Outside all
82
+ * of them every link returns false and the key is NOT swallowed: Tab keeps its
83
+ * default meaning, which is the body's only keyboard exit and the open seam for a
84
+ * shell structural keymap (VISUAL_EDITOR §Settled and open).
85
+ *
86
+ * `Enter` is a chain in precedence order: the paragraph-above gesture, then
87
+ * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
88
+ * its own: it splits a non-empty item, and on an empty item it either splits the
89
+ * WRAPPING item (a nested empty item, so Enter lifts exactly one level) or bails
90
+ * so the third link lifts a top-level empty item out to a paragraph. `Backspace`
91
+ * forks on the item's index: the first item lifts, any later one merges.
92
+ */
93
+ export function listKeymap(schema) {
94
+ const itemType = schema.nodes.list_item;
95
+ const paragraph = schema.nodes.paragraph;
96
+ if (!itemType || !paragraph)
97
+ return {};
98
+ return {
99
+ Tab: sinkListItem(itemType),
100
+ 'Shift-Tab': liftListItem(itemType),
101
+ Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitListItem(itemType), liftListItem(itemType)),
102
+ Backspace: chainCommands(liftAtListStart(itemType), mergeIntoPreviousItem(itemType))
103
+ };
104
+ }
@@ -0,0 +1,37 @@
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. */
4
+ export declare function pmMarkFromContent(schema: Schema, m: ContentMark): Mark | null;
5
+ /**
6
+ * 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
+ */
10
+ export declare function contentDescriptorFromPM(mark: Mark): Record<string, unknown>;
11
+ /**
12
+ * 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
+ */
17
+ export declare function descriptorOf(m: ContentMark): Record<string, unknown>;
18
+ /**
19
+ * 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
+ */
24
+ export declare function markKey(descriptor: Record<string, unknown>): string;
25
+ /** A held anchor position: an identity id at a USV content offset (zero-width). */
26
+ export interface AnchorPos {
27
+ id: string;
28
+ pos: number;
29
+ }
30
+ /**
31
+ * The identity anchors of a `Content` as `{ id, pos }` in USV: the seed for the
32
+ * field's anchor-position plugin and the `oldAnchors` the mark diff rebases.
33
+ * Anchors are zero-width, so `start` is the position.
34
+ */
35
+ export declare function anchorsFromContent(rt: {
36
+ marks: ContentMark[];
37
+ }): AnchorPos[];
@@ -0,0 +1,74 @@
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. */
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
+ });
19
+ }
20
+ /**
21
+ * 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.
24
+ */
25
+ export function contentDescriptorFromPM(mark) {
26
+ const name = mark.type.name;
27
+ if (name === 'em')
28
+ return { type: 'emph' };
29
+ 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
34
+ return { type: name };
35
+ }
36
+ /**
37
+ * 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.
41
+ */
42
+ 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 };
48
+ }
49
+ /**
50
+ * 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.
54
+ */
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;
62
+ }
63
+ /**
64
+ * The identity anchors of a `Content` as `{ id, pos }` in USV: the seed for the
65
+ * field's anchor-position plugin and the `oldAnchors` the mark diff rebases.
66
+ * Anchors are zero-width, so `start` is the position.
67
+ */
68
+ export function anchorsFromContent(rt) {
69
+ const out = [];
70
+ for (const m of rt.marks)
71
+ if (isAnchorMark(m))
72
+ out.push({ id: m.id, pos: m.start });
73
+ return out;
74
+ }
@@ -0,0 +1,17 @@
1
+ import type { Node as PMNode } from 'prosemirror-model';
2
+ import { type PosRun } from './encode.js';
3
+ /** A prebuilt line→position index for a PM doc, rebuilt on structural change. */
4
+ export interface LineIndex {
5
+ runs: PosRun[];
6
+ usvEnd: number;
7
+ /** PM position at the end of the last content run (the caret-max). */
8
+ pmEndContent: number;
9
+ /** PM content position of the first line (the caret for an empty doc). */
10
+ firstContentStart: number;
11
+ }
12
+ /** Build the position index for `doc` (walks it once via the shared scanner). */
13
+ export declare function buildLineIndex(doc: PMNode): LineIndex;
14
+ /** USV content offset → PM position. */
15
+ export declare function usvToPM(index: LineIndex, usvPos: number): number;
16
+ /** PM position → USV content offset. */
17
+ export declare function pmToUsv(index: LineIndex, pmPos: number): number;