@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.
- package/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +241 -0
- package/THEMING.md +153 -0
- package/dist/core/address.d.ts +56 -0
- package/dist/core/address.js +95 -0
- package/dist/core/bloom.d.ts +37 -0
- package/dist/core/bloom.js +116 -0
- package/dist/core/codec/anchor.d.ts +19 -0
- package/dist/core/codec/anchor.js +36 -0
- package/dist/core/codec/code.d.ts +12 -0
- package/dist/core/codec/code.js +118 -0
- package/dist/core/codec/decode.d.ts +24 -0
- package/dist/core/codec/decode.js +270 -0
- package/dist/core/codec/encode.d.ts +71 -0
- package/dist/core/codec/encode.js +480 -0
- package/dist/core/codec/field.d.ts +171 -0
- package/dist/core/codec/field.js +493 -0
- package/dist/core/codec/index.d.ts +22 -0
- package/dist/core/codec/index.js +43 -0
- package/dist/core/codec/inputrules.d.ts +7 -0
- package/dist/core/codec/inputrules.js +168 -0
- package/dist/core/codec/islands.d.ts +36 -0
- package/dist/core/codec/islands.js +82 -0
- package/dist/core/codec/keymap.d.ts +13 -0
- package/dist/core/codec/keymap.js +29 -0
- package/dist/core/codec/lists.d.ts +22 -0
- package/dist/core/codec/lists.js +104 -0
- package/dist/core/codec/marks.d.ts +37 -0
- package/dist/core/codec/marks.js +74 -0
- package/dist/core/codec/positions.d.ts +17 -0
- package/dist/core/codec/positions.js +108 -0
- package/dist/core/codec/prose.css +458 -0
- package/dist/core/codec/reconcile.d.ts +26 -0
- package/dist/core/codec/reconcile.js +64 -0
- package/dist/core/codec/schema.d.ts +13 -0
- package/dist/core/codec/schema.js +155 -0
- package/dist/core/codec/slash.d.ts +79 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +50 -0
- package/dist/core/codec/table-view.js +928 -0
- package/dist/core/codec/table.d.ts +85 -0
- package/dist/core/codec/table.js +224 -0
- package/dist/core/errors.d.ts +73 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lifecycle.d.ts +7 -0
- package/dist/core/lifecycle.js +21 -0
- package/dist/core/rebind.svelte.d.ts +18 -0
- package/dist/core/rebind.svelte.js +44 -0
- package/dist/core/teardown.d.ts +20 -0
- package/dist/core/teardown.js +37 -0
- package/dist/core/theme.css +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/preset/index.css +10 -0
- package/dist/preset/recipes.css +188 -0
- package/dist/preset/scale.css +97 -0
- package/dist/preview/Preview.svelte +118 -0
- package/dist/preview/Preview.svelte.d.ts +39 -0
- package/dist/preview/bridge.d.ts +13 -0
- package/dist/preview/bridge.js +88 -0
- package/dist/preview/controller.d.ts +40 -0
- package/dist/preview/controller.js +141 -0
- package/dist/preview/geometry.d.ts +37 -0
- package/dist/preview/geometry.js +45 -0
- package/dist/preview/index.d.ts +5 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/overlay.d.ts +10 -0
- package/dist/preview/overlay.js +82 -0
- package/dist/preview/paint.d.ts +17 -0
- package/dist/preview/paint.js +262 -0
- package/dist/preview/strings.d.ts +14 -0
- package/dist/preview/strings.js +22 -0
- package/dist/visual/ArrayField.svelte +362 -0
- package/dist/visual/ArrayField.svelte.d.ts +24 -0
- package/dist/visual/BooleanField.svelte +88 -0
- package/dist/visual/BooleanField.svelte.d.ts +20 -0
- package/dist/visual/Card.svelte +849 -0
- package/dist/visual/Card.svelte.d.ts +43 -0
- package/dist/visual/CardControls.svelte +92 -0
- package/dist/visual/CardControls.svelte.d.ts +11 -0
- package/dist/visual/DateField.svelte +203 -0
- package/dist/visual/DateField.svelte.d.ts +28 -0
- package/dist/visual/DiagnosticList.svelte +50 -0
- package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
- package/dist/visual/EnumField.svelte +169 -0
- package/dist/visual/EnumField.svelte.d.ts +25 -0
- package/dist/visual/Field.svelte +270 -0
- package/dist/visual/Field.svelte.d.ts +37 -0
- package/dist/visual/FieldHint.svelte +185 -0
- package/dist/visual/FieldHint.svelte.d.ts +13 -0
- package/dist/visual/FieldLabel.svelte +141 -0
- package/dist/visual/FieldLabel.svelte.d.ts +19 -0
- package/dist/visual/FormatPopover.svelte +396 -0
- package/dist/visual/FormatPopover.svelte.d.ts +9 -0
- package/dist/visual/NumberField.svelte +72 -0
- package/dist/visual/NumberField.svelte.d.ts +19 -0
- package/dist/visual/ObjectField.svelte +131 -0
- package/dist/visual/ObjectField.svelte.d.ts +17 -0
- package/dist/visual/ProseArrayElement.svelte +98 -0
- package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
- package/dist/visual/ProseField.svelte +213 -0
- package/dist/visual/ProseField.svelte.d.ts +50 -0
- package/dist/visual/SlashMenu.svelte +126 -0
- package/dist/visual/SlashMenu.svelte.d.ts +14 -0
- package/dist/visual/TextField.svelte +63 -0
- package/dist/visual/TextField.svelte.d.ts +23 -0
- package/dist/visual/TipsCard.svelte +191 -0
- package/dist/visual/TipsCard.svelte.d.ts +13 -0
- package/dist/visual/VisualEditor.svelte +96 -0
- package/dist/visual/VisualEditor.svelte.d.ts +14 -0
- package/dist/visual/VisualEditorInner.svelte +1032 -0
- package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
- package/dist/visual/controls.css +272 -0
- package/dist/visual/diagnostics.d.ts +52 -0
- package/dist/visual/diagnostics.js +83 -0
- package/dist/visual/domid.d.ts +21 -0
- package/dist/visual/domid.js +36 -0
- package/dist/visual/ext.d.ts +19 -0
- package/dist/visual/ext.js +29 -0
- package/dist/visual/icons/Icon.svelte +39 -0
- package/dist/visual/icons/Icon.svelte.d.ts +12 -0
- package/dist/visual/icons/nodes.d.ts +103 -0
- package/dist/visual/icons/nodes.js +55 -0
- package/dist/visual/index.d.ts +9 -0
- package/dist/visual/index.js +24 -0
- package/dist/visual/motion.d.ts +18 -0
- package/dist/visual/motion.js +53 -0
- package/dist/visual/props.d.ts +77 -0
- package/dist/visual/props.js +1 -0
- package/dist/visual/signals.d.ts +59 -0
- package/dist/visual/signals.js +1 -0
- package/dist/visual/strings.d.ts +123 -0
- package/dist/visual/strings.js +80 -0
- package/dist/visual/structure.d.ts +224 -0
- package/dist/visual/structure.js +270 -0
- package/dist/visual/synced.svelte.d.ts +15 -0
- package/dist/visual/synced.svelte.js +34 -0
- package/dist/visual/tips.d.ts +24 -0
- package/dist/visual/tips.js +50 -0
- 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;
|