@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,56 @@
1
+ import { type Addr } from '@quillmark/wasm';
2
+ /**
3
+ * A canonical field address: `main.<field>` / `main.body` /
4
+ * `cards.<kind>[<i>].<field>`, cards keyed by ABSOLUTE document index.
5
+ *
6
+ * The grammar `parseDocPath` / `formatDocPath` speak, and the one
7
+ * `Diagnostic.path`, `ContentHit.field`, `FieldRegion.field` and
8
+ * `session.regions()` keys already use. An alias over `string`: the boundary
9
+ * exports the parser and the segment type, not a nominal type, so this names the
10
+ * grammar at a signature rather than inventing a checked one.
11
+ */
12
+ export type DocPath = string;
13
+ /**
14
+ * A place in the document: a field and a caret within it. What the editor reports
15
+ * when its caret moves and what the preview scrolls to, so the editor→preview hop
16
+ * is `onCaretMove={preview.focusPosition}` and translates nothing.
17
+ *
18
+ * `pos` is USV, the shared content coordinate on both sides of the boundary: a
19
+ * `ContentHit` is a `Place` with its own extras, and fits wherever one is taken.
20
+ */
21
+ export interface Place {
22
+ field: DocPath;
23
+ /** The caret in USV. */
24
+ pos: number;
25
+ }
26
+ /**
27
+ * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
28
+ * `kinds` array (a stale address: drop it rather than mis-target). A field-less card
29
+ * addresses its BODY, which is the leaf `{card: i}` names.
30
+ *
31
+ * - `{}` → `"main.body"`
32
+ * - `{field}` → `"main.<field>"`
33
+ * - `{card: i}` → `"cards.<kind>[i].body"`
34
+ * - `{card: i, field}` → `"cards.<kind>[i].<field>"`
35
+ *
36
+ * `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
37
+ * per-kind counting, since `DocPath` addresses cards by document-array index.
38
+ */
39
+ export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[]): DocPath | undefined;
40
+ /**
41
+ * A CARD's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
42
+ * where the change is the card rather than anything inside it. `undefined` for an
43
+ * index outside `kinds`.
44
+ */
45
+ export declare function cardPath(index: number, kinds: readonly string[]): DocPath | undefined;
46
+ /**
47
+ * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
48
+ * `undefined` for a path that names no single commit address — a nested or
49
+ * array-element path (`main.references.0`), a field-rooted one, or a malformed one
50
+ * (which `parseDocPath` throws on). A bare card and a `.body` terminal both land on
51
+ * the field-less `{card: i}` the body leaf answers to.
52
+ *
53
+ * Needs no `kinds`: the path carries the absolute index, and the kind in it is
54
+ * decoration the `Addr` has no room for.
55
+ */
56
+ export declare function addrForFieldPath(path: DocPath): Addr | undefined;
@@ -0,0 +1,95 @@
1
+ // The one address vocabulary the surfaces speak in public: canonical `DocPath`
2
+ // strings for places, `Addr` for the document verbs. Declared HERE because both
3
+ // `/preview` and `/visual` name a place in their hooks, and a type declared twice
4
+ // structurally is drift with nothing to catch it: `/core` is the module both
5
+ // already import.
6
+ //
7
+ // Slogan: paths for places, indexes for structure ops. `Addr` (`{card?, field?}`,
8
+ // from `@quillmark/wasm`) is the mutator currency; the conversions at the foot of
9
+ // this module are the hop between the two. `fieldPathForAddr` and `addrForFieldPath`
10
+ // are public, because a host given a path by a hook and holding a verb that takes an
11
+ // `Addr` needs them; `cardPath` is the editor's own.
12
+ import { formatDocPath, parseDocPath } from '@quillmark/wasm';
13
+ // ── Addr ↔ DocPath ──────────────────────────────────────────────────────────
14
+ // The one hop between the two vocabularies, both directions, pure and
15
+ // document-free: `kinds` is the kind of each composable card by content index
16
+ // (`doc.cards.map(c => c.kind)`), which is all the card segment needs. The editor
17
+ // mints it off its own derived card tree rather than re-reading `doc.cards`, which
18
+ // serializes every card per read and is not a thing to do per keystroke.
19
+ /**
20
+ * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
21
+ * `kinds` array (a stale address: drop it rather than mis-target). A field-less card
22
+ * addresses its BODY, which is the leaf `{card: i}` names.
23
+ *
24
+ * - `{}` → `"main.body"`
25
+ * - `{field}` → `"main.<field>"`
26
+ * - `{card: i}` → `"cards.<kind>[i].body"`
27
+ * - `{card: i, field}` → `"cards.<kind>[i].<field>"`
28
+ *
29
+ * `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
30
+ * per-kind counting, since `DocPath` addresses cards by document-array index.
31
+ */
32
+ export function fieldPathForAddr(addr, kinds) {
33
+ const head = cardHead(addr.card, kinds);
34
+ if (!head)
35
+ return undefined;
36
+ const tail = addr.field != null ? { seg: 'field', name: addr.field } : { seg: 'body' };
37
+ return formatDocPath([head, tail]);
38
+ }
39
+ /**
40
+ * A CARD's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
41
+ * where the change is the card rather than anything inside it. `undefined` for an
42
+ * index outside `kinds`.
43
+ */
44
+ export function cardPath(index, kinds) {
45
+ const head = cardHead(index, kinds);
46
+ return head ? formatDocPath([head]) : undefined;
47
+ }
48
+ /**
49
+ * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
50
+ * `undefined` for a path that names no single commit address — a nested or
51
+ * array-element path (`main.references.0`), a field-rooted one, or a malformed one
52
+ * (which `parseDocPath` throws on). A bare card and a `.body` terminal both land on
53
+ * the field-less `{card: i}` the body leaf answers to.
54
+ *
55
+ * Needs no `kinds`: the path carries the absolute index, and the kind in it is
56
+ * decoration the `Addr` has no room for.
57
+ */
58
+ export function addrForFieldPath(path) {
59
+ let segs;
60
+ try {
61
+ segs = parseDocPath(path);
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ const [head, ...rest] = segs;
67
+ if (!head)
68
+ return undefined;
69
+ let addr;
70
+ if (head.seg === 'main')
71
+ addr = {};
72
+ else if (head.seg === 'card')
73
+ addr = { card: head.index };
74
+ else
75
+ return undefined;
76
+ if (rest.length === 0)
77
+ return addr;
78
+ if (rest.length > 1)
79
+ return undefined;
80
+ const tail = rest[0];
81
+ if (tail.seg === 'body')
82
+ return addr;
83
+ if (tail.seg === 'field')
84
+ return { ...addr, field: tail.name };
85
+ return undefined;
86
+ }
87
+ /** The head segment a card index resolves to, or `undefined` when it is out of the
88
+ * live array. `null` kind is the unknown-kind form the grammar spells `cards[i]`. */
89
+ function cardHead(index, kinds) {
90
+ if (index == null)
91
+ return { seg: 'main' };
92
+ if (!Number.isInteger(index) || index < 0 || index >= kinds.length)
93
+ return undefined;
94
+ return { seg: 'card', kind: kinds[index] || null, index };
95
+ }
@@ -0,0 +1,37 @@
1
+ /** The resting state a bloom animates from and back to: the wash, held at zero. */
2
+ export declare function primeWash(el: HTMLElement): void;
3
+ /** What one bloom runs: resolved from the scale, and from the viewer's motion
4
+ * preference. */
5
+ export interface BloomTiming {
6
+ duration: number;
7
+ frames: Keyframe[];
8
+ }
9
+ /**
10
+ * Resolve the timing off `el`, so the dwell is single-sourced in CSS.
11
+ *
12
+ * ONE property, both motion preferences: `--_qm-bloom-dwell` already shortens under
13
+ * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
14
+ * the SHAPE: a hold and a cut carry no ramps, which is a frame list rather than a
15
+ * number and the one half CSS cannot hand over.
16
+ *
17
+ * Read ONCE PER BATCH, not per element: `getComputedStyle` forces a style recalc and
18
+ * `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
19
+ * each). The rungs inherit, so every element in a batch resolves the same numbers.
20
+ */
21
+ export declare function bloomTiming(el: HTMLElement): BloomTiming;
22
+ /**
23
+ * Bloom the wash on `el` itself: the caller owns the element and has primed it
24
+ * ({@link primeWash}). Returns the running animation, or `undefined` when there is
25
+ * nothing left to run: `elapsed` past the decay, or an environment without WAAPI.
26
+ * Pass `timing` to share one resolve across a batch.
27
+ */
28
+ export declare function bloom(el: HTMLElement, elapsed?: number, timing?: BloomTiming): Animation | undefined;
29
+ /**
30
+ * Bloom over `host`'s CONTENT: an inset child, so the wash neither tints the host's
31
+ * own background nor fades the text under it. `host` must be positioned.
32
+ *
33
+ * ONE wash node per host, reused while it lives: two quick landings on the same leaf
34
+ * must not stack two alphas over it. No `elapsed` seam: the surfaces that use this
35
+ * (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
36
+ */
37
+ export declare function bloomInside(host: HTMLElement): void;
@@ -0,0 +1,116 @@
1
+ // The correlation bloom: a soft accent wash that rises over a target and decays to
2
+ // nothing. The editor↔preview address is an EVENT, not a state: the preview claims
3
+ // to be the rendered output, so it carries no resting ink, and a highlight that
4
+ // outlives the hop that caused it is ink the document did not ask for. Both
5
+ // directions land here: the preview's field boxes bloom themselves (they are already
6
+ // empty rects over the page), an editor leaf blooms a transient child over its
7
+ // surface.
8
+ //
9
+ // OPACITY is the animated property, never the colour. The wash's alpha is a peak
10
+ // fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
11
+ // `color-mix` resolves, instead of in keyframe values a script would have to
12
+ // pre-resolve and re-resolve on a theme change.
13
+ //
14
+ // `elapsed` is what lets a bloom survive the element that carries it. `overlay.ts`
15
+ // rebuilds every box on refresh and the playground recompiles 120ms after each
16
+ // keystroke burst, so a bloom re-STARTED per rebuild would re-bloom continuously
17
+ // while the user writes; a rebuilt node resumes at the offset the old one reached.
18
+ /** Marks a transient wash node: the wash is decoration, never a hit target. */
19
+ const BLOOM_CLASS = 'qm-bloom';
20
+ const WASH = 'var(--_qm-accent-wash)';
21
+ /** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
22
+ * where `getComputedStyle` reports custom properties as empty). A fallback is the
23
+ * absence of the scale, not a copy of it. */
24
+ const FALLBACK_MS = 1100;
25
+ // Rise fast, hold, then leave slowly: a highlighter's decay, not a linear fade. A
26
+ // keyframe's easing governs the segment that STARTS at it. Under reduced motion the
27
+ // ramps are dropped entirely: the wash holds at full for a beat and cuts, which
28
+ // still answers "here" with nothing to track.
29
+ const FRAMES = [
30
+ { opacity: 0, offset: 0, easing: 'ease-out' },
31
+ { opacity: 1, offset: 0.08 },
32
+ { opacity: 1, offset: 0.3, easing: 'ease-in' },
33
+ { opacity: 0, offset: 1 }
34
+ ];
35
+ const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
36
+ /** The resting state a bloom animates from and back to: the wash, held at zero. */
37
+ export function primeWash(el) {
38
+ Object.assign(el.style, { background: WASH, opacity: '0' });
39
+ }
40
+ /**
41
+ * Resolve the timing off `el`, so the dwell is single-sourced in CSS.
42
+ *
43
+ * ONE property, both motion preferences: `--_qm-bloom-dwell` already shortens under
44
+ * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
45
+ * the SHAPE: a hold and a cut carry no ramps, which is a frame list rather than a
46
+ * number and the one half CSS cannot hand over.
47
+ *
48
+ * Read ONCE PER BATCH, not per element: `getComputedStyle` forces a style recalc and
49
+ * `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
50
+ * each). The rungs inherit, so every element in a batch resolves the same numbers.
51
+ */
52
+ export function bloomTiming(el) {
53
+ const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
54
+ const raw = getComputedStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
55
+ const n = Number.parseFloat(raw);
56
+ const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
57
+ return { duration, frames: reduced ? FRAMES_REDUCED : FRAMES };
58
+ }
59
+ /**
60
+ * Bloom the wash on `el` itself: the caller owns the element and has primed it
61
+ * ({@link primeWash}). Returns the running animation, or `undefined` when there is
62
+ * nothing left to run: `elapsed` past the decay, or an environment without WAAPI.
63
+ * Pass `timing` to share one resolve across a batch.
64
+ */
65
+ export function bloom(el, elapsed = 0, timing) {
66
+ if (typeof el.animate !== 'function')
67
+ return undefined;
68
+ const { duration, frames } = timing ?? bloomTiming(el);
69
+ if (elapsed >= duration)
70
+ return undefined;
71
+ const anim = el.animate(frames, { duration });
72
+ if (elapsed > 0)
73
+ anim.currentTime = elapsed;
74
+ return anim;
75
+ }
76
+ /**
77
+ * Bloom over `host`'s CONTENT: an inset child, so the wash neither tints the host's
78
+ * own background nor fades the text under it. `host` must be positioned.
79
+ *
80
+ * ONE wash node per host, reused while it lives: two quick landings on the same leaf
81
+ * must not stack two alphas over it. No `elapsed` seam: the surfaces that use this
82
+ * (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
83
+ */
84
+ export function bloomInside(host) {
85
+ let el = host.querySelector(`:scope > .${BLOOM_CLASS}`);
86
+ if (el) {
87
+ // Drop the in-flight run's handlers before cancelling: its `oncancel` would
88
+ // otherwise remove the node the new run is about to animate.
89
+ for (const a of el.getAnimations()) {
90
+ a.onfinish = null;
91
+ a.oncancel = null;
92
+ a.cancel();
93
+ }
94
+ }
95
+ else {
96
+ el = document.createElement('div');
97
+ el.className = BLOOM_CLASS;
98
+ Object.assign(el.style, {
99
+ position: 'absolute',
100
+ inset: '0',
101
+ pointerEvents: 'none',
102
+ borderRadius: 'inherit'
103
+ });
104
+ primeWash(el);
105
+ host.appendChild(el);
106
+ }
107
+ const node = el;
108
+ const anim = bloom(node);
109
+ if (!anim) {
110
+ node.remove();
111
+ return;
112
+ }
113
+ const drop = () => node.remove();
114
+ anim.onfinish = drop;
115
+ anim.oncancel = drop;
116
+ }
@@ -0,0 +1,19 @@
1
+ import type { EditorView } from 'prosemirror-view';
2
+ /** A floating-ui `Measurable` over a PM range, carrying the `contextElement`
3
+ * `autoUpdate` tracks in the range's place. */
4
+ export interface RangeAnchor {
5
+ contextElement: Element;
6
+ getBoundingClientRect: () => DOMRect;
7
+ }
8
+ /**
9
+ * A virtual anchor over one PM range, measured when asked rather than when made.
10
+ *
11
+ * The measure is TOTAL. `coordsAtPos` reads layout, and layout can be gone: a
12
+ * position the document has since invalidated, a leaf inside a collapsed section, a
13
+ * view torn down between a scroll firing and the measure running. `autoUpdate` calls
14
+ * this at moments none of its callers choose, so an unmeasurable anchor returns the
15
+ * last rect that measured rather than throwing out of floating-ui's positioning pass.
16
+ *
17
+ * `from === to` is a caret, which is what the slash menu hangs off.
18
+ */
19
+ export declare function rangeAnchor(view: EditorView, from: number, to: number): RangeAnchor;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A virtual anchor over one PM range, measured when asked rather than when made.
3
+ *
4
+ * The measure is TOTAL. `coordsAtPos` reads layout, and layout can be gone: a
5
+ * position the document has since invalidated, a leaf inside a collapsed section, a
6
+ * view torn down between a scroll firing and the measure running. `autoUpdate` calls
7
+ * this at moments none of its callers choose, so an unmeasurable anchor returns the
8
+ * last rect that measured rather than throwing out of floating-ui's positioning pass.
9
+ *
10
+ * `from === to` is a caret, which is what the slash menu hangs off.
11
+ */
12
+ export function rangeAnchor(view, from, to) {
13
+ // Plain numbers, not a `DOMRect`: this runs at construction, and the codec's suite
14
+ // is a `node` environment with no DOM in it. The `DOMRect` is minted in the measure,
15
+ // which is a layout read and browser-only by construction.
16
+ let last = { left: 0, top: 0, right: 0, bottom: 0 };
17
+ return {
18
+ contextElement: view.dom,
19
+ getBoundingClientRect: () => {
20
+ try {
21
+ const a = view.coordsAtPos(from);
22
+ const b = view.coordsAtPos(to);
23
+ last = {
24
+ left: Math.min(a.left, b.left),
25
+ top: Math.min(a.top, b.top),
26
+ right: Math.max(a.right, b.right),
27
+ bottom: Math.max(a.bottom, b.bottom)
28
+ };
29
+ }
30
+ catch {
31
+ // unmeasurable: keep the last good rect
32
+ }
33
+ return new DOMRect(last.left, last.top, last.right - last.left, last.bottom - last.top);
34
+ }
35
+ };
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { Schema } from 'prosemirror-model';
2
+ import { type Command } from 'prosemirror-state';
3
+ /**
4
+ * The `code_block` link of the body's key chains: `{}` for the inline/plaintext
5
+ * schemas, which declare no code node.
6
+ *
7
+ * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
8
+ * cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
9
+ * otherwise splits the ITEM in two, each half holding a code block. Every command
10
+ * here declines outside a code block, so the list links keep the keys elsewhere.
11
+ */
12
+ export declare function codeKeymap(schema: Schema): Record<string, Command>;
@@ -0,0 +1,118 @@
1
+ import { TextSelection } from 'prosemirror-state';
2
+ import { newlineInCode } from 'prosemirror-commands';
3
+ /** One indent level. */
4
+ const UNIT = ' ';
5
+ /** The enclosing code block's content start and text, or `null` when the selection
6
+ * is not wholly inside one: a selection that escapes the block is a document-level
7
+ * range, where the structural links keep their meaning. */
8
+ function codeBlockAt(state) {
9
+ const { $from, $to } = state.selection;
10
+ if (!$from.parent.type.spec.code)
11
+ return null;
12
+ if ($from.parent !== $to.parent)
13
+ return null;
14
+ return { start: $from.start(), text: $from.parent.textContent };
15
+ }
16
+ /**
17
+ * Start offsets of the lines the range `[f, t)` covers, in document order.
18
+ *
19
+ * A line whose start IS `t` is excluded on a non-empty range: a selection ending at
20
+ * a line start does not reach into that line, the editor convention (else selecting
21
+ * whole lines by dragging to the next line's start indents one line too many).
22
+ */
23
+ function coveredLineStarts(text, f, t) {
24
+ const last = t > f ? t - 1 : t;
25
+ const starts = [];
26
+ let s = 0;
27
+ for (const line of text.split('\n')) {
28
+ if (s <= last && s + line.length >= f)
29
+ starts.push(s);
30
+ s += line.length + 1;
31
+ }
32
+ return starts;
33
+ }
34
+ /** Width of the indent one outdent removes at line offset `s`: a leading tab, else
35
+ * up to one unit of leading spaces (both forms, whatever Tab inserts). `0` when the
36
+ * line carries no indent. */
37
+ function outdentWidth(text, s) {
38
+ if (text[s] === '\t')
39
+ return 1;
40
+ let n = 0;
41
+ while (n < UNIT.length && text[s + n] === ' ')
42
+ n++;
43
+ return n;
44
+ }
45
+ /**
46
+ * Tab in a code block: insert one indent unit, or indent every covered line when the
47
+ * selection spans a newline. A single-line selection is REPLACED by the unit: Tab
48
+ * types, the way any other key does.
49
+ */
50
+ const indentInCode = (state, dispatch) => {
51
+ const block = codeBlockAt(state);
52
+ if (!block)
53
+ return false;
54
+ const { from, to } = state.selection;
55
+ const f = from - block.start;
56
+ const t = to - block.start;
57
+ if (!dispatch)
58
+ return true;
59
+ const tr = state.tr;
60
+ if (!block.text.slice(f, t).includes('\n')) {
61
+ tr.insertText(UNIT, from, to);
62
+ }
63
+ else {
64
+ const starts = coveredLineStarts(block.text, f, t);
65
+ // Back to front: an earlier insert would shift every later offset.
66
+ for (const s of [...starts].reverse())
67
+ tr.insertText(UNIT, block.start + s);
68
+ // Keep the covered lines covered. Mapping alone would push the head past
69
+ // the indent it just inserted at the first line's start.
70
+ tr.setSelection(TextSelection.create(tr.doc, block.start + starts[0], tr.mapping.map(to)));
71
+ }
72
+ dispatch(tr.scrollIntoView());
73
+ return true;
74
+ };
75
+ /**
76
+ * Shift-Tab in a code block: remove one indent level from every covered line.
77
+ *
78
+ * Declines when no covered line carries an indent, so the key is NOT swallowed: it
79
+ * falls through to the list link and then out of the leaf, which is the body's
80
+ * keyboard exit from inside a code block (VISUAL_EDITOR §Chrome).
81
+ */
82
+ const outdentInCode = (state, dispatch) => {
83
+ const block = codeBlockAt(state);
84
+ if (!block)
85
+ return false;
86
+ const { from, to } = state.selection;
87
+ const cuts = coveredLineStarts(block.text, from - block.start, to - block.start)
88
+ .map((s) => ({ s, width: outdentWidth(block.text, s) }))
89
+ .filter((c) => c.width > 0);
90
+ if (!cuts.length)
91
+ return false;
92
+ if (dispatch) {
93
+ const tr = state.tr;
94
+ for (const c of cuts.reverse()) {
95
+ tr.delete(block.start + c.s, block.start + c.s + c.width);
96
+ }
97
+ dispatch(tr.scrollIntoView());
98
+ }
99
+ return true;
100
+ };
101
+ /**
102
+ * The `code_block` link of the body's key chains: `{}` for the inline/plaintext
103
+ * schemas, which declare no code node.
104
+ *
105
+ * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
106
+ * cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
107
+ * otherwise splits the ITEM in two, each half holding a code block. Every command
108
+ * here declines outside a code block, so the list links keep the keys elsewhere.
109
+ */
110
+ export function codeKeymap(schema) {
111
+ if (!schema.nodes.code_block)
112
+ return {};
113
+ return {
114
+ Tab: indentInCode,
115
+ 'Shift-Tab': outdentInCode,
116
+ Enter: newlineInCode
117
+ };
118
+ }
@@ -0,0 +1,24 @@
1
+ import { type Node as PMNode, type Schema } from 'prosemirror-model';
2
+ import type { Content } from '@quillmark/wasm';
3
+ /** Code points of `s` (USV units): the iteration granularity the content speaks. */
4
+ export declare function codePoints(s: string): string[];
5
+ /** USV length of `s` (code points, not UTF-16 units). */
6
+ export declare function usvLength(s: string): number;
7
+ interface DecodeOpts {
8
+ /** Strip all marks (a `plaintext` field). */
9
+ plaintext?: boolean;
10
+ }
11
+ /**
12
+ * A `Content` as READ-ONLY DOM: decode under `schema`, then the nodes' own `toDOM`.
13
+ * The rendering half of the codec's job with no editing attached (no PM view, no
14
+ * plugins, no `contenteditable`) for chrome that must show content in the same
15
+ * mark vocabulary a leaf edits it in (the tips card). A second renderer over the
16
+ * same content would drift from `decode`; this cannot.
17
+ *
18
+ * `DOMSerializer.fromSchema` memoizes on the schema, so repeat calls build no
19
+ * serializer.
20
+ */
21
+ export declare function renderContent(rt: Content, schema: Schema, opts?: DecodeOpts): Node;
22
+ /** Decode a `Content` to a PM document under `schema`. */
23
+ export declare function decode(rt: Content, schema: Schema, opts?: DecodeOpts): PMNode;
24
+ export {};