@typbase/codemirror 0.2.1

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/index.mjs ADDED
@@ -0,0 +1,2110 @@
1
+ import { acceptCompletion, autocompletion, closeBracketsKeymap, completionKeymap, snippet, startCompletion } from "@codemirror/autocomplete";
2
+ import { indentService } from "@codemirror/language";
3
+ import { Decoration, EditorView, ViewPlugin, WidgetType, hoverTooltip, keymap, showTooltip } from "@codemirror/view";
4
+ import { ChangeSet, EditorSelection, EditorState, Facet, Prec, RangeSetBuilder, StateEffect, StateField, countColumn, findColumn } from "@codemirror/state";
5
+ import { copyLineDown, copyLineUp, indentLess, indentMore, insertBlankLine, moveLineDown, moveLineUp, redo, redoSelection, simplifySelection, standardKeymap, toggleBlockComment, toggleLineComment, undo, undoSelection } from "@codemirror/commands";
6
+ import { linter, nextDiagnostic, setDiagnostics } from "@codemirror/lint";
7
+ import { closeSearchPanel, findNext, findPrevious, gotoLine, openSearchPanel, selectNextOccurrence, selectSelectionMatches } from "@codemirror/search";
8
+ import { LRUCache } from "lru-cache";
9
+ //#region src/highlight.ts
10
+ const typstSyntaxHighlighting = (fileId, typstState) => StateField.define({
11
+ create(state) {
12
+ return buildDecorations$2(state, fileId, typstState);
13
+ },
14
+ update(decoration, transaction) {
15
+ if (transaction.docChanged) return buildDecorations$2(transaction.state, fileId, typstState);
16
+ return decoration;
17
+ },
18
+ provide(field) {
19
+ return EditorView.decorations.from(field);
20
+ }
21
+ });
22
+ function buildDecorations$2(state, fileId, typstState) {
23
+ const builder = new RangeSetBuilder();
24
+ let tokens;
25
+ try {
26
+ tokens = typstState.highlight(fileId, state.doc.toString());
27
+ } catch (error) {
28
+ console.error("[typst] highlight panicked:", error);
29
+ return Decoration.none;
30
+ }
31
+ for (const token of tokens) builder.add(token.range.start, token.range.end, Decoration.mark({ class: token.tag }));
32
+ return builder.finish();
33
+ }
34
+ const parseBackticks = (str, into) => {
35
+ const result = str.split("`").map((sub, i) => {
36
+ if (i % 2) {
37
+ const code = document.createElement("code");
38
+ code.textContent = sub;
39
+ return code;
40
+ }
41
+ return document.createTextNode(sub);
42
+ });
43
+ into.append(...result);
44
+ };
45
+ //#endregion
46
+ //#region src/hover.ts
47
+ const typstHoverTooltip = (fileId, typstState) => hoverTooltip((_, pos, side) => {
48
+ let tooltip;
49
+ try {
50
+ tooltip = typstState.hover(fileId, pos, side);
51
+ } catch (error) {
52
+ console.error("[typst] hover panicked:", error);
53
+ return null;
54
+ }
55
+ if (tooltip) return {
56
+ pos,
57
+ create() {
58
+ const div = document.createElement("div");
59
+ if (tooltip.startsWith("<code>")) div.innerHTML = "<pre>" + tooltip.replace(/span data-tag=(\w+)/g, (_match, tag) => `span class="${"typ-" + tag}"`) + "</pre>";
60
+ else parseBackticks(tooltip, div);
61
+ return { dom: div };
62
+ }
63
+ };
64
+ return null;
65
+ });
66
+ //#endregion
67
+ //#region src/commands.ts
68
+ function toggleStrong(view) {
69
+ toggleAroundSelection(view, ["*"], ["*"]);
70
+ return true;
71
+ }
72
+ function toggleEmph(view) {
73
+ toggleAroundSelection(view, ["_"], ["_"]);
74
+ return true;
75
+ }
76
+ function toggleUnderline(view) {
77
+ toggleAroundSelection(view, ["#underline["], ["]"]);
78
+ return true;
79
+ }
80
+ function toggleStrike(view) {
81
+ toggleAroundSelection(view, ["#strike["], ["]"]);
82
+ return true;
83
+ }
84
+ function toggleMath(view) {
85
+ toggleAroundSelection(view, ["$", "$ "], ["$", " $"]);
86
+ return true;
87
+ }
88
+ function insertTabLike(view) {
89
+ return view.state.selection.ranges.some((range) => !range.empty) ? indentMore(view) : (view.dispatch(view.state.changeByRange(({ from: pos }) => {
90
+ const line = view.state.doc.lineAt(pos);
91
+ const spaces = countColumn(line.text, 2, pos - line.from) % 2 == 0 ? " " : " ";
92
+ return {
93
+ changes: {
94
+ from: pos,
95
+ insert: spaces
96
+ },
97
+ range: EditorSelection.cursor(pos + spaces.length)
98
+ };
99
+ })), true);
100
+ }
101
+ function toggleCode(view) {
102
+ return toggleAroundSelection(view, ["`", {
103
+ regex: /(`{3,}([a-z0-9][a-z0-9-]*)?(\s*)*)/i,
104
+ replacement: "```\n"
105
+ }], ["`", {
106
+ regex: /((\s*)*`{3,})/,
107
+ replacement: "\n```"
108
+ }], true) === 0 && view.state.selection.ranges.every((range) => range.empty) && startCompletion(view), true;
109
+ }
110
+ function insertFontFamily(n, font) {
111
+ return wrapOrInsert(n, `#text(font: "${font}")[`, "]", `#set text(font: "${font}")\n`), true;
112
+ }
113
+ function cycleHeading(view) {
114
+ if (view.state.facet(EditorState.readOnly)) return true;
115
+ const { state } = view;
116
+ return view.dispatch(state.changeByRange((r) => {
117
+ let o = r.from;
118
+ let c = r.to;
119
+ const u = state.doc.lineAt(o), d = u.from;
120
+ if (r.empty) {
121
+ const f = state.doc.sliceString(u.from, o).match(/^\s*/);
122
+ if (f) o = u.from + f[0].length;
123
+ else {
124
+ o = u.from;
125
+ c = u.to;
126
+ }
127
+ }
128
+ const m = state.sliceDoc(d, c).match(/^(\s*)(=+)/);
129
+ if (m) {
130
+ if (!r.empty) {
131
+ const g = state.doc.sliceString(u.from, o).match(/^\s*/);
132
+ if (g) o = u.from + g[0].length;
133
+ else o = u.from;
134
+ }
135
+ const f = m[2].length;
136
+ return f < 6 ? {
137
+ range: EditorSelection.range(r.from + 1, r.to + 1),
138
+ changes: [{
139
+ from: o + f,
140
+ insert: "="
141
+ }]
142
+ } : {
143
+ range: EditorSelection.range(r.from - f - 1, r.to - f - 1),
144
+ changes: [{
145
+ from: o,
146
+ to: o + f + 1,
147
+ insert: ""
148
+ }]
149
+ };
150
+ }
151
+ const f = state.doc.lineAt(o);
152
+ let g;
153
+ const v = f.from === 0 ? void 0 : state.doc.lineAt(f.from - 1), y = v === void 0 || state.sliceDoc(v.from, v.to).trim().length === 0;
154
+ if (f.from === o || state.sliceDoc(f.from, o).trim().length === 0) g = y ? 0 : 1;
155
+ else {
156
+ g = 2;
157
+ if (!y) g++;
158
+ }
159
+ const E = f.text.match(/^\s*/)?.[0] ?? "", N = state.sliceDoc(o, c).split("\n"), _ = N.length > 1 && !N[0].startsWith("#[") && !N[N.length - 1].endsWith("]"), T = (g > 0 ? "\n".repeat(g) + E : "") + "= ", M = E + " ";
160
+ let O = N.map((U, J) => J === 0 ? _ ? "#[\n" + E + " " + U : U : U.trim() != "]" && !U.startsWith(M) ? M + U : U).join("\n");
161
+ if (_) O += "\n" + E + "]";
162
+ let W = T + O;
163
+ const P = state.doc.length === c ? void 0 : state.doc.lineAt(c + 1);
164
+ if (!P || P.from >= c) {
165
+ if (P && state.sliceDoc(P.from, P.to).match(/^\s*$/) === null) W += "\n";
166
+ }
167
+ return {
168
+ range: r.empty ? EditorSelection.cursor(o + T.length + O.length) : EditorSelection.range(o + T.length, o + T.length + O.length),
169
+ changes: [{
170
+ from: o,
171
+ to: c,
172
+ insert: W
173
+ }]
174
+ };
175
+ })), view.focus(), true;
176
+ }
177
+ function insertAtCursor(n, e) {
178
+ const r = n.state.selection.main.head;
179
+ n.dispatch({
180
+ selection: EditorSelection.cursor(r + e.length),
181
+ changes: ChangeSet.of({
182
+ from: r,
183
+ insert: e
184
+ }, n.state.doc.length)
185
+ });
186
+ }
187
+ function wrapOrInsert(view, prefix, suffix, insert) {
188
+ if (view.state.facet(EditorState.readOnly)) return;
189
+ const c = view.state;
190
+ if (c.selection.main.empty) view.dispatch(c.changeByRange((u) => ({
191
+ range: EditorSelection.cursor(u.from + insert.length),
192
+ changes: [{
193
+ from: u.from,
194
+ insert
195
+ }]
196
+ })));
197
+ else toggleAroundSelection(view, [prefix], [suffix]);
198
+ }
199
+ function toggleAroundSelection(view, prefixes, suffixes, preferNextLevel) {
200
+ if (view.state.facet(EditorState.readOnly)) return null;
201
+ const { state } = view, { doc } = state;
202
+ let matchIndex = null;
203
+ return view.dispatch(state.changeByRange((range) => {
204
+ let selRange = range;
205
+ if (range.empty) {
206
+ const line = doc.lineAt(range.from), wordMatches = line.text.matchAll(/[\p{L},.]+/gu);
207
+ for (const match of wordMatches) {
208
+ const wordStart = line.from + (match.index ?? 0), wordEnd = wordStart + match[0].length;
209
+ if (wordStart <= range.from && range.from <= wordEnd) selRange = EditorSelection.range(wordStart, wordEnd);
210
+ }
211
+ }
212
+ const matchResults = [];
213
+ for (let i = 0; i < prefixes.length; i++) {
214
+ const prefix = prefixes[i], suffix = suffixes[i];
215
+ let isMatch, beforeText = "", afterText = "";
216
+ const CONTEXT_SIZE = 50;
217
+ let beforeOffset = 0, afterOffset = 0;
218
+ if (typeof prefix === "string") {
219
+ beforeText = state.sliceDoc(Math.max(selRange.from - prefix.length, 0), selRange.from);
220
+ isMatch = beforeText === prefix;
221
+ } else {
222
+ const beforeContext = state.sliceDoc(Math.max(selRange.from - CONTEXT_SIZE, 0), selRange.from);
223
+ const prefixMatch = beforeContext.match(prefix.regex);
224
+ if (prefixMatch) {
225
+ beforeText = prefixMatch[1];
226
+ beforeOffset = beforeContext.length - (beforeText.length + beforeContext.indexOf(beforeText));
227
+ isMatch = true;
228
+ } else isMatch = false;
229
+ }
230
+ if (typeof suffix === "string") {
231
+ afterText = state.sliceDoc(selRange.to, Math.min(selRange.to + suffix.length, doc.length));
232
+ isMatch = isMatch && afterText === suffix;
233
+ } else {
234
+ const afterContext = state.sliceDoc(selRange.to, Math.min(selRange.to + CONTEXT_SIZE, doc.length));
235
+ const suffixMatch = afterContext.match(suffix.regex);
236
+ if (suffixMatch) {
237
+ afterText = suffixMatch[1];
238
+ afterOffset = afterContext.indexOf(afterText);
239
+ isMatch = isMatch && true;
240
+ }
241
+ }
242
+ if (isMatch && (beforeOffset > 0 || afterOffset > 0)) selRange = EditorSelection.range(selRange.from - beforeOffset, selRange.to + afterOffset);
243
+ matchResults.push({
244
+ before: beforeText,
245
+ after: afterText,
246
+ match: isMatch
247
+ });
248
+ }
249
+ for (let i = 0; i < matchResults.length; i++) {
250
+ if (matchResults[i].match && preferNextLevel && matchResults.slice(i + 1).some((result) => result.match)) continue;
251
+ const { before, after, match } = matchResults[i];
252
+ if (match) {
253
+ matchIndex = i;
254
+ if (i + 1 < prefixes.length) {
255
+ const nextPrefix = prefixes[i + 1], nextSuffix = suffixes[i + 1], newPrefix = typeof nextPrefix === "string" ? nextPrefix : nextPrefix.replacement, newSuffix = typeof nextSuffix === "string" ? nextSuffix : nextSuffix.replacement, prefixOffset = newPrefix.length - before.length;
256
+ return {
257
+ range: preferNextLevel && range.empty && i === 0 ? EditorSelection.cursor(selRange.from + prefixOffset - 1) : EditorSelection.range(selRange.from + prefixOffset, selRange.to + prefixOffset),
258
+ changes: [{
259
+ from: selRange.from - before.length,
260
+ to: selRange.from,
261
+ insert: newPrefix
262
+ }, {
263
+ from: selRange.to,
264
+ to: selRange.to + before.length,
265
+ insert: newSuffix
266
+ }]
267
+ };
268
+ }
269
+ return {
270
+ range: EditorSelection.range(selRange.from - before.length, selRange.to - before.length),
271
+ changes: [{
272
+ from: selRange.from - before.length,
273
+ to: selRange.from,
274
+ insert: ""
275
+ }, {
276
+ from: selRange.to,
277
+ to: selRange.to + after.length,
278
+ insert: ""
279
+ }]
280
+ };
281
+ }
282
+ }
283
+ const firstPrefix = prefixes[0], firstSuffix = suffixes[0], prefixText = typeof firstPrefix === "string" ? firstPrefix : firstPrefix.replacement, suffixText = typeof firstSuffix === "string" ? firstSuffix : firstSuffix.replacement;
284
+ return {
285
+ range: EditorSelection.range(selRange.from + prefixText.length, selRange.to + prefixText.length),
286
+ changes: [{
287
+ from: selRange.from,
288
+ insert: prefixText
289
+ }, {
290
+ from: selRange.to,
291
+ insert: suffixText
292
+ }]
293
+ };
294
+ })), state.selection.ranges.length > 1 && (matchIndex = null), view.focus(), matchIndex;
295
+ }
296
+ function toggleList(n) {
297
+ toggleListLike(n, "- ");
298
+ return true;
299
+ }
300
+ function toggleEnum(n) {
301
+ toggleListLike(n, "+ ");
302
+ return true;
303
+ }
304
+ function toggleListLike(view, prefix) {
305
+ if (view.state.facet(EditorState.readOnly)) return;
306
+ const { doc } = view.state;
307
+ view.dispatch(view.state.changeByRange((range) => {
308
+ const changes = [];
309
+ let fromOffset = 0, toOffset = 0;
310
+ const calculateOffset = (pos, change) => change.from <= pos ? change.insert.length - (Math.min(change.to, pos) - change.from) : 0;
311
+ const addChange = (change) => {
312
+ changes.push(change);
313
+ fromOffset += calculateOffset(range.from, change);
314
+ toOffset += calculateOffset(range.to, change);
315
+ };
316
+ const getLineStart = (lineNum) => {
317
+ const line = doc.line(lineNum);
318
+ const indentMatch = line.text.match(/^\s*/);
319
+ return line.from + (indentMatch?.[0].length ?? 0);
320
+ };
321
+ const startLine = doc.lineAt(range.from), endLine = doc.lineAt(range.to), listMarkers = [];
322
+ let currentLineNum = startLine.number, hasListMarkers = false;
323
+ const lineIter = doc.iterLines(startLine.number, endLine.number + 1);
324
+ while (lineIter.next() && !lineIter.done) {
325
+ const listMatch = lineIter.value.trimStart().match(/^(-|\+|\d*\.)\s?/);
326
+ if (listMatch !== null) {
327
+ listMarkers.push([currentLineNum, listMatch[0].length]);
328
+ hasListMarkers = true;
329
+ }
330
+ currentLineNum++;
331
+ }
332
+ if (hasListMarkers) for (const [lineNum, markerLength] of listMarkers) {
333
+ const lineStart = getLineStart(lineNum);
334
+ addChange({
335
+ from: lineStart,
336
+ to: lineStart + markerLength,
337
+ insert: ""
338
+ });
339
+ }
340
+ else for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
341
+ const lineStart = getLineStart(lineNum);
342
+ addChange({
343
+ from: lineStart,
344
+ to: lineStart,
345
+ insert: prefix
346
+ });
347
+ }
348
+ return {
349
+ range: EditorSelection.range(range.from + fromOffset, range.to + toOffset),
350
+ changes
351
+ };
352
+ }));
353
+ view.focus();
354
+ }
355
+ function cursorAddVertical(view, offset) {
356
+ const { state } = view, newCursors = [];
357
+ for (const range of state.selection.ranges) {
358
+ const currentLine = state.doc.lineAt(range.from), currentColumn = countColumn(currentLine.text, state.tabSize, range.from - currentLine.from), targetLineNum = currentLine.number + offset;
359
+ if (targetLineNum >= 1 && targetLineNum <= state.doc.lines) {
360
+ const targetLine = state.doc.line(currentLine.number + offset), targetPos = targetLine.from + findColumn(targetLine.text, currentColumn, state.tabSize);
361
+ newCursors.push(EditorSelection.cursor(targetPos));
362
+ }
363
+ }
364
+ if (offset > 0) newCursors.reverse();
365
+ newCursors.push(...state.selection.ranges);
366
+ view.dispatch({
367
+ selection: EditorSelection.create(newCursors),
368
+ scrollIntoView: true,
369
+ userEvent: "select"
370
+ });
371
+ return true;
372
+ }
373
+ //#endregion
374
+ //#region src/keymap.ts
375
+ const typstKeymap = keymap.of([
376
+ ...completionKeymap,
377
+ ...closeBracketsKeymap,
378
+ {
379
+ key: "Tab",
380
+ run: acceptCompletion
381
+ },
382
+ {
383
+ key: "Mod-z",
384
+ run: undo,
385
+ preventDefault: true
386
+ },
387
+ {
388
+ key: "Mod-y",
389
+ mac: "Mod-Shift-z",
390
+ run: redo,
391
+ preventDefault: true
392
+ },
393
+ {
394
+ key: "Mod-Shift-z",
395
+ run: redo,
396
+ preventDefault: true
397
+ },
398
+ {
399
+ key: "Mod-u",
400
+ run: undoSelection,
401
+ preventDefault: true
402
+ },
403
+ {
404
+ key: "Alt-u",
405
+ mac: "Mod-Shift-u",
406
+ run: redoSelection,
407
+ preventDefault: true
408
+ },
409
+ {
410
+ key: "Mod-/",
411
+ run: toggleLineComment
412
+ },
413
+ {
414
+ mac: "Cmd-Alt-/",
415
+ win: "Shift-Alt-a",
416
+ linux: "Ctrl+Shift+a",
417
+ run: toggleBlockComment
418
+ },
419
+ {
420
+ key: "Mod-#",
421
+ run: toggleLineComment
422
+ },
423
+ {
424
+ key: "Mod-Shift-7",
425
+ run: toggleLineComment
426
+ },
427
+ {
428
+ key: "Mod-Shift-/",
429
+ run: toggleLineComment
430
+ },
431
+ {
432
+ key: "Shift-Alt-a",
433
+ run: toggleBlockComment
434
+ },
435
+ {
436
+ key: "Mod-b",
437
+ run: toggleStrong
438
+ },
439
+ {
440
+ key: "Mod-Shift-f",
441
+ run: toggleStrong
442
+ },
443
+ {
444
+ key: "Mod-i",
445
+ run: toggleEmph
446
+ },
447
+ {
448
+ key: "Mod-Shift-K",
449
+ run: toggleEmph
450
+ },
451
+ {
452
+ key: "Mod-u",
453
+ run: toggleUnderline
454
+ },
455
+ {
456
+ key: "Alt-ArrowUp",
457
+ run: moveLineUp
458
+ },
459
+ {
460
+ key: "Shift-Alt-ArrowUp",
461
+ run: copyLineUp
462
+ },
463
+ {
464
+ key: "Alt-ArrowDown",
465
+ run: moveLineDown
466
+ },
467
+ {
468
+ key: "Shift-Alt-ArrowDown",
469
+ run: copyLineDown
470
+ },
471
+ {
472
+ key: "Tab",
473
+ run: insertTabLike,
474
+ shift: indentLess
475
+ },
476
+ {
477
+ key: "Mod-Enter",
478
+ run: insertBlankLine
479
+ },
480
+ {
481
+ key: "F8",
482
+ run: nextDiagnostic
483
+ },
484
+ {
485
+ key: "Mod-g",
486
+ run: gotoLine,
487
+ scope: "editor goto-panel search-panel"
488
+ },
489
+ {
490
+ key: "Escape",
491
+ run: simplifySelection
492
+ },
493
+ {
494
+ key: "Mod-Alt-ArrowUp",
495
+ run: (n) => cursorAddVertical(n, -1)
496
+ },
497
+ {
498
+ key: "Mod-Alt-ArrowDown",
499
+ run: (n) => cursorAddVertical(n, 1)
500
+ },
501
+ {
502
+ key: "Mod-f",
503
+ run: openSearchPanel,
504
+ scope: "editor search-panel goto-panel"
505
+ },
506
+ {
507
+ key: "F3",
508
+ run: findNext,
509
+ shift: findPrevious,
510
+ scope: "editor search-panel",
511
+ preventDefault: true
512
+ },
513
+ {
514
+ key: "Escape",
515
+ run: closeSearchPanel,
516
+ scope: "editor search-panel"
517
+ },
518
+ {
519
+ key: "Mod-Shift-l",
520
+ run: selectSelectionMatches
521
+ },
522
+ {
523
+ key: "Mod-d",
524
+ run: selectNextOccurrence,
525
+ preventDefault: true
526
+ },
527
+ ...standardKeymap
528
+ ]);
529
+ //#endregion
530
+ //#region src/language.ts
531
+ async function autocomplete(context, fileId, typstState) {
532
+ const { pos, explicit } = context;
533
+ let result;
534
+ try {
535
+ result = typstState.autocomplete(fileId, pos, explicit);
536
+ } catch (error) {
537
+ console.error("[typst] autocomplete panicked:", error);
538
+ return null;
539
+ }
540
+ if (!result) return null;
541
+ const { offset, completions } = result;
542
+ return {
543
+ from: offset,
544
+ options: completions.map((completion) => {
545
+ const complete = {
546
+ type: typeof completion.type === "object" ? "symbol" : completion.type,
547
+ apply: completion.apply,
548
+ label: completion.label,
549
+ info: completion.detail
550
+ };
551
+ if (completion.apply?.includes("$")) {
552
+ const tt = completion.apply.match(/\$\{\w*\}/)?.at(0) == "${}" && (completion.type !== "syntax" || completion.label != "linebreak"), applySnippet = snippet(completion.apply);
553
+ complete.apply = (applyView, applyCompletion, from, to) => {
554
+ applySnippet(applyView, applyCompletion, from, to);
555
+ if (tt) startCompletion(applyView);
556
+ };
557
+ }
558
+ return complete;
559
+ })
560
+ };
561
+ }
562
+ const typstLanguageData = EditorState.languageData.of(() => [{
563
+ closeBrackets: {
564
+ brackets: [
565
+ "(",
566
+ "[",
567
+ "{",
568
+ "\"",
569
+ "`",
570
+ "$"
571
+ ],
572
+ before: ")]}\"`$,;"
573
+ },
574
+ commentTokens: {
575
+ line: "//",
576
+ block: {
577
+ open: "/*",
578
+ close: "*/"
579
+ }
580
+ },
581
+ wordChars: "-_",
582
+ indentOnInput: /^\s*[[\]{}()$]$/
583
+ }]);
584
+ //#endregion
585
+ //#region src/notebook.ts
586
+ /** The options of the notebook extension active in this state, if any. */
587
+ const notebookOptionsFacet = Facet.define({ combine: (values) => values.at(-1) });
588
+ function notebookOptions(state) {
589
+ return state.facet(notebookOptionsFacet);
590
+ }
591
+ /** Re-renders notebooks from cached frames without recompiling. */
592
+ const notebookRefreshEffect = StateEffect.define();
593
+ /** Asks the plugin to recompile and report the result through `onRun`. */
594
+ const notebookRunEffect = StateEffect.define();
595
+ /** First position that belongs to the cell (marker line, or content). */
596
+ function cellStart(cell) {
597
+ return cell.marker_end > cell.marker_start ? cell.marker_start : cell.content_start;
598
+ }
599
+ function hasMarker(cell) {
600
+ return cell.marker_end > cell.marker_start;
601
+ }
602
+ /** Index of the cell containing `pos`; gaps belong to the cell above. */
603
+ function cellIndexAt(cells, pos) {
604
+ let index = cells.length ? 0 : -1;
605
+ for (let i = 0; i < cells.length; i++) if (pos >= cellStart(cells[i])) index = i;
606
+ else break;
607
+ return index;
608
+ }
609
+ function cellsOf(state) {
610
+ const options = notebookOptions(state);
611
+ return options ? options.cells(state.doc.toString()) : [];
612
+ }
613
+ /** Position just past a cell's block, including the gap before the next one. */
614
+ function blockEnd(cells, index, text) {
615
+ const next = cells[index + 1];
616
+ return next ? cellStart(next) : text.length;
617
+ }
618
+ /** The marker line for a cell type. Markup is the unlabeled default. */
619
+ function markerText(type) {
620
+ return type === "code" ? "// %% [code]" : "// %%";
621
+ }
622
+ function insertCellText(text, cells, index, where, type, content = "") {
623
+ const marker = markerText(type);
624
+ if (where === "end" || cells.length === 0) {
625
+ const from = text.length;
626
+ const insert = `${from === 0 || text.endsWith("\n\n") ? "" : text.endsWith("\n") ? "\n" : "\n\n"}${marker}\n${content ? `${content}\n` : ""}`;
627
+ return {
628
+ changes: {
629
+ from,
630
+ insert
631
+ },
632
+ anchor: from + insert.length
633
+ };
634
+ }
635
+ const cell = cells[index];
636
+ const from = where === "above" ? cellStart(cell) : blockEnd(cells, index, text);
637
+ return {
638
+ changes: {
639
+ from,
640
+ insert: `${marker}\n${content ? `${content}\n` : ""}\n`
641
+ },
642
+ anchor: from + marker.length + 1 + content.length
643
+ };
644
+ }
645
+ function duplicateCellText(text, cells, index) {
646
+ const cell = cells[index];
647
+ return insertCellText(text, cells, index, "below", cell.kind, text.slice(cell.content_start, cell.content_end));
648
+ }
649
+ function deleteCellText(text, cells, index) {
650
+ const cell = cells[index];
651
+ if (cells.length <= 1) return {
652
+ changes: {
653
+ from: 0,
654
+ to: text.length,
655
+ insert: ""
656
+ },
657
+ anchor: 0
658
+ };
659
+ const from = cellStart(cell);
660
+ return {
661
+ changes: {
662
+ from,
663
+ to: blockEnd(cells, index, text),
664
+ insert: ""
665
+ },
666
+ anchor: from
667
+ };
668
+ }
669
+ function moveCellText(text, cells, index, delta) {
670
+ const target = index + delta;
671
+ if (target < 0 || target >= cells.length) return null;
672
+ const aStart = cellStart(cells[index]);
673
+ const aEnd = blockEnd(cells, index, text);
674
+ const bStart = cellStart(cells[target]);
675
+ const bEnd = blockEnd(cells, target, text);
676
+ const from = Math.min(aStart, bStart);
677
+ const to = Math.max(aEnd, bEnd);
678
+ const a = text.slice(aStart, aEnd).trimEnd();
679
+ const b = text.slice(bStart, bEnd).trimEnd();
680
+ return {
681
+ changes: {
682
+ from,
683
+ to,
684
+ insert: `${delta < 0 ? `${a}\n\n${b}` : `${b}\n\n${a}`}${to < text.length ? "\n\n" : "\n"}`
685
+ },
686
+ anchor: delta < 0 ? bStart : bStart + b.length + 2
687
+ };
688
+ }
689
+ function splitCellText(text, cells, index, pos) {
690
+ const cell = cells[index];
691
+ if (pos < cell.content_start || pos > cell.content_end) return null;
692
+ return {
693
+ changes: {
694
+ from: pos,
695
+ insert: `\n\n${markerText(cell.kind)}\n\n`
696
+ },
697
+ anchor: pos
698
+ };
699
+ }
700
+ function mergeCellText(text, cells, index) {
701
+ const cell = cells[index];
702
+ if (index === 0 || !hasMarker(cell)) return null;
703
+ return {
704
+ changes: {
705
+ from: cell.marker_start,
706
+ to: cell.content_start,
707
+ insert: ""
708
+ },
709
+ anchor: cell.marker_start
710
+ };
711
+ }
712
+ function setCellTypeText(text, cells, index, type) {
713
+ const cell = cells[index];
714
+ const marker = markerText(type);
715
+ if (hasMarker(cell)) return {
716
+ changes: {
717
+ from: cell.marker_start,
718
+ to: cell.marker_end,
719
+ insert: marker
720
+ },
721
+ anchor: cell.content_start + (marker.length - (cell.marker_end - cell.marker_start))
722
+ };
723
+ return {
724
+ changes: {
725
+ from: cellStart(cell),
726
+ insert: `${marker}\n`
727
+ },
728
+ anchor: cell.content_start + marker.length + 1
729
+ };
730
+ }
731
+ /** Matches a whole cell marker line, indentation and newline included. */
732
+ const MARKER_LINE = /^[ \t]*\/\/[ \t]*%%(?:[ \t]*\[(?:code|markup|text)\])?[ \t]*\r?\n?/gm;
733
+ /**
734
+ * Removes `// %%` cell markers. Notebooks are comments-plus-content, so a
735
+ * cleaned file is the same document for anyone who does not know the
736
+ * convention.
737
+ */
738
+ function stripCellMarkers(text) {
739
+ return text.replace(MARKER_LINE, "");
740
+ }
741
+ function dispatchEdit(view, edit) {
742
+ view.dispatch({
743
+ changes: edit.changes,
744
+ selection: { anchor: edit.anchor },
745
+ scrollIntoView: true
746
+ });
747
+ return true;
748
+ }
749
+ /** Compiles and reports through `onRun`; does not move the cursor. */
750
+ function runCell(view, index) {
751
+ if (!notebookOptions(view.state) || index < 0) return false;
752
+ view.dispatch({ effects: notebookRunEffect.of({ index }) });
753
+ return true;
754
+ }
755
+ function runAllCells(view) {
756
+ if (!notebookOptions(view.state)) return false;
757
+ view.dispatch({ effects: notebookRunEffect.of({ index: "all" }) });
758
+ return true;
759
+ }
760
+ function focusCell(view, index, edge = "start") {
761
+ const cell = cellsOf(view.state)[index];
762
+ if (!cell) return false;
763
+ view.dispatch({
764
+ selection: { anchor: edge === "start" ? cell.content_start : cell.content_end },
765
+ scrollIntoView: true
766
+ });
767
+ view.focus();
768
+ return true;
769
+ }
770
+ function currentCell(view) {
771
+ return cellIndexAt(cellsOf(view.state), view.state.selection.main.head);
772
+ }
773
+ function insertCell(view, where) {
774
+ return insertCellAt(view, currentCell(view), where);
775
+ }
776
+ function insertCellAt(view, index, where) {
777
+ const cells = cellsOf(view.state);
778
+ if (index < 0 || !cells[index]) return false;
779
+ const type = cells[index]?.kind ?? "markup";
780
+ return dispatchEdit(view, insertCellText(view.state.doc.toString(), cells, index, where, type));
781
+ }
782
+ function duplicateCell(view) {
783
+ return duplicateCellAt(view, currentCell(view));
784
+ }
785
+ function duplicateCellAt(view, index) {
786
+ const cells = cellsOf(view.state);
787
+ if (index < 0 || !cells[index]) return false;
788
+ return dispatchEdit(view, duplicateCellText(view.state.doc.toString(), cells, index));
789
+ }
790
+ function deleteCell(view) {
791
+ return deleteCellAt(view, currentCell(view));
792
+ }
793
+ function deleteCellAt(view, index) {
794
+ const cells = cellsOf(view.state);
795
+ if (index < 0 || !cells[index]) return false;
796
+ return dispatchEdit(view, deleteCellText(view.state.doc.toString(), cells, index));
797
+ }
798
+ function moveCell(view, delta) {
799
+ return moveCellAt(view, currentCell(view), delta);
800
+ }
801
+ function moveCellAt(view, index, delta) {
802
+ const cells = cellsOf(view.state);
803
+ if (index < 0 || !cells[index]) return false;
804
+ const edit = moveCellText(view.state.doc.toString(), cells, index, delta);
805
+ if (!edit) return false;
806
+ return dispatchEdit(view, edit);
807
+ }
808
+ function splitCell(view) {
809
+ const cells = cellsOf(view.state);
810
+ const index = currentCell(view);
811
+ if (index < 0) return false;
812
+ const edit = splitCellText(view.state.doc.toString(), cells, index, view.state.selection.main.head);
813
+ if (!edit) return false;
814
+ return dispatchEdit(view, edit);
815
+ }
816
+ function mergeCell(view) {
817
+ const cells = cellsOf(view.state);
818
+ const index = currentCell(view);
819
+ if (index < 0) return false;
820
+ const edit = mergeCellText(view.state.doc.toString(), cells, index);
821
+ if (!edit) return false;
822
+ return dispatchEdit(view, edit);
823
+ }
824
+ function toggleCellType(view, index) {
825
+ const cells = cellsOf(view.state);
826
+ const cell = cells[index];
827
+ if (!cell) return false;
828
+ const next = cell.kind === "code" ? "markup" : "code";
829
+ return dispatchEdit(view, setCellTypeText(view.state.doc.toString(), cells, index, next));
830
+ }
831
+ function setCellType(view, index, type) {
832
+ const cells = cellsOf(view.state);
833
+ if (!cells[index]) return false;
834
+ return dispatchEdit(view, setCellTypeText(view.state.doc.toString(), cells, index, type));
835
+ }
836
+ function runAndAdvance(view) {
837
+ const cells = cellsOf(view.state);
838
+ const index = currentCell(view);
839
+ if (index < 0) return false;
840
+ runCell(view, index);
841
+ if (cells[index + 1]) focusCell(view, index + 1);
842
+ else dispatchEdit(view, insertCellText(view.state.doc.toString(), cells, index, "below", cells[index]?.kind ?? "markup"));
843
+ return true;
844
+ }
845
+ function runAndInsert(view) {
846
+ const cells = cellsOf(view.state);
847
+ const index = currentCell(view);
848
+ if (index < 0) return false;
849
+ runCell(view, index);
850
+ return dispatchEdit(view, insertCellText(view.state.doc.toString(), cells, index, "below", cells[index]?.kind ?? "markup"));
851
+ }
852
+ function runCurrent(view) {
853
+ const index = currentCell(view);
854
+ return index < 0 ? false : runCell(view, index);
855
+ }
856
+ /** Arrow keys at a cell's first or last line move between cells. */
857
+ function boundaryMove(view, direction) {
858
+ const { state } = view;
859
+ const range = state.selection.main;
860
+ if (!range.empty) return false;
861
+ const cells = cellsOf(state);
862
+ const index = cellIndexAt(cells, range.head);
863
+ const cell = cells[index];
864
+ if (!cell) return false;
865
+ if (direction === -1) {
866
+ const first = state.doc.lineAt(cell.content_start);
867
+ if (range.head < first.from || range.head > first.to || index === 0) return false;
868
+ return focusCell(view, index - 1, "end");
869
+ }
870
+ const last = state.doc.lineAt(cell.content_end);
871
+ if (range.head < last.from || range.head > last.to || index >= cells.length - 1) return false;
872
+ return focusCell(view, index + 1);
873
+ }
874
+ /**
875
+ * Notebook keymap. Mounted with high precedence so Alt-ArrowUp/Down move
876
+ * cells instead of lines and Escape reaches command mode.
877
+ */
878
+ const notebookKeymap = Prec.high(keymap.of([
879
+ {
880
+ key: "Shift-Enter",
881
+ run: runAndAdvance
882
+ },
883
+ {
884
+ key: "Mod-Enter",
885
+ run: runCurrent
886
+ },
887
+ {
888
+ key: "Alt-Enter",
889
+ run: runAndInsert
890
+ },
891
+ {
892
+ key: "Alt-ArrowUp",
893
+ run: (view) => moveCell(view, -1)
894
+ },
895
+ {
896
+ key: "Alt-ArrowDown",
897
+ run: (view) => moveCell(view, 1)
898
+ },
899
+ {
900
+ key: "Escape",
901
+ run: (view) => {
902
+ notebookOptions(view.state)?.onCommandMode?.();
903
+ return true;
904
+ }
905
+ },
906
+ {
907
+ key: "ArrowUp",
908
+ run: (view) => boundaryMove(view, -1)
909
+ },
910
+ {
911
+ key: "ArrowDown",
912
+ run: (view) => boundaryMove(view, 1)
913
+ }
914
+ ]));
915
+ //#endregion
916
+ //#region src/diagnostics.ts
917
+ /**
918
+ * Latest Typst diagnostics per page path. The WYSIWYG plugin caches the
919
+ * diagnostics of every compile here, so the linter source can reuse them
920
+ * instead of compiling the same text a second time.
921
+ */
922
+ const diagnosticsCache = /* @__PURE__ */ new Map();
923
+ function rememberDiagnostics(path, text, diagnostics) {
924
+ diagnosticsCache.set(path, {
925
+ text,
926
+ diagnostics
927
+ });
928
+ }
929
+ function cachedDiagnostics(path, text) {
930
+ const entry = diagnosticsCache.get(path);
931
+ return entry && entry.text === text ? entry.diagnostics : void 0;
932
+ }
933
+ /** Converts engine diagnostics into CodeMirror lint diagnostics. */
934
+ function toLintDiagnostics(typstDiagnostics) {
935
+ return typstDiagnostics.map((diagnostic) => ({
936
+ from: diagnostic.range.start,
937
+ to: diagnostic.range.end,
938
+ severity: diagnostic.severity,
939
+ message: diagnostic.message,
940
+ renderMessage() {
941
+ const fragment = document.createDocumentFragment();
942
+ const paragraph = document.createElement("p");
943
+ parseBackticks(diagnostic.message, paragraph);
944
+ fragment.append(paragraph);
945
+ if (diagnostic.hints.length) {
946
+ const list = document.createElement("ul");
947
+ list.className = "typst-hints";
948
+ for (const hint of diagnostic.hints) {
949
+ const item = document.createElement("li");
950
+ parseBackticks(hint, item);
951
+ list.append(item);
952
+ }
953
+ fragment.append(list);
954
+ }
955
+ return fragment;
956
+ }
957
+ }));
958
+ }
959
+ /**
960
+ * Typst diagnostics as a CodeMirror linter source.
961
+ *
962
+ * This runs in every editor mode and combines with other sources (the
963
+ * spellcheck linter), so neither one replaces the other. The WYSIWYG plugin's
964
+ * compile feeds the cache; split and source mode fall back to a compile here
965
+ * so they get diagnostics too.
966
+ */
967
+ const typstLinter = (fileId, spaceId, path, prelude, typstState, options = {}) => linter(async (view) => {
968
+ const text = view.state.doc.toString();
969
+ const cached = cachedDiagnostics(path, text);
970
+ if (cached) return toLintDiagnostics(cached);
971
+ let result;
972
+ try {
973
+ result = typstState.compilePaged(fileId, text, prelude.value);
974
+ } catch (error) {
975
+ console.error("[typst] diagnostics compile panicked:", error);
976
+ options.onPanic?.(fileId);
977
+ return [];
978
+ }
979
+ if (result.requests.length > 0 && options.onRequests) {
980
+ if (await options.onRequests(result.requests, spaceId)) result = typstState.compilePaged(fileId, text, prelude.value);
981
+ }
982
+ rememberDiagnostics(path, text, result.diagnostics);
983
+ options.onCompile?.();
984
+ return toLintDiagnostics(result.diagnostics);
985
+ }, { delay: 400 });
986
+ //#endregion
987
+ //#region src/frames.ts
988
+ const containerCache = new LRUCache({ max: 128 });
989
+ const openInNewTab = (url) => {
990
+ window.open(url, "_blank", "noopener,noreferrer");
991
+ };
992
+ /**
993
+ * The compiled size of an SVG frame, from the markup's viewBox. The viewBox
994
+ * is the page width the engine laid out at (pt), and the app renders one
995
+ * Typst point per CSS pixel, so the numbers map to px directly.
996
+ *
997
+ * `frame.render.width` is the chunk's bounding-box width, which does not
998
+ * change with the pane for fixed-size blocks (images, boxes). The viewBox
999
+ * width is the correct resize key and the size to pin: a render laid out
1000
+ * for a wider pane must keep its size, not be stretched by CSS while a
1001
+ * resize is in flight.
1002
+ */
1003
+ function frameSize(frame) {
1004
+ const match = frame.render.svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
1005
+ return {
1006
+ width: match ? Number(match[1]) : frame.render.width,
1007
+ height: match ? Number(match[2]) : frame.render.height
1008
+ };
1009
+ }
1010
+ /**
1011
+ * Pins a container's SVG to the compiled frame size (via CSS variables). The
1012
+ * container keeps max-width: 100% so it never exceeds the line. An explicit
1013
+ * container width would make CodeMirror's flex layout grow the content width
1014
+ * forever (resize -> wider widget -> wider content). The pinned SVG overflows
1015
+ * the clamped container while the pane is narrower than the compiled width;
1016
+ * the scroller clips it, so the render stays its size instead of scaling.
1017
+ */
1018
+ function syncFrameContainer(container, frame) {
1019
+ const size = frameSize(frame);
1020
+ const sizeKey = `${Math.round(size.width)}x${Math.round(size.height)}`;
1021
+ if (container.dataset.renderSize === sizeKey) return;
1022
+ container.dataset.renderSize = sizeKey;
1023
+ container.style.setProperty("--render-w", size.width + "px");
1024
+ container.style.setProperty("--render-h", size.height + "px");
1025
+ container.style.height = size.height + "px";
1026
+ container.setHTMLUnsafe(frame.render.svg);
1027
+ }
1028
+ /** Fresh frame container for widgets that own their DOM (notebook outputs). */
1029
+ function createFrameContainer(frame) {
1030
+ const container = document.createElement("div");
1031
+ container.dataset.hash = frame.render.hash.toString();
1032
+ container.classList.add("typst-render");
1033
+ syncFrameContainer(container, frame);
1034
+ return container;
1035
+ }
1036
+ /**
1037
+ * Click handling shared by inline widgets and notebook outputs: typbase://
1038
+ * links are left for the host's anchor router, external links confirm first
1039
+ * and go through `openExternal`, anything else maps back into the compiled
1040
+ * document and moves the cursor. Alt-click edits the link instead of
1041
+ * following it.
1042
+ */
1043
+ function attachFrameInteractions(container, view, frame, fileId, typstState, openExternal = openInNewTab) {
1044
+ const handle = (event) => {
1045
+ event.preventDefault();
1046
+ const anchor = event.target?.closest?.("a[href]");
1047
+ if (anchor && !event.altKey) {
1048
+ if (event.type !== "click") return;
1049
+ const href = anchor.getAttribute("href") ?? "";
1050
+ if (href.startsWith("typbase://")) return;
1051
+ if (window.confirm(`Open external link?\n\n${href}\n\nIt opens in a new tab.`)) openExternal(href);
1052
+ return;
1053
+ }
1054
+ const { top, left } = container.getBoundingClientRect();
1055
+ const x = event.clientX - left;
1056
+ const y = event.clientY - top + frame.render.yOffset;
1057
+ const jump = (() => {
1058
+ try {
1059
+ return typstState.jumpPaged(fileId, x, y);
1060
+ } catch (error) {
1061
+ console.error("[typst] jump panicked:", error);
1062
+ return;
1063
+ }
1064
+ })();
1065
+ const position = jump ? jump.position : frame.range.end;
1066
+ view.focus();
1067
+ view.dispatch({ selection: { anchor: position } });
1068
+ };
1069
+ container.addEventListener("click", handle);
1070
+ container.addEventListener("mousedown", handle);
1071
+ }
1072
+ /**
1073
+ * True when the selection does not touch `start..end`, so the frame can render
1074
+ * as a widget instead of showing source. Shared by the inline WYSIWYG pass and
1075
+ * the notebook markup cells.
1076
+ */
1077
+ function frameIsInactive(view, state, start, end) {
1078
+ return !view.hasFocus || state.selection.ranges.every((range) => (range.from < start || range.from > end) && (range.to < start || range.to > end) && (start < range.from || start > range.to) && (end < range.from || end > range.to));
1079
+ }
1080
+ /**
1081
+ * Line decorations that stand in for a frame while the cursor is inside it:
1082
+ * the source stays visible with the frame's height reserved, so the pane does
1083
+ * not jump between render and source. Matches the inline WYSIWYG behavior.
1084
+ *
1085
+ * `lineHeights` carries measured source heights from the remeasure pass; when
1086
+ * it is absent the view still reports the widget's height for those lines.
1087
+ */
1088
+ function frameActiveDecorations(view, state, frame, lineHeights) {
1089
+ const decorations = [];
1090
+ let lineHeight = 0;
1091
+ const { number: startLine } = state.doc.lineAt(frame.range.start);
1092
+ const { number: endLine } = state.doc.lineAt(frame.range.end);
1093
+ for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
1094
+ const line = state.doc.line(currentLine);
1095
+ let style = "";
1096
+ if (currentLine == startLine) style += "border-top-left-radius:0.25rem;border-top-right-radius:0.25rem;";
1097
+ if (currentLine == endLine) style += `border-bottom-left-radius:0.25rem;border-bottom-right-radius:0.25rem;min-height:${Math.max(0, frame.render.height - lineHeight)}px`;
1098
+ else lineHeight += lineHeights?.get(line.from) ?? view.lineBlockAt(line.from).height;
1099
+ decorations.push(Decoration.line({
1100
+ class: "cm-activeLine",
1101
+ attributes: { style }
1102
+ }).range(line.from));
1103
+ }
1104
+ return decorations;
1105
+ }
1106
+ var TypstWidget = class extends WidgetType {
1107
+ view;
1108
+ frame;
1109
+ locked;
1110
+ fileId;
1111
+ typstState;
1112
+ openExternal;
1113
+ container;
1114
+ size;
1115
+ constructor(view, frame, locked, fileId, typstState, openExternal) {
1116
+ super();
1117
+ this.view = view;
1118
+ this.frame = frame;
1119
+ this.locked = locked;
1120
+ this.fileId = fileId;
1121
+ this.typstState = typstState;
1122
+ this.openExternal = openExternal;
1123
+ this.size = frameSize(frame);
1124
+ const cached = containerCache.get(frame.render.hash);
1125
+ if (cached?.isConnected) {
1126
+ this.container = cached;
1127
+ syncFrameContainer(cached, frame);
1128
+ } else {
1129
+ const container = document.createElement("div");
1130
+ container.dataset.hash = frame.render.hash.toString();
1131
+ container.classList.add("typst-render");
1132
+ if (!locked) attachFrameInteractions(container, view, frame, fileId, typstState, this.openExternal);
1133
+ containerCache.set(frame.render.hash, container);
1134
+ this.container = container;
1135
+ syncFrameContainer(container, frame);
1136
+ }
1137
+ }
1138
+ eq(other) {
1139
+ return other.frame.render.hash === this.frame.render.hash && other.size.width === this.size.width && other.size.height === this.size.height;
1140
+ }
1141
+ toDOM() {
1142
+ return this.container;
1143
+ }
1144
+ get estimatedHeight() {
1145
+ return this.frame.render.height;
1146
+ }
1147
+ };
1148
+ //#endregion
1149
+ //#region src/notebook-widgets.ts
1150
+ function iconSpan(name) {
1151
+ const span = document.createElement("span");
1152
+ span.className = "ms-icon material-symbols-rounded";
1153
+ span.textContent = name;
1154
+ span.setAttribute("aria-hidden", "true");
1155
+ return span;
1156
+ }
1157
+ /** Fallback labels for consumers that enable cells without i18n strings. */
1158
+ const FALLBACK_LABELS = {
1159
+ run: "Run cell",
1160
+ code: "Code",
1161
+ markup: "Text",
1162
+ moveUp: "Move cell up",
1163
+ moveDown: "Move cell down",
1164
+ duplicate: "Duplicate cell",
1165
+ remove: "Delete cell",
1166
+ clearOutput: "Clear output",
1167
+ toggleSource: "Collapse cell",
1168
+ noOutput: "No output",
1169
+ error: "Cell error"
1170
+ };
1171
+ /** Buttons must not hand the mousedown to CodeMirror's selection handling. */
1172
+ function actionButton(icon, label, onClick, className = "") {
1173
+ const button = document.createElement("button");
1174
+ button.type = "button";
1175
+ button.className = `tb-cell-btn ${className}`.trim();
1176
+ button.title = label;
1177
+ button.setAttribute("aria-label", label);
1178
+ button.append(iconSpan(icon));
1179
+ button.addEventListener("mousedown", (event) => event.preventDefault());
1180
+ button.addEventListener("click", (event) => {
1181
+ event.preventDefault();
1182
+ event.stopPropagation();
1183
+ onClick(event);
1184
+ });
1185
+ return button;
1186
+ }
1187
+ var NotebookHeaderWidget = class extends WidgetType {
1188
+ view;
1189
+ args;
1190
+ constructor(view, args) {
1191
+ super();
1192
+ this.view = view;
1193
+ this.args = args;
1194
+ }
1195
+ eq(other) {
1196
+ const a = this.args;
1197
+ const b = other.args;
1198
+ return a.index === b.index && a.kind === b.kind && a.counter === b.counter && a.count === b.count && a.collapsed === b.collapsed && a.labels === b.labels;
1199
+ }
1200
+ toDOM() {
1201
+ const { index, kind, labels } = this.args;
1202
+ const header = document.createElement("div");
1203
+ header.className = "tb-cell-header";
1204
+ header.dataset.kind = kind;
1205
+ header.dataset.cell = String(index);
1206
+ const run = actionButton("play_arrow", labels.run, () => runCell(this.view, index));
1207
+ run.classList.add("tb-cell-btn--run");
1208
+ header.append(run);
1209
+ if (this.args.counter) {
1210
+ const chip = document.createElement("span");
1211
+ chip.className = "tb-cell-counter";
1212
+ chip.textContent = this.args.count === void 0 ? "[ ]" : `[${this.args.count}]`;
1213
+ header.append(chip);
1214
+ }
1215
+ header.append(actionButton(kind === "code" ? "code" : "notes", kind === "code" ? labels.code : labels.markup, () => toggleCellType(this.view, index), "tb-cell-btn--type"));
1216
+ const spacer = document.createElement("span");
1217
+ spacer.className = "tb-cell-spacer";
1218
+ header.append(spacer);
1219
+ const actions = document.createElement("span");
1220
+ actions.className = "tb-cell-actions";
1221
+ actions.append(actionButton("arrow_upward", labels.moveUp, () => moveCellAt(this.view, index, -1)), actionButton("arrow_downward", labels.moveDown, () => moveCellAt(this.view, index, 1)), actionButton("content_copy", labels.duplicate, () => duplicateCellAt(this.view, index)), actionButton(this.args.collapsed ? "unfold_more" : "unfold_less", labels.toggleSource, () => {
1222
+ notebookOptions(this.view.state)?.onToggleCollapse?.(index);
1223
+ this.view.dispatch({ effects: notebookRefreshEffect.of(null) });
1224
+ }), actionButton("delete_sweep", labels.clearOutput, () => {
1225
+ notebookOptions(this.view.state)?.onClearOutput?.(index);
1226
+ this.view.dispatch({ effects: notebookRefreshEffect.of(null) });
1227
+ }), actionButton("delete", labels.remove, () => deleteCellAt(this.view, index)));
1228
+ header.append(actions);
1229
+ header.addEventListener("mousedown", (event) => {
1230
+ if (event.target?.closest("button")) return;
1231
+ event.preventDefault();
1232
+ focusCell(this.view, index);
1233
+ });
1234
+ return header;
1235
+ }
1236
+ get estimatedHeight() {
1237
+ return 24;
1238
+ }
1239
+ };
1240
+ var NotebookOutputWidget = class extends WidgetType {
1241
+ view;
1242
+ fileId;
1243
+ typstState;
1244
+ args;
1245
+ openExternal;
1246
+ constructor(view, fileId, typstState, args, openExternal) {
1247
+ super();
1248
+ this.view = view;
1249
+ this.fileId = fileId;
1250
+ this.typstState = typstState;
1251
+ this.args = args;
1252
+ this.openExternal = openExternal;
1253
+ }
1254
+ /** Hash of everything the DOM depends on; DOM identity is expensive. */
1255
+ get key() {
1256
+ const frames = this.args.frames.map((frame) => {
1257
+ const size = frameSize(frame);
1258
+ return `${frame.render.hash}:${Math.round(size.width)}x${Math.round(size.height)}`;
1259
+ }).join(",");
1260
+ const diagnostics = this.args.diagnostics.map((diagnostic) => diagnostic.message).join("|");
1261
+ return `${this.args.index}:${this.args.cleared}:${this.args.hasRun}:${frames}:${diagnostics}`;
1262
+ }
1263
+ eq(other) {
1264
+ return this.key === other.key;
1265
+ }
1266
+ toDOM() {
1267
+ const root = document.createElement("div");
1268
+ const { cleared, labels } = this.args;
1269
+ root.className = "tb-cell-output";
1270
+ if (cleared) {
1271
+ root.classList.add("tb-cell-output--cleared");
1272
+ return root;
1273
+ }
1274
+ if (!(this.args.frames.length > 0 || this.args.diagnostics.length > 0 || this.args.hasRun)) {
1275
+ root.classList.add("tb-cell-output--empty");
1276
+ return root;
1277
+ }
1278
+ const body = document.createElement("div");
1279
+ body.className = "tb-cell-output-body";
1280
+ for (const diagnostic of this.args.diagnostics) {
1281
+ const item = document.createElement("div");
1282
+ item.className = `tb-cell-diagnostic tb-cell-diagnostic--${diagnostic.severity}`;
1283
+ item.textContent = diagnostic.message;
1284
+ if (diagnostic.hints.length) {
1285
+ const hints = document.createElement("ul");
1286
+ for (const hint of diagnostic.hints) {
1287
+ const entry = document.createElement("li");
1288
+ entry.textContent = hint;
1289
+ hints.append(entry);
1290
+ }
1291
+ item.append(hints);
1292
+ }
1293
+ body.append(item);
1294
+ }
1295
+ for (const frame of this.args.frames) {
1296
+ const holder = document.createElement("div");
1297
+ holder.className = "tb-cell-frame";
1298
+ const container = createFrameContainer(frame);
1299
+ attachFrameInteractions(container, this.view, frame, this.fileId, this.typstState, this.openExternal);
1300
+ holder.append(container);
1301
+ body.append(holder);
1302
+ }
1303
+ if (!this.args.frames.length && !this.args.diagnostics.length && this.args.hasRun) {
1304
+ const empty = document.createElement("div");
1305
+ empty.className = "tb-cell-empty";
1306
+ empty.textContent = labels.noOutput;
1307
+ body.append(empty);
1308
+ }
1309
+ root.append(body);
1310
+ return root;
1311
+ }
1312
+ get estimatedHeight() {
1313
+ let height = 0;
1314
+ for (const frame of this.args.frames) height += frameSize(frame).height;
1315
+ return height + 16;
1316
+ }
1317
+ };
1318
+ function decorateNotebook(args) {
1319
+ const { view, state, cells, frames, diagnostics, fileId, typstState, locked, onExternalLink, options } = args;
1320
+ const decorations = [];
1321
+ const byCell = cells.map(() => []);
1322
+ for (const frame of frames) {
1323
+ const index = cellIndexAt(cells, frame.range.start);
1324
+ if (index >= 0) byCell[index].push(frame);
1325
+ }
1326
+ const counter = options.counters?.() ?? false;
1327
+ cells.forEach((cell, index) => {
1328
+ const cellState = options.state?.(index) ?? {};
1329
+ const headerPos = state.doc.lineAt(cellStart(cell)).from;
1330
+ decorations.push(Decoration.widget({
1331
+ widget: new NotebookHeaderWidget(view, {
1332
+ index,
1333
+ kind: cell.kind,
1334
+ counter,
1335
+ count: cellState.count,
1336
+ collapsed: cellState.collapsed ?? false,
1337
+ labels: options.labels ?? FALLBACK_LABELS
1338
+ }),
1339
+ side: -1
1340
+ }).range(headerPos));
1341
+ if (hasMarker(cell)) decorations.push(Decoration.line({ class: "tb-cell-marker" }).range(state.doc.lineAt(cell.marker_start).from));
1342
+ if (cellState.collapsed && cell.content_end > cell.content_start) decorations.push(Decoration.replace({}).range(cell.content_start, cell.content_end));
1343
+ if (cell.kind === "markup") {
1344
+ if (cellState.collapsed) return;
1345
+ for (const frame of byCell[index] ?? []) {
1346
+ const { start, end } = frame.range;
1347
+ if (!frame.render || end <= start) continue;
1348
+ if (frameIsInactive(view, state, start, end)) decorations.push(Decoration.replace({ widget: new TypstWidget(view, frame, locked, fileId, typstState, onExternalLink) }).range(start, end));
1349
+ else decorations.push(...frameActiveDecorations(view, state, frame));
1350
+ }
1351
+ return;
1352
+ }
1353
+ const outputPos = cell.content_end > cell.content_start ? state.doc.lineAt(cell.content_end).to : cell.content_start;
1354
+ const cellDiagnostics = diagnostics.filter((diagnostic) => diagnostic.range.start >= cell.content_start && diagnostic.range.start <= cell.content_end);
1355
+ decorations.push(Decoration.widget({
1356
+ widget: new NotebookOutputWidget(view, fileId, typstState, {
1357
+ index,
1358
+ frames: byCell[index] ?? [],
1359
+ diagnostics: cellDiagnostics,
1360
+ cleared: cellState.cleared ?? false,
1361
+ hasRun: cellState.count !== void 0,
1362
+ labels: options.labels ?? FALLBACK_LABELS
1363
+ }, onExternalLink),
1364
+ block: true,
1365
+ side: 1
1366
+ }).range(outputPos));
1367
+ });
1368
+ return decorations;
1369
+ }
1370
+ //#endregion
1371
+ //#region src/widgets.ts
1372
+ const compileCache = new LRUCache({ max: 8 });
1373
+ const updateFlagStore = /* @__PURE__ */ new Set();
1374
+ /**
1375
+ * Dispatch this to recompile even when the document did not change. Used when
1376
+ * app data changed (fonts installed, workspace data, query results).
1377
+ */
1378
+ const typstRecompileEffect = StateEffect.define({});
1379
+ /** Compile cache keys include the wasm revision so new fonts re-render. */
1380
+ function compileCacheKey(path, typstState, revision) {
1381
+ return `${path}:${typstState.revision()}:${revision?.() ?? ""}`;
1382
+ }
1383
+ const SCROLLBAR_ALLOWANCE = 12;
1384
+ function editorRenderWidth(scrollDOM, contentDOM) {
1385
+ const pane = scrollDOM.getBoundingClientRect();
1386
+ const gutters = scrollDOM.querySelector(".cm-gutters");
1387
+ const style = getComputedStyle(contentDOM);
1388
+ return Math.max(0, pane.width - (gutters?.getBoundingClientRect().width ?? 0) - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - SCROLLBAR_ALLOWANCE);
1389
+ }
1390
+ const tooltipsStateEffect = StateEffect.define();
1391
+ const tooltipsStateField = StateField.define({
1392
+ create() {
1393
+ return [];
1394
+ },
1395
+ update(frames, transaction) {
1396
+ const effect = transaction.effects.find((e) => e.is(tooltipsStateEffect));
1397
+ return effect ? effect.value : frames;
1398
+ }
1399
+ });
1400
+ /**
1401
+ * Builds the decoration set for a compile result: rendered blocks become
1402
+ * replace widgets, the block the cursor is in shows its source and stretches
1403
+ * its last line to the render's height. Returns the frames that show source,
1404
+ * for the remeasure pass.
1405
+ */
1406
+ function buildDecorations$1({ state, view, frames, locked, fileId, typstState, openExternal, lineHeights }) {
1407
+ const decorations = [];
1408
+ const active = [];
1409
+ for (const frame of frames) {
1410
+ const { start, end } = frame.range;
1411
+ if (!frame.render) continue;
1412
+ if (frameIsInactive(view, state, start, end)) {
1413
+ const widget = new TypstWidget(view, frame, locked, fileId, typstState, openExternal);
1414
+ decorations.push(Decoration.replace({ widget }).range(start, end));
1415
+ continue;
1416
+ }
1417
+ active.push(frame);
1418
+ decorations.push(...frameActiveDecorations(view, state, frame, lineHeights));
1419
+ }
1420
+ return {
1421
+ decorations: Decoration.set(decorations, true),
1422
+ active
1423
+ };
1424
+ }
1425
+ /**
1426
+ * Source line heights for the blocks whose widgets were just removed. Runs
1427
+ * inside the view's measure pass, after the source lines are in the DOM, so
1428
+ * the heights are the text's and not the widget's.
1429
+ */
1430
+ function measureLineHeights(view, frames) {
1431
+ const heights = /* @__PURE__ */ new Map();
1432
+ const { state } = view;
1433
+ for (const frame of frames) {
1434
+ const { number: startLine } = state.doc.lineAt(frame.range.start);
1435
+ const { number: endLine } = state.doc.lineAt(frame.range.end);
1436
+ for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
1437
+ const line = state.doc.line(currentLine);
1438
+ if (!heights.has(line.from)) heights.set(line.from, view.lineBlockAt(line.from).height);
1439
+ }
1440
+ }
1441
+ return heights;
1442
+ }
1443
+ function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidget: _updateInWidget, widthChanged, forced, typstState, revision, onRequests, onPanic, onCompile, onExternalLink, notebook }) {
1444
+ const text = update.state.doc.toString();
1445
+ const isFlaggedForUpdate = updateFlagStore.has(path);
1446
+ const cacheKey = compileCacheKey(path, typstState, revision);
1447
+ let frames;
1448
+ let tooltips;
1449
+ let diagnostics;
1450
+ if (forced || update.docChanged || widthChanged || !compileCache.has(cacheKey)) {
1451
+ if (isFlaggedForUpdate) updateFlagStore.delete(path);
1452
+ else updateFlagStore.add(path);
1453
+ let compileResult;
1454
+ try {
1455
+ compileResult = typstState.compilePaged(fileId, text, prelude);
1456
+ } catch (error) {
1457
+ console.error("[typst] compile panicked:", error);
1458
+ onPanic?.(fileId);
1459
+ return {
1460
+ decorations: Decoration.none,
1461
+ tooltips: [],
1462
+ frames: [],
1463
+ active: []
1464
+ };
1465
+ }
1466
+ dispatchDiagnostics(compileResult.diagnostics, update.state, update.view);
1467
+ rememberDiagnostics(path, text, compileResult.diagnostics);
1468
+ onCompile?.();
1469
+ if (compileResult.requests.length > 0 && onRequests) Promise.resolve(onRequests(compileResult.requests, spaceId)).then((wasUpdated) => {
1470
+ if (wasUpdated) {
1471
+ const doc = update.view.state.doc.toString();
1472
+ update.view.dispatch({
1473
+ changes: {
1474
+ from: 0,
1475
+ to: doc.length,
1476
+ insert: doc
1477
+ },
1478
+ effects: typstRecompileEffect.of(null)
1479
+ });
1480
+ }
1481
+ });
1482
+ ({frames, tooltips} = compileResult);
1483
+ diagnostics = compileResult.diagnostics;
1484
+ compileCache.set(cacheKey, {
1485
+ frames,
1486
+ tooltips,
1487
+ diagnostics
1488
+ });
1489
+ notebook?.onCompile?.({
1490
+ text,
1491
+ frames,
1492
+ diagnostics
1493
+ });
1494
+ } else {
1495
+ const cached = compileCache.get(cacheKey);
1496
+ ({frames, tooltips, diagnostics} = cached);
1497
+ }
1498
+ const { view, state } = update;
1499
+ if (notebook) {
1500
+ let cells;
1501
+ try {
1502
+ cells = notebook.cells(text);
1503
+ } catch (error) {
1504
+ console.error("[typst] cell extraction panicked:", error);
1505
+ onPanic?.(fileId);
1506
+ return {
1507
+ decorations: Decoration.none,
1508
+ tooltips: [],
1509
+ frames: [],
1510
+ active: []
1511
+ };
1512
+ }
1513
+ notebook.onCells?.(cells);
1514
+ notebook.onActiveCell?.(cells.length ? cellIndexAt(cells, state.selection.main.head) : null);
1515
+ return {
1516
+ decorations: Decoration.set(decorateNotebook({
1517
+ view,
1518
+ state,
1519
+ cells,
1520
+ frames,
1521
+ diagnostics,
1522
+ fileId,
1523
+ typstState,
1524
+ locked,
1525
+ onExternalLink,
1526
+ options: notebook
1527
+ }), true),
1528
+ tooltips,
1529
+ frames,
1530
+ active: []
1531
+ };
1532
+ }
1533
+ const built = buildDecorations$1({
1534
+ state,
1535
+ view,
1536
+ frames,
1537
+ locked,
1538
+ fileId,
1539
+ typstState,
1540
+ openExternal: onExternalLink
1541
+ });
1542
+ return {
1543
+ decorations: built.decorations,
1544
+ tooltips,
1545
+ frames,
1546
+ active: built.active
1547
+ };
1548
+ }
1549
+ const typstStateEffect = StateEffect.define({});
1550
+ const typstStateField = StateField.define({
1551
+ create() {
1552
+ return Decoration.none;
1553
+ },
1554
+ update(decorations, transaction) {
1555
+ const effect = transaction.effects.find((e) => e.is(typstStateEffect));
1556
+ if (effect) return effect.value.decorations;
1557
+ return decorations.map(transaction.changes);
1558
+ },
1559
+ provide: (field) => [EditorView.decorations.from(field)]
1560
+ });
1561
+ const typstViewPlugin = (fileId, spaceId, path, text, prelude, locked, typstState, options = {}) => ViewPlugin.define((_view) => {
1562
+ let firstUpdate = true;
1563
+ let resizeTimer;
1564
+ const remeasureKey = {};
1565
+ /**
1566
+ * A pass that shows a block's source runs while the block's widget is still
1567
+ * mounted, so the view reports the widget's height for those lines and the
1568
+ * min-height that should keep the render's height comes out as zero. Once
1569
+ * the source lines are in the DOM and measured, rebuild the active
1570
+ * decorations with the real text heights. The measure pass runs before
1571
+ * paint, so the rebuild is not visible.
1572
+ */
1573
+ const remeasureActive = (update, result) => {
1574
+ if (result.active.length === 0) return;
1575
+ const { view, state } = update;
1576
+ const focused = view.hasFocus;
1577
+ const stale = () => view.state.doc !== state.doc || !view.state.selection.eq(state.selection) || view.hasFocus !== focused;
1578
+ view.requestMeasure({
1579
+ key: remeasureKey,
1580
+ read: (readView) => stale() ? null : measureLineHeights(readView, result.active),
1581
+ write: (heights) => {
1582
+ if (!heights) return;
1583
+ queueMicrotask(() => {
1584
+ if (stale()) return;
1585
+ const rebuilt = buildDecorations$1({
1586
+ state: view.state,
1587
+ view,
1588
+ frames: result.frames,
1589
+ locked,
1590
+ fileId,
1591
+ typstState,
1592
+ openExternal: options.onExternalLink,
1593
+ lineHeights: heights
1594
+ });
1595
+ view.dispatch({ effects: typstStateEffect.of({ decorations: rebuilt.decorations }) });
1596
+ });
1597
+ }
1598
+ });
1599
+ };
1600
+ return {
1601
+ update(update) {
1602
+ let widthChanged = false;
1603
+ const effects = update.transactions.flatMap((transaction) => transaction.effects);
1604
+ const run = effects.find((effect) => effect.is(notebookRunEffect));
1605
+ const refresh = effects.some((effect) => effect.is(notebookRefreshEffect));
1606
+ const forced = firstUpdate || update.transactions.some((transaction) => transaction.effects.some((effect) => effect.is(typstRecompileEffect)));
1607
+ firstUpdate = false;
1608
+ if (run) options.notebook?.onRun?.(run.value.index);
1609
+ if (update.geometryChanged) {
1610
+ const { scrollDOM, contentDOM } = update.view;
1611
+ try {
1612
+ widthChanged = typstState.resize(fileId, editorRenderWidth(scrollDOM, contentDOM), locked ? scrollDOM.clientHeight : void 0);
1613
+ } catch (error) {
1614
+ console.error("[typst] resize panicked:", error);
1615
+ options.onPanic?.(fileId);
1616
+ return;
1617
+ }
1618
+ }
1619
+ if (widthChanged) {
1620
+ clearTimeout(resizeTimer);
1621
+ resizeTimer = window.setTimeout(() => {
1622
+ update.view.dispatch({ effects: typstRecompileEffect.of(null) });
1623
+ }, 150);
1624
+ }
1625
+ if (update.docChanged || update.selectionSet || update.focusChanged || forced || run || refresh) {
1626
+ queueMicrotask(() => {
1627
+ let result;
1628
+ try {
1629
+ result = decorate({
1630
+ fileId,
1631
+ spaceId,
1632
+ path,
1633
+ prelude: prelude.value,
1634
+ locked,
1635
+ update,
1636
+ updateInWidget: false,
1637
+ widthChanged,
1638
+ forced: forced || Boolean(run),
1639
+ typstState,
1640
+ revision: options.revision,
1641
+ onRequests: options.onRequests,
1642
+ onPanic: options.onPanic,
1643
+ onCompile: options.onCompile,
1644
+ onExternalLink: options.onExternalLink,
1645
+ notebook: options.notebook
1646
+ });
1647
+ } catch (error) {
1648
+ console.error("[typst] decorate panicked:", error);
1649
+ options.onPanic?.(fileId);
1650
+ return;
1651
+ }
1652
+ if (result) {
1653
+ const stateEffects = [typstStateEffect.of({ decorations: result.decorations }), tooltipsStateEffect.of(result.tooltips)];
1654
+ update.view.dispatch({ effects: stateEffects });
1655
+ remeasureActive(update, result);
1656
+ }
1657
+ });
1658
+ if (update.docChanged) text.value = update.state.doc.toString();
1659
+ }
1660
+ },
1661
+ destroy() {
1662
+ clearTimeout(resizeTimer);
1663
+ }
1664
+ };
1665
+ });
1666
+ function dispatchDiagnostics(typstDiagnostics, state, view) {
1667
+ view.dispatch(setDiagnostics(state, toLintDiagnostics(typstDiagnostics)));
1668
+ }
1669
+ //#endregion
1670
+ //#region src/tooltip.ts
1671
+ const tooltipStateEffect = StateEffect.define();
1672
+ const tooltipStateField = StateField.define({
1673
+ create() {
1674
+ return null;
1675
+ },
1676
+ update(tooltip, transaction) {
1677
+ const effect = transaction.effects.find((e) => e.is(tooltipStateEffect));
1678
+ if (effect) return effect.value;
1679
+ return tooltip;
1680
+ },
1681
+ provide: (field) => showTooltip.from(field)
1682
+ });
1683
+ const tooltipViewPlugin = () => ViewPlugin.define((view) => ({ update(update) {
1684
+ if (!update.selectionSet && !update.docChanged && !update.focusChanged) return;
1685
+ queueMicrotask(() => {
1686
+ if (!view.hasFocus) {
1687
+ view.dispatch({ effects: tooltipStateEffect.of(null) });
1688
+ return;
1689
+ }
1690
+ const state = view.state;
1691
+ const pos = state.selection.main.from;
1692
+ const tooltips = state.field(tooltipsStateField);
1693
+ let tooltip = null;
1694
+ for (const { render, range } of tooltips) {
1695
+ const { start, end } = range;
1696
+ if (pos >= start && pos <= end && render) {
1697
+ const container = document.createElement("div");
1698
+ container.classList.add("typst-popup-render");
1699
+ const svg = document.createElement("div");
1700
+ svg.style.width = render.width + "px";
1701
+ svg.style.height = render.height + "px";
1702
+ svg.style.transform = `translateX(-${render.xOffset}px)`;
1703
+ svg.setHTMLUnsafe(render.svg);
1704
+ container.append(svg);
1705
+ tooltip = {
1706
+ pos: start + 1,
1707
+ end: end - 1,
1708
+ create(tooltipView) {
1709
+ return {
1710
+ dom: container,
1711
+ getCoords(_pos) {
1712
+ const startCoords = tooltipView.coordsAtPos(start);
1713
+ const endCoords = tooltipView.coordsAtPos(end);
1714
+ return {
1715
+ left: startCoords.left,
1716
+ right: startCoords.right,
1717
+ top: endCoords.top,
1718
+ bottom: endCoords.bottom
1719
+ };
1720
+ }
1721
+ };
1722
+ }
1723
+ };
1724
+ break;
1725
+ }
1726
+ }
1727
+ view.dispatch({ effects: tooltipStateEffect.of(tooltip) });
1728
+ });
1729
+ } }));
1730
+ //#endregion
1731
+ //#region src/plugin.ts
1732
+ const typstPlugin = (fileId, spaceId, path, text, prelude, locked, typstState, options = {}) => {
1733
+ const extensions = [
1734
+ typstStateField,
1735
+ tooltipsStateField,
1736
+ typstViewPlugin(fileId, spaceId, path, text, prelude, locked, typstState, options),
1737
+ tooltipViewPlugin(),
1738
+ tooltipStateField,
1739
+ autocompletion({ override: [(context) => autocomplete(context, fileId, typstState)] }),
1740
+ typstKeymap,
1741
+ typstLanguageData,
1742
+ typstSyntaxHighlighting(fileId, typstState),
1743
+ typstHoverTooltip(fileId, typstState),
1744
+ addSpaceBeforeClosingBracket,
1745
+ indentService.of((ctx, pos) => {
1746
+ const last = Math.max(0, pos - 1);
1747
+ const prev = ctx.lineAt(last).text;
1748
+ if (prev.endsWith("$") && prev !== "$") return 0;
1749
+ const indent = /[{[($]\s*$/.test(prev);
1750
+ return ctx.lineIndent(last) + (indent ? ctx.unit : 0);
1751
+ })
1752
+ ];
1753
+ if (options.notebook) extensions.push(notebookOptionsFacet.of(options.notebook), notebookKeymap);
1754
+ return extensions;
1755
+ };
1756
+ const addSpaceBeforeClosingBracket = EditorView.inputHandler.of((view, from, to, text) => {
1757
+ if (text === " ") {
1758
+ const { state } = view;
1759
+ const pos = from;
1760
+ const bracketPairs = {
1761
+ "(": ")",
1762
+ "[": "]",
1763
+ "{": "}",
1764
+ $: "$"
1765
+ };
1766
+ const before = state.doc.sliceString(pos - 1, pos);
1767
+ const after = state.doc.sliceString(pos, pos + 1);
1768
+ if (bracketPairs[before] && after === bracketPairs[before]) view.dispatch({
1769
+ changes: {
1770
+ from: pos,
1771
+ to: pos,
1772
+ insert: " "
1773
+ },
1774
+ selection: { anchor: pos + 1 }
1775
+ });
1776
+ }
1777
+ return false;
1778
+ });
1779
+ //#endregion
1780
+ //#region src/static-highlight.ts
1781
+ const KEYWORDS = /* @__PURE__ */ new Set([
1782
+ "and",
1783
+ "as",
1784
+ "auto",
1785
+ "break",
1786
+ "context",
1787
+ "continue",
1788
+ "else",
1789
+ "false",
1790
+ "for",
1791
+ "if",
1792
+ "import",
1793
+ "in",
1794
+ "include",
1795
+ "let",
1796
+ "none",
1797
+ "not",
1798
+ "or",
1799
+ "return",
1800
+ "set",
1801
+ "show",
1802
+ "true",
1803
+ "while"
1804
+ ]);
1805
+ const IDENT = /^[A-Za-z_][\w-]*/;
1806
+ /** Math identifiers cannot start with `_`, which is the attach operator. */
1807
+ const MATH_IDENT = /^[A-Za-z][A-Za-z0-9-]*/;
1808
+ function classForIdent(ident, rest) {
1809
+ if (KEYWORDS.has(ident)) return "typ-key";
1810
+ if (/^\s*[([{]/.test(rest)) return "typ-func";
1811
+ return "typ-pol";
1812
+ }
1813
+ function runLength(text, from) {
1814
+ let end = from;
1815
+ while (end < text.length && text[end] === "`") end += 1;
1816
+ return end - from;
1817
+ }
1818
+ /** Start of the first backtick run of at least `fence` at or after `from`. */
1819
+ function findFence(text, fence, from) {
1820
+ let index = text.indexOf("`", from);
1821
+ while (index >= 0) {
1822
+ const run = runLength(text, index);
1823
+ if (run >= fence) return index;
1824
+ index = text.indexOf("`", index + run);
1825
+ }
1826
+ return -1;
1827
+ }
1828
+ const typstStaticHighlighting = StateField.define({
1829
+ create(state) {
1830
+ return buildDecorations(state.doc);
1831
+ },
1832
+ update(decorations, transaction) {
1833
+ if (transaction.docChanged) return buildDecorations(transaction.state.doc);
1834
+ return decorations;
1835
+ },
1836
+ provide: (field) => EditorView.decorations.from(field)
1837
+ });
1838
+ function buildDecorations(doc) {
1839
+ const builder = new RangeSetBuilder();
1840
+ const state = {
1841
+ mode: "markup",
1842
+ mathReturn: "markup",
1843
+ fence: 0,
1844
+ depth: 0
1845
+ };
1846
+ const add = (from, to, cls) => {
1847
+ if (to > from) builder.add(from, to, Decoration.mark({ class: cls }));
1848
+ };
1849
+ for (let lineNo = 1; lineNo <= doc.lines; lineNo++) {
1850
+ const line = doc.line(lineNo);
1851
+ scanLine(line.text, state, (from, to, cls) => add(line.from + from, line.from + to, cls));
1852
+ }
1853
+ return builder.finish();
1854
+ }
1855
+ function scanLine(line, state, emit) {
1856
+ let i = 0;
1857
+ if (state.mode === "comment") {
1858
+ const close = line.indexOf("*/");
1859
+ if (close < 0) {
1860
+ emit(0, line.length, "typ-comment");
1861
+ return;
1862
+ }
1863
+ emit(0, close + 2, "typ-comment");
1864
+ state.mode = "markup";
1865
+ i = close + 2;
1866
+ } else if (state.mode === "raw") {
1867
+ const close = findFence(line, state.fence, 0);
1868
+ if (close < 0) {
1869
+ emit(0, line.length, "typ-raw");
1870
+ return;
1871
+ }
1872
+ const run = runLength(line, close);
1873
+ emit(0, close + run, "typ-raw");
1874
+ state.mode = "markup";
1875
+ state.fence = 0;
1876
+ i = close + run;
1877
+ } else if (state.mode === "code" && state.depth === 0) state.mode = "markup";
1878
+ if (state.mode === "markup" && i === 0) i = markupLineStart(line, emit);
1879
+ while (i < line.length) if (state.mode === "markup") i = markupToken(line, i, state, emit);
1880
+ else if (state.mode === "code") i = codeToken(line, i, state, emit);
1881
+ else i = mathToken(line, i, state, emit);
1882
+ if (state.mode === "code" && state.depth === 0) state.mode = "markup";
1883
+ }
1884
+ /** Headings, list markers, and term text, which only count at line start. */
1885
+ function markupLineStart(line, emit) {
1886
+ const indent = /^\s*/.exec(line)?.[0].length ?? 0;
1887
+ const rest = line.slice(indent);
1888
+ const heading = /^(=+)(\s|$)/.exec(rest);
1889
+ if (heading) {
1890
+ const level = Math.min(heading[1].length, 6);
1891
+ emit(indent, line.length, `typ-heading typ-heading-level-${level}`);
1892
+ return line.length;
1893
+ }
1894
+ const marker = /^([-+/]|\d+\.)(\s|$)/.exec(rest);
1895
+ if (marker) {
1896
+ const end = indent + marker[1].length;
1897
+ emit(indent, end, "typ-marker");
1898
+ if (marker[1] === "/") {
1899
+ const colon = line.indexOf(":", end + 1);
1900
+ if (colon > 0) {
1901
+ emit(end + 1, colon, "typ-term");
1902
+ return colon + 1;
1903
+ }
1904
+ }
1905
+ return end;
1906
+ }
1907
+ return indent;
1908
+ }
1909
+ function markupToken(line, i, state, emit) {
1910
+ const rest = line.slice(i);
1911
+ const ch = line[i];
1912
+ if (rest.startsWith("//")) {
1913
+ emit(i, line.length, "typ-comment");
1914
+ return line.length;
1915
+ }
1916
+ if (rest.startsWith("/*")) {
1917
+ const close = line.indexOf("*/", i + 2);
1918
+ if (close < 0) {
1919
+ emit(i, line.length, "typ-comment");
1920
+ state.mode = "comment";
1921
+ return line.length;
1922
+ }
1923
+ emit(i, close + 2, "typ-comment");
1924
+ return close + 2;
1925
+ }
1926
+ if (ch === "`") {
1927
+ const run = runLength(line, i);
1928
+ const close = findFence(line, run, i + run);
1929
+ if (close < 0) {
1930
+ emit(i + run, line.length, "typ-raw");
1931
+ if (run >= 3) {
1932
+ state.mode = "raw";
1933
+ state.fence = run;
1934
+ }
1935
+ return line.length;
1936
+ }
1937
+ emit(i + run, close, "typ-raw");
1938
+ return close + runLength(line, close);
1939
+ }
1940
+ if (ch === "$") {
1941
+ emit(i, i + 1, "typ-math-delim");
1942
+ state.mode = "math";
1943
+ state.mathReturn = "markup";
1944
+ return i + 1;
1945
+ }
1946
+ if (ch === "#") {
1947
+ const ident = IDENT.exec(line.slice(i + 1));
1948
+ if (ident) {
1949
+ const end = i + 1 + ident[0].length;
1950
+ emit(i, end, classForIdent(ident[0], line.slice(end)));
1951
+ if (KEYWORDS.has(ident[0])) {
1952
+ state.mode = "code";
1953
+ state.depth = 0;
1954
+ }
1955
+ return end;
1956
+ }
1957
+ emit(i, i + 1, "typ-punct");
1958
+ return i + 1;
1959
+ }
1960
+ if (ch === "<") {
1961
+ const label = /^<[A-Za-z_][\w:.-]*>/.exec(rest);
1962
+ if (label) {
1963
+ emit(i, i + label[0].length, "typ-label");
1964
+ return i + label[0].length;
1965
+ }
1966
+ }
1967
+ if (ch === "@") {
1968
+ const ref = /^@[\w:.-]+/.exec(rest);
1969
+ if (ref) {
1970
+ emit(i, i + ref[0].length, "typ-ref");
1971
+ return i + ref[0].length;
1972
+ }
1973
+ }
1974
+ if (ch === "\\") {
1975
+ emit(i, Math.min(i + 2, line.length), "typ-escape");
1976
+ return i + 2;
1977
+ }
1978
+ if (ch === "*" || ch === "_") {
1979
+ const before = i === 0 ? "" : line[i - 1];
1980
+ const close = line.indexOf(ch, i + 1);
1981
+ const after = close < 0 ? "" : line[close + 1] ?? "";
1982
+ if (close > i + 1 && !/\w/.test(before) && !/\s/.test(line[i + 1]) && !/\s/.test(line[close - 1]) && !/\w/.test(after)) {
1983
+ emit(i, close + 1, ch === "*" ? "typ-strong" : "typ-emph");
1984
+ return close + 1;
1985
+ }
1986
+ }
1987
+ const link = /^(https?:\/\/|www\.)\S+/.exec(rest);
1988
+ if (link) {
1989
+ emit(i, i + link[0].length, "typ-link");
1990
+ return i + link[0].length;
1991
+ }
1992
+ return i + 1;
1993
+ }
1994
+ function codeToken(line, i, state, emit) {
1995
+ const rest = line.slice(i);
1996
+ const ch = line[i];
1997
+ if (rest.startsWith("//")) {
1998
+ emit(i, line.length, "typ-comment");
1999
+ return line.length;
2000
+ }
2001
+ if (rest.startsWith("/*")) {
2002
+ const close = line.indexOf("*/", i + 2);
2003
+ if (close < 0) {
2004
+ emit(i, line.length, "typ-comment");
2005
+ state.mode = "comment";
2006
+ return line.length;
2007
+ }
2008
+ emit(i, close + 2, "typ-comment");
2009
+ return close + 2;
2010
+ }
2011
+ if (ch === "\"") {
2012
+ const end = stringEnd(line, i);
2013
+ emit(i, end, "typ-str");
2014
+ return end;
2015
+ }
2016
+ if (ch === "$") {
2017
+ emit(i, i + 1, "typ-math-delim");
2018
+ state.mode = "math";
2019
+ state.mathReturn = "code";
2020
+ return i + 1;
2021
+ }
2022
+ if (/[0-9]/.test(ch)) {
2023
+ const number = /^[0-9][\w.]*/.exec(rest);
2024
+ emit(i, i + number[0].length, "typ-num");
2025
+ return i + number[0].length;
2026
+ }
2027
+ if (/[A-Za-z_]/.test(ch)) {
2028
+ const ident = IDENT.exec(rest);
2029
+ emit(i, i + ident[0].length, classForIdent(ident[0], line.slice(i + ident[0].length)));
2030
+ return i + ident[0].length;
2031
+ }
2032
+ if ("([{".includes(ch)) {
2033
+ state.depth += 1;
2034
+ emit(i, i + 1, "typ-punct");
2035
+ return i + 1;
2036
+ }
2037
+ if (")]}".includes(ch)) {
2038
+ state.depth = Math.max(0, state.depth - 1);
2039
+ emit(i, i + 1, "typ-punct");
2040
+ return i + 1;
2041
+ }
2042
+ if (".,;:".includes(ch)) {
2043
+ emit(i, i + 1, "typ-punct");
2044
+ return i + 1;
2045
+ }
2046
+ const operator = /^(==|!=|<=|>=|=>|\.\.\.|\.\.|[+\-*/=!<>&|])/.exec(rest);
2047
+ if (operator) {
2048
+ emit(i, i + operator[0].length, "typ-op");
2049
+ return i + operator[0].length;
2050
+ }
2051
+ return i + 1;
2052
+ }
2053
+ function mathToken(line, i, state, emit) {
2054
+ const rest = line.slice(i);
2055
+ const ch = line[i];
2056
+ if (ch === "$") {
2057
+ emit(i, i + 1, "typ-math-delim");
2058
+ state.mode = state.mathReturn;
2059
+ return i + 1;
2060
+ }
2061
+ if (ch === "\"") {
2062
+ const end = stringEnd(line, i);
2063
+ emit(i, end, "typ-str");
2064
+ return end;
2065
+ }
2066
+ if (/[0-9]/.test(ch)) {
2067
+ const number = /^[0-9][\w.]*/.exec(rest);
2068
+ emit(i, i + number[0].length, "typ-num");
2069
+ return i + number[0].length;
2070
+ }
2071
+ if (/[A-Za-z_]/.test(ch)) {
2072
+ const ident = MATH_IDENT.exec(rest);
2073
+ if (ident) {
2074
+ emit(i, i + ident[0].length, classForIdent(ident[0], line.slice(i + ident[0].length)));
2075
+ return i + ident[0].length;
2076
+ }
2077
+ }
2078
+ if (ch === "(" || ch === ")") {
2079
+ emit(i, i + 1, "typ-math-group");
2080
+ return i + 1;
2081
+ }
2082
+ if ("[]{}.,;:".includes(ch)) {
2083
+ emit(i, i + 1, "typ-punct");
2084
+ return i + 1;
2085
+ }
2086
+ if ("^_&/".includes(ch)) {
2087
+ emit(i, i + 1, "typ-math-op");
2088
+ return i + 1;
2089
+ }
2090
+ if ("+-*=<>.!|".includes(ch)) {
2091
+ emit(i, i + 1, "typ-op");
2092
+ return i + 1;
2093
+ }
2094
+ return i + 1;
2095
+ }
2096
+ /** End of a string literal starting at `from`, or the line end. */
2097
+ function stringEnd(line, from) {
2098
+ let index = from + 1;
2099
+ while (index < line.length) {
2100
+ if (line[index] === "\\") {
2101
+ index += 2;
2102
+ continue;
2103
+ }
2104
+ if (line[index] === "\"") return index + 1;
2105
+ index += 1;
2106
+ }
2107
+ return line.length;
2108
+ }
2109
+ //#endregion
2110
+ export { TypstWidget, attachFrameInteractions, autocomplete, cellIndexAt, cellStart, cellsOf, compileCache, createFrameContainer, cursorAddVertical, cycleHeading, deleteCell, deleteCellAt, deleteCellText, duplicateCell, duplicateCellAt, duplicateCellText, focusCell, frameActiveDecorations, frameIsInactive, frameSize, hasMarker, insertAtCursor, insertCell, insertCellAt, insertCellText, insertFontFamily, insertTabLike, markerText, mergeCell, mergeCellText, moveCell, moveCellAt, moveCellText, notebookKeymap, notebookOptions, notebookOptionsFacet, notebookRefreshEffect, notebookRunEffect, parseBackticks, rememberDiagnostics, runAllCells, runCell, setCellType, setCellTypeText, splitCell, splitCellText, stripCellMarkers, syncFrameContainer, toLintDiagnostics, toggleAroundSelection, toggleCellType, toggleCode, toggleEmph, toggleEnum, toggleList, toggleListLike, toggleMath, toggleStrike, toggleStrong, toggleUnderline, tooltipStateField, tooltipViewPlugin, tooltipsStateEffect, tooltipsStateField, typstHoverTooltip, typstKeymap, typstLanguageData, typstLinter, typstPlugin, typstRecompileEffect, typstStateField, typstStaticHighlighting, typstSyntaxHighlighting, typstViewPlugin, wrapOrInsert };