@bendyline/squisq-editor-react 2.11.17 → 2.11.18
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-HTO5MTTJ.js → chunk-45NQUR62.js} +1104 -129
- package/dist/{chunk-XZVMKR5Q.js → chunk-CGENKHWY.js} +14 -93
- package/dist/chunk-DNEVUOBZ.js +14 -0
- package/dist/chunk-DNJ6HOES.js +93 -0
- package/dist/{chunk-US5DALTZ.js → chunk-GIXXD2O3.js} +2117 -1941
- package/dist/{chunk-QZWFSSDE.js → chunk-H7YTL6SL.js} +1 -1
- package/dist/chunk-LRZJD5YD.js +107 -0
- package/dist/{chunk-C6QVA6TO.js → chunk-U6NVT3BY.js} +17 -112
- package/dist/{chunk-EJKGXWNK.js → chunk-WKZLBGPX.js} +1 -1
- package/dist/dictationSession-PN3OT5LJ.js +521 -0
- package/dist/index.d.ts +27 -4
- package/dist/index.js +50 -18
- package/dist/recorder/index.js +17 -13
- package/dist/review/index.js +2 -1
- package/dist/shell/index.d.ts +2 -1
- package/dist/shell/index.js +8 -5
- package/dist/{shell-X0EoQbbN.d.ts → shell-Cb0P_ln8.d.ts} +170 -5
- package/dist/speech/index.d.ts +126 -0
- package/dist/speech/index.js +10 -0
- package/dist/styles/index.css +125 -0
- package/dist/teleprompter/index.d.ts +11 -3
- package/dist/teleprompter/index.js +3 -2
- package/package.json +10 -6
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSpeechInputProviderFactory,
|
|
3
|
+
resolveSpeechInputProvider
|
|
4
|
+
} from "./chunk-DNEVUOBZ.js";
|
|
1
5
|
import {
|
|
2
6
|
markdownToTiptap
|
|
3
7
|
} from "./chunk-DPAMJV6A.js";
|
|
@@ -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,
|
|
@@ -1193,6 +2151,7 @@ function EditorProvider({
|
|
|
1193
2151
|
proofingSpellingEnabled,
|
|
1194
2152
|
proofingGrammarEnabled,
|
|
1195
2153
|
proofingIgnoreStore,
|
|
2154
|
+
dictation,
|
|
1196
2155
|
articleId,
|
|
1197
2156
|
documentLinkProvider,
|
|
1198
2157
|
fenceRenderers,
|
|
@@ -1226,6 +2185,8 @@ function EditorProvider({
|
|
|
1226
2185
|
getBlockAtCursor,
|
|
1227
2186
|
insertAtCursor,
|
|
1228
2187
|
replaceAll,
|
|
2188
|
+
applySourceEdits,
|
|
2189
|
+
insertBlockAfterCursor,
|
|
1229
2190
|
openImageEdit,
|
|
1230
2191
|
closeImageEdit,
|
|
1231
2192
|
openMediaEdit,
|
|
@@ -1271,6 +2232,7 @@ function EditorProvider({
|
|
|
1271
2232
|
proofingSpellingEnabled,
|
|
1272
2233
|
proofingGrammarEnabled,
|
|
1273
2234
|
proofingIgnoreStore,
|
|
2235
|
+
dictation,
|
|
1274
2236
|
articleId,
|
|
1275
2237
|
documentLinkProvider,
|
|
1276
2238
|
fenceRenderers,
|
|
@@ -1304,6 +2266,8 @@ function EditorProvider({
|
|
|
1304
2266
|
getBlockAtCursor,
|
|
1305
2267
|
insertAtCursor,
|
|
1306
2268
|
replaceAll,
|
|
2269
|
+
applySourceEdits,
|
|
2270
|
+
insertBlockAfterCursor,
|
|
1307
2271
|
imageEditTarget,
|
|
1308
2272
|
mediaEditTarget,
|
|
1309
2273
|
mediaRevision,
|
|
@@ -1317,7 +2281,7 @@ function EditorProvider({
|
|
|
1317
2281
|
bumpMediaRevision
|
|
1318
2282
|
]
|
|
1319
2283
|
);
|
|
1320
|
-
return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children });
|
|
2284
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children: /* @__PURE__ */ jsx(DictationStateContext.Provider, { value: dictationController.state, children }) });
|
|
1321
2285
|
}
|
|
1322
2286
|
|
|
1323
2287
|
// src/review/decorations.ts
|
|
@@ -1395,17 +2359,17 @@ function suggestionLabel(kind) {
|
|
|
1395
2359
|
}
|
|
1396
2360
|
|
|
1397
2361
|
// 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
|
|
2362
|
+
import { Extension as Extension2 } from "@tiptap/core";
|
|
2363
|
+
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
|
|
2364
|
+
import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
|
|
2365
|
+
var REVIEW_KEY = new PluginKey2("squisq-review");
|
|
1402
2366
|
function buildSet(doc, specs) {
|
|
1403
2367
|
const max = doc.content.size;
|
|
1404
2368
|
const decorations = [];
|
|
1405
2369
|
for (const spec of specs) {
|
|
1406
2370
|
if (spec.from >= spec.to || spec.to > max) continue;
|
|
1407
2371
|
decorations.push(
|
|
1408
|
-
|
|
2372
|
+
Decoration2.inline(
|
|
1409
2373
|
spec.from,
|
|
1410
2374
|
spec.to,
|
|
1411
2375
|
{
|
|
@@ -1417,16 +2381,16 @@ function buildSet(doc, specs) {
|
|
|
1417
2381
|
)
|
|
1418
2382
|
);
|
|
1419
2383
|
}
|
|
1420
|
-
return
|
|
2384
|
+
return DecorationSet2.create(doc, decorations);
|
|
1421
2385
|
}
|
|
1422
|
-
var ReviewExtension =
|
|
2386
|
+
var ReviewExtension = Extension2.create({
|
|
1423
2387
|
name: "squisqReview",
|
|
1424
2388
|
addProseMirrorPlugins() {
|
|
1425
2389
|
return [
|
|
1426
|
-
new
|
|
2390
|
+
new Plugin2({
|
|
1427
2391
|
key: REVIEW_KEY,
|
|
1428
2392
|
state: {
|
|
1429
|
-
init: () => ({ decorations:
|
|
2393
|
+
init: () => ({ decorations: DecorationSet2.empty }),
|
|
1430
2394
|
apply: (transaction, previous) => {
|
|
1431
2395
|
const meta = transaction.getMeta(REVIEW_KEY);
|
|
1432
2396
|
if (meta) return { decorations: buildSet(transaction.doc, meta.specs) };
|
|
@@ -1685,7 +2649,7 @@ function messageOf(error) {
|
|
|
1685
2649
|
}
|
|
1686
2650
|
|
|
1687
2651
|
// src/review/useReview.ts
|
|
1688
|
-
import { useCallback as
|
|
2652
|
+
import { useCallback as useCallback4, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
|
|
1689
2653
|
var EMPTY = [];
|
|
1690
2654
|
var EMPTY_FINDINGS = [];
|
|
1691
2655
|
function useReview(capabilities = EMPTY) {
|
|
@@ -1698,23 +2662,23 @@ function useReview(capabilities = EMPTY) {
|
|
|
1698
2662
|
articleId,
|
|
1699
2663
|
goToBlockByLine
|
|
1700
2664
|
} = useEditorContext();
|
|
1701
|
-
const providers =
|
|
2665
|
+
const providers = useMemo4(
|
|
1702
2666
|
() => capabilities.map(resolveReviewProvider),
|
|
1703
2667
|
[capabilities]
|
|
1704
2668
|
);
|
|
1705
|
-
const [snapshot, setSnapshot] =
|
|
1706
|
-
const sourceRef =
|
|
1707
|
-
const revisionRef =
|
|
1708
|
-
const blocksRef =
|
|
1709
|
-
const runnerRef =
|
|
2669
|
+
const [snapshot, setSnapshot] = useState4(null);
|
|
2670
|
+
const sourceRef = useRef3(markdownSource);
|
|
2671
|
+
const revisionRef = useRef3(0);
|
|
2672
|
+
const blocksRef = useRef3([]);
|
|
2673
|
+
const runnerRef = useRef3(null);
|
|
1710
2674
|
sourceRef.current = markdownSource;
|
|
1711
|
-
const hueFor =
|
|
2675
|
+
const hueFor = useMemo4(() => {
|
|
1712
2676
|
const hues = new Map(
|
|
1713
2677
|
providers.map((provider) => [provider.id, provider.hue ?? DEFAULT_REVIEW_HUE])
|
|
1714
2678
|
);
|
|
1715
2679
|
return (source) => hues.get(source) ?? DEFAULT_REVIEW_HUE;
|
|
1716
2680
|
}, [providers]);
|
|
1717
|
-
|
|
2681
|
+
useEffect3(() => {
|
|
1718
2682
|
if (providers.length === 0) {
|
|
1719
2683
|
setSnapshot(null);
|
|
1720
2684
|
return;
|
|
@@ -1741,12 +2705,12 @@ function useReview(capabilities = EMPTY) {
|
|
|
1741
2705
|
runnerRef.current = null;
|
|
1742
2706
|
};
|
|
1743
2707
|
}, [providers, articleId, fileName]);
|
|
1744
|
-
|
|
2708
|
+
useEffect3(() => {
|
|
1745
2709
|
revisionRef.current += 1;
|
|
1746
2710
|
runnerRef.current?.schedule();
|
|
1747
2711
|
}, [markdownSource]);
|
|
1748
|
-
const findings =
|
|
1749
|
-
|
|
2712
|
+
const findings = useMemo4(() => snapshot?.findings ?? EMPTY_FINDINGS, [snapshot]);
|
|
2713
|
+
useEffect3(() => {
|
|
1750
2714
|
if (!tiptapEditor || activeView !== "wysiwyg") return;
|
|
1751
2715
|
if (findings.length === 0) {
|
|
1752
2716
|
clearReviewDecorations(tiptapEditor);
|
|
@@ -1757,8 +2721,8 @@ function useReview(capabilities = EMPTY) {
|
|
|
1757
2721
|
buildWriteViewDecorations(tiptapEditor.state.doc, findings, hueFor)
|
|
1758
2722
|
);
|
|
1759
2723
|
}, [tiptapEditor, activeView, findings, hueFor]);
|
|
1760
|
-
const collectionRef =
|
|
1761
|
-
|
|
2724
|
+
const collectionRef = useRef3(null);
|
|
2725
|
+
useEffect3(() => {
|
|
1762
2726
|
if (!monacoEditor || activeView !== "raw") return;
|
|
1763
2727
|
const model = monacoEditor.getModel();
|
|
1764
2728
|
if (!model) return;
|
|
@@ -1768,7 +2732,7 @@ function useReview(capabilities = EMPTY) {
|
|
|
1768
2732
|
collectionRef.current?.clear();
|
|
1769
2733
|
};
|
|
1770
2734
|
}, [monacoEditor, activeView, findings, hueFor]);
|
|
1771
|
-
const goToFinding =
|
|
2735
|
+
const goToFinding = useCallback4(
|
|
1772
2736
|
(findingId) => {
|
|
1773
2737
|
const finding = findings.find((candidate) => candidate.id === findingId);
|
|
1774
2738
|
if (!finding) return;
|
|
@@ -1779,22 +2743,22 @@ function useReview(capabilities = EMPTY) {
|
|
|
1779
2743
|
},
|
|
1780
2744
|
[findings, goToBlockByLine]
|
|
1781
2745
|
);
|
|
1782
|
-
const reviewNow =
|
|
1783
|
-
const dismissFinding =
|
|
2746
|
+
const reviewNow = useCallback4(() => runnerRef.current?.runNow(), []);
|
|
2747
|
+
const dismissFinding = useCallback4((findingId) => {
|
|
1784
2748
|
runnerRef.current?.removeFinding(findingId);
|
|
1785
2749
|
}, []);
|
|
1786
|
-
return
|
|
2750
|
+
return useMemo4(
|
|
1787
2751
|
() => snapshot ? { ...snapshot, reviewNow, dismissFinding, goToFinding } : null,
|
|
1788
2752
|
[snapshot, reviewNow, dismissFinding, goToFinding]
|
|
1789
2753
|
);
|
|
1790
2754
|
}
|
|
1791
2755
|
|
|
1792
2756
|
// src/review/ReviewContext.tsx
|
|
1793
|
-
import { createContext as
|
|
2757
|
+
import { createContext as createContext3, useContext as useContext3 } from "react";
|
|
1794
2758
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1795
|
-
var ReviewStateContext =
|
|
2759
|
+
var ReviewStateContext = createContext3(null);
|
|
1796
2760
|
function useReviewState() {
|
|
1797
|
-
return
|
|
2761
|
+
return useContext3(ReviewStateContext);
|
|
1798
2762
|
}
|
|
1799
2763
|
function ReviewRoot({
|
|
1800
2764
|
providers,
|
|
@@ -1888,12 +2852,23 @@ function Row({
|
|
|
1888
2852
|
}
|
|
1889
2853
|
|
|
1890
2854
|
export {
|
|
2855
|
+
DictationExtension,
|
|
2856
|
+
useDictationState,
|
|
1891
2857
|
stripFrontmatter,
|
|
1892
2858
|
getBlockSlices,
|
|
1893
2859
|
spliceBlock,
|
|
1894
2860
|
lineToOffset,
|
|
1895
2861
|
offsetToLine,
|
|
1896
2862
|
sliceIndexAtOffset,
|
|
2863
|
+
normalizeSourceEdits,
|
|
2864
|
+
applySourceEditsToText,
|
|
2865
|
+
minimalReplaceEdit,
|
|
2866
|
+
resolveOutlineInsertion,
|
|
2867
|
+
headingDepthForInsertion,
|
|
2868
|
+
sourceOutline,
|
|
2869
|
+
blockInsertionPoint,
|
|
2870
|
+
joinBlockAt,
|
|
2871
|
+
tiptapBlockInsertion,
|
|
1897
2872
|
detectLanguageFromFileName,
|
|
1898
2873
|
resolveFileKind,
|
|
1899
2874
|
useBlockNavigator,
|