@hachej/boring-ui-kit 0.1.111 → 0.2.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 (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +255 -239
  3. package/THIRD_PARTY_NOTICES.md +85 -0
  4. package/dist/anchor-conformance.d.ts +78 -0
  5. package/dist/anchor-conformance.js +288 -0
  6. package/dist/canvas-document.d.ts +25 -0
  7. package/dist/canvas-document.js +141 -0
  8. package/dist/canvas-editor-native.d.ts +22 -0
  9. package/dist/canvas-editor-native.js +112 -0
  10. package/dist/canvas-editor.d.ts +6 -0
  11. package/dist/canvas-editor.js +13 -0
  12. package/dist/canvas-mounted.d.ts +41 -0
  13. package/dist/canvas-mounted.js +180 -0
  14. package/dist/canvas-proposals.d.ts +11 -0
  15. package/dist/canvas-proposals.js +45 -0
  16. package/dist/canvas-records.d.ts +3 -0
  17. package/dist/canvas-records.js +39 -0
  18. package/dist/canvas.d.ts +92 -0
  19. package/dist/canvas.js +264 -0
  20. package/dist/contracts.d.ts +125 -0
  21. package/dist/contracts.js +1 -0
  22. package/dist/experience-catalog.d.ts +142 -0
  23. package/dist/experience-catalog.js +25 -0
  24. package/dist/experience-compose.d.ts +83 -0
  25. package/dist/experience-compose.js +218 -0
  26. package/dist/experience-composition-schema.d.ts +29 -0
  27. package/dist/experience-composition-schema.js +22 -0
  28. package/dist/experience-document-viewer.d.ts +13 -0
  29. package/dist/experience-document-viewer.js +54 -0
  30. package/dist/experience-document.d.ts +64 -0
  31. package/dist/experience-document.js +234 -0
  32. package/dist/experience-region-text.d.ts +3 -0
  33. package/dist/experience-region-text.js +84 -0
  34. package/dist/experience-region-tree.d.ts +22 -0
  35. package/dist/experience-region-tree.js +50 -0
  36. package/dist/experience-regions.d.ts +9 -0
  37. package/dist/experience-regions.js +122 -0
  38. package/dist/experience-renderer.d.ts +16 -0
  39. package/dist/experience-renderer.js +37 -0
  40. package/dist/experience.d.ts +12 -0
  41. package/dist/experience.js +23 -0
  42. package/dist/file-tree-shadcn.d.ts +8 -0
  43. package/dist/file-tree-shadcn.js +14 -0
  44. package/dist/file-tree-view.d.ts +64 -0
  45. package/dist/file-tree-view.js +256 -0
  46. package/dist/file-tree.d.ts +57 -0
  47. package/dist/file-tree.js +204 -0
  48. package/dist/html-preview.d.ts +2 -0
  49. package/dist/html-preview.js +47 -0
  50. package/dist/html-viewer.d.ts +13 -0
  51. package/dist/html-viewer.js +69 -0
  52. package/dist/html.d.ts +21 -0
  53. package/dist/html.js +21 -0
  54. package/dist/index.d.ts +2 -472
  55. package/dist/index.js +1 -2671
  56. package/dist/markdown-editor.d.ts +28 -0
  57. package/dist/markdown-editor.js +230 -0
  58. package/dist/markdown-extensions.d.ts +16 -0
  59. package/dist/markdown-extensions.js +96 -0
  60. package/dist/markdown-icons.d.ts +244 -0
  61. package/dist/markdown-icons.js +32 -0
  62. package/dist/markdown-mounted.d.ts +63 -0
  63. package/dist/markdown-mounted.js +263 -0
  64. package/dist/markdown-parser.d.ts +4 -0
  65. package/dist/markdown-parser.js +33 -0
  66. package/dist/markdown-proposal-diff.d.ts +5 -0
  67. package/dist/markdown-proposal-diff.js +122 -0
  68. package/dist/markdown-safety.d.ts +21 -0
  69. package/dist/markdown-safety.js +135 -0
  70. package/dist/markdown.d.ts +86 -0
  71. package/dist/markdown.js +121 -0
  72. package/dist/native-chat.d.ts +150 -0
  73. package/dist/native-chat.js +376 -0
  74. package/dist/pi.d.ts +12 -0
  75. package/dist/pi.js +1 -0
  76. package/dist/public-text-buffer.d.ts +5 -0
  77. package/dist/public-text-buffer.js +1 -0
  78. package/dist/remote-chat.d.ts +76 -0
  79. package/dist/remote-chat.js +324 -0
  80. package/dist/resources.d.ts +34 -0
  81. package/dist/resources.js +1 -0
  82. package/dist/text-buffer-state.d.ts +24 -0
  83. package/dist/text-buffer-state.js +1 -0
  84. package/dist/text-buffer.d.ts +52 -0
  85. package/dist/text-buffer.js +315 -0
  86. package/dist/text-draft-controls.d.ts +9 -0
  87. package/dist/text-draft-controls.js +33 -0
  88. package/dist/text-draft-types.d.ts +111 -0
  89. package/dist/text-draft-types.js +1 -0
  90. package/dist/text-drafts.d.ts +29 -0
  91. package/dist/text-drafts.js +378 -0
  92. package/package.json +226 -58
  93. package/styles/file-tree.css +46 -0
  94. package/dist/styles.css +0 -2308
  95. package/dist/tokens.css +0 -155
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Toolbar icons for the Markdown editor, as inline SVG so the package needs no icon dependency.
3
+ // Path data is from Lucide (https://lucide.dev, ISC license), 24x24 outline icons.
4
+ import { createElement } from 'react';
5
+ const ICONS = {
6
+ Bold: [["path", { "d": "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" }]],
7
+ Italic: [["line", { "x1": "19", "x2": "10", "y1": "4", "y2": "4" }], ["line", { "x1": "14", "x2": "5", "y1": "20", "y2": "20" }], ["line", { "x1": "15", "x2": "9", "y1": "4", "y2": "20" }]],
8
+ Strike: [["path", { "d": "M16 4H9a3 3 0 0 0-2.83 4" }], ["path", { "d": "M14 12a4 4 0 0 1 0 8H6" }], ["line", { "x1": "4", "x2": "20", "y1": "12", "y2": "12" }]],
9
+ Heading1: [["path", { "d": "M4 12h8" }], ["path", { "d": "M4 18V6" }], ["path", { "d": "M12 18V6" }], ["path", { "d": "m17 12 3-2v8" }]],
10
+ Heading2: [["path", { "d": "M4 12h8" }], ["path", { "d": "M4 18V6" }], ["path", { "d": "M12 18V6" }], ["path", { "d": "M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1" }]],
11
+ Heading3: [["path", { "d": "M4 12h8" }], ["path", { "d": "M4 18V6" }], ["path", { "d": "M12 18V6" }], ["path", { "d": "M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2" }], ["path", { "d": "M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2" }]],
12
+ BulletList: [["path", { "d": "M3 5h.01" }], ["path", { "d": "M3 12h.01" }], ["path", { "d": "M3 19h.01" }], ["path", { "d": "M8 5h13" }], ["path", { "d": "M8 12h13" }], ["path", { "d": "M8 19h13" }]],
13
+ OrderedList: [["path", { "d": "M11 5h10" }], ["path", { "d": "M11 12h10" }], ["path", { "d": "M11 19h10" }], ["path", { "d": "M4 4h1v5" }], ["path", { "d": "M4 9h2" }], ["path", { "d": "M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02" }]],
14
+ TaskList: [["path", { "d": "M13 5h8" }], ["path", { "d": "M13 12h8" }], ["path", { "d": "M13 19h8" }], ["path", { "d": "m3 17 2 2 4-4" }], ["rect", { "x": "3", "y": "4", "width": "6", "height": "6", "rx": "1" }]],
15
+ Quote: [["path", { "d": "M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" }], ["path", { "d": "M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" }]],
16
+ Code: [["path", { "d": "m16 18 6-6-6-6" }], ["path", { "d": "m8 6-6 6 6 6" }]],
17
+ CodeBlock: [["path", { "d": "m10 9-3 3 3 3" }], ["path", { "d": "m14 15 3-3-3-3" }], ["rect", { "x": "3", "y": "3", "width": "18", "height": "18", "rx": "2" }]],
18
+ Link: [["path", { "d": "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }], ["path", { "d": "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }]],
19
+ Unlink: [["path", { "d": "m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71" }], ["path", { "d": "m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71" }], ["line", { "x1": "8", "x2": "8", "y1": "2", "y2": "5" }], ["line", { "x1": "2", "x2": "5", "y1": "8", "y2": "8" }], ["line", { "x1": "16", "x2": "16", "y1": "19", "y2": "22" }], ["line", { "x1": "19", "x2": "22", "y1": "16", "y2": "16" }]],
20
+ Highlight: [["path", { "d": "m9 11-6 6v3h9l3-3" }], ["path", { "d": "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" }]],
21
+ Rule: [["path", { "d": "M5 12h14" }]],
22
+ Table: [["path", { "d": "M3 9h18" }], ["path", { "d": "M9 3v18" }], ["rect", { "x": "3", "y": "3", "width": "18", "height": "18", "rx": "2" }]],
23
+ Rows: [["path", { "d": "M14 10h2" }], ["path", { "d": "M15 22v-8" }], ["path", { "d": "M15 2v4" }], ["path", { "d": "M2 10h2" }], ["path", { "d": "M20 10h2" }], ["path", { "d": "M3 19h18" }], ["path", { "d": "M3 22v-6a2 2 135 0 1 2-2h14a2 2 45 0 1 2 2v6" }], ["path", { "d": "M3 2v2a2 2 45 0 0 2 2h14a2 2 135 0 0 2-2V2" }], ["path", { "d": "M8 10h2" }], ["path", { "d": "M9 22v-8" }], ["path", { "d": "M9 2v4" }]],
24
+ Columns: [["path", { "d": "M14 14v2" }], ["path", { "d": "M14 20v2" }], ["path", { "d": "M14 2v2" }], ["path", { "d": "M14 8v2" }], ["path", { "d": "M2 15h8" }], ["path", { "d": "M2 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2" }], ["path", { "d": "M2 9h8" }], ["path", { "d": "M22 15h-4" }], ["path", { "d": "M22 3h-2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2" }], ["path", { "d": "M22 9h-4" }], ["path", { "d": "M5 3v18" }]],
25
+ Trash: [["path", { "d": "M10 11v6" }], ["path", { "d": "M14 11v6" }], ["path", { "d": "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }], ["path", { "d": "M3 6h18" }], ["path", { "d": "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }]],
26
+ Check: [["path", { "d": "M20 6 9 17l-5-5" }]],
27
+ X: [["path", { "d": "M18 6 6 18" }], ["path", { "d": "m6 6 12 12" }]],
28
+ };
29
+ /** Decorative: the button that holds it carries the accessible name. */
30
+ export function MarkdownIcon({ name }) {
31
+ return _jsx("svg", { className: "boring-markdown-icon", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: ICONS[name].map(([tag, attributes], index) => createElement(tag, { key: index, ...attributes })) });
32
+ }
@@ -0,0 +1,63 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ import type { PresentationCommand, ViewerTarget } from './contracts.js';
3
+ import type { MarkdownController } from './markdown.js';
4
+ import type { SaveSelection } from './resources.js';
5
+ export type MarkdownMountedSubject = SaveSelection['target']['subject'] & {
6
+ readonly mountId: string;
7
+ readonly projectionId: string;
8
+ readonly mode: 'rich' | 'source';
9
+ };
10
+ export type MarkdownMountedSelection = {
11
+ readonly kind: 'rich';
12
+ readonly anchor: number;
13
+ readonly head: number;
14
+ } | {
15
+ readonly kind: 'source';
16
+ readonly start: number;
17
+ readonly end: number;
18
+ readonly direction: 'forward' | 'backward' | 'none';
19
+ };
20
+ export interface MarkdownMountedInspection {
21
+ readonly selection: SaveSelection;
22
+ readonly dirty: boolean;
23
+ readonly headings: readonly {
24
+ readonly index: number;
25
+ readonly level: number;
26
+ readonly text: string;
27
+ }[];
28
+ readonly currentSelection: MarkdownMountedSelection;
29
+ readonly text: string;
30
+ }
31
+ export interface MarkdownMountedTools {
32
+ readonly getTarget: () => ViewerTarget<MarkdownMountedSubject> | null;
33
+ readonly inspect: PresentationCommand<{
34
+ readonly expiresAt: number;
35
+ }, MarkdownMountedInspection, MarkdownMountedSubject>;
36
+ readonly select: PresentationCommand<{
37
+ readonly expiresAt: number;
38
+ readonly selection: MarkdownMountedSelection;
39
+ }, void, MarkdownMountedSubject>;
40
+ readonly revealHeading: PresentationCommand<{
41
+ readonly expiresAt: number;
42
+ readonly index: number;
43
+ }, void, MarkdownMountedSubject>;
44
+ }
45
+ type MountedView = {
46
+ readonly kind: 'rich';
47
+ readonly editor: Editor;
48
+ readonly projectedText: () => string | null;
49
+ } | {
50
+ readonly kind: 'source';
51
+ readonly element: () => HTMLTextAreaElement | null;
52
+ };
53
+ export declare function createMountedMarkdownTools({ controller, current, view }: {
54
+ readonly controller: MarkdownController;
55
+ readonly current: () => boolean;
56
+ readonly view: MountedView;
57
+ }): {
58
+ tools: MarkdownMountedTools;
59
+ activate: () => void;
60
+ dispose: () => void;
61
+ };
62
+ export {};
63
+ //# sourceMappingURL=markdown-mounted.d.ts.map
@@ -0,0 +1,263 @@
1
+ import { TextSelection } from '@tiptap/pm/state';
2
+ import { freeze, sameBase } from './text-buffer.js';
3
+ import { randomUUID } from '@hachej/boring-files/platform';
4
+ const expirySchema = { type: 'integer', minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER };
5
+ const positionSchema = { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER };
6
+ function expiry(value) {
7
+ if (!value || typeof value !== 'object' || !('expiresAt' in value) || typeof value.expiresAt !== 'number' || !Number.isSafeInteger(value.expiresAt))
8
+ throw new TypeError('Command expiry is required');
9
+ return { expiresAt: value.expiresAt };
10
+ }
11
+ function selectionInput(value) {
12
+ const deadline = expiry(value);
13
+ if (!value || typeof value !== 'object' || !('selection' in value) || !value.selection || typeof value.selection !== 'object')
14
+ throw new TypeError('Selection is required');
15
+ const selected = value.selection;
16
+ if ('kind' in selected && selected.kind === 'rich' && 'anchor' in selected && 'head' in selected
17
+ && typeof selected.anchor === 'number' && Number.isSafeInteger(selected.anchor) && selected.anchor >= 0
18
+ && typeof selected.head === 'number' && Number.isSafeInteger(selected.head) && selected.head >= 0) {
19
+ return { ...deadline, selection: { kind: 'rich', anchor: selected.anchor, head: selected.head } };
20
+ }
21
+ if ('kind' in selected && selected.kind === 'source' && 'start' in selected && 'end' in selected && 'direction' in selected
22
+ && typeof selected.start === 'number' && Number.isSafeInteger(selected.start) && selected.start >= 0
23
+ && typeof selected.end === 'number' && Number.isSafeInteger(selected.end) && selected.end >= selected.start
24
+ && (selected.direction === 'forward' || selected.direction === 'backward' || selected.direction === 'none')) {
25
+ return { ...deadline, selection: { kind: 'source', start: selected.start, end: selected.end, direction: selected.direction } };
26
+ }
27
+ throw new TypeError('Expected a rich document selection or source UTF-16 selection');
28
+ }
29
+ function headingInput(value) {
30
+ const deadline = expiry(value);
31
+ if (!value || typeof value !== 'object' || !('index' in value) || typeof value.index !== 'number' || !Number.isSafeInteger(value.index) || value.index < 0)
32
+ throw new TypeError('Heading index must be a nonnegative integer');
33
+ return { ...deadline, index: value.index };
34
+ }
35
+ export function createMountedMarkdownTools({ controller, current, view }) {
36
+ const mountId = randomUUID();
37
+ let alive = false, projectionId = randomUUID();
38
+ let document;
39
+ const pending = new Set();
40
+ const element = () => {
41
+ if (view.kind === 'source')
42
+ return view.element();
43
+ if (view.editor.isDestroyed)
44
+ return null;
45
+ try {
46
+ return view.editor.view.dom;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ };
52
+ const getTarget = () => {
53
+ if (!alive || !current() || controller.getSnapshot().lifecycle !== 'active' || !element()?.isConnected)
54
+ return null;
55
+ const state = controller.getSnapshot();
56
+ if (view.kind === 'source') {
57
+ if (view.element()?.value !== state.text)
58
+ return null;
59
+ }
60
+ else {
61
+ if (view.editor.isDestroyed || view.projectedText() !== state.text)
62
+ return null;
63
+ if (document !== view.editor.state.doc) {
64
+ document = view.editor.state.doc;
65
+ projectionId = randomUUID();
66
+ }
67
+ }
68
+ const selected = controller.actions.selection().target;
69
+ return freeze({ ...selected, subject: { ...selected.subject, mountId, projectionId, mode: view.kind } });
70
+ };
71
+ const check = (target, expiresAt, signal) => {
72
+ if (!alive || !current() || controller.getSnapshot().lifecycle !== 'active' || !element()?.isConnected)
73
+ return { kind: 'unavailable', reason: 'Mounted editor is unavailable' };
74
+ if (signal?.aborted)
75
+ return { kind: 'denied', reason: 'Command was cancelled' };
76
+ if (expiresAt <= Date.now())
77
+ return { kind: 'stale', reason: 'Command expired' };
78
+ const selected = getTarget();
79
+ if (!selected)
80
+ return { kind: 'stale', reason: 'Mounted editor has not projected the current buffer' };
81
+ if (selected.instanceId !== target.instanceId || selected.epoch !== target.epoch
82
+ || selected.subject.mountId !== target.subject.mountId || selected.subject.projectionId !== target.subject.projectionId
83
+ || selected.subject.mode !== target.subject.mode || selected.subject.scopeId !== target.subject.scopeId
84
+ || selected.subject.bufferVersion !== target.subject.bufferVersion || !sameBase(selected.subject.base, target.subject.base)) {
85
+ return { kind: 'stale', reason: 'Mounted command target changed' };
86
+ }
87
+ return null;
88
+ };
89
+ const headings = () => {
90
+ const values = [];
91
+ if (view.kind === 'rich')
92
+ view.editor.state.doc.descendants((node, position) => {
93
+ if (node.type.name === 'heading')
94
+ values.push({ index: values.length, level: Number(node.attrs['level']), text: node.textContent, position });
95
+ });
96
+ return values;
97
+ };
98
+ const applySelection = (target, expiresAt, selected, signal) => {
99
+ const refused = check(target, expiresAt, signal);
100
+ if (refused)
101
+ return refused;
102
+ if (selected.kind !== view.kind)
103
+ return { kind: 'denied', reason: 'Selection coordinates belong to another editor mode' };
104
+ if (view.kind === 'source' && selected.kind === 'source') {
105
+ const input = view.element();
106
+ if (!input)
107
+ return { kind: 'unavailable', reason: 'Source editor is unavailable' };
108
+ if (selected.end > input.value.length)
109
+ return { kind: 'denied', reason: 'Selection is outside the source buffer' };
110
+ input.focus({ preventScroll: true });
111
+ const changed = check(target, expiresAt, signal);
112
+ if (changed)
113
+ return changed;
114
+ input.setSelectionRange(selected.start, selected.end, selected.direction);
115
+ const after = check(target, expiresAt, signal);
116
+ if (after)
117
+ return after;
118
+ const root = input.getRootNode();
119
+ if (!('activeElement' in root) || root.activeElement !== input || input.selectionStart !== selected.start || input.selectionEnd !== selected.end
120
+ || selected.start !== selected.end && input.selectionDirection !== selected.direction)
121
+ return { kind: 'unavailable', reason: 'Source selection was not applied' };
122
+ }
123
+ else if (view.kind === 'rich' && selected.kind === 'rich') {
124
+ const editor = view.editor, doc = editor.state.doc;
125
+ if (selected.anchor > doc.content.size || selected.head > doc.content.size
126
+ || !doc.resolve(selected.anchor).parent.inlineContent || !doc.resolve(selected.head).parent.inlineContent)
127
+ return { kind: 'denied', reason: 'Selection must address document text' };
128
+ editor.view.dom.focus({ preventScroll: true });
129
+ const moved = check(target, expiresAt, signal);
130
+ if (moved)
131
+ return moved;
132
+ const domSelection = editor.view.dom.ownerDocument.getSelection();
133
+ if (!domSelection)
134
+ return { kind: 'unavailable', reason: 'Browser selection is unavailable' };
135
+ const anchor = editor.view.domAtPos(selected.anchor);
136
+ domSelection.collapse(anchor.node, anchor.offset);
137
+ const selectedDom = check(target, expiresAt, signal);
138
+ if (selectedDom)
139
+ return selectedDom;
140
+ editor.view.dispatch(editor.state.tr.setSelection(TextSelection.create(editor.state.doc, selected.anchor, selected.head)));
141
+ const changed = check(target, expiresAt, signal);
142
+ if (changed)
143
+ return changed;
144
+ editor.view.focus();
145
+ const focused = check(target, expiresAt, signal);
146
+ if (focused)
147
+ return focused;
148
+ editor.view.dispatch(editor.state.tr.scrollIntoView());
149
+ const after = check(target, expiresAt, signal);
150
+ if (after)
151
+ return after;
152
+ if (!editor.view.hasFocus() || editor.state.selection.anchor !== selected.anchor || editor.state.selection.head !== selected.head
153
+ || !domSelection.anchorNode || !domSelection.focusNode || !editor.view.dom.contains(domSelection.anchorNode) || !editor.view.dom.contains(domSelection.focusNode)
154
+ || editor.view.posAtDOM(domSelection.anchorNode, domSelection.anchorOffset) !== selected.anchor
155
+ || editor.view.posAtDOM(domSelection.focusNode, domSelection.focusOffset) !== selected.head)
156
+ return { kind: 'unavailable', reason: 'Rich selection was not applied' };
157
+ }
158
+ return { kind: 'applied', value: undefined };
159
+ };
160
+ const schedule = (target, expiresAt, selected, signal) => {
161
+ const refused = check(target, expiresAt, signal);
162
+ if (refused)
163
+ return Promise.resolve(refused);
164
+ const window = element()?.ownerDocument.defaultView;
165
+ if (!window)
166
+ return Promise.resolve({ kind: 'unavailable', reason: 'Editor window is unavailable' });
167
+ return new Promise(resolve => {
168
+ let frame = 0, timer, settled = false;
169
+ const finish = (result) => {
170
+ if (settled)
171
+ return;
172
+ settled = true;
173
+ window.cancelAnimationFrame(frame);
174
+ clearTimeout(timer);
175
+ signal?.removeEventListener('abort', abort);
176
+ pending.delete(finish);
177
+ resolve(result);
178
+ };
179
+ const abort = () => finish({ kind: 'denied', reason: 'Command was cancelled' });
180
+ const expire = () => {
181
+ const left = expiresAt - Date.now();
182
+ if (left <= 0)
183
+ finish({ kind: 'stale', reason: 'Command expired' });
184
+ else
185
+ timer = setTimeout(expire, Math.min(left, 2147483647));
186
+ };
187
+ pending.add(finish);
188
+ signal?.addEventListener('abort', abort, { once: true });
189
+ expire();
190
+ if (settled)
191
+ return;
192
+ try {
193
+ frame = window.requestAnimationFrame(() => {
194
+ if (settled)
195
+ return;
196
+ try {
197
+ finish(applySelection(target, expiresAt, selected, signal));
198
+ }
199
+ catch {
200
+ finish(check(target, expiresAt, signal) ?? { kind: 'unavailable', reason: 'Browser selection failed' });
201
+ }
202
+ });
203
+ }
204
+ catch {
205
+ finish({ kind: 'unavailable', reason: 'Editor frame is unavailable' });
206
+ }
207
+ if (signal?.aborted)
208
+ abort();
209
+ });
210
+ };
211
+ const tools = {
212
+ getTarget,
213
+ inspect: {
214
+ name: 'inspect_mounted_markdown',
215
+ input: { jsonSchema: { type: 'object', properties: { expiresAt: expirySchema }, required: ['expiresAt'] }, parse: expiry },
216
+ invoke: async (target, input, signal) => {
217
+ const captured = structuredClone(target), { expiresAt } = expiry(input), refused = check(captured, expiresAt, signal);
218
+ if (refused)
219
+ return refused;
220
+ let currentSelection, text;
221
+ if (view.kind === 'source') {
222
+ const input = view.element();
223
+ if (!input)
224
+ return { kind: 'unavailable', reason: 'Source editor is unavailable' };
225
+ currentSelection = { kind: 'source', start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection };
226
+ text = input.value.slice(input.selectionStart, input.selectionEnd);
227
+ }
228
+ else {
229
+ const selected = view.editor.state.selection;
230
+ currentSelection = { kind: 'rich', anchor: selected.anchor, head: selected.head };
231
+ text = view.editor.state.doc.textBetween(selected.from, selected.to, '\n');
232
+ }
233
+ return { kind: 'applied', value: freeze({ selection: controller.actions.selection(), dirty: controller.getSnapshot().dirty, headings: headings().map(({ index, level, text }) => ({ index, level, text })), currentSelection, text }) };
234
+ },
235
+ },
236
+ select: {
237
+ name: 'select_mounted_markdown', input: { jsonSchema: { type: 'object', properties: { expiresAt: expirySchema, selection: { oneOf: [
238
+ { type: 'object', properties: { kind: { const: 'rich' }, anchor: positionSchema, head: positionSchema }, required: ['kind', 'anchor', 'head'] },
239
+ { type: 'object', properties: { kind: { const: 'source' }, start: positionSchema, end: positionSchema, direction: { enum: ['forward', 'backward', 'none'] } }, required: ['kind', 'start', 'end', 'direction'] },
240
+ ] } }, required: ['expiresAt', 'selection'] }, parse: selectionInput },
241
+ invoke: async (target, input, signal) => { const captured = structuredClone(target), parsed = selectionInput(input); return schedule(captured, parsed.expiresAt, parsed.selection, signal); },
242
+ },
243
+ revealHeading: {
244
+ name: 'reveal_markdown_heading', input: { jsonSchema: { type: 'object', properties: { expiresAt: expirySchema, index: positionSchema }, required: ['expiresAt', 'index'] }, parse: headingInput },
245
+ invoke: async (target, input, signal) => {
246
+ const captured = structuredClone(target), parsed = headingInput(input), refused = check(captured, parsed.expiresAt, signal);
247
+ if (refused)
248
+ return refused;
249
+ if (view.kind !== 'rich')
250
+ return { kind: 'unavailable', reason: 'Heading navigation requires the rich editor' };
251
+ const heading = headings()[parsed.index];
252
+ if (!heading)
253
+ return { kind: 'unavailable', reason: 'Heading is not present' };
254
+ return schedule(captured, parsed.expiresAt, { kind: 'rich', anchor: heading.position + 1, head: heading.position + 1 }, signal);
255
+ },
256
+ },
257
+ };
258
+ return { tools, activate: () => { alive = true; }, dispose: () => {
259
+ alive = false;
260
+ for (const finish of [...pending])
261
+ finish({ kind: 'unavailable', reason: 'Mounted editor was detached' });
262
+ } };
263
+ }
@@ -0,0 +1,4 @@
1
+ import { marked } from 'marked';
2
+ /** Tiptap expects the callable marked API; keep its mutable options local to one editor. */
3
+ export declare function createMarkdownParser(): typeof marked;
4
+ //# sourceMappingURL=markdown-parser.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { Marked, marked } from 'marked';
2
+ /** Tiptap expects the callable marked API; keep its mutable options local to one editor. */
3
+ export function createMarkdownParser() {
4
+ const instance = new Marked({ tokenizer: { html: () => undefined, tag: () => undefined } });
5
+ // marked HTML-escapes the text of inline tokens (`&` becomes `&amp;`); the editor holds plain text, so decode it again or a
6
+ // serialised document would not match what was written.
7
+ const plain = (text) => text.replace(/&(?:amp|lt|gt|quot|#39);/g, entity => ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" })[entity] ?? entity);
8
+ const decode = (tokens) => {
9
+ marked.walkTokens(tokens, token => {
10
+ if ((token.type === 'text' || token.type === 'escape' || token.type === 'codespan') && !('tokens' in token && token.tokens))
11
+ token.text = plain(token.text);
12
+ });
13
+ return tokens;
14
+ };
15
+ const parser = Object.assign(instance.parse, marked);
16
+ parser.parse = parser;
17
+ parser.defaults = instance.defaults;
18
+ parser.lexer = (source, options) => decode(options ? marked.Lexer.lex(source, options) : marked.Lexer.lex(source, instance.defaults));
19
+ parser.parseInline = instance.parseInline;
20
+ parser.walkTokens = instance.walkTokens.bind(instance);
21
+ parser.use = (...extensions) => {
22
+ instance.use(...extensions);
23
+ parser.defaults = instance.defaults;
24
+ return parser;
25
+ };
26
+ parser.setOptions = options => {
27
+ instance.setOptions(options);
28
+ parser.defaults = instance.defaults;
29
+ return parser;
30
+ };
31
+ parser.options = parser.setOptions;
32
+ return parser;
33
+ }
@@ -0,0 +1,5 @@
1
+ import type { MarkdownProposal } from './markdown.js';
2
+ export declare function MarkdownProposalDiff({ proposal }: {
3
+ readonly proposal: MarkdownProposal;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=markdown-proposal-diff.d.ts.map
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ const maxMatrixCells = 250_000;
4
+ const contextLines = 2;
5
+ function lineAt(values, index) {
6
+ const value = values[index];
7
+ if (value === undefined)
8
+ throw new RangeError('Line index is outside the comparison');
9
+ return value;
10
+ }
11
+ function scoreAt(values, index) {
12
+ const value = values[index];
13
+ if (value === undefined)
14
+ throw new RangeError('Comparison index is outside the matrix');
15
+ return value;
16
+ }
17
+ function lines(text) {
18
+ const result = [];
19
+ let start = 0;
20
+ for (let index = 0; index < text.length; index++) {
21
+ if (text.charCodeAt(index) === 10) {
22
+ result.push(text.slice(start, index + 1));
23
+ start = index + 1;
24
+ }
25
+ }
26
+ if (start < text.length)
27
+ result.push(text.slice(start));
28
+ return result;
29
+ }
30
+ function review(before, after) {
31
+ const left = lines(before), right = lines(after);
32
+ let start = 0, endLeft = left.length, endRight = right.length;
33
+ while (start < endLeft && start < endRight && left[start] === right[start])
34
+ start++;
35
+ while (endLeft > start && endRight > start && left[endLeft - 1] === right[endRight - 1]) {
36
+ endLeft--;
37
+ endRight--;
38
+ }
39
+ const result = left.slice(0, start).map(value => ({ kind: 'same', value }));
40
+ const middleLeft = endLeft - start, middleRight = endRight - start;
41
+ const fallback = middleLeft > 0 && middleRight > 0 && middleLeft + 1 > Math.floor(maxMatrixCells / (middleRight + 1));
42
+ if (fallback || middleLeft === 0 || middleRight === 0) {
43
+ for (let index = start; index < endLeft; index++)
44
+ result.push({ kind: 'remove', value: lineAt(left, index) });
45
+ for (let index = start; index < endRight; index++)
46
+ result.push({ kind: 'add', value: lineAt(right, index) });
47
+ }
48
+ else {
49
+ const width = middleRight + 1;
50
+ const matches = new Uint32Array((middleLeft + 1) * width);
51
+ for (let leftIndex = middleLeft - 1; leftIndex >= 0; leftIndex--) {
52
+ for (let rightIndex = middleRight - 1; rightIndex >= 0; rightIndex--) {
53
+ const offset = leftIndex * width + rightIndex;
54
+ matches[offset] = lineAt(left, start + leftIndex) === lineAt(right, start + rightIndex)
55
+ ? scoreAt(matches, offset + width + 1) + 1
56
+ : Math.max(scoreAt(matches, offset + width), scoreAt(matches, offset + 1));
57
+ }
58
+ }
59
+ let leftIndex = 0, rightIndex = 0;
60
+ while (leftIndex < middleLeft || rightIndex < middleRight) {
61
+ if (leftIndex < middleLeft && rightIndex < middleRight && lineAt(left, start + leftIndex) === lineAt(right, start + rightIndex)) {
62
+ result.push({ kind: 'same', value: lineAt(left, start + leftIndex) });
63
+ leftIndex++;
64
+ rightIndex++;
65
+ }
66
+ else if (leftIndex < middleLeft && (rightIndex === middleRight
67
+ || scoreAt(matches, (leftIndex + 1) * width + rightIndex) >= scoreAt(matches, leftIndex * width + rightIndex + 1))) {
68
+ result.push({ kind: 'remove', value: lineAt(left, start + leftIndex) });
69
+ leftIndex++;
70
+ }
71
+ else {
72
+ result.push({ kind: 'add', value: lineAt(right, start + rightIndex) });
73
+ rightIndex++;
74
+ }
75
+ }
76
+ }
77
+ for (let index = endLeft; index < left.length; index++)
78
+ result.push({ kind: 'same', value: lineAt(left, index) });
79
+ return { lines: result, fallback };
80
+ }
81
+ function entries(diff) {
82
+ const visible = new Set();
83
+ for (let index = 0; index < diff.length; index++) {
84
+ if (diff[index]?.kind === 'same')
85
+ continue;
86
+ for (let nearby = Math.max(0, index - contextLines); nearby <= Math.min(diff.length - 1, index + contextLines); nearby++)
87
+ visible.add(nearby);
88
+ }
89
+ const result = [];
90
+ for (let index = 0; index < diff.length;) {
91
+ const line = diff[index];
92
+ if (line?.kind !== 'same' || visible.has(index)) {
93
+ if (line)
94
+ result.push(line);
95
+ index++;
96
+ continue;
97
+ }
98
+ const start = index;
99
+ while (index < diff.length && diff[index]?.kind === 'same' && !visible.has(index))
100
+ index++;
101
+ result.push({ kind: 'fold', index: start, lines: diff.slice(start, index) });
102
+ }
103
+ return result;
104
+ }
105
+ function DiffRow({ line }) {
106
+ const terminated = line.value.endsWith('\n');
107
+ const display = terminated ? line.value.slice(0, -1) : line.value;
108
+ return _jsxs("div", { "data-diff": line.kind, "data-line-ending": terminated ? 'lf' : 'none', children: [_jsx("span", { children: line.kind === 'add' ? '+ ' : line.kind === 'remove' ? '- ' : ' ' }), _jsx("span", { "data-diff-text": "", children: display }), line.kind !== 'same' && line.value.endsWith('\r\n') && _jsx("span", { "data-diff-crlf": "", children: " CRLF line ending" }), !terminated && _jsx("span", { "data-diff-no-newline": "", children: " No newline at end" })] });
109
+ }
110
+ export function MarkdownProposalDiff({ proposal }) {
111
+ const diff = useMemo(() => review(proposal.before, proposal.after), [proposal.before, proposal.after]);
112
+ const shown = useMemo(() => entries(diff.lines), [diff]);
113
+ const [expanded, setExpanded] = useState(() => new Set());
114
+ return _jsxs("div", { "data-boring": "proposal-diff", role: "group", "aria-label": "Proposed line changes", children: [diff.fallback && _jsx("p", { role: "status", "data-diff-fallback": "", children: "Detailed line alignment unavailable; showing changed block." }), proposal.before === proposal.after && _jsx("p", { children: "No text changes" }), _jsx("div", { style: { overflowX: 'auto', whiteSpace: 'pre-wrap' }, children: shown.map((entry, index) => entry.kind === 'fold' ? _jsxs("div", { children: [_jsxs("button", { type: "button", "data-diff-fold": "", "aria-expanded": expanded.has(entry.index), onClick: () => setExpanded(current => {
115
+ const next = new Set(current);
116
+ if (next.has(entry.index))
117
+ next.delete(entry.index);
118
+ else
119
+ next.add(entry.index);
120
+ return next;
121
+ }), children: [expanded.has(entry.index) ? 'Hide' : 'Show', " ", entry.lines.length, " unchanged lines"] }), expanded.has(entry.index) && entry.lines.map((line, lineIndex) => _jsx(DiffRow, { line: line }, lineIndex))] }, `fold-${entry.index}`) : _jsx(DiffRow, { line: entry }, `line-${index}`)) })] });
122
+ }
@@ -0,0 +1,21 @@
1
+ export type MarkdownRichSafety = {
2
+ readonly safe: true;
3
+ } | {
4
+ readonly safe: false;
5
+ readonly reasons: readonly string[];
6
+ };
7
+ /** Blank-line runs outside fenced code collapse to one blank line, and the ends are trimmed: the only whitespace the rich editor may normalise. */
8
+ export declare function normalizeMarkdownWhitespace(text: string): string;
9
+ /** What follows the last visible character, so a rich edit keeps the document's own final newline. */
10
+ export declare function trailingWhitespace(text: string): string;
11
+ /** The text a rich edit publishes: the serialised document, whitespace normalised, with the original's trailing newline. */
12
+ export declare function finishRichMarkdown(serialized: string, previous: string): string;
13
+ /**
14
+ * Decides whether the rich editor can open and edit `text` without changing anything but what the person edits: the text is
15
+ * parsed into the editor's document and serialised back without any edit, and must equal the original apart from blank-line runs and
16
+ * the ends. Named constructs the editor cannot hold (front matter, raw HTML, footnotes, reference links, CRLF) are reported as the
17
+ * reason. Without a DOM only the named constructs can be checked.
18
+ */
19
+ export declare function checkMarkdownRichSafety(text: string): MarkdownRichSafety;
20
+ export declare function describeUnsafe(safety: MarkdownRichSafety): string;
21
+ //# sourceMappingURL=markdown-safety.d.ts.map