@bendyline/squisq-editor-react 2.11.17 → 2.11.19
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/dist/{chunk-US5DALTZ.js → chunk-5EB3BGPE.js} +2419 -2025
- package/dist/{chunk-QZWFSSDE.js → chunk-DJS5I5NO.js} +1 -1
- package/dist/chunk-DNEVUOBZ.js +14 -0
- package/dist/chunk-DNJ6HOES.js +93 -0
- package/dist/{chunk-DPAMJV6A.js → chunk-HSWWEWT4.js} +31 -2
- package/dist/{chunk-EJKGXWNK.js → chunk-LR6Q2MK4.js} +1 -1
- package/dist/chunk-LRZJD5YD.js +107 -0
- package/dist/{chunk-2XXBDECJ.js → chunk-OCQEWWHI.js} +1 -1
- package/dist/{chunk-HTO5MTTJ.js → chunk-TZX7RKZV.js} +1107 -130
- package/dist/{chunk-C6QVA6TO.js → chunk-UFK47ZAI.js} +58 -133
- package/dist/{chunk-UBFNLWCN.js → chunk-V2OLZOYJ.js} +2 -2
- package/dist/{chunk-XZVMKR5Q.js → chunk-ZS6G2P57.js} +41 -96
- package/dist/dictationSession-PN3OT5LJ.js +521 -0
- package/dist/index.d.ts +30 -11
- package/dist/index.js +53 -21
- package/dist/json-editor/index.js +3 -3
- package/dist/recorder/index.d.ts +2 -2
- package/dist/recorder/index.js +17 -13
- package/dist/{recorder-CRsw0eC7.d.ts → recorder-DKN3vZPi.d.ts} +1 -1
- package/dist/review/index.js +3 -2
- package/dist/shell/index.d.ts +2 -1
- package/dist/shell/index.js +10 -7
- package/dist/{shell-X0EoQbbN.d.ts → shell-Cje1A3zN.d.ts} +176 -5
- package/dist/speech/index.d.ts +126 -0
- package/dist/speech/index.js +10 -0
- package/dist/styles/index.css +142 -3
- package/dist/teleprompter/index.d.ts +13 -5
- package/dist/teleprompter/index.js +3 -2
- package/dist/{useNarrationStage-vKtk6H-V.d.ts → useNarrationStage-CB8dynUg.d.ts} +15 -1
- package/package.json +10 -6
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSpeechInputProviderFactory,
|
|
3
|
+
resolveSpeechInputProvider
|
|
4
|
+
} from "./chunk-DNEVUOBZ.js";
|
|
1
5
|
import {
|
|
2
6
|
markdownToTiptap
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HSWWEWT4.js";
|
|
4
8
|
|
|
5
9
|
// src/blockRange.ts
|
|
6
10
|
import { parseMarkdown } from "@bendyline/squisq/markdown";
|
|
@@ -87,6 +91,175 @@ function sliceIndexAtOffset(slices, offset) {
|
|
|
87
91
|
return slices.findIndex((s) => offset >= s.range.startOffset && offset < s.range.endOffset);
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// src/sourceEdits.ts
|
|
95
|
+
import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
|
|
96
|
+
import { isContainerTemplate } from "@bendyline/squisq/doc";
|
|
97
|
+
function normalizeSourceEdits(source, edits) {
|
|
98
|
+
const sorted = edits.map((edit, order) => ({ edit, order })).sort((a, b) => a.edit.start - b.edit.start || a.edit.end - b.edit.end || a.order - b.order).map(({ edit }) => edit);
|
|
99
|
+
let previousEnd = 0;
|
|
100
|
+
for (const edit of sorted) {
|
|
101
|
+
if (!Number.isInteger(edit.start) || !Number.isInteger(edit.end) || typeof edit.text !== "string" || edit.start < 0 || edit.end < edit.start || edit.end > source.length || edit.start < previousEnd) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
previousEnd = edit.end;
|
|
105
|
+
}
|
|
106
|
+
return sorted;
|
|
107
|
+
}
|
|
108
|
+
function applySourceEditsToText(source, edits) {
|
|
109
|
+
const sorted = normalizeSourceEdits(source, edits);
|
|
110
|
+
if (!sorted) throw new RangeError("Source edits are out of range or overlap.");
|
|
111
|
+
let output = "";
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
for (const edit of sorted) {
|
|
114
|
+
output += source.slice(cursor, edit.start) + edit.text;
|
|
115
|
+
cursor = edit.end;
|
|
116
|
+
}
|
|
117
|
+
return output + source.slice(cursor);
|
|
118
|
+
}
|
|
119
|
+
function minimalReplaceEdit(previous, next) {
|
|
120
|
+
if (previous === next) return null;
|
|
121
|
+
const limit = Math.min(previous.length, next.length);
|
|
122
|
+
let prefix = 0;
|
|
123
|
+
while (prefix < limit && previous.charCodeAt(prefix) === next.charCodeAt(prefix)) prefix++;
|
|
124
|
+
let suffix = 0;
|
|
125
|
+
while (suffix < limit - prefix && previous.charCodeAt(previous.length - 1 - suffix) === next.charCodeAt(next.length - 1 - suffix)) {
|
|
126
|
+
suffix++;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
start: prefix,
|
|
130
|
+
end: previous.length - suffix,
|
|
131
|
+
text: next.slice(prefix, next.length - suffix)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function containerOwners(blocks) {
|
|
135
|
+
const owners = [];
|
|
136
|
+
let active = null;
|
|
137
|
+
blocks.forEach((block, index) => {
|
|
138
|
+
if (block.headingDepth !== null) {
|
|
139
|
+
if (active && block.headingDepth <= active.depth) active = null;
|
|
140
|
+
if (!active && block.container) active = { index, depth: block.headingDepth };
|
|
141
|
+
}
|
|
142
|
+
owners.push(active ? active.index : -1);
|
|
143
|
+
});
|
|
144
|
+
return owners;
|
|
145
|
+
}
|
|
146
|
+
function resolveOutlineInsertion(blocks, cursorIndex, placement) {
|
|
147
|
+
const owners = containerOwners(blocks);
|
|
148
|
+
const cursor = Math.min(Math.max(cursorIndex, -1), blocks.length - 1);
|
|
149
|
+
const firstHeadingFrom = (from) => {
|
|
150
|
+
for (let i = Math.max(from, 0); i < blocks.length; i++) {
|
|
151
|
+
if (blocks[i]?.headingDepth !== null) return i;
|
|
152
|
+
}
|
|
153
|
+
return blocks.length;
|
|
154
|
+
};
|
|
155
|
+
let index;
|
|
156
|
+
const container = cursor >= 0 ? owners[cursor] ?? -1 : -1;
|
|
157
|
+
if (container >= 0) {
|
|
158
|
+
const depth = blocks[container]?.headingDepth ?? 1;
|
|
159
|
+
if (placement === "afterBlock") {
|
|
160
|
+
index = container;
|
|
161
|
+
} else {
|
|
162
|
+
index = blocks.length;
|
|
163
|
+
for (let i = container + 1; i < blocks.length; i++) {
|
|
164
|
+
const headingDepth = blocks[i]?.headingDepth ?? null;
|
|
165
|
+
if (headingDepth !== null && headingDepth <= depth) {
|
|
166
|
+
index = i;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else if (placement === "afterBlock") {
|
|
172
|
+
index = cursor + 1;
|
|
173
|
+
} else {
|
|
174
|
+
index = firstHeadingFrom(cursor + 1);
|
|
175
|
+
}
|
|
176
|
+
const nextHeading = firstHeadingFrom(index);
|
|
177
|
+
const nextHeadingDepth = nextHeading < blocks.length ? blocks[nextHeading]?.headingDepth ?? null : null;
|
|
178
|
+
let previousHeading = -1;
|
|
179
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
180
|
+
if (blocks[i]?.headingDepth !== null) {
|
|
181
|
+
previousHeading = i;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let sectionDepth = 0;
|
|
186
|
+
let maxHeadingDepth = 6;
|
|
187
|
+
if (previousHeading >= 0) {
|
|
188
|
+
const owner = owners[previousHeading] ?? -1;
|
|
189
|
+
if (owner >= 0) {
|
|
190
|
+
const ownerDepth = blocks[owner]?.headingDepth ?? 1;
|
|
191
|
+
sectionDepth = ownerDepth - 1;
|
|
192
|
+
maxHeadingDepth = ownerDepth;
|
|
193
|
+
} else {
|
|
194
|
+
sectionDepth = blocks[previousHeading]?.headingDepth ?? 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { index, sectionDepth, nextHeadingDepth, maxHeadingDepth };
|
|
198
|
+
}
|
|
199
|
+
function headingDepthForInsertion(context, options = {}) {
|
|
200
|
+
const deepest = Math.min(context.maxHeadingDepth, 6 - (options.childLevels ?? 0));
|
|
201
|
+
const wanted = Math.max(context.sectionDepth + 1, 2, context.nextHeadingDepth ?? 0);
|
|
202
|
+
const depth = Math.min(wanted, deepest);
|
|
203
|
+
if (depth < 1 || depth < (context.nextHeadingDepth ?? 1)) return null;
|
|
204
|
+
return depth;
|
|
205
|
+
}
|
|
206
|
+
function sourceOutline(source) {
|
|
207
|
+
const blocks = [];
|
|
208
|
+
const ranges = [];
|
|
209
|
+
const bodyStart = frontmatterEndOffset(source);
|
|
210
|
+
let children;
|
|
211
|
+
try {
|
|
212
|
+
children = parseMarkdown2(source).children;
|
|
213
|
+
} catch {
|
|
214
|
+
return { blocks, ranges };
|
|
215
|
+
}
|
|
216
|
+
for (const node of children) {
|
|
217
|
+
const start = node.position?.start.offset;
|
|
218
|
+
const end = node.position?.end.offset;
|
|
219
|
+
if (typeof start !== "number" || typeof end !== "number" || start < bodyStart) continue;
|
|
220
|
+
if (node.type === "heading") {
|
|
221
|
+
blocks.push({
|
|
222
|
+
headingDepth: node.depth,
|
|
223
|
+
container: isContainerTemplate(node.templateAnnotation?.template)
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
blocks.push({ headingDepth: null, container: false });
|
|
227
|
+
}
|
|
228
|
+
ranges.push({ start, end });
|
|
229
|
+
}
|
|
230
|
+
return { blocks, ranges };
|
|
231
|
+
}
|
|
232
|
+
function blockInsertionPoint(source, cursorOffset, placement) {
|
|
233
|
+
const { blocks, ranges } = sourceOutline(source);
|
|
234
|
+
let cursorIndex = -1;
|
|
235
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
236
|
+
if ((ranges[i]?.start ?? Infinity) <= cursorOffset) cursorIndex = i;
|
|
237
|
+
else break;
|
|
238
|
+
}
|
|
239
|
+
const { index, ...context } = resolveOutlineInsertion(blocks, cursorIndex, placement);
|
|
240
|
+
const offset = index > 0 ? ranges[index - 1]?.end ?? source.length : ranges[0]?.start ?? source.length;
|
|
241
|
+
return { offset, ...context };
|
|
242
|
+
}
|
|
243
|
+
function joinBlockAt(source, offset, block) {
|
|
244
|
+
const bodyStart = frontmatterEndOffset(source);
|
|
245
|
+
const isSpace = (char) => char === "\n" || char === "\r" || char === " " || char === " ";
|
|
246
|
+
let start = Math.min(Math.max(offset, bodyStart), source.length);
|
|
247
|
+
while (start > bodyStart && isSpace(source[start - 1])) start--;
|
|
248
|
+
let end = Math.max(start, Math.min(offset, source.length));
|
|
249
|
+
while (end < source.length) {
|
|
250
|
+
const newline = source.indexOf("\n", end);
|
|
251
|
+
const lineEnd = newline === -1 ? source.length : newline + 1;
|
|
252
|
+
if (source.slice(end, lineEnd).trim() !== "") break;
|
|
253
|
+
end = lineEnd;
|
|
254
|
+
}
|
|
255
|
+
const body = block.replace(/^\s*\n/, "").replace(/\s+$/, "");
|
|
256
|
+
const before = source.slice(0, start);
|
|
257
|
+
const trailingNewlines = before.endsWith("\n\n") ? 2 : before.endsWith("\n") ? 1 : 0;
|
|
258
|
+
const prefix = before.length === 0 ? "" : "\n".repeat(2 - trailingNewlines);
|
|
259
|
+
const suffix = end >= source.length ? "\n" : "\n\n";
|
|
260
|
+
return { start, end, text: prefix + body + suffix };
|
|
261
|
+
}
|
|
262
|
+
|
|
90
263
|
// src/fileKind.ts
|
|
91
264
|
var DATA_MODE_EXTENSIONS = /* @__PURE__ */ new Set(["csv", "tsv", "parquet"]);
|
|
92
265
|
var EXT_TO_LANGUAGE = {
|
|
@@ -258,20 +431,679 @@ function useBlockNavigator(source, setSource, opts) {
|
|
|
258
431
|
}
|
|
259
432
|
|
|
260
433
|
// src/EditorContext.tsx
|
|
434
|
+
import {
|
|
435
|
+
createContext as createContext2,
|
|
436
|
+
useContext as useContext2,
|
|
437
|
+
useState as useState3,
|
|
438
|
+
useCallback as useCallback3,
|
|
439
|
+
useMemo as useMemo3,
|
|
440
|
+
useRef as useRef2,
|
|
441
|
+
useEffect as useEffect2
|
|
442
|
+
} from "react";
|
|
443
|
+
import { parseMarkdown as parseMarkdown3, stringifyMarkdown } from "@bendyline/squisq/markdown";
|
|
444
|
+
import { markdownToDoc } from "@bendyline/squisq/doc";
|
|
445
|
+
|
|
446
|
+
// src/speech/useDictationController.ts
|
|
261
447
|
import {
|
|
262
448
|
createContext,
|
|
263
|
-
useContext,
|
|
264
|
-
useState as useState2,
|
|
265
449
|
useCallback as useCallback2,
|
|
450
|
+
useContext,
|
|
451
|
+
useEffect,
|
|
266
452
|
useMemo as useMemo2,
|
|
267
453
|
useRef,
|
|
268
|
-
|
|
454
|
+
useState as useState2
|
|
269
455
|
} from "react";
|
|
270
|
-
|
|
271
|
-
|
|
456
|
+
|
|
457
|
+
// src/speech/dictationInsertion.ts
|
|
458
|
+
import { Selection, TextSelection } from "@tiptap/pm/state";
|
|
459
|
+
import { closeHistory } from "@tiptap/pm/history";
|
|
460
|
+
var INLINE_OBJECT = "\uFFFC";
|
|
461
|
+
function normalizeDictatedText(transcript) {
|
|
462
|
+
return transcript.replace(/\s+/g, " ").trim();
|
|
463
|
+
}
|
|
464
|
+
function needsLeadingSpace(charBefore, text) {
|
|
465
|
+
if (charBefore === null || charBefore === "") return false;
|
|
466
|
+
if (/\s/.test(charBefore)) return false;
|
|
467
|
+
if (/^[.,;:!?…)\]}]/.test(text)) return false;
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
function needsTrailingSpace(charAfter) {
|
|
471
|
+
if (charAfter === null || charAfter === "") return false;
|
|
472
|
+
if (/\s/.test(charAfter)) return false;
|
|
473
|
+
return !/^[.,;:!?…)\]}]/.test(charAfter);
|
|
474
|
+
}
|
|
475
|
+
function composeDictatedInsertion(charBefore, transcript, charAfter = null) {
|
|
476
|
+
const words = normalizeDictatedText(transcript);
|
|
477
|
+
if (!words) return null;
|
|
478
|
+
const lead = needsLeadingSpace(charBefore, words) ? " " : "";
|
|
479
|
+
const trail = needsTrailingSpace(charAfter) ? " " : "";
|
|
480
|
+
return { text: `${lead}${words}${trail}`, caret: lead.length + words.length };
|
|
481
|
+
}
|
|
482
|
+
function focusIsInOtherEditable() {
|
|
483
|
+
if (typeof document === "undefined") return false;
|
|
484
|
+
const active = document.activeElement;
|
|
485
|
+
if (!active || active === document.body) return false;
|
|
486
|
+
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement) {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
return active instanceof HTMLElement && active.isContentEditable;
|
|
490
|
+
}
|
|
491
|
+
function charBeforeInTiptap($pos) {
|
|
492
|
+
if ($pos.parentOffset === 0) return null;
|
|
493
|
+
const before = $pos.nodeBefore;
|
|
494
|
+
if (!before) return null;
|
|
495
|
+
if (before.isText) return before.text ? before.text.slice(-1) : null;
|
|
496
|
+
if (before.type.name === "hardBreak") return null;
|
|
497
|
+
return INLINE_OBJECT;
|
|
498
|
+
}
|
|
499
|
+
function charAfterInTiptap($pos) {
|
|
500
|
+
if ($pos.parentOffset === $pos.parent.content.size) return null;
|
|
501
|
+
const after = $pos.nodeAfter;
|
|
502
|
+
if (!after) return null;
|
|
503
|
+
if (after.isText) return after.text ? after.text.charAt(0) : null;
|
|
504
|
+
if (after.type.name === "hardBreak") return null;
|
|
505
|
+
return INLINE_OBJECT;
|
|
506
|
+
}
|
|
507
|
+
function insertDictationIntoTiptap(editor, transcript) {
|
|
508
|
+
if (editor.isDestroyed || !editor.isEditable) return false;
|
|
509
|
+
const { state } = editor;
|
|
510
|
+
let pos = state.selection.to;
|
|
511
|
+
let $pos = state.doc.resolve(pos);
|
|
512
|
+
if (!$pos.parent.inlineContent) {
|
|
513
|
+
const near = Selection.near($pos, 1);
|
|
514
|
+
if (!(near instanceof TextSelection)) return false;
|
|
515
|
+
pos = near.head;
|
|
516
|
+
$pos = near.$head;
|
|
517
|
+
}
|
|
518
|
+
const insertion = composeDictatedInsertion(
|
|
519
|
+
charBeforeInTiptap($pos),
|
|
520
|
+
transcript,
|
|
521
|
+
charAfterInTiptap($pos)
|
|
522
|
+
);
|
|
523
|
+
if (insertion === null) return false;
|
|
524
|
+
const alreadyFocused = editor.view.hasFocus();
|
|
525
|
+
const chain = alreadyFocused || focusIsInOtherEditable() ? editor.chain() : editor.chain().focus();
|
|
526
|
+
const inserted = chain.command(({ tr }) => {
|
|
527
|
+
closeHistory(tr);
|
|
528
|
+
tr.insertText(insertion.text, pos, pos);
|
|
529
|
+
tr.setSelection(TextSelection.create(tr.doc, pos + insertion.caret));
|
|
530
|
+
tr.scrollIntoView();
|
|
531
|
+
return true;
|
|
532
|
+
}).run();
|
|
533
|
+
if (inserted && !editor.isDestroyed) {
|
|
534
|
+
editor.view.dispatch(closeHistory(editor.state.tr).setMeta("addToHistory", false));
|
|
535
|
+
}
|
|
536
|
+
return inserted;
|
|
537
|
+
}
|
|
538
|
+
function insertDictationIntoMonaco(editor, transcript) {
|
|
539
|
+
const model = editor.getModel();
|
|
540
|
+
if (!model) return false;
|
|
541
|
+
const selection = editor.getSelection();
|
|
542
|
+
const caret = selection ? { lineNumber: selection.endLineNumber, column: selection.endColumn } : editor.getPosition();
|
|
543
|
+
if (!caret) return false;
|
|
544
|
+
const line = model.getLineContent(caret.lineNumber);
|
|
545
|
+
const charBefore = caret.column > 1 ? line.charAt(caret.column - 2) : null;
|
|
546
|
+
const charAfter = caret.column <= line.length ? line.charAt(caret.column - 1) : null;
|
|
547
|
+
const insertion = composeDictatedInsertion(charBefore, transcript, charAfter);
|
|
548
|
+
if (insertion === null) return false;
|
|
549
|
+
const range = {
|
|
550
|
+
startLineNumber: caret.lineNumber,
|
|
551
|
+
startColumn: caret.column,
|
|
552
|
+
endLineNumber: caret.lineNumber,
|
|
553
|
+
endColumn: caret.column
|
|
554
|
+
};
|
|
555
|
+
editor.pushUndoStop();
|
|
556
|
+
const applied = editor.executeEdits("squisq-dictation", [
|
|
557
|
+
{ range, text: insertion.text, forceMoveMarkers: true }
|
|
558
|
+
]);
|
|
559
|
+
editor.pushUndoStop();
|
|
560
|
+
if (applied === false) return false;
|
|
561
|
+
const end = { lineNumber: caret.lineNumber, column: caret.column + insertion.caret };
|
|
562
|
+
editor.setPosition(end);
|
|
563
|
+
editor.revealPositionInCenterIfOutsideViewport(end);
|
|
564
|
+
if (!editor.hasTextFocus() && !focusIsInOtherEditable()) editor.focus();
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/speech/DictationExtension.ts
|
|
569
|
+
import { Extension } from "@tiptap/core";
|
|
570
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
571
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
572
|
+
var DICTATION_KEY = new PluginKey("squisq-dictation-interim");
|
|
573
|
+
var DICTATION_INTERIM_CLASS = "squisq-dictation-interim";
|
|
574
|
+
function interimElement(doc, label) {
|
|
575
|
+
const span = doc.createElement("span");
|
|
576
|
+
span.className = DICTATION_INTERIM_CLASS;
|
|
577
|
+
span.setAttribute("contenteditable", "false");
|
|
578
|
+
span.setAttribute("aria-hidden", "true");
|
|
579
|
+
span.dataset.squisqDictation = "interim";
|
|
580
|
+
span.textContent = label;
|
|
581
|
+
return span;
|
|
582
|
+
}
|
|
583
|
+
var DictationExtension = Extension.create({
|
|
584
|
+
name: "squisqDictation",
|
|
585
|
+
addProseMirrorPlugins() {
|
|
586
|
+
return [
|
|
587
|
+
new Plugin({
|
|
588
|
+
key: DICTATION_KEY,
|
|
589
|
+
state: {
|
|
590
|
+
init: () => ({ label: null }),
|
|
591
|
+
apply: (transaction, previous) => transaction.getMeta(DICTATION_KEY) ?? previous
|
|
592
|
+
},
|
|
593
|
+
props: {
|
|
594
|
+
decorations: (state) => {
|
|
595
|
+
const label = DICTATION_KEY.getState(state)?.label;
|
|
596
|
+
if (!label) return null;
|
|
597
|
+
return DecorationSet.create(state.doc, [
|
|
598
|
+
Decoration.widget(
|
|
599
|
+
state.selection.to,
|
|
600
|
+
(view) => interimElement(view.dom.ownerDocument, label),
|
|
601
|
+
{
|
|
602
|
+
// Same key ⇒ ProseMirror reuses the DOM node as the caret moves.
|
|
603
|
+
key: `squisq-dictation:${label}`,
|
|
604
|
+
// Drawn after the caret, so typed or dictated text lands before it.
|
|
605
|
+
side: 1,
|
|
606
|
+
ignoreSelection: true
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
]);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
function setDictationInterim(editor, label) {
|
|
617
|
+
if (editor.isDestroyed || !editor.state) return;
|
|
618
|
+
const current = DICTATION_KEY.getState(editor.state);
|
|
619
|
+
if (!current || current.label === label) return;
|
|
620
|
+
editor.view.dispatch(
|
|
621
|
+
editor.state.tr.setMeta(DICTATION_KEY, { label }).setMeta("addToHistory", false)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/speech/monacoDictationInterim.ts
|
|
626
|
+
function createMonacoDictationInterim(editor) {
|
|
627
|
+
const collection = editor.createDecorationsCollection();
|
|
628
|
+
let label = null;
|
|
629
|
+
let cursorListener = null;
|
|
630
|
+
const render = () => {
|
|
631
|
+
const selection = editor.getSelection();
|
|
632
|
+
if (!label || !selection) {
|
|
633
|
+
collection.clear();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const lineNumber = selection.endLineNumber;
|
|
637
|
+
const column = selection.endColumn;
|
|
638
|
+
collection.set([
|
|
639
|
+
{
|
|
640
|
+
range: {
|
|
641
|
+
startLineNumber: lineNumber,
|
|
642
|
+
startColumn: column,
|
|
643
|
+
endLineNumber: lineNumber,
|
|
644
|
+
endColumn: column
|
|
645
|
+
},
|
|
646
|
+
options: {
|
|
647
|
+
showIfCollapsed: true,
|
|
648
|
+
after: {
|
|
649
|
+
content: label,
|
|
650
|
+
inlineClassName: `${DICTATION_INTERIM_CLASS} ${DICTATION_INTERIM_CLASS}--source`
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
]);
|
|
655
|
+
};
|
|
656
|
+
const safely = (action) => {
|
|
657
|
+
try {
|
|
658
|
+
action();
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
update(next) {
|
|
664
|
+
label = next;
|
|
665
|
+
safely(() => {
|
|
666
|
+
if (label && !cursorListener) cursorListener = editor.onDidChangeCursorSelection(render);
|
|
667
|
+
if (!label && cursorListener) {
|
|
668
|
+
cursorListener.dispose();
|
|
669
|
+
cursorListener = null;
|
|
670
|
+
}
|
|
671
|
+
render();
|
|
672
|
+
});
|
|
673
|
+
},
|
|
674
|
+
dispose() {
|
|
675
|
+
label = null;
|
|
676
|
+
safely(() => {
|
|
677
|
+
cursorListener?.dispose();
|
|
678
|
+
cursorListener = null;
|
|
679
|
+
collection.clear();
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/speech/dictationErrors.ts
|
|
686
|
+
function humanizeDictationError(caught) {
|
|
687
|
+
const error = caught instanceof Error ? caught : new Error(String(caught));
|
|
688
|
+
const name = error.name;
|
|
689
|
+
const message = error.message || String(caught);
|
|
690
|
+
if (name === "NotAllowedError" || name === "SecurityError" || /permission|not allowed|denied/i.test(message)) {
|
|
691
|
+
return "Microphone access was not granted. Allow it in your system settings, then try again.";
|
|
692
|
+
}
|
|
693
|
+
if (name === "OverconstrainedError" || /constraint|selected microphone/i.test(message)) {
|
|
694
|
+
return "The selected microphone is unavailable. Choose another microphone, then try again.";
|
|
695
|
+
}
|
|
696
|
+
if (name === "NotFoundError" || /requested device not found|no microphone/i.test(message)) {
|
|
697
|
+
return "No microphone is available.";
|
|
698
|
+
}
|
|
699
|
+
if (name === "NotReadableError" || /could not start audio source/i.test(message)) {
|
|
700
|
+
return "The microphone is in use by another application.";
|
|
701
|
+
}
|
|
702
|
+
if (/getUserMedia is not available|MediaRecorder is not defined/i.test(message)) {
|
|
703
|
+
return "Voice input is unavailable in this browser.";
|
|
704
|
+
}
|
|
705
|
+
return `Dictation stopped: ${message}`;
|
|
706
|
+
}
|
|
707
|
+
var NO_SPEECH_DETECTED_MESSAGE = "No speech was detected, so dictation stopped. Check that the right microphone is selected.";
|
|
708
|
+
function setupRequiredMessage(reason) {
|
|
709
|
+
return reason ? `Dictation needs a one-time setup first: ${reason}` : "Dictation needs a one-time setup before it can start.";
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/speech/useDictationController.ts
|
|
713
|
+
var DICTATION_METER_BARS = 12;
|
|
714
|
+
var EMPTY_LEVELS = Object.freeze(
|
|
715
|
+
Array.from({ length: DICTATION_METER_BARS }, () => 0)
|
|
716
|
+
);
|
|
717
|
+
function normalizeMeterLevel(level) {
|
|
718
|
+
return Math.min(1, Math.max(0, level * 14));
|
|
719
|
+
}
|
|
720
|
+
function createLevelStore() {
|
|
721
|
+
let levels = EMPTY_LEVELS;
|
|
722
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
723
|
+
const notify = () => {
|
|
724
|
+
for (const listener of listeners) listener();
|
|
725
|
+
};
|
|
726
|
+
return {
|
|
727
|
+
subscribe(listener) {
|
|
728
|
+
listeners.add(listener);
|
|
729
|
+
return () => {
|
|
730
|
+
listeners.delete(listener);
|
|
731
|
+
};
|
|
732
|
+
},
|
|
733
|
+
getSnapshot: () => levels,
|
|
734
|
+
push(level) {
|
|
735
|
+
levels = [...levels.slice(1), normalizeMeterLevel(level)];
|
|
736
|
+
notify();
|
|
737
|
+
},
|
|
738
|
+
reset() {
|
|
739
|
+
if (levels === EMPTY_LEVELS) return;
|
|
740
|
+
levels = EMPTY_LEVELS;
|
|
741
|
+
notify();
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
var DictationStateContext = createContext(null);
|
|
746
|
+
function useDictationState() {
|
|
747
|
+
return useContext(DictationStateContext);
|
|
748
|
+
}
|
|
749
|
+
function dictationInterimText(status, pendingTakes) {
|
|
750
|
+
switch (status) {
|
|
751
|
+
case "preparing":
|
|
752
|
+
return "Starting\u2026";
|
|
753
|
+
case "listening":
|
|
754
|
+
return pendingTakes > 0 ? "Transcribing\u2026" : "Listening\u2026";
|
|
755
|
+
case "transcribing":
|
|
756
|
+
return "Transcribing\u2026";
|
|
757
|
+
default:
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function isTextView(view) {
|
|
762
|
+
return view === "wysiwyg" || view === "raw";
|
|
763
|
+
}
|
|
764
|
+
function useDictationController({
|
|
765
|
+
capability,
|
|
766
|
+
activeView,
|
|
767
|
+
editorMode,
|
|
768
|
+
tiptapEditor,
|
|
769
|
+
monacoEditor
|
|
770
|
+
}) {
|
|
771
|
+
const [provider, setProvider] = useState2(null);
|
|
772
|
+
const [readiness, setReadiness] = useState2(null);
|
|
773
|
+
const [status, setStatus] = useState2("idle");
|
|
774
|
+
const [pendingTakes, setPendingTakes] = useState2(0);
|
|
775
|
+
const [error, setError] = useState2(null);
|
|
776
|
+
const levelsRef = useRef(null);
|
|
777
|
+
if (!levelsRef.current) levelsRef.current = createLevelStore();
|
|
778
|
+
const levels = levelsRef.current;
|
|
779
|
+
const providerRef = useRef(provider);
|
|
780
|
+
providerRef.current = provider;
|
|
781
|
+
const readinessRef = useRef(readiness);
|
|
782
|
+
readinessRef.current = readiness;
|
|
783
|
+
const activeViewRef = useRef(activeView);
|
|
784
|
+
activeViewRef.current = activeView;
|
|
785
|
+
const tiptapRef = useRef(tiptapEditor);
|
|
786
|
+
tiptapRef.current = tiptapEditor;
|
|
787
|
+
const monacoRef = useRef(monacoEditor);
|
|
788
|
+
monacoRef.current = monacoEditor;
|
|
789
|
+
const sessionRef = useRef(null);
|
|
790
|
+
const heldPhrasesRef = useRef([]);
|
|
791
|
+
const textViewActive = editorMode === "markdown" && isTextView(activeView);
|
|
792
|
+
const available = provider !== null && readiness !== null && readiness.state !== "unavailable" && textViewActive;
|
|
793
|
+
const availableRef = useRef(available);
|
|
794
|
+
availableRef.current = available;
|
|
795
|
+
const restoreEditorFocus = useCallback2(() => {
|
|
796
|
+
if (typeof document === "undefined") return;
|
|
797
|
+
const active = document.activeElement;
|
|
798
|
+
const reclaimable = !active || active === document.body || active instanceof Element && active.closest(".squisq-dictation") !== null;
|
|
799
|
+
if (!reclaimable) return;
|
|
800
|
+
const view = activeViewRef.current;
|
|
801
|
+
const tiptap = tiptapRef.current;
|
|
802
|
+
if (view === "wysiwyg" && tiptap && !tiptap.isDestroyed) {
|
|
803
|
+
tiptap.commands.focus();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const monaco = monacoRef.current;
|
|
807
|
+
if (view === "raw" && monaco) monaco.focus();
|
|
808
|
+
}, []);
|
|
809
|
+
const endSession = useCallback2(
|
|
810
|
+
(session, options = {}) => {
|
|
811
|
+
if (sessionRef.current !== session) return;
|
|
812
|
+
sessionRef.current = null;
|
|
813
|
+
session.abort.abort();
|
|
814
|
+
levels.reset();
|
|
815
|
+
setPendingTakes(0);
|
|
816
|
+
setStatus("idle");
|
|
817
|
+
if (options.restoreFocus) restoreEditorFocus();
|
|
818
|
+
},
|
|
819
|
+
[levels, restoreEditorFocus]
|
|
820
|
+
);
|
|
821
|
+
const cancel = useCallback2(() => {
|
|
822
|
+
const session = sessionRef.current;
|
|
823
|
+
if (!session) return;
|
|
824
|
+
session.handle?.cancel();
|
|
825
|
+
endSession(session, { restoreFocus: true });
|
|
826
|
+
}, [endSession]);
|
|
827
|
+
const stop = useCallback2(async () => {
|
|
828
|
+
const session = sessionRef.current;
|
|
829
|
+
if (!session) return;
|
|
830
|
+
if (!session.handle) {
|
|
831
|
+
cancel();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (session.stopping) return;
|
|
835
|
+
session.stopping = true;
|
|
836
|
+
setStatus("transcribing");
|
|
837
|
+
try {
|
|
838
|
+
await session.handle.stop();
|
|
839
|
+
} finally {
|
|
840
|
+
endSession(session, { restoreFocus: true });
|
|
841
|
+
}
|
|
842
|
+
}, [cancel, endSession]);
|
|
843
|
+
const insertPhrase = useCallback2((text) => {
|
|
844
|
+
const view = activeViewRef.current;
|
|
845
|
+
const tiptap = tiptapRef.current;
|
|
846
|
+
if (view === "wysiwyg" && tiptap && !tiptap.isDestroyed) {
|
|
847
|
+
insertDictationIntoTiptap(tiptap, text);
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
const monaco = monacoRef.current;
|
|
851
|
+
if (view === "raw" && monaco) {
|
|
852
|
+
insertDictationIntoMonaco(monaco, text);
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
return false;
|
|
856
|
+
}, []);
|
|
857
|
+
const start = useCallback2(async () => {
|
|
858
|
+
const active = providerRef.current;
|
|
859
|
+
if (!active || sessionRef.current) return;
|
|
860
|
+
const session = { abort: new AbortController(), handle: null, stopping: false };
|
|
861
|
+
sessionRef.current = session;
|
|
862
|
+
const isCurrent = () => sessionRef.current === session;
|
|
863
|
+
setError(null);
|
|
864
|
+
setPendingTakes(0);
|
|
865
|
+
levels.reset();
|
|
866
|
+
setStatus("preparing");
|
|
867
|
+
try {
|
|
868
|
+
const fresh = await active.status();
|
|
869
|
+
if (!isCurrent()) return;
|
|
870
|
+
setReadiness(fresh);
|
|
871
|
+
if (fresh.state === "download-required" || fresh.state === "unavailable") {
|
|
872
|
+
endSession(session);
|
|
873
|
+
if (fresh.state === "download-required") {
|
|
874
|
+
if (active.requestSetup) active.requestSetup();
|
|
875
|
+
else setError(setupRequiredMessage(fresh.reason));
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const { startDictationSession } = await import("./dictationSession-PN3OT5LJ.js");
|
|
880
|
+
if (!isCurrent()) return;
|
|
881
|
+
const handle = await startDictationSession({
|
|
882
|
+
provider: active,
|
|
883
|
+
signal: session.abort.signal,
|
|
884
|
+
onTranscript: (text) => {
|
|
885
|
+
if (!isCurrent()) return;
|
|
886
|
+
if (!insertPhrase(text)) heldPhrasesRef.current.push(text);
|
|
887
|
+
},
|
|
888
|
+
onAudioLevel: (level) => {
|
|
889
|
+
if (isCurrent()) levels.push(level);
|
|
890
|
+
},
|
|
891
|
+
onPendingChange: (pending) => {
|
|
892
|
+
if (isCurrent()) setPendingTakes(pending);
|
|
893
|
+
},
|
|
894
|
+
onLongPause: (hadTranscript) => {
|
|
895
|
+
if (!isCurrent()) return;
|
|
896
|
+
if (!hadTranscript) setError(NO_SPEECH_DETECTED_MESSAGE);
|
|
897
|
+
void stop();
|
|
898
|
+
},
|
|
899
|
+
onError: (caught) => {
|
|
900
|
+
if (!isCurrent()) return;
|
|
901
|
+
session.handle?.cancel();
|
|
902
|
+
endSession(session, { restoreFocus: true });
|
|
903
|
+
setError(humanizeDictationError(caught));
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
if (!isCurrent()) {
|
|
907
|
+
handle.cancel();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
session.handle = handle;
|
|
911
|
+
setStatus("listening");
|
|
912
|
+
} catch (caught) {
|
|
913
|
+
if (!isCurrent()) return;
|
|
914
|
+
endSession(session, { restoreFocus: true });
|
|
915
|
+
setError(humanizeDictationError(caught));
|
|
916
|
+
}
|
|
917
|
+
}, [endSession, insertPhrase, levels, stop]);
|
|
918
|
+
const toggle = useCallback2(() => {
|
|
919
|
+
const session = sessionRef.current;
|
|
920
|
+
if (session) {
|
|
921
|
+
void stop();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const active = providerRef.current;
|
|
925
|
+
if (!active || !availableRef.current) return;
|
|
926
|
+
const known = readinessRef.current;
|
|
927
|
+
if (known?.state === "download-required") {
|
|
928
|
+
setError(null);
|
|
929
|
+
if (active.requestSetup) active.requestSetup();
|
|
930
|
+
else setError(setupRequiredMessage(known.reason));
|
|
931
|
+
active.status().then(
|
|
932
|
+
(next) => {
|
|
933
|
+
if (providerRef.current === active) setReadiness(next);
|
|
934
|
+
},
|
|
935
|
+
() => void 0
|
|
936
|
+
);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
void start();
|
|
940
|
+
}, [start, stop]);
|
|
941
|
+
const dismissError = useCallback2(() => setError(null), []);
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
if (!capability) {
|
|
944
|
+
setProvider(null);
|
|
945
|
+
setReadiness(null);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const owned = isSpeechInputProviderFactory(capability);
|
|
949
|
+
const instance = resolveSpeechInputProvider(capability);
|
|
950
|
+
let alive = true;
|
|
951
|
+
setProvider(instance);
|
|
952
|
+
setReadiness(null);
|
|
953
|
+
instance.status().then(
|
|
954
|
+
(next) => {
|
|
955
|
+
if (alive) setReadiness(next);
|
|
956
|
+
},
|
|
957
|
+
(caught) => {
|
|
958
|
+
if (!alive) return;
|
|
959
|
+
const reason = caught instanceof Error ? caught.message : String(caught);
|
|
960
|
+
setReadiness({ state: "unavailable", reason });
|
|
961
|
+
}
|
|
962
|
+
);
|
|
963
|
+
const unsubscribe = instance.onStatus?.((next) => {
|
|
964
|
+
if (alive) setReadiness(next);
|
|
965
|
+
});
|
|
966
|
+
return () => {
|
|
967
|
+
alive = false;
|
|
968
|
+
unsubscribe?.();
|
|
969
|
+
const session = sessionRef.current;
|
|
970
|
+
if (session) {
|
|
971
|
+
session.handle?.cancel();
|
|
972
|
+
sessionRef.current = null;
|
|
973
|
+
session.abort.abort();
|
|
974
|
+
}
|
|
975
|
+
setStatus("idle");
|
|
976
|
+
setPendingTakes(0);
|
|
977
|
+
if (owned) instance.dispose?.();
|
|
978
|
+
};
|
|
979
|
+
}, [capability]);
|
|
980
|
+
useEffect(
|
|
981
|
+
() => () => {
|
|
982
|
+
const session = sessionRef.current;
|
|
983
|
+
if (!session) return;
|
|
984
|
+
session.handle?.cancel();
|
|
985
|
+
sessionRef.current = null;
|
|
986
|
+
session.abort.abort();
|
|
987
|
+
},
|
|
988
|
+
[]
|
|
989
|
+
);
|
|
990
|
+
useEffect(() => {
|
|
991
|
+
if (!textViewActive && sessionRef.current) void stop();
|
|
992
|
+
}, [textViewActive, stop]);
|
|
993
|
+
const sessionActive = status !== "idle";
|
|
994
|
+
useEffect(() => {
|
|
995
|
+
if (!sessionActive || typeof window === "undefined") return;
|
|
996
|
+
const onKeyDown = (event) => {
|
|
997
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
998
|
+
void stop();
|
|
999
|
+
};
|
|
1000
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1001
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
1002
|
+
}, [sessionActive, stop]);
|
|
1003
|
+
useEffect(() => {
|
|
1004
|
+
if (heldPhrasesRef.current.length === 0) return;
|
|
1005
|
+
const held = heldPhrasesRef.current;
|
|
1006
|
+
heldPhrasesRef.current = [];
|
|
1007
|
+
for (let index = 0; index < held.length; index += 1) {
|
|
1008
|
+
if (!insertPhrase(held[index])) {
|
|
1009
|
+
heldPhrasesRef.current = held.slice(index);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}, [activeView, tiptapEditor, monacoEditor, insertPhrase]);
|
|
1014
|
+
const hasCapability = capability !== null;
|
|
1015
|
+
const interimLabel = hasCapability ? dictationInterimText(status, pendingTakes) : null;
|
|
1016
|
+
useEffect(() => {
|
|
1017
|
+
if (!tiptapEditor || !hasCapability) return;
|
|
1018
|
+
setDictationInterim(tiptapEditor, activeView === "wysiwyg" ? interimLabel : null);
|
|
1019
|
+
}, [tiptapEditor, hasCapability, activeView, interimLabel]);
|
|
1020
|
+
useEffect(() => {
|
|
1021
|
+
if (!tiptapEditor || !hasCapability) return;
|
|
1022
|
+
return () => setDictationInterim(tiptapEditor, null);
|
|
1023
|
+
}, [tiptapEditor, hasCapability]);
|
|
1024
|
+
const monacoInterimRef = useRef(null);
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
if (!monacoEditor) return;
|
|
1027
|
+
return () => {
|
|
1028
|
+
monacoInterimRef.current?.dispose();
|
|
1029
|
+
monacoInterimRef.current = null;
|
|
1030
|
+
};
|
|
1031
|
+
}, [monacoEditor]);
|
|
1032
|
+
useEffect(() => {
|
|
1033
|
+
if (!monacoEditor) return;
|
|
1034
|
+
const label = activeView === "raw" ? interimLabel : null;
|
|
1035
|
+
if (!label && !monacoInterimRef.current) return;
|
|
1036
|
+
if (!monacoInterimRef.current)
|
|
1037
|
+
monacoInterimRef.current = createMonacoDictationInterim(monacoEditor);
|
|
1038
|
+
monacoInterimRef.current.update(label);
|
|
1039
|
+
}, [monacoEditor, activeView, interimLabel]);
|
|
1040
|
+
const control = useMemo2(
|
|
1041
|
+
() => hasCapability ? { active: sessionActive, available, toggle } : null,
|
|
1042
|
+
[hasCapability, sessionActive, available, toggle]
|
|
1043
|
+
);
|
|
1044
|
+
const providerLabel = provider?.label ?? "";
|
|
1045
|
+
const state = useMemo2(
|
|
1046
|
+
() => hasCapability ? {
|
|
1047
|
+
status,
|
|
1048
|
+
readiness,
|
|
1049
|
+
available,
|
|
1050
|
+
providerLabel,
|
|
1051
|
+
pendingTakes,
|
|
1052
|
+
error,
|
|
1053
|
+
levels,
|
|
1054
|
+
toggle,
|
|
1055
|
+
dismissError
|
|
1056
|
+
} : null,
|
|
1057
|
+
[
|
|
1058
|
+
hasCapability,
|
|
1059
|
+
status,
|
|
1060
|
+
readiness,
|
|
1061
|
+
available,
|
|
1062
|
+
providerLabel,
|
|
1063
|
+
pendingTakes,
|
|
1064
|
+
error,
|
|
1065
|
+
levels,
|
|
1066
|
+
toggle,
|
|
1067
|
+
dismissError
|
|
1068
|
+
]
|
|
1069
|
+
);
|
|
1070
|
+
return { control, state };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/EditorContext.tsx
|
|
272
1074
|
import {
|
|
273
1075
|
DocumentVersionManager
|
|
274
1076
|
} from "@bendyline/squisq/versions";
|
|
1077
|
+
import { createDocument } from "@tiptap/core";
|
|
1078
|
+
import { closeHistory as closeHistory2 } from "@tiptap/pm/history";
|
|
1079
|
+
|
|
1080
|
+
// src/blockInsertion.ts
|
|
1081
|
+
import { isContainerTemplate as isContainerTemplate2 } from "@bendyline/squisq/doc";
|
|
1082
|
+
function tiptapOutline(doc) {
|
|
1083
|
+
const blocks = [];
|
|
1084
|
+
const positions = [];
|
|
1085
|
+
doc.forEach((node, offset) => {
|
|
1086
|
+
positions.push(offset);
|
|
1087
|
+
if (node.type.name === "heading") {
|
|
1088
|
+
const attrs = node.attrs;
|
|
1089
|
+
blocks.push({
|
|
1090
|
+
headingDepth: attrs.level ?? 1,
|
|
1091
|
+
container: isContainerTemplate2(attrs.dataTemplate ?? void 0)
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
blocks.push({ headingDepth: null, container: false });
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
return { blocks, positions };
|
|
1098
|
+
}
|
|
1099
|
+
function tiptapBlockInsertion(state, placement) {
|
|
1100
|
+
const { blocks, positions } = tiptapOutline(state.doc);
|
|
1101
|
+
const { $to } = state.selection;
|
|
1102
|
+
const cursorIndex = $to.depth > 0 ? $to.index(0) : $to.index(0) - 1;
|
|
1103
|
+
const { index, ...context } = resolveOutlineInsertion(blocks, cursorIndex, placement);
|
|
1104
|
+
const pos = index < positions.length ? positions[index] ?? 0 : state.doc.content.size;
|
|
1105
|
+
return { pos, ...context };
|
|
1106
|
+
}
|
|
275
1107
|
|
|
276
1108
|
// src/mediaEdit/mediaEditTargets.ts
|
|
277
1109
|
import { collectDocMediaClips } from "@bendyline/squisq/mediaEdit";
|
|
@@ -569,16 +1401,16 @@ function restoreWysiwygScrollRatio(editor, ratio) {
|
|
|
569
1401
|
|
|
570
1402
|
// src/EditorContext.tsx
|
|
571
1403
|
import { jsx } from "react/jsx-runtime";
|
|
572
|
-
var EditorContext =
|
|
1404
|
+
var EditorContext = createContext2(null);
|
|
573
1405
|
function useEditorContext() {
|
|
574
|
-
const ctx =
|
|
1406
|
+
const ctx = useContext2(EditorContext);
|
|
575
1407
|
if (!ctx) {
|
|
576
1408
|
throw new Error("useEditorContext must be used within an <EditorProvider>");
|
|
577
1409
|
}
|
|
578
1410
|
return ctx;
|
|
579
1411
|
}
|
|
580
1412
|
function useEditorContextOptional() {
|
|
581
|
-
return
|
|
1413
|
+
return useContext2(EditorContext);
|
|
582
1414
|
}
|
|
583
1415
|
var MEDIA_EDIT_GC_DELAY_MS = 5e3;
|
|
584
1416
|
var DEFAULT_PRUNE_POLICY = { type: "keep-last-n", n: 50 };
|
|
@@ -608,6 +1440,7 @@ function EditorProvider({
|
|
|
608
1440
|
proofingSpellingEnabled = true,
|
|
609
1441
|
proofingGrammarEnabled = true,
|
|
610
1442
|
proofingIgnoreStore = null,
|
|
1443
|
+
speechInput = null,
|
|
611
1444
|
documentLinkProvider = null,
|
|
612
1445
|
fenceRenderers = null,
|
|
613
1446
|
onCopyCode,
|
|
@@ -640,20 +1473,22 @@ function EditorProvider({
|
|
|
640
1473
|
);
|
|
641
1474
|
const effectiveThemeInheritance = viewPreferences?.themeInheritance ?? themeInheritance;
|
|
642
1475
|
const effectiveLayoutMode = viewPreferences?.layoutMode ?? layoutMode;
|
|
643
|
-
const { mode: fileMode, language: resolvedLanguage } =
|
|
1476
|
+
const { mode: fileMode, language: resolvedLanguage } = useMemo3(
|
|
644
1477
|
() => resolveFileKind(fileName, language),
|
|
645
1478
|
[fileName, language]
|
|
646
1479
|
);
|
|
647
1480
|
const editorMode = fileMode === "data" ? "code" : fileMode;
|
|
648
|
-
const [markdownSource, setMarkdownSourceRaw] =
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
const [
|
|
1481
|
+
const [markdownSource, setMarkdownSourceRaw] = useState3(initialMarkdown);
|
|
1482
|
+
const markdownSourceRef = useRef2(markdownSource);
|
|
1483
|
+
markdownSourceRef.current = markdownSource;
|
|
1484
|
+
const [markdownDoc, setMarkdownDocState] = useState3(null);
|
|
1485
|
+
const [doc, setDoc] = useState3(null);
|
|
1486
|
+
const [activeView, setActiveViewRaw] = useState3(
|
|
652
1487
|
editorMode === "markdown" ? initialView : "raw"
|
|
653
1488
|
);
|
|
654
|
-
const [uncontrolledFindMode, setUncontrolledFindMode] =
|
|
1489
|
+
const [uncontrolledFindMode, setUncontrolledFindMode] = useState3(false);
|
|
655
1490
|
const findMode = editorMode !== "image" && (controlledFindMode ?? uncontrolledFindMode);
|
|
656
|
-
const setFindMode =
|
|
1491
|
+
const setFindMode = useCallback3(
|
|
657
1492
|
(active) => {
|
|
658
1493
|
if (editorMode === "image") return;
|
|
659
1494
|
if (controlledFindMode === void 0) setUncontrolledFindMode(active);
|
|
@@ -661,12 +1496,12 @@ function EditorProvider({
|
|
|
661
1496
|
},
|
|
662
1497
|
[controlledFindMode, editorMode, onFindModeChange]
|
|
663
1498
|
);
|
|
664
|
-
const activeViewRef =
|
|
1499
|
+
const activeViewRef = useRef2(activeView);
|
|
665
1500
|
activeViewRef.current = activeView;
|
|
666
|
-
const tiptapEditorRef =
|
|
667
|
-
const monacoEditorRef =
|
|
668
|
-
const pendingEditorScrollRef =
|
|
669
|
-
const setActiveView =
|
|
1501
|
+
const tiptapEditorRef = useRef2(null);
|
|
1502
|
+
const monacoEditorRef = useRef2(null);
|
|
1503
|
+
const pendingEditorScrollRef = useRef2(null);
|
|
1504
|
+
const setActiveView = useCallback3(
|
|
670
1505
|
(view) => {
|
|
671
1506
|
if (editorMode === "code" && view !== "raw") return;
|
|
672
1507
|
if (editorMode === "image") return;
|
|
@@ -686,59 +1521,59 @@ function EditorProvider({
|
|
|
686
1521
|
},
|
|
687
1522
|
[editorMode]
|
|
688
1523
|
);
|
|
689
|
-
const [parseError, setParseError] =
|
|
690
|
-
const [isParsing, setIsParsing] =
|
|
691
|
-
const [colorScheme, setColorScheme] =
|
|
692
|
-
const [inlinePreviewVisible, setInlinePreviewVisibleRaw] =
|
|
693
|
-
|
|
1524
|
+
const [parseError, setParseError] = useState3(null);
|
|
1525
|
+
const [isParsing, setIsParsing] = useState3(false);
|
|
1526
|
+
const [colorScheme, setColorScheme] = useState3(initialColorScheme);
|
|
1527
|
+
const [inlinePreviewVisible, setInlinePreviewVisibleRaw] = useState3(effectiveInlinePreview);
|
|
1528
|
+
useEffect2(() => {
|
|
694
1529
|
if (viewPreferences?.inlinePreview === void 0) setInlinePreviewVisibleRaw(inlinePreview);
|
|
695
1530
|
}, [inlinePreview, viewPreferences?.inlinePreview]);
|
|
696
|
-
const [statusBarVisible, setStatusBarVisibleRaw] =
|
|
697
|
-
|
|
1531
|
+
const [statusBarVisible, setStatusBarVisibleRaw] = useState3(effectiveShowStatusBar);
|
|
1532
|
+
useEffect2(() => {
|
|
698
1533
|
if (viewPreferences?.showStatusBar === void 0) setStatusBarVisibleRaw(showStatusBar);
|
|
699
1534
|
}, [showStatusBar, viewPreferences?.showStatusBar]);
|
|
700
|
-
const [outlineVisible, setOutlineVisibleRaw] =
|
|
701
|
-
|
|
1535
|
+
const [outlineVisible, setOutlineVisibleRaw] = useState3(effectiveOutline);
|
|
1536
|
+
useEffect2(() => {
|
|
702
1537
|
if (viewPreferences?.outline === void 0) setOutlineVisibleRaw(outline);
|
|
703
1538
|
}, [outline, viewPreferences?.outline]);
|
|
704
|
-
const [blockTagVisibilityState, setBlockTagVisibilityRaw] =
|
|
1539
|
+
const [blockTagVisibilityState, setBlockTagVisibilityRaw] = useState3(
|
|
705
1540
|
effectiveBlockTagVisibility
|
|
706
1541
|
);
|
|
707
1542
|
const blockTagsVisible = blockTagVisibilityState !== "none";
|
|
708
|
-
|
|
1543
|
+
useEffect2(() => {
|
|
709
1544
|
if (viewPreferences?.blockTagVisibility === void 0 && viewPreferences?.blockTags === void 0) {
|
|
710
1545
|
setBlockTagVisibilityRaw(normalizeBlockTagVisibility(initialBlockTagVisibility ?? blockTags));
|
|
711
1546
|
}
|
|
712
1547
|
}, [blockTags, initialBlockTagVisibility, viewPreferences]);
|
|
713
|
-
const [themeInheritanceState, setThemeInheritanceRaw] =
|
|
714
|
-
|
|
1548
|
+
const [themeInheritanceState, setThemeInheritanceRaw] = useState3(effectiveThemeInheritance);
|
|
1549
|
+
useEffect2(() => {
|
|
715
1550
|
if (viewPreferences?.themeInheritance === void 0) {
|
|
716
1551
|
setThemeInheritanceRaw(themeInheritance);
|
|
717
1552
|
}
|
|
718
1553
|
}, [themeInheritance, viewPreferences?.themeInheritance]);
|
|
719
|
-
const [layoutModeState, setLayoutModeRaw] =
|
|
720
|
-
|
|
1554
|
+
const [layoutModeState, setLayoutModeRaw] = useState3(effectiveLayoutMode);
|
|
1555
|
+
useEffect2(() => {
|
|
721
1556
|
if (viewPreferences?.layoutMode === void 0) setLayoutModeRaw(layoutMode);
|
|
722
1557
|
}, [layoutMode, viewPreferences?.layoutMode]);
|
|
723
|
-
const [imageEditTarget, setImageEditTarget] =
|
|
724
|
-
const [mediaRevision, setMediaRevision] =
|
|
725
|
-
const openImageEdit =
|
|
1558
|
+
const [imageEditTarget, setImageEditTarget] = useState3(null);
|
|
1559
|
+
const [mediaRevision, setMediaRevision] = useState3(0);
|
|
1560
|
+
const openImageEdit = useCallback3((relativePath) => {
|
|
726
1561
|
setImageEditTarget(relativePath);
|
|
727
1562
|
}, []);
|
|
728
|
-
const closeImageEdit =
|
|
1563
|
+
const closeImageEdit = useCallback3(() => {
|
|
729
1564
|
setImageEditTarget(null);
|
|
730
1565
|
}, []);
|
|
731
|
-
const [mediaEditTarget, setMediaEditTarget] =
|
|
732
|
-
const openMediaEdit =
|
|
1566
|
+
const [mediaEditTarget, setMediaEditTarget] = useState3(null);
|
|
1567
|
+
const openMediaEdit = useCallback3((target) => {
|
|
733
1568
|
setMediaEditTarget(target);
|
|
734
1569
|
}, []);
|
|
735
|
-
const closeMediaEdit =
|
|
1570
|
+
const closeMediaEdit = useCallback3(() => {
|
|
736
1571
|
setMediaEditTarget(null);
|
|
737
1572
|
}, []);
|
|
738
|
-
const bumpMediaRevision =
|
|
1573
|
+
const bumpMediaRevision = useCallback3(() => {
|
|
739
1574
|
setMediaRevision((n) => n + 1);
|
|
740
1575
|
}, []);
|
|
741
|
-
|
|
1576
|
+
useEffect2(() => {
|
|
742
1577
|
if (!viewPreferences) return;
|
|
743
1578
|
if (viewPreferences.inlinePreview !== void 0) {
|
|
744
1579
|
setInlinePreviewVisibleRaw(viewPreferences.inlinePreview);
|
|
@@ -761,21 +1596,21 @@ function EditorProvider({
|
|
|
761
1596
|
setLayoutModeRaw(viewPreferences.layoutMode);
|
|
762
1597
|
}
|
|
763
1598
|
}, [viewPreferences]);
|
|
764
|
-
const onViewPreferencesChangeRef =
|
|
1599
|
+
const onViewPreferencesChangeRef = useRef2(onViewPreferencesChange);
|
|
765
1600
|
onViewPreferencesChangeRef.current = onViewPreferencesChange;
|
|
766
|
-
const inlinePreviewRef =
|
|
1601
|
+
const inlinePreviewRef = useRef2(inlinePreviewVisible);
|
|
767
1602
|
inlinePreviewRef.current = inlinePreviewVisible;
|
|
768
|
-
const statusBarRef =
|
|
1603
|
+
const statusBarRef = useRef2(statusBarVisible);
|
|
769
1604
|
statusBarRef.current = statusBarVisible;
|
|
770
|
-
const outlineRef =
|
|
1605
|
+
const outlineRef = useRef2(outlineVisible);
|
|
771
1606
|
outlineRef.current = outlineVisible;
|
|
772
|
-
const blockTagVisibilityRef =
|
|
1607
|
+
const blockTagVisibilityRef = useRef2(blockTagVisibilityState);
|
|
773
1608
|
blockTagVisibilityRef.current = blockTagVisibilityState;
|
|
774
|
-
const themeInheritanceRef =
|
|
1609
|
+
const themeInheritanceRef = useRef2(themeInheritanceState);
|
|
775
1610
|
themeInheritanceRef.current = themeInheritanceState;
|
|
776
|
-
const layoutModeRef =
|
|
1611
|
+
const layoutModeRef = useRef2(layoutModeState);
|
|
777
1612
|
layoutModeRef.current = layoutModeState;
|
|
778
|
-
const setInlinePreviewVisible =
|
|
1613
|
+
const setInlinePreviewVisible = useCallback3((visible) => {
|
|
779
1614
|
setInlinePreviewVisibleRaw(visible);
|
|
780
1615
|
onViewPreferencesChangeRef.current?.({
|
|
781
1616
|
inlinePreview: visible,
|
|
@@ -787,7 +1622,7 @@ function EditorProvider({
|
|
|
787
1622
|
layoutMode: layoutModeRef.current
|
|
788
1623
|
});
|
|
789
1624
|
}, []);
|
|
790
|
-
const setStatusBarVisible =
|
|
1625
|
+
const setStatusBarVisible = useCallback3((visible) => {
|
|
791
1626
|
setStatusBarVisibleRaw(visible);
|
|
792
1627
|
onViewPreferencesChangeRef.current?.({
|
|
793
1628
|
inlinePreview: inlinePreviewRef.current,
|
|
@@ -799,7 +1634,7 @@ function EditorProvider({
|
|
|
799
1634
|
layoutMode: layoutModeRef.current
|
|
800
1635
|
});
|
|
801
1636
|
}, []);
|
|
802
|
-
const setOutlineVisible =
|
|
1637
|
+
const setOutlineVisible = useCallback3((visible) => {
|
|
803
1638
|
setOutlineVisibleRaw(visible);
|
|
804
1639
|
onViewPreferencesChangeRef.current?.({
|
|
805
1640
|
inlinePreview: inlinePreviewRef.current,
|
|
@@ -811,7 +1646,7 @@ function EditorProvider({
|
|
|
811
1646
|
layoutMode: layoutModeRef.current
|
|
812
1647
|
});
|
|
813
1648
|
}, []);
|
|
814
|
-
const setBlockTagVisibility =
|
|
1649
|
+
const setBlockTagVisibility = useCallback3((visibility) => {
|
|
815
1650
|
setBlockTagVisibilityRaw(visibility);
|
|
816
1651
|
onViewPreferencesChangeRef.current?.({
|
|
817
1652
|
inlinePreview: inlinePreviewRef.current,
|
|
@@ -823,11 +1658,11 @@ function EditorProvider({
|
|
|
823
1658
|
layoutMode: layoutModeRef.current
|
|
824
1659
|
});
|
|
825
1660
|
}, []);
|
|
826
|
-
const setBlockTagsVisible =
|
|
1661
|
+
const setBlockTagsVisible = useCallback3(
|
|
827
1662
|
(visible) => setBlockTagVisibility(visible ? "always" : "none"),
|
|
828
1663
|
[setBlockTagVisibility]
|
|
829
1664
|
);
|
|
830
|
-
const setThemeInheritance =
|
|
1665
|
+
const setThemeInheritance = useCallback3((mode) => {
|
|
831
1666
|
setThemeInheritanceRaw(mode);
|
|
832
1667
|
onViewPreferencesChangeRef.current?.({
|
|
833
1668
|
inlinePreview: inlinePreviewRef.current,
|
|
@@ -839,7 +1674,7 @@ function EditorProvider({
|
|
|
839
1674
|
layoutMode: layoutModeRef.current
|
|
840
1675
|
});
|
|
841
1676
|
}, []);
|
|
842
|
-
const setLayoutMode =
|
|
1677
|
+
const setLayoutMode = useCallback3((mode) => {
|
|
843
1678
|
setLayoutModeRaw(mode);
|
|
844
1679
|
onViewPreferencesChangeRef.current?.({
|
|
845
1680
|
inlinePreview: inlinePreviewRef.current,
|
|
@@ -851,17 +1686,17 @@ function EditorProvider({
|
|
|
851
1686
|
layoutMode: mode
|
|
852
1687
|
});
|
|
853
1688
|
}, []);
|
|
854
|
-
const [tiptapEditor, setTiptapEditorRaw] =
|
|
855
|
-
const [monacoEditor, setMonacoEditorRaw] =
|
|
856
|
-
const setTiptapEditor =
|
|
1689
|
+
const [tiptapEditor, setTiptapEditorRaw] = useState3(null);
|
|
1690
|
+
const [monacoEditor, setMonacoEditorRaw] = useState3(null);
|
|
1691
|
+
const setTiptapEditor = useCallback3((editor) => {
|
|
857
1692
|
tiptapEditorRef.current = editor;
|
|
858
1693
|
setTiptapEditorRaw(editor);
|
|
859
1694
|
}, []);
|
|
860
|
-
const setMonacoEditor =
|
|
1695
|
+
const setMonacoEditor = useCallback3((editor) => {
|
|
861
1696
|
monacoEditorRef.current = editor;
|
|
862
1697
|
setMonacoEditorRaw(editor);
|
|
863
1698
|
}, []);
|
|
864
|
-
|
|
1699
|
+
useEffect2(() => {
|
|
865
1700
|
const pending = pendingEditorScrollRef.current;
|
|
866
1701
|
if (!pending || pending.view !== activeView) return;
|
|
867
1702
|
const restore = activeView === "raw" && monacoEditor ? () => restoreMonacoScrollRatio(monacoEditor, pending.ratio) : activeView === "wysiwyg" && tiptapEditor ? () => restoreWysiwygScrollRatio(tiptapEditor, pending.ratio) : null;
|
|
@@ -885,22 +1720,22 @@ function EditorProvider({
|
|
|
885
1720
|
clearTimeout(timeout);
|
|
886
1721
|
};
|
|
887
1722
|
}, [activeView, monacoEditor, tiptapEditor]);
|
|
888
|
-
const [activeSceneText, setActiveSceneText] =
|
|
889
|
-
const sceneTextChannel =
|
|
890
|
-
|
|
891
|
-
const articleIdRef =
|
|
1723
|
+
const [activeSceneText, setActiveSceneText] = useState3(null);
|
|
1724
|
+
const sceneTextChannel = useMemo3(createSceneTextChannel, []);
|
|
1725
|
+
useEffect2(() => sceneTextChannel.subscribe(setActiveSceneText), [sceneTextChannel]);
|
|
1726
|
+
const articleIdRef = useRef2(articleId);
|
|
892
1727
|
articleIdRef.current = articleId;
|
|
893
|
-
const fileNameRef =
|
|
1728
|
+
const fileNameRef = useRef2(fileName);
|
|
894
1729
|
fileNameRef.current = fileName;
|
|
895
|
-
|
|
1730
|
+
useEffect2(() => {
|
|
896
1731
|
setColorScheme(initialColorScheme);
|
|
897
1732
|
}, [initialColorScheme]);
|
|
898
|
-
const parseTimeoutRef =
|
|
899
|
-
const skipInitialDebouncedParseRef =
|
|
900
|
-
const doParse =
|
|
1733
|
+
const parseTimeoutRef = useRef2(null);
|
|
1734
|
+
const skipInitialDebouncedParseRef = useRef2(Boolean(initialMarkdown));
|
|
1735
|
+
const doParse = useCallback3((source) => {
|
|
901
1736
|
setIsParsing(true);
|
|
902
1737
|
try {
|
|
903
|
-
const parsed =
|
|
1738
|
+
const parsed = parseMarkdown3(source);
|
|
904
1739
|
setMarkdownDocState(parsed);
|
|
905
1740
|
setParseError(null);
|
|
906
1741
|
try {
|
|
@@ -921,7 +1756,7 @@ function EditorProvider({
|
|
|
921
1756
|
setIsParsing(false);
|
|
922
1757
|
}
|
|
923
1758
|
}, []);
|
|
924
|
-
|
|
1759
|
+
useEffect2(() => {
|
|
925
1760
|
if (editorMode !== "markdown") return;
|
|
926
1761
|
if (skipInitialDebouncedParseRef.current) {
|
|
927
1762
|
skipInitialDebouncedParseRef.current = false;
|
|
@@ -940,13 +1775,13 @@ function EditorProvider({
|
|
|
940
1775
|
}
|
|
941
1776
|
};
|
|
942
1777
|
}, [markdownSource, doParse, editorMode]);
|
|
943
|
-
|
|
1778
|
+
useEffect2(() => {
|
|
944
1779
|
if (editorMode !== "markdown") return;
|
|
945
1780
|
if (initialMarkdown) {
|
|
946
1781
|
doParse(initialMarkdown);
|
|
947
1782
|
}
|
|
948
1783
|
}, []);
|
|
949
|
-
const setMarkdownSource =
|
|
1784
|
+
const setMarkdownSource = useCallback3((source) => {
|
|
950
1785
|
setMarkdownSourceRaw(source);
|
|
951
1786
|
}, []);
|
|
952
1787
|
const blockNav = useBlockNavigator(markdownSource, setMarkdownSource, {
|
|
@@ -964,8 +1799,8 @@ function EditorProvider({
|
|
|
964
1799
|
nextBlock,
|
|
965
1800
|
addBlock
|
|
966
1801
|
} = blockNav;
|
|
967
|
-
const [selectionVersion, setSelectionVersion] =
|
|
968
|
-
|
|
1802
|
+
const [selectionVersion, setSelectionVersion] = useState3(0);
|
|
1803
|
+
useEffect2(() => {
|
|
969
1804
|
const bump = () => setSelectionVersion((version) => version + 1);
|
|
970
1805
|
const disposers = [];
|
|
971
1806
|
if (typeof tiptapEditor?.on === "function" && typeof tiptapEditor.off === "function") {
|
|
@@ -982,7 +1817,7 @@ function EditorProvider({
|
|
|
982
1817
|
for (const dispose of disposers) dispose();
|
|
983
1818
|
};
|
|
984
1819
|
}, [tiptapEditor, monacoEditor]);
|
|
985
|
-
const getSelection =
|
|
1820
|
+
const getSelection = useCallback3(() => {
|
|
986
1821
|
if (activeView === "wysiwyg" && tiptapEditor) {
|
|
987
1822
|
const { from, to, empty } = tiptapEditor.state.selection;
|
|
988
1823
|
return {
|
|
@@ -1000,7 +1835,7 @@ function EditorProvider({
|
|
|
1000
1835
|
}
|
|
1001
1836
|
return null;
|
|
1002
1837
|
}, [activeView, tiptapEditor, monacoEditor]);
|
|
1003
|
-
const replaceSelection =
|
|
1838
|
+
const replaceSelection = useCallback3(
|
|
1004
1839
|
(markdown) => {
|
|
1005
1840
|
if (activeView === "wysiwyg" && tiptapEditor) {
|
|
1006
1841
|
tiptapEditor.chain().focus().insertContent(markdownToTiptap(markdown)).run();
|
|
@@ -1019,7 +1854,7 @@ function EditorProvider({
|
|
|
1019
1854
|
},
|
|
1020
1855
|
[activeView, tiptapEditor, monacoEditor]
|
|
1021
1856
|
);
|
|
1022
|
-
const getBlockAtCursor =
|
|
1857
|
+
const getBlockAtCursor = useCallback3(() => {
|
|
1023
1858
|
if (activeView !== "raw" || !monacoEditor) return null;
|
|
1024
1859
|
const model = monacoEditor.getModel();
|
|
1025
1860
|
const position = monacoEditor.getPosition();
|
|
@@ -1028,7 +1863,7 @@ function EditorProvider({
|
|
|
1028
1863
|
if (slices.length === 0) return null;
|
|
1029
1864
|
return slices[sliceIndexAtOffset(slices, model.getOffsetAt(position))] ?? null;
|
|
1030
1865
|
}, [activeView, monacoEditor]);
|
|
1031
|
-
const insertAtCursor =
|
|
1866
|
+
const insertAtCursor = useCallback3(
|
|
1032
1867
|
(text) => {
|
|
1033
1868
|
if (activeView === "wysiwyg" && tiptapEditor) {
|
|
1034
1869
|
const html = markdownToTiptap(text);
|
|
@@ -1055,11 +1890,126 @@ function EditorProvider({
|
|
|
1055
1890
|
},
|
|
1056
1891
|
[activeView, tiptapEditor, monacoEditor]
|
|
1057
1892
|
);
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1893
|
+
const applySourceEdits = useCallback3(
|
|
1894
|
+
(edits, options) => {
|
|
1895
|
+
if (editorMode !== "markdown" || layoutModeState !== "document") return false;
|
|
1896
|
+
if (activeView === "raw" && monacoEditor) {
|
|
1897
|
+
const model = monacoEditor.getModel();
|
|
1898
|
+
if (!model) return false;
|
|
1899
|
+
const base = model.getValue();
|
|
1900
|
+
if (options?.baseSource !== void 0 && options.baseSource !== base) return false;
|
|
1901
|
+
const sorted = normalizeSourceEdits(base, edits);
|
|
1902
|
+
if (!sorted) return false;
|
|
1903
|
+
if (sorted.length === 0) return true;
|
|
1904
|
+
const ops = sorted.map((edit) => {
|
|
1905
|
+
const start = model.getPositionAt(edit.start);
|
|
1906
|
+
const end = model.getPositionAt(edit.end);
|
|
1907
|
+
return {
|
|
1908
|
+
range: {
|
|
1909
|
+
startLineNumber: start.lineNumber,
|
|
1910
|
+
startColumn: start.column,
|
|
1911
|
+
endLineNumber: end.lineNumber,
|
|
1912
|
+
endColumn: end.column
|
|
1913
|
+
},
|
|
1914
|
+
text: edit.text
|
|
1915
|
+
};
|
|
1916
|
+
});
|
|
1917
|
+
monacoEditor.pushUndoStop();
|
|
1918
|
+
monacoEditor.executeEdits("squisq-source-edits", ops);
|
|
1919
|
+
monacoEditor.pushUndoStop();
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
if (activeView === "wysiwyg" && tiptapEditor) {
|
|
1923
|
+
const base = markdownSourceRef.current;
|
|
1924
|
+
if (options?.baseSource !== void 0 && options.baseSource !== base) return false;
|
|
1925
|
+
const sorted = normalizeSourceEdits(base, edits);
|
|
1926
|
+
if (!sorted) return false;
|
|
1927
|
+
if (sorted.length === 0) return true;
|
|
1928
|
+
setMarkdownSourceRaw(applySourceEditsToText(base, sorted));
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
return false;
|
|
1932
|
+
},
|
|
1933
|
+
[activeView, editorMode, layoutModeState, monacoEditor, tiptapEditor]
|
|
1934
|
+
);
|
|
1935
|
+
const replaceAll = useCallback3(
|
|
1936
|
+
(text) => {
|
|
1937
|
+
const base = activeView === "raw" && monacoEditor ? monacoEditor.getModel()?.getValue() ?? null : markdownSourceRef.current;
|
|
1938
|
+
const edit = base === null ? null : minimalReplaceEdit(base, text);
|
|
1939
|
+
if (edit && applySourceEdits([edit])) return;
|
|
1940
|
+
setMarkdownSourceRaw(text);
|
|
1941
|
+
},
|
|
1942
|
+
[activeView, monacoEditor, applySourceEdits]
|
|
1943
|
+
);
|
|
1944
|
+
const insertBlockAfterCursor = useCallback3(
|
|
1945
|
+
(block, options) => {
|
|
1946
|
+
if (editorMode !== "markdown") return false;
|
|
1947
|
+
const placement = options?.placement ?? "afterBlock";
|
|
1948
|
+
const build = (context) => {
|
|
1949
|
+
const markdown = typeof block === "function" ? block(context) : block;
|
|
1950
|
+
return markdown && markdown.trim() ? markdown : null;
|
|
1951
|
+
};
|
|
1952
|
+
if (activeView === "wysiwyg" && tiptapEditor) {
|
|
1953
|
+
const { pos, ...context } = tiptapBlockInsertion(tiptapEditor.state, placement);
|
|
1954
|
+
const markdown = build(context);
|
|
1955
|
+
if (markdown === null) return false;
|
|
1956
|
+
let content;
|
|
1957
|
+
try {
|
|
1958
|
+
content = createDocument(markdownToTiptap(markdown), tiptapEditor.schema).content;
|
|
1959
|
+
} catch {
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
if (content.size === 0) return false;
|
|
1963
|
+
const inserted = tiptapEditor.chain().command(({ tr, dispatch }) => {
|
|
1964
|
+
if (dispatch) {
|
|
1965
|
+
closeHistory2(tr);
|
|
1966
|
+
tr.insert(pos, content);
|
|
1967
|
+
}
|
|
1968
|
+
return true;
|
|
1969
|
+
}).run();
|
|
1970
|
+
if (inserted) {
|
|
1971
|
+
const dom = tiptapEditor.view.nodeDOM(pos);
|
|
1972
|
+
if (dom instanceof HTMLElement && typeof dom.scrollIntoView === "function") {
|
|
1973
|
+
dom.scrollIntoView({ block: "nearest" });
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return inserted;
|
|
1977
|
+
}
|
|
1978
|
+
if (activeView === "raw" && monacoEditor) {
|
|
1979
|
+
const model = monacoEditor.getModel();
|
|
1980
|
+
const selection = monacoEditor.getSelection();
|
|
1981
|
+
if (!model || !selection) return false;
|
|
1982
|
+
const source = model.getValue();
|
|
1983
|
+
const cursorOffset = model.getOffsetAt(selection.getEndPosition());
|
|
1984
|
+
const { offset, ...context } = blockInsertionPoint(source, cursorOffset, placement);
|
|
1985
|
+
const markdown = build(context);
|
|
1986
|
+
if (markdown === null) return false;
|
|
1987
|
+
const edit = joinBlockAt(source, offset, markdown);
|
|
1988
|
+
const start = model.getPositionAt(edit.start);
|
|
1989
|
+
const end = model.getPositionAt(edit.end);
|
|
1990
|
+
monacoEditor.pushUndoStop();
|
|
1991
|
+
monacoEditor.executeEdits("squisq-insert-block", [
|
|
1992
|
+
{
|
|
1993
|
+
range: {
|
|
1994
|
+
startLineNumber: start.lineNumber,
|
|
1995
|
+
startColumn: start.column,
|
|
1996
|
+
endLineNumber: end.lineNumber,
|
|
1997
|
+
endColumn: end.column
|
|
1998
|
+
},
|
|
1999
|
+
text: edit.text
|
|
2000
|
+
}
|
|
2001
|
+
]);
|
|
2002
|
+
monacoEditor.pushUndoStop();
|
|
2003
|
+
const firstLine = model.getPositionAt(Math.min(edit.start + 1, model.getValueLength()));
|
|
2004
|
+
monacoEditor.revealLineInCenterIfOutsideViewport?.(firstLine.lineNumber);
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
2007
|
+
return false;
|
|
2008
|
+
},
|
|
2009
|
+
[activeView, editorMode, monacoEditor, tiptapEditor]
|
|
2010
|
+
);
|
|
2011
|
+
const versioningWarnedRef = useRef2(false);
|
|
2012
|
+
useEffect2(() => {
|
|
1063
2013
|
if (allowVersioning && !workspaceContainer && !versioningWarnedRef.current) {
|
|
1064
2014
|
console.warn(
|
|
1065
2015
|
"[squisq-editor] allowVersioning requires a `workspaceContainer` prop; versioning is disabled."
|
|
@@ -1067,15 +2017,15 @@ function EditorProvider({
|
|
|
1067
2017
|
versioningWarnedRef.current = true;
|
|
1068
2018
|
}
|
|
1069
2019
|
}, [allowVersioning, workspaceContainer]);
|
|
1070
|
-
const versioning =
|
|
2020
|
+
const versioning = useMemo3(() => {
|
|
1071
2021
|
if (!allowVersioning || !workspaceContainer) return null;
|
|
1072
2022
|
return new DocumentVersionManager(workspaceContainer, { basename: versionBasename });
|
|
1073
2023
|
}, [allowVersioning, workspaceContainer, versionBasename]);
|
|
1074
|
-
const onSaveVersionRef =
|
|
2024
|
+
const onSaveVersionRef = useRef2(onSaveVersion);
|
|
1075
2025
|
onSaveVersionRef.current = onSaveVersion;
|
|
1076
|
-
const prunePolicyRef =
|
|
2026
|
+
const prunePolicyRef = useRef2(versioningPrunePolicy);
|
|
1077
2027
|
prunePolicyRef.current = versioningPrunePolicy;
|
|
1078
|
-
const saveVersion =
|
|
2028
|
+
const saveVersion = useCallback3(
|
|
1079
2029
|
async (options) => {
|
|
1080
2030
|
if (!versioning) {
|
|
1081
2031
|
const skipped = { saved: false, version: null, reason: "no-document" };
|
|
@@ -1096,8 +2046,8 @@ function EditorProvider({
|
|
|
1096
2046
|
},
|
|
1097
2047
|
[versioning]
|
|
1098
2048
|
);
|
|
1099
|
-
const autoSaveSourceRef =
|
|
1100
|
-
|
|
2049
|
+
const autoSaveSourceRef = useRef2(null);
|
|
2050
|
+
useEffect2(() => {
|
|
1101
2051
|
if (!versioning) return;
|
|
1102
2052
|
if (versioningAutoSaveIdleMs <= 0) return;
|
|
1103
2053
|
const previous = autoSaveSourceRef.current;
|
|
@@ -1113,7 +2063,7 @@ function EditorProvider({
|
|
|
1113
2063
|
}, versioningAutoSaveIdleMs);
|
|
1114
2064
|
return () => clearTimeout(timer);
|
|
1115
2065
|
}, [markdownSource, versioning, versioningAutoSaveIdleMs, saveVersion]);
|
|
1116
|
-
const setMarkdownDoc =
|
|
2066
|
+
const setMarkdownDoc = useCallback3((newDoc) => {
|
|
1117
2067
|
setMarkdownDocState(newDoc);
|
|
1118
2068
|
try {
|
|
1119
2069
|
const newSource = stringifyMarkdown(newDoc);
|
|
@@ -1133,12 +2083,12 @@ function EditorProvider({
|
|
|
1133
2083
|
setParseError(err instanceof Error ? err.message : "Stringify error");
|
|
1134
2084
|
}
|
|
1135
2085
|
}, []);
|
|
1136
|
-
|
|
2086
|
+
useEffect2(() => {
|
|
1137
2087
|
if (mediaEditRenders && doc) mediaEditRenders.sync(doc, embeddedMediaClips(doc));
|
|
1138
2088
|
}, [mediaEditRenders, doc]);
|
|
1139
|
-
const latestDocRef =
|
|
2089
|
+
const latestDocRef = useRef2(doc);
|
|
1140
2090
|
latestDocRef.current = doc;
|
|
1141
|
-
|
|
2091
|
+
useEffect2(() => {
|
|
1142
2092
|
if (!mediaEditRenders) return;
|
|
1143
2093
|
const timer = setTimeout(() => {
|
|
1144
2094
|
const current = latestDocRef.current;
|
|
@@ -1147,7 +2097,15 @@ function EditorProvider({
|
|
|
1147
2097
|
}, MEDIA_EDIT_GC_DELAY_MS);
|
|
1148
2098
|
return () => clearTimeout(timer);
|
|
1149
2099
|
}, [mediaEditRenders]);
|
|
1150
|
-
const
|
|
2100
|
+
const dictationController = useDictationController({
|
|
2101
|
+
capability: speechInput,
|
|
2102
|
+
activeView,
|
|
2103
|
+
editorMode,
|
|
2104
|
+
tiptapEditor,
|
|
2105
|
+
monacoEditor
|
|
2106
|
+
});
|
|
2107
|
+
const dictation = dictationController.control;
|
|
2108
|
+
const value = useMemo3(
|
|
1151
2109
|
() => ({
|
|
1152
2110
|
markdownSource,
|
|
1153
2111
|
markdownDoc,
|
|
@@ -1183,6 +2141,7 @@ function EditorProvider({
|
|
|
1183
2141
|
sceneTextChannel,
|
|
1184
2142
|
workspaceContainer,
|
|
1185
2143
|
versioning,
|
|
2144
|
+
versioningAutoSaves: versioningAutoSaveIdleMs > 0,
|
|
1186
2145
|
saveVersion,
|
|
1187
2146
|
mediaProvider,
|
|
1188
2147
|
mediaEditRenders,
|
|
@@ -1193,6 +2152,7 @@ function EditorProvider({
|
|
|
1193
2152
|
proofingSpellingEnabled,
|
|
1194
2153
|
proofingGrammarEnabled,
|
|
1195
2154
|
proofingIgnoreStore,
|
|
2155
|
+
dictation,
|
|
1196
2156
|
articleId,
|
|
1197
2157
|
documentLinkProvider,
|
|
1198
2158
|
fenceRenderers,
|
|
@@ -1226,6 +2186,8 @@ function EditorProvider({
|
|
|
1226
2186
|
getBlockAtCursor,
|
|
1227
2187
|
insertAtCursor,
|
|
1228
2188
|
replaceAll,
|
|
2189
|
+
applySourceEdits,
|
|
2190
|
+
insertBlockAfterCursor,
|
|
1229
2191
|
openImageEdit,
|
|
1230
2192
|
closeImageEdit,
|
|
1231
2193
|
openMediaEdit,
|
|
@@ -1261,6 +2223,7 @@ function EditorProvider({
|
|
|
1261
2223
|
sceneTextChannel,
|
|
1262
2224
|
workspaceContainer,
|
|
1263
2225
|
versioning,
|
|
2226
|
+
versioningAutoSaveIdleMs,
|
|
1264
2227
|
saveVersion,
|
|
1265
2228
|
mediaProvider,
|
|
1266
2229
|
mediaEditRenders,
|
|
@@ -1271,6 +2234,7 @@ function EditorProvider({
|
|
|
1271
2234
|
proofingSpellingEnabled,
|
|
1272
2235
|
proofingGrammarEnabled,
|
|
1273
2236
|
proofingIgnoreStore,
|
|
2237
|
+
dictation,
|
|
1274
2238
|
articleId,
|
|
1275
2239
|
documentLinkProvider,
|
|
1276
2240
|
fenceRenderers,
|
|
@@ -1304,6 +2268,8 @@ function EditorProvider({
|
|
|
1304
2268
|
getBlockAtCursor,
|
|
1305
2269
|
insertAtCursor,
|
|
1306
2270
|
replaceAll,
|
|
2271
|
+
applySourceEdits,
|
|
2272
|
+
insertBlockAfterCursor,
|
|
1307
2273
|
imageEditTarget,
|
|
1308
2274
|
mediaEditTarget,
|
|
1309
2275
|
mediaRevision,
|
|
@@ -1317,7 +2283,7 @@ function EditorProvider({
|
|
|
1317
2283
|
bumpMediaRevision
|
|
1318
2284
|
]
|
|
1319
2285
|
);
|
|
1320
|
-
return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children });
|
|
2286
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children: /* @__PURE__ */ jsx(DictationStateContext.Provider, { value: dictationController.state, children }) });
|
|
1321
2287
|
}
|
|
1322
2288
|
|
|
1323
2289
|
// src/review/decorations.ts
|
|
@@ -1395,17 +2361,17 @@ function suggestionLabel(kind) {
|
|
|
1395
2361
|
}
|
|
1396
2362
|
|
|
1397
2363
|
// src/review/ReviewExtension.ts
|
|
1398
|
-
import { Extension } from "@tiptap/core";
|
|
1399
|
-
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
1400
|
-
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
1401
|
-
var REVIEW_KEY = new
|
|
2364
|
+
import { Extension as Extension2 } from "@tiptap/core";
|
|
2365
|
+
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
|
|
2366
|
+
import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
|
|
2367
|
+
var REVIEW_KEY = new PluginKey2("squisq-review");
|
|
1402
2368
|
function buildSet(doc, specs) {
|
|
1403
2369
|
const max = doc.content.size;
|
|
1404
2370
|
const decorations = [];
|
|
1405
2371
|
for (const spec of specs) {
|
|
1406
2372
|
if (spec.from >= spec.to || spec.to > max) continue;
|
|
1407
2373
|
decorations.push(
|
|
1408
|
-
|
|
2374
|
+
Decoration2.inline(
|
|
1409
2375
|
spec.from,
|
|
1410
2376
|
spec.to,
|
|
1411
2377
|
{
|
|
@@ -1417,16 +2383,16 @@ function buildSet(doc, specs) {
|
|
|
1417
2383
|
)
|
|
1418
2384
|
);
|
|
1419
2385
|
}
|
|
1420
|
-
return
|
|
2386
|
+
return DecorationSet2.create(doc, decorations);
|
|
1421
2387
|
}
|
|
1422
|
-
var ReviewExtension =
|
|
2388
|
+
var ReviewExtension = Extension2.create({
|
|
1423
2389
|
name: "squisqReview",
|
|
1424
2390
|
addProseMirrorPlugins() {
|
|
1425
2391
|
return [
|
|
1426
|
-
new
|
|
2392
|
+
new Plugin2({
|
|
1427
2393
|
key: REVIEW_KEY,
|
|
1428
2394
|
state: {
|
|
1429
|
-
init: () => ({ decorations:
|
|
2395
|
+
init: () => ({ decorations: DecorationSet2.empty }),
|
|
1430
2396
|
apply: (transaction, previous) => {
|
|
1431
2397
|
const meta = transaction.getMeta(REVIEW_KEY);
|
|
1432
2398
|
if (meta) return { decorations: buildSet(transaction.doc, meta.specs) };
|
|
@@ -1685,7 +2651,7 @@ function messageOf(error) {
|
|
|
1685
2651
|
}
|
|
1686
2652
|
|
|
1687
2653
|
// src/review/useReview.ts
|
|
1688
|
-
import { useCallback as
|
|
2654
|
+
import { useCallback as useCallback4, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
|
|
1689
2655
|
var EMPTY = [];
|
|
1690
2656
|
var EMPTY_FINDINGS = [];
|
|
1691
2657
|
function useReview(capabilities = EMPTY) {
|
|
@@ -1698,23 +2664,23 @@ function useReview(capabilities = EMPTY) {
|
|
|
1698
2664
|
articleId,
|
|
1699
2665
|
goToBlockByLine
|
|
1700
2666
|
} = useEditorContext();
|
|
1701
|
-
const providers =
|
|
2667
|
+
const providers = useMemo4(
|
|
1702
2668
|
() => capabilities.map(resolveReviewProvider),
|
|
1703
2669
|
[capabilities]
|
|
1704
2670
|
);
|
|
1705
|
-
const [snapshot, setSnapshot] =
|
|
1706
|
-
const sourceRef =
|
|
1707
|
-
const revisionRef =
|
|
1708
|
-
const blocksRef =
|
|
1709
|
-
const runnerRef =
|
|
2671
|
+
const [snapshot, setSnapshot] = useState4(null);
|
|
2672
|
+
const sourceRef = useRef3(markdownSource);
|
|
2673
|
+
const revisionRef = useRef3(0);
|
|
2674
|
+
const blocksRef = useRef3([]);
|
|
2675
|
+
const runnerRef = useRef3(null);
|
|
1710
2676
|
sourceRef.current = markdownSource;
|
|
1711
|
-
const hueFor =
|
|
2677
|
+
const hueFor = useMemo4(() => {
|
|
1712
2678
|
const hues = new Map(
|
|
1713
2679
|
providers.map((provider) => [provider.id, provider.hue ?? DEFAULT_REVIEW_HUE])
|
|
1714
2680
|
);
|
|
1715
2681
|
return (source) => hues.get(source) ?? DEFAULT_REVIEW_HUE;
|
|
1716
2682
|
}, [providers]);
|
|
1717
|
-
|
|
2683
|
+
useEffect3(() => {
|
|
1718
2684
|
if (providers.length === 0) {
|
|
1719
2685
|
setSnapshot(null);
|
|
1720
2686
|
return;
|
|
@@ -1741,12 +2707,12 @@ function useReview(capabilities = EMPTY) {
|
|
|
1741
2707
|
runnerRef.current = null;
|
|
1742
2708
|
};
|
|
1743
2709
|
}, [providers, articleId, fileName]);
|
|
1744
|
-
|
|
2710
|
+
useEffect3(() => {
|
|
1745
2711
|
revisionRef.current += 1;
|
|
1746
2712
|
runnerRef.current?.schedule();
|
|
1747
2713
|
}, [markdownSource]);
|
|
1748
|
-
const findings =
|
|
1749
|
-
|
|
2714
|
+
const findings = useMemo4(() => snapshot?.findings ?? EMPTY_FINDINGS, [snapshot]);
|
|
2715
|
+
useEffect3(() => {
|
|
1750
2716
|
if (!tiptapEditor || activeView !== "wysiwyg") return;
|
|
1751
2717
|
if (findings.length === 0) {
|
|
1752
2718
|
clearReviewDecorations(tiptapEditor);
|
|
@@ -1757,8 +2723,8 @@ function useReview(capabilities = EMPTY) {
|
|
|
1757
2723
|
buildWriteViewDecorations(tiptapEditor.state.doc, findings, hueFor)
|
|
1758
2724
|
);
|
|
1759
2725
|
}, [tiptapEditor, activeView, findings, hueFor]);
|
|
1760
|
-
const collectionRef =
|
|
1761
|
-
|
|
2726
|
+
const collectionRef = useRef3(null);
|
|
2727
|
+
useEffect3(() => {
|
|
1762
2728
|
if (!monacoEditor || activeView !== "raw") return;
|
|
1763
2729
|
const model = monacoEditor.getModel();
|
|
1764
2730
|
if (!model) return;
|
|
@@ -1768,7 +2734,7 @@ function useReview(capabilities = EMPTY) {
|
|
|
1768
2734
|
collectionRef.current?.clear();
|
|
1769
2735
|
};
|
|
1770
2736
|
}, [monacoEditor, activeView, findings, hueFor]);
|
|
1771
|
-
const goToFinding =
|
|
2737
|
+
const goToFinding = useCallback4(
|
|
1772
2738
|
(findingId) => {
|
|
1773
2739
|
const finding = findings.find((candidate) => candidate.id === findingId);
|
|
1774
2740
|
if (!finding) return;
|
|
@@ -1779,22 +2745,22 @@ function useReview(capabilities = EMPTY) {
|
|
|
1779
2745
|
},
|
|
1780
2746
|
[findings, goToBlockByLine]
|
|
1781
2747
|
);
|
|
1782
|
-
const reviewNow =
|
|
1783
|
-
const dismissFinding =
|
|
2748
|
+
const reviewNow = useCallback4(() => runnerRef.current?.runNow(), []);
|
|
2749
|
+
const dismissFinding = useCallback4((findingId) => {
|
|
1784
2750
|
runnerRef.current?.removeFinding(findingId);
|
|
1785
2751
|
}, []);
|
|
1786
|
-
return
|
|
2752
|
+
return useMemo4(
|
|
1787
2753
|
() => snapshot ? { ...snapshot, reviewNow, dismissFinding, goToFinding } : null,
|
|
1788
2754
|
[snapshot, reviewNow, dismissFinding, goToFinding]
|
|
1789
2755
|
);
|
|
1790
2756
|
}
|
|
1791
2757
|
|
|
1792
2758
|
// src/review/ReviewContext.tsx
|
|
1793
|
-
import { createContext as
|
|
2759
|
+
import { createContext as createContext3, useContext as useContext3 } from "react";
|
|
1794
2760
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1795
|
-
var ReviewStateContext =
|
|
2761
|
+
var ReviewStateContext = createContext3(null);
|
|
1796
2762
|
function useReviewState() {
|
|
1797
|
-
return
|
|
2763
|
+
return useContext3(ReviewStateContext);
|
|
1798
2764
|
}
|
|
1799
2765
|
function ReviewRoot({
|
|
1800
2766
|
providers,
|
|
@@ -1888,12 +2854,23 @@ function Row({
|
|
|
1888
2854
|
}
|
|
1889
2855
|
|
|
1890
2856
|
export {
|
|
2857
|
+
DictationExtension,
|
|
2858
|
+
useDictationState,
|
|
1891
2859
|
stripFrontmatter,
|
|
1892
2860
|
getBlockSlices,
|
|
1893
2861
|
spliceBlock,
|
|
1894
2862
|
lineToOffset,
|
|
1895
2863
|
offsetToLine,
|
|
1896
2864
|
sliceIndexAtOffset,
|
|
2865
|
+
normalizeSourceEdits,
|
|
2866
|
+
applySourceEditsToText,
|
|
2867
|
+
minimalReplaceEdit,
|
|
2868
|
+
resolveOutlineInsertion,
|
|
2869
|
+
headingDepthForInsertion,
|
|
2870
|
+
sourceOutline,
|
|
2871
|
+
blockInsertionPoint,
|
|
2872
|
+
joinBlockAt,
|
|
2873
|
+
tiptapBlockInsertion,
|
|
1897
2874
|
detectLanguageFromFileName,
|
|
1898
2875
|
resolveFileKind,
|
|
1899
2876
|
useBlockNavigator,
|