mrmd-editor 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +3 -1
  2. package/src/commands.js +112 -4
  3. package/src/comment-syntax.js +364 -39
  4. package/src/config/handlers.js +1 -2
  5. package/src/config/schema.js +46 -4
  6. package/src/document-template.js +2236 -0
  7. package/src/frontmatter-updater.js +204 -74
  8. package/src/grammar.js +758 -0
  9. package/src/index.js +1074 -55
  10. package/src/keymap.js +11 -2
  11. package/src/markdown/block-decorations.js +108 -5
  12. package/src/markdown/facets.js +37 -0
  13. package/src/markdown/html-inline.js +9 -5
  14. package/src/markdown/index.js +13 -3
  15. package/src/markdown/inline-commands.js +256 -0
  16. package/src/markdown/inline-model.js +578 -0
  17. package/src/markdown/inline-state.js +103 -0
  18. package/src/markdown/renderer.js +219 -12
  19. package/src/markdown/styles.js +290 -3
  20. package/src/markdown/widgets/alert-title.js +10 -8
  21. package/src/markdown/widgets/frontmatter.js +0 -6
  22. package/src/markdown/widgets/index.js +1 -0
  23. package/src/markdown/widgets/list-marker.js +29 -0
  24. package/src/markdown/wysiwyg.js +1158 -0
  25. package/src/mrp-types.js +2 -0
  26. package/src/output-widget.js +532 -18
  27. package/src/page-view-pagination.js +127 -0
  28. package/src/runtime-lsp.js +1757 -150
  29. package/src/section-controls/commands.js +617 -0
  30. package/src/section-controls/index.js +63 -0
  31. package/src/section-controls/plugin.js +165 -0
  32. package/src/section-controls/widgets.js +936 -0
  33. package/src/shell/ai-menu.js +11 -0
  34. package/src/shell/components/context-panel.js +572 -0
  35. package/src/shell/components/status-bar.js +10 -2
  36. package/src/shell/layouts/studio.js +206 -14
  37. package/src/shell/orchestrator-client.js +69 -0
  38. package/src/spellcheck.js +166 -0
  39. package/src/tables/README.md +97 -0
  40. package/src/tables/commands/insert-linked-table.js +122 -0
  41. package/src/tables/commands/open-table-workspace.js +43 -0
  42. package/src/tables/index.js +24 -0
  43. package/src/tables/jobs/client.js +158 -0
  44. package/src/tables/parsing/anchors.js +82 -0
  45. package/src/tables/parsing/linked-table-blocks.js +61 -0
  46. package/src/tables/state/linked-table-state.js +68 -0
  47. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  48. package/src/tables/widgets/linked-table-widget.js +256 -0
  49. package/src/tables/workspace/controller.js +616 -0
  50. package/src/term-pty-client.js +51 -2
  51. package/src/term-widget.js +43 -3
  52. package/src/widgets/theme-utils.js +24 -16
  53. package/src/widgets/theme.js +1015 -1
  54. package/src/runtime-codelens/detector.js +0 -279
  55. package/src/runtime-codelens/index.js +0 -76
  56. package/src/runtime-codelens/plugin.js +0 -142
  57. package/src/runtime-codelens/styles.js +0 -184
  58. package/src/runtime-codelens/widgets.js +0 -216
package/src/index.js CHANGED
@@ -31,7 +31,7 @@
31
31
  // #region IMPORTS
32
32
  import { EditorView, basicSetup } from 'codemirror';
33
33
  import { EditorState, StateEffect, Compartment, Text, Transaction } from '@codemirror/state';
34
- import { keymap, Decoration, ViewPlugin, WidgetType, placeholder } from '@codemirror/view';
34
+ import { keymap, Decoration, ViewPlugin, WidgetType, placeholder, highlightWhitespace } from '@codemirror/view';
35
35
  import { StreamLanguage, syntaxTree, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
36
36
  import { createCodemirrorTheme } from './widgets/codemirror-theme.js';
37
37
 
@@ -64,8 +64,44 @@ import { WebsocketProvider } from 'y-websocket';
64
64
  // Internal modules
65
65
  import { findCells, getCellAtCursor, countCells, findTerminalBlocks, isTerminalLanguage } from './cells.js';
66
66
  import { RuntimeRegistry, createRuntimeRegistry } from './runtime.js';
67
+ import {
68
+ defaultDocumentTemplate,
69
+ documentTemplatePresets,
70
+ normalizeDocumentTemplate,
71
+ cloneDocumentTemplate,
72
+ createDocumentTemplateExtension,
73
+ compileDocumentTemplateCSS,
74
+ serializeDocumentTemplateToCss,
75
+ findDocumentTemplatePreset,
76
+ resolveFontForExport,
77
+ serializeDocumentTemplateToPandocMeta,
78
+ serializeDocumentTemplateToPandocYaml,
79
+ serializeDocumentTemplateToLatexPreamble,
80
+ serializeDocumentTemplateToHtml,
81
+ serializeDocumentTemplateToWordStyleMap,
82
+ buildPandocCommand,
83
+ } from './document-template.js';
84
+ import { parseFrontmatter, readFrontmatterValue, updateFrontmatterField } from './frontmatter-updater.js';
67
85
  import { ExecutionManager, createExecutionManager } from './execution.js';
68
86
  import { MonitorCoordination, EXECUTION_STATUS, createMonitorCoordination } from './monitor-coordination.js';
87
+ import * as linkedTables from './tables/index.js';
88
+ import {
89
+ LINKED_TABLE_EVENT,
90
+ dispatchLinkedTableAction,
91
+ openLinkedTableWorkspace,
92
+ canImportLinkedTableFromHost,
93
+ normalizeLinkedTableBlockInsertion,
94
+ insertLinkedTableBlock,
95
+ importLinkedTableFromHost,
96
+ TableJobsClient,
97
+ TABLE_JOB_STATUS,
98
+ createTableJobsClient,
99
+ createLinkedTableController,
100
+ LinkedTableController,
101
+ createLinkedTableBlockAnchor,
102
+ resolveLinkedTableBlockAnchor,
103
+ linkedTableMarkdownState,
104
+ } from './tables/index.js';
69
105
  import { MRPClient } from './mrp-client.js';
70
106
 
71
107
  // Shell (status bar, file management, studio layout)
@@ -83,20 +119,8 @@ import * as commentSyntaxModule from './comment-syntax.js';
83
119
  // Cell controls (run buttons, queue, status)
84
120
  import { createCellControls, CellControlsSystem } from './cell-controls/index.js';
85
121
 
86
- // Runtime CodeLens (inline session controls)
87
- import {
88
- createRuntimeCodeLensExtensions,
89
- rebuildRuntimeCodeLens,
90
- runtimeCodeLensFacet,
91
- runtimeCodeLensPlugin,
92
- rebuildRuntimeCodeLensEffect,
93
- injectRuntimeCodeLensStyles,
94
- removeRuntimeCodeLensStyles,
95
- findRuntimeBlocks,
96
- findYamlConfigBlocks,
97
- findSessionFrontmatter,
98
- RuntimeCodeLensWidget,
99
- } from './runtime-codelens/index.js';
122
+ // Section controls (AI/formatting next to focused line)
123
+ import { sectionControls } from './section-controls/index.js';
100
124
 
101
125
  // Commands and keymap
102
126
  import { commandRegistry } from './commands.js';
@@ -108,10 +132,24 @@ import {
108
132
  adaptMRPClient,
109
133
  createRuntimeHoverExtension,
110
134
  createRuntimeCompletionExtension,
135
+ createRuntimeSignatureHelpExtension,
111
136
  createVariableExplorer,
112
137
  injectRuntimeLspStyles,
113
138
  } from './runtime-lsp.js';
114
139
 
140
+ // Spellcheck (prose-only, disabled in code blocks)
141
+ import { createSpellcheckExtensions } from './spellcheck.js';
142
+ // Grammar diagnostics (LanguageTool host integration)
143
+ import {
144
+ createLanguageToolDiagnosticsExtension,
145
+ collectVisibleProseFragments,
146
+ forceLanguageToolRefresh,
147
+ refreshLanguageToolDiagnostics,
148
+ applyFirstLanguageToolSuggestion,
149
+ getLanguageToolSuggestionMenu,
150
+ applyLanguageToolSuggestionAt,
151
+ } from './grammar.js';
152
+
115
153
  // Wiki-link completion ([[internal-links]])
116
154
  import {
117
155
  projectFilesFacet,
@@ -157,6 +195,14 @@ import {
157
195
  markdown as markdownRendering,
158
196
  markdownRenderer,
159
197
  assetResolverFacet, // Facet for resolving asset URLs in Electron/desktop apps
198
+ sourceModeFacet, // Facet to toggle source/raw markdown view
199
+ wysiwygModeFacet, // Facet to toggle protected WYSIWYG rendering
200
+ createWysiwygExtensions,
201
+ createInlineEditingExtensions,
202
+ toggleInlineFormat,
203
+ toggleInlineMark,
204
+ getSelectionFormattingState,
205
+ findFencedCodeAt,
160
206
  blockDecorations, // StateField for tables, display math (multi-line replace)
161
207
  lineHeightTracker, // ViewPlugin for accurate line height tracking
162
208
  markdownStyles,
@@ -174,6 +220,9 @@ import {
174
220
  AlertTitleWidget,
175
221
  } from './markdown/index.js';
176
222
 
223
+ // Page view pagination (spacer-based page breaks)
224
+ import { pageViewPagination } from './page-view-pagination.js';
225
+
177
226
  // Awareness system
178
227
  import {
179
228
  createAwareness,
@@ -681,6 +730,12 @@ const codeBlockBackground = ViewPlugin.fromClass(class {
681
730
  const firstLine = view.state.doc.lineAt(from);
682
731
  const lastLine = view.state.doc.lineAt(to);
683
732
 
733
+ // Extract language from the opening fence line
734
+ const fenceText = firstLine.text;
735
+ const langMatch = fenceText.match(/^(\s*`{3,}|~{3,})\s*(\S*)/);
736
+ const rawLang = langMatch?.[2] || '';
737
+ const language = normalizeCodeLanguage(rawLang);
738
+
684
739
  // Iterate through each line in the code block
685
740
  for (let pos = from; pos < to;) {
686
741
  const line = view.state.doc.lineAt(pos);
@@ -690,12 +745,18 @@ const codeBlockBackground = ViewPlugin.fromClass(class {
690
745
  if (isFirstLine || isLastLine) {
691
746
  // Fence lines - subtle styling
692
747
  decorations.push(
693
- Decoration.line({ class: 'cm-codeblock-fence' }).range(line.from)
748
+ Decoration.line({
749
+ class: 'cm-codeblock-fence',
750
+ attributes: language ? { 'data-lang': language } : undefined,
751
+ }).range(line.from)
694
752
  );
695
753
  } else {
696
754
  // Content lines - normal code block styling
697
755
  decorations.push(
698
- Decoration.line({ class: 'cm-codeblock-line' }).range(line.from)
756
+ Decoration.line({
757
+ class: 'cm-codeblock-line',
758
+ attributes: language ? { 'data-lang': language } : undefined,
759
+ }).range(line.from)
699
760
  );
700
761
  }
701
762
  pos = line.to + 1;
@@ -754,6 +815,99 @@ const codeBlockStyles = EditorView.theme({
754
815
  });
755
816
  // #endregion CODE_BLOCK_BACKGROUND
756
817
 
818
+ // #region INVISIBLE_CHARACTERS
819
+ /**
820
+ * Extension that shows newline markers (¶) at the end of each line.
821
+ * Used in combination with CM6's built-in highlightWhitespace() for
822
+ * spaces and tabs. Together they provide full invisible character rendering.
823
+ */
824
+ class NewlineMarkerWidget extends WidgetType {
825
+ eq() { return true; }
826
+ toDOM() {
827
+ const span = document.createElement('span');
828
+ span.className = 'cm-newline-marker';
829
+ span.textContent = '¶';
830
+ span.setAttribute('aria-hidden', 'true');
831
+ return span;
832
+ }
833
+ ignoreEvent() { return true; }
834
+ }
835
+
836
+ const newlineMarkerPlugin = ViewPlugin.fromClass(
837
+ class {
838
+ constructor(view) {
839
+ this.decorations = this.buildDecorations(view);
840
+ }
841
+
842
+ update(update) {
843
+ if (update.docChanged || update.viewportChanged) {
844
+ this.decorations = this.buildDecorations(update.view);
845
+ }
846
+ }
847
+
848
+ buildDecorations(view) {
849
+ const decorations = [];
850
+ const doc = view.state.doc;
851
+
852
+ for (let i = doc.lineAt(view.viewport.from).number; i <= doc.lineAt(view.viewport.to).number; i++) {
853
+ const line = doc.line(i);
854
+ // Add newline marker at the end of each line (except the last line if it has no trailing newline)
855
+ if (i < doc.lines) {
856
+ decorations.push(
857
+ Decoration.widget({
858
+ widget: new NewlineMarkerWidget(),
859
+ side: 1, // After the line content
860
+ }).range(line.to)
861
+ );
862
+ }
863
+ }
864
+
865
+ return Decoration.set(decorations, true);
866
+ }
867
+ },
868
+ { decorations: (v) => v.decorations }
869
+ );
870
+
871
+ /**
872
+ * Styles for invisible character markers.
873
+ * Overrides CM6's default whitespace styling with more visible symbols.
874
+ */
875
+ const invisibleCharStyles = EditorView.theme({
876
+ // Newline markers (¶)
877
+ '.cm-newline-marker': {
878
+ color: 'var(--md-marker-color, #aaa)',
879
+ opacity: '0.5',
880
+ fontSize: '0.8em',
881
+ userSelect: 'none',
882
+ pointerEvents: 'none',
883
+ },
884
+ // Override CM6's default space dots - make them subtler
885
+ '.cm-highlightSpace': {
886
+ backgroundImage: 'radial-gradient(circle at 50% 55%, var(--md-marker-color, #aaa) 20%, transparent 5%)',
887
+ opacity: '0.4',
888
+ },
889
+ // Override CM6's tab arrows
890
+ '.cm-highlightTab': {
891
+ backgroundImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="20"><path stroke="%23aaa" stroke-width="1" fill="none" d="M1 10H196L190 5M190 15L196 10M197 4L197 16"/></svg>')`,
892
+ opacity: '0.5',
893
+ },
894
+ });
895
+
896
+ /**
897
+ * Create the invisibles extension bundle.
898
+ * Includes whitespace highlighting + newline markers + styles.
899
+ *
900
+ * @returns {import('@codemirror/state').Extension}
901
+ */
902
+ function createInvisiblesExtension() {
903
+ return [
904
+ highlightWhitespace(),
905
+ newlineMarkerPlugin,
906
+ invisibleCharStyles,
907
+ ];
908
+ }
909
+ // #endregion INVISIBLE_CHARACTERS
910
+
757
911
  // #region WRITER
758
912
  /**
759
913
  * Writer for streaming content into the editor.
@@ -833,6 +987,413 @@ function findInitialCursorPosition(content) {
833
987
 
834
988
  return 0; // fallback to start
835
989
  }
990
+
991
+ function wrapSelectionsWith(view, open, close = open, userEvent = 'input.wysiwyg.format') {
992
+ const state = view.state;
993
+ const changes = [];
994
+ const ranges = [];
995
+ let delta = 0;
996
+
997
+ for (const range of state.selection.ranges) {
998
+ if (range.empty) {
999
+ changes.push({ from: range.from, insert: open + close });
1000
+ const pos = range.from + open.length + delta;
1001
+ ranges.push({ anchor: pos, head: pos });
1002
+ delta += open.length + close.length;
1003
+ } else {
1004
+ changes.push({ from: range.from, insert: open });
1005
+ changes.push({ from: range.to, insert: close });
1006
+ ranges.push({ anchor: range.from + open.length + delta, head: range.to + open.length + delta });
1007
+ delta += open.length + close.length;
1008
+ }
1009
+ }
1010
+
1011
+ view.dispatch({
1012
+ changes,
1013
+ selection: { ranges, mainIndex: state.selection.mainIndex },
1014
+ userEvent,
1015
+ });
1016
+ }
1017
+
1018
+ function currentLineStructuralPrefix(lineText) {
1019
+ const heading = lineText.match(/^\s{0,3}(#{1,6})\s+/);
1020
+ if (heading) return heading[0];
1021
+ const quote = lineText.match(/^(\s*>\s?)+/);
1022
+ if (quote) return quote[0];
1023
+ const list = lineText.match(/^(\s*)(?:[-+*]|\d+\.)\s+/);
1024
+ if (list) return list[0];
1025
+ return '';
1026
+ }
1027
+
1028
+ function getCurrentBlockTypeInfo(view) {
1029
+ const pos = view.state.selection.main.head;
1030
+ const line = view.state.doc.lineAt(pos);
1031
+ const text = line.text;
1032
+
1033
+ // ── Check if inside a fenced code block ──
1034
+ const tree = syntaxTree(view.state);
1035
+ let fencedCode = null;
1036
+ tree.iterate({
1037
+ from: Math.max(0, pos - 1),
1038
+ to: pos + 1,
1039
+ enter: (node) => {
1040
+ if (node.name === 'FencedCode' && node.from <= pos && node.to >= pos) {
1041
+ fencedCode = node;
1042
+ }
1043
+ },
1044
+ });
1045
+
1046
+ if (fencedCode) {
1047
+ // Extract language from the opening fence line (```python, ```r, etc.)
1048
+ const fenceLine = view.state.doc.lineAt(fencedCode.from);
1049
+ const fenceText = fenceLine.text;
1050
+ const langMatch = fenceText.match(/^(\s*`{3,}|~{3,})\s*(\S*)/);
1051
+ const rawLang = langMatch?.[2] || '';
1052
+ const language = normalizeCodeLanguage(rawLang);
1053
+
1054
+ // Determine if cursor is on the fence line itself or inside code content
1055
+ const isOnFence = line.number === fenceLine.number ||
1056
+ line.number === view.state.doc.lineAt(fencedCode.to).number;
1057
+
1058
+ // Detect the syntax token under the cursor
1059
+ const syntaxToken = isOnFence ? null : getSyntaxTokenAtPos(view, pos);
1060
+
1061
+ return {
1062
+ type: 'codeblock',
1063
+ level: 0,
1064
+ label: language ? `codeblock-${language}` : 'codeblock',
1065
+ language,
1066
+ isOnFence,
1067
+ syntaxToken,
1068
+ fenceFrom: fencedCode.from,
1069
+ fenceTo: fencedCode.to,
1070
+ };
1071
+ }
1072
+
1073
+ const heading = text.match(/^\s{0,3}(#{1,6})\s+/);
1074
+ if (heading) {
1075
+ return { type: 'heading', level: heading[1].length, label: `h${heading[1].length}` };
1076
+ }
1077
+
1078
+ if (/^(\s*>\s?)+/.test(text)) {
1079
+ return { type: 'blockquote', level: 1, label: 'blockquote' };
1080
+ }
1081
+
1082
+ if (/^(\s*)(?:[-+*])\s+/.test(text)) {
1083
+ return { type: 'unordered-list', level: 1, label: 'unordered-list' };
1084
+ }
1085
+
1086
+ if (/^(\s*)(?:\d+\.)\s+/.test(text)) {
1087
+ return { type: 'ordered-list', level: 1, label: 'ordered-list' };
1088
+ }
1089
+
1090
+ return { type: 'paragraph', level: 0, label: 'paragraph' };
1091
+ }
1092
+
1093
+ /**
1094
+ * Normalize code language aliases to canonical names matching template keys.
1095
+ */
1096
+ function normalizeCodeLanguage(raw) {
1097
+ if (!raw) return '';
1098
+ const lang = raw.toLowerCase().trim();
1099
+ const map = {
1100
+ 'js': 'javascript', 'node': 'javascript', 'ecmascript': 'javascript',
1101
+ 'ts': 'typescript',
1102
+ 'py': 'python', 'python3': 'python',
1103
+ 'rb': 'ruby',
1104
+ 'rs': 'rust',
1105
+ 'sh': 'shell', 'bash': 'shell', 'zsh': 'shell', 'fish': 'shell',
1106
+ 'ps1': 'powershell', 'pwsh': 'powershell',
1107
+ 'yml': 'yaml',
1108
+ 'htm': 'html',
1109
+ 'c': 'cpp', 'c++': 'cpp', 'cxx': 'cpp', 'h': 'cpp', 'hpp': 'cpp',
1110
+ 'golang': 'go',
1111
+ 'jl': 'julia',
1112
+ 'rlang': 'r',
1113
+ 'mysql': 'sql', 'postgresql': 'sql', 'postgres': 'sql', 'sqlite': 'sql',
1114
+ 'jsonc': 'json',
1115
+ 'jsx': 'javascript', 'tsx': 'typescript',
1116
+ 'xml': 'html', 'svg': 'html',
1117
+ };
1118
+ return map[lang] || lang;
1119
+ }
1120
+
1121
+ /**
1122
+ * Get the semantic syntax token type at a position within a code block.
1123
+ * Maps CodeMirror/Lezer highlight tags to our template token names.
1124
+ *
1125
+ * @param {EditorView} view
1126
+ * @param {number} pos
1127
+ * @returns {string|null} Token name like 'keyword', 'string', 'comment', etc.
1128
+ */
1129
+ function getSyntaxTokenAtPos(view, pos) {
1130
+ const tree = syntaxTree(view.state);
1131
+ let bestNode = null;
1132
+ let bestSize = Infinity;
1133
+
1134
+ // Find the most specific (smallest) node at pos
1135
+ tree.iterate({
1136
+ from: pos,
1137
+ to: pos + 1,
1138
+ enter: (node) => {
1139
+ const size = node.to - node.from;
1140
+ if (size < bestSize && node.from <= pos && node.to > pos) {
1141
+ bestNode = node;
1142
+ bestSize = size;
1143
+ }
1144
+ },
1145
+ });
1146
+
1147
+ if (!bestNode) return null;
1148
+ const name = bestNode.name;
1149
+
1150
+ // Map Lezer tree node names to our semantic token names.
1151
+ // These are the node names from @lezer/highlight and language parsers.
1152
+ const tokenMap = {
1153
+ // Keywords
1154
+ 'Keyword': 'keyword',
1155
+ 'keyword': 'keyword',
1156
+ 'ControlKeyword': 'controlKeyword',
1157
+ 'controlKeyword': 'controlKeyword',
1158
+ 'for': 'controlKeyword',
1159
+ 'if': 'controlKeyword',
1160
+ 'else': 'controlKeyword',
1161
+ 'while': 'controlKeyword',
1162
+ 'return': 'keyword',
1163
+ 'def': 'keyword',
1164
+ 'class': 'keyword',
1165
+ 'import': 'keyword',
1166
+ 'from': 'keyword',
1167
+ 'as': 'keyword',
1168
+ 'in': 'keyword',
1169
+ 'not': 'keyword',
1170
+ 'and': 'keyword',
1171
+ 'or': 'keyword',
1172
+ 'is': 'keyword',
1173
+ 'with': 'keyword',
1174
+ 'try': 'controlKeyword',
1175
+ 'except': 'controlKeyword',
1176
+ 'finally': 'controlKeyword',
1177
+ 'raise': 'keyword',
1178
+ 'yield': 'keyword',
1179
+ 'lambda': 'keyword',
1180
+ 'pass': 'keyword',
1181
+ 'break': 'controlKeyword',
1182
+ 'continue': 'controlKeyword',
1183
+ 'del': 'keyword',
1184
+ 'global': 'keyword',
1185
+ 'nonlocal': 'keyword',
1186
+ 'assert': 'keyword',
1187
+ 'async': 'keyword',
1188
+ 'await': 'keyword',
1189
+ 'let': 'keyword',
1190
+ 'const': 'keyword',
1191
+ 'var': 'keyword',
1192
+ 'function': 'keyword',
1193
+ 'switch': 'controlKeyword',
1194
+ 'case': 'controlKeyword',
1195
+ 'default': 'controlKeyword',
1196
+ 'do': 'controlKeyword',
1197
+ 'throw': 'keyword',
1198
+ 'catch': 'controlKeyword',
1199
+ 'new': 'keyword',
1200
+ 'this': 'keyword',
1201
+ 'super': 'keyword',
1202
+ 'extends': 'keyword',
1203
+ 'implements': 'keyword',
1204
+ 'interface': 'keyword',
1205
+ 'enum': 'keyword',
1206
+ 'export': 'keyword',
1207
+ 'typeof': 'keyword',
1208
+ 'instanceof': 'keyword',
1209
+ 'void': 'keyword',
1210
+ 'delete': 'keyword',
1211
+
1212
+ // Strings
1213
+ 'String': 'string',
1214
+ 'string': 'string',
1215
+ 'TemplateString': 'string',
1216
+ 'FormatString': 'string',
1217
+ 'Character': 'string',
1218
+
1219
+ // Numbers
1220
+ 'Number': 'number',
1221
+ 'number': 'number',
1222
+ 'Integer': 'number',
1223
+ 'Float': 'number',
1224
+
1225
+ // Comments
1226
+ 'Comment': 'comment',
1227
+ 'comment': 'comment',
1228
+ 'LineComment': 'comment',
1229
+ 'BlockComment': 'comment',
1230
+
1231
+ // Functions
1232
+ 'FunctionDefinition': 'function',
1233
+ 'FunctionDeclaration': 'function',
1234
+ 'CallExpression': 'function',
1235
+
1236
+ // Variables
1237
+ 'VariableName': 'variable',
1238
+ 'VariableDefinition': 'variable',
1239
+
1240
+ // Types
1241
+ 'TypeName': 'type',
1242
+ 'TypeDefinition': 'type',
1243
+ 'ClassName': 'type',
1244
+ 'ClassDefinition': 'type',
1245
+
1246
+ // Operators
1247
+ 'ArithOp': 'operator',
1248
+ 'LogicOp': 'operator',
1249
+ 'BitOp': 'operator',
1250
+ 'CompareOp': 'operator',
1251
+ 'AssignOp': 'operator',
1252
+ 'Equals': 'operator',
1253
+
1254
+ // Punctuation
1255
+ 'Punctuation': 'punctuation',
1256
+ '(': 'punctuation',
1257
+ ')': 'punctuation',
1258
+ '[': 'punctuation',
1259
+ ']': 'punctuation',
1260
+ '{': 'punctuation',
1261
+ '}': 'punctuation',
1262
+ '.': 'punctuation',
1263
+ ',': 'punctuation',
1264
+ ';': 'punctuation',
1265
+ ':': 'punctuation',
1266
+
1267
+ // Properties
1268
+ 'PropertyName': 'property',
1269
+ 'PropertyDefinition': 'property',
1270
+
1271
+ // Constants
1272
+ 'BooleanLiteral': 'constant',
1273
+ 'Boolean': 'constant',
1274
+ 'True': 'constant',
1275
+ 'False': 'constant',
1276
+ 'None': 'constant',
1277
+ 'Null': 'constant',
1278
+ 'null': 'constant',
1279
+ 'undefined': 'constant',
1280
+
1281
+ // Regex
1282
+ 'RegExp': 'regexp',
1283
+
1284
+ // Escape
1285
+ 'Escape': 'escape',
1286
+ 'EscapeSequence': 'escape',
1287
+
1288
+ // Tags (HTML/XML)
1289
+ 'TagName': 'tag',
1290
+ 'StartTag': 'tag',
1291
+ 'EndTag': 'tag',
1292
+ 'SelfClosingTag': 'tag',
1293
+
1294
+ // Attributes
1295
+ 'AttributeName': 'attribute',
1296
+ 'AttributeValue': 'attributeValue',
1297
+
1298
+ // Meta / decorators
1299
+ 'Decorator': 'meta',
1300
+ 'Annotation': 'meta',
1301
+ 'Meta': 'meta',
1302
+ 'ProcessingInstruction': 'meta',
1303
+ };
1304
+
1305
+ // Direct match
1306
+ if (tokenMap[name]) return tokenMap[name];
1307
+
1308
+ // Try resolving from the tree directly and walking up parent nodes
1309
+ try {
1310
+ let node = tree.resolveInner(pos, 1);
1311
+ let depth = 0;
1312
+ while (node && depth < 8) {
1313
+ if (tokenMap[node.name]) return tokenMap[node.name];
1314
+ node = node.parent;
1315
+ depth++;
1316
+ }
1317
+ } catch (e) { /* ignore tree resolution errors */ }
1318
+
1319
+ return null;
1320
+ }
1321
+
1322
+ function getSelectionFormattingInfo(view) {
1323
+ return getSelectionFormattingState(view);
1324
+ }
1325
+
1326
+ function setCurrentBlockType(view, type, options = {}) {
1327
+ const state = view.state;
1328
+ const pos = state.selection.main.head;
1329
+ const line = state.doc.lineAt(pos);
1330
+ const prefix = currentLineStructuralPrefix(line.text);
1331
+ const contentStart = line.from + prefix.length;
1332
+ let newPrefix = '';
1333
+
1334
+ if (type === 'paragraph') {
1335
+ newPrefix = '';
1336
+ } else if (type === 'heading') {
1337
+ const level = Math.max(1, Math.min(6, Number(options.level) || 1));
1338
+ newPrefix = '#'.repeat(level) + ' ';
1339
+ } else if (type === 'blockquote') {
1340
+ newPrefix = '> ';
1341
+ } else if (type === 'unordered-list') {
1342
+ newPrefix = '- ';
1343
+ } else if (type === 'ordered-list') {
1344
+ newPrefix = '1. ';
1345
+ } else {
1346
+ return false;
1347
+ }
1348
+
1349
+ view.dispatch({
1350
+ changes: { from: line.from, to: contentStart, insert: newPrefix },
1351
+ selection: { anchor: line.from + newPrefix.length + Math.max(0, pos - contentStart) },
1352
+ userEvent: 'input.wysiwyg.blocktype',
1353
+ });
1354
+ return true;
1355
+ }
1356
+
1357
+ function insertLinkAtSelection(view, url, text = null) {
1358
+ const state = view.state;
1359
+ const range = state.selection.main;
1360
+ const selected = state.sliceDoc(range.from, range.to);
1361
+ const label = text ?? selected ?? 'link';
1362
+ const link = `[${label}](${url})`;
1363
+ const insertFrom = range.from;
1364
+ const insertTo = range.to;
1365
+ view.dispatch({
1366
+ changes: { from: insertFrom, to: insertTo, insert: link },
1367
+ selection: { anchor: insertFrom + 1, head: insertFrom + 1 + label.length },
1368
+ userEvent: 'input.wysiwyg.link',
1369
+ });
1370
+ return true;
1371
+ }
1372
+
1373
+ function insertImageAtSelection(view, url, alt = 'image') {
1374
+ const pos = view.state.selection.main.head;
1375
+ const image = `![${alt}](${url})`;
1376
+ view.dispatch({
1377
+ changes: { from: pos, insert: image },
1378
+ selection: { anchor: pos + 2, head: pos + 2 + alt.length },
1379
+ userEvent: 'input.wysiwyg.image',
1380
+ });
1381
+ return true;
1382
+ }
1383
+
1384
+ function insertCodeBlockAtCursor(view, language = '') {
1385
+ const pos = view.state.selection.main.head;
1386
+ const lang = language ? String(language).trim() : '';
1387
+ const prefix = pos > 0 && view.state.sliceDoc(pos - 1, pos) !== '\n' ? '\n' : '';
1388
+ const block = `${prefix}\`\`\`${lang}\n\n\`\`\``;
1389
+ const cursor = pos + prefix.length + 3 + lang.length + 1;
1390
+ view.dispatch({
1391
+ changes: { from: pos, insert: block },
1392
+ selection: { anchor: cursor },
1393
+ userEvent: 'input.wysiwyg.codeblock',
1394
+ });
1395
+ return true;
1396
+ }
836
1397
  // #endregion INITIAL_CURSOR
837
1398
 
838
1399
  // #region CREATE
@@ -869,6 +1430,7 @@ function create(target, options = {}) {
869
1430
  const dark = config.appearance.dark;
870
1431
  const placeholderText = config.appearance.placeholder;
871
1432
  const readonly = config.appearance.readonly;
1433
+ const spellcheck = config.appearance.spellcheck;
872
1434
  const userName = config.user.name;
873
1435
  const userColor = config.user.color;
874
1436
  const userType = config.user.type;
@@ -883,6 +1445,11 @@ function create(target, options = {}) {
883
1445
  awarenessUI = true,
884
1446
  } = options;
885
1447
 
1448
+ const linkedTableHostContext = {
1449
+ projectRoot: options.projectRoot || null,
1450
+ documentPath: options.documentPath || null,
1451
+ };
1452
+
886
1453
  // Runtimes from normalized config
887
1454
  const runtimes = {};
888
1455
  for (const [name, rtConfig] of Object.entries(config.runtimes)) {
@@ -935,10 +1502,16 @@ function create(target, options = {}) {
935
1502
 
936
1503
  // Always read initial content from Yjs (source of truth)
937
1504
  const initialContent = yText.toString();
1505
+ const initialDocumentTemplate = normalizeDocumentTemplate(options.documentTemplate || defaultDocumentTemplate);
938
1506
  const themeCompartment = new Compartment();
1507
+ const documentTemplateCompartment = new Compartment();
939
1508
  const readonlyCompartment = new Compartment();
940
1509
  const keymapCompartment = new Compartment();
941
1510
  const projectFilesCompartment = new Compartment();
1511
+ const sectionControlsCompartment = new Compartment();
1512
+ const sourceModeCompartment = new Compartment();
1513
+ const wysiwygModeCompartment = new Compartment();
1514
+ const invisiblesCompartment = new Compartment();
942
1515
 
943
1516
  // Create UndoManager for undo/redo tracking
944
1517
  // We create it ourselves so we can listen to stack changes
@@ -977,7 +1550,7 @@ function create(target, options = {}) {
977
1550
  // Use explicit theme if set, otherwise auto-select based on dark mode
978
1551
  const resolveThemeName = (theme, isDarkMode) => {
979
1552
  if (theme) return theme;
980
- return isDarkMode ? 'midnight' : 'daylight';
1553
+ return 'plain-light';
981
1554
  };
982
1555
  const initialThemeName = resolveThemeName(config.appearance?.theme, isDark);
983
1556
 
@@ -1003,8 +1576,11 @@ function create(target, options = {}) {
1003
1576
  documentTheme,
1004
1577
  codeBlockBackground, // Add gray background to code blocks
1005
1578
  codeBlockStyles,
1579
+ // Spellcheck: enable browser-native spellcheck on prose, disable in code
1580
+ ...(spellcheck !== false ? createSpellcheckExtensions() : []),
1006
1581
  EditorView.lineWrapping, // Always wrap markdown text
1007
1582
  themeCompartment.of(initialCMTheme),
1583
+ documentTemplateCompartment.of(createDocumentTemplateExtension(initialDocumentTemplate)),
1008
1584
  readonlyCompartment.of(readonly ? EditorState.readOnly.of(true) : []),
1009
1585
  placeholderText ? placeholder(placeholderText) : [],
1010
1586
  // Yjs collaboration - y-codemirror.next handles sync and undo
@@ -1016,13 +1592,26 @@ function create(target, options = {}) {
1016
1592
  // Initially empty, configured after api is created
1017
1593
  keymapCompartment.of([]),
1018
1594
  outputWidgetPlugin, // ANSI output rendering
1595
+ ...createInlineEditingExtensions(),
1019
1596
  lineHeightTracker, // ViewPlugin: tracks line height for spacer calculations
1597
+ linkedTableMarkdownState,
1020
1598
  blockDecorations, // StateField for tables, display math (multi-line)
1021
1599
  markdownRenderer, // ViewPlugin for everything else (inline)
1600
+ pageViewPagination, // ViewPlugin: page-view spacers at page boundaries
1601
+ ...createWysiwygExtensions(),
1602
+ ...commentSyntaxModule.createCommentSyntaxExtension(),
1022
1603
  // Wiki-link completion - just the facet for project files
1023
1604
  // The actual completion is provided by runtime-lsp (via additionalSources)
1024
1605
  // or by a standalone autocompletion added below if no runtime providers exist
1025
1606
  projectFilesCompartment.of(projectFilesFacet.of([])),
1607
+ // Section controls are configured after API creation
1608
+ sectionControlsCompartment.of([]),
1609
+ // Source mode (show all raw markdown syntax)
1610
+ sourceModeCompartment.of(sourceModeFacet.of(false)),
1611
+ // WYSIWYG mode (fully rendered, syntax-protected editing)
1612
+ wysiwygModeCompartment.of(wysiwygModeFacet.of(false)),
1613
+ // Invisible characters (whitespace visualization)
1614
+ invisiblesCompartment.of([]),
1026
1615
  ];
1027
1616
 
1028
1617
  // Inject markdown styles
@@ -1092,6 +1681,7 @@ function create(target, options = {}) {
1092
1681
 
1093
1682
  // Event handlers
1094
1683
  const changeHandlers = [];
1684
+ const selectionHandlers = [];
1095
1685
  const saveHandlers = [];
1096
1686
  const frontmatterTitleCommitHandlers = [];
1097
1687
  const viewSourceHandlers = [];
@@ -1219,6 +1809,12 @@ function create(target, options = {}) {
1219
1809
  });
1220
1810
  runtimeLspExtensions.push(completionExt);
1221
1811
 
1812
+ const signatureHelpExt = createRuntimeSignatureHelpExtension({
1813
+ providers: runtimeLspProviders,
1814
+ getContent: () => view.state.doc.toString(),
1815
+ });
1816
+ runtimeLspExtensions.push(signatureHelpExt);
1817
+
1222
1818
  // Add extensions to the view
1223
1819
  view.dispatch({
1224
1820
  effects: StateEffect.appendConfig.of(runtimeLspExtensions),
@@ -1285,6 +1881,7 @@ function create(target, options = {}) {
1285
1881
  // Runtime
1286
1882
  registry,
1287
1883
  execution: null, // Set below
1884
+ linkedTables: null, // Set below
1288
1885
 
1289
1886
  // Runtime LSP (hover, completions, variables)
1290
1887
  runtimeLspProviders,
@@ -1310,6 +1907,36 @@ function create(target, options = {}) {
1310
1907
  return yText;
1311
1908
  },
1312
1909
 
1910
+ getLinkedTableHostContext() {
1911
+ return {
1912
+ ...linkedTableHostContext,
1913
+ ...(this.linkedTables?.getHostContext?.() || {}),
1914
+ };
1915
+ },
1916
+
1917
+ setLinkedTableHostContext(context = {}) {
1918
+ if (context.projectRoot !== undefined) linkedTableHostContext.projectRoot = context.projectRoot;
1919
+ if (context.documentPath !== undefined) linkedTableHostContext.documentPath = context.documentPath;
1920
+ this.linkedTables?.setHostContext?.(linkedTableHostContext);
1921
+ return this.getLinkedTableHostContext();
1922
+ },
1923
+
1924
+ canImportLinkedTableFromHost(hostApi) {
1925
+ return canImportLinkedTableFromHost(hostApi);
1926
+ },
1927
+
1928
+ insertLinkedTableBlock(blockMarkdown, options = {}) {
1929
+ return insertLinkedTableBlock(this, blockMarkdown, options);
1930
+ },
1931
+
1932
+ async importLinkedTableFromHost(sourceFilePath, options = {}) {
1933
+ return importLinkedTableFromHost(this, {
1934
+ ...this.getLinkedTableHostContext(),
1935
+ ...options,
1936
+ sourceFilePath,
1937
+ });
1938
+ },
1939
+
1313
1940
  setContent(text) {
1314
1941
  view.dispatch({
1315
1942
  changes: { from: 0, to: view.state.doc.length, insert: text }
@@ -1335,6 +1962,82 @@ function create(target, options = {}) {
1335
1962
  });
1336
1963
  },
1337
1964
 
1965
+ // ===========================================================================
1966
+ // WYSIWYG Editing Helpers
1967
+ // ===========================================================================
1968
+
1969
+ toggleBold() {
1970
+ return toggleInlineMark(view, 'bold');
1971
+ },
1972
+
1973
+ toggleItalic() {
1974
+ return toggleInlineMark(view, 'italic');
1975
+ },
1976
+
1977
+ toggleUnderline() {
1978
+ return toggleInlineMark(view, 'underline');
1979
+ },
1980
+
1981
+ toggleStrikethrough() {
1982
+ return toggleInlineMark(view, 'strike');
1983
+ },
1984
+
1985
+ toggleInlineCode() {
1986
+ return toggleInlineMark(view, 'code');
1987
+ },
1988
+
1989
+ setBlockType(type, options = {}) {
1990
+ return setCurrentBlockType(view, type, options);
1991
+ },
1992
+
1993
+ insertLink(url, text = null) {
1994
+ return insertLinkAtSelection(view, url, text);
1995
+ },
1996
+
1997
+ insertCodeBlock(language = '') {
1998
+ return insertCodeBlockAtCursor(view, language);
1999
+ },
2000
+
2001
+ /**
2002
+ * Delete the fenced code block surrounding the cursor.
2003
+ * @returns {boolean} true if a code block was found and deleted
2004
+ */
2005
+ deleteCodeBlock() {
2006
+ const pos = view.state.selection.main.head;
2007
+ const fence = findFencedCodeAt(view.state, pos);
2008
+ if (!fence) return false;
2009
+ const doc = view.state.doc;
2010
+ let delFrom = fence.from;
2011
+ let delTo = Math.min(fence.to, doc.length);
2012
+ if (delFrom > 0 && doc.sliceString(delFrom - 1, delFrom) === '\n') delFrom--;
2013
+ if (delTo < doc.length && doc.sliceString(delTo, delTo + 1) === '\n') delTo++;
2014
+ view.dispatch({
2015
+ changes: { from: delFrom, to: delTo, insert: '' },
2016
+ userEvent: 'delete.wysiwyg.delete-codeblock',
2017
+ });
2018
+ return true;
2019
+ },
2020
+
2021
+ insertImage(url, alt = 'image') {
2022
+ return insertImageAtSelection(view, url, alt);
2023
+ },
2024
+
2025
+ getCurrentBlockType() {
2026
+ return getCurrentBlockTypeInfo(view);
2027
+ },
2028
+
2029
+ getSelectionFormatting() {
2030
+ return getSelectionFormattingInfo(view);
2031
+ },
2032
+
2033
+ onSelectionChange(callback) {
2034
+ selectionHandlers.push(callback);
2035
+ return () => {
2036
+ const idx = selectionHandlers.indexOf(callback);
2037
+ if (idx >= 0) selectionHandlers.splice(idx, 1);
2038
+ };
2039
+ },
2040
+
1338
2041
  // ===========================================================================
1339
2042
  // Streaming Writer
1340
2043
  // ===========================================================================
@@ -1381,6 +2084,218 @@ function create(target, options = {}) {
1381
2084
  return getThemeNames();
1382
2085
  },
1383
2086
 
2087
+ /**
2088
+ * Apply a semantic document template to the editor content surface.
2089
+ * This is separate from the app/editor chrome theme.
2090
+ *
2091
+ * @param {object} template
2092
+ * @returns {object} normalized template
2093
+ */
2094
+ setDocumentTemplate(template) {
2095
+ const next = normalizeDocumentTemplate(template || defaultDocumentTemplate);
2096
+ this._documentTemplate = cloneDocumentTemplate(next);
2097
+ this._documentTemplateName = next.name || 'Untitled Template';
2098
+ view.dispatch({
2099
+ effects: documentTemplateCompartment.reconfigure(
2100
+ createDocumentTemplateExtension(next)
2101
+ ),
2102
+ });
2103
+ return this.getDocumentTemplate();
2104
+ },
2105
+
2106
+ getDocumentTemplate() {
2107
+ return cloneDocumentTemplate(this._documentTemplate || initialDocumentTemplate);
2108
+ },
2109
+
2110
+ getDocumentTemplateName() {
2111
+ return this._documentTemplateName || this._documentTemplate?.name || initialDocumentTemplate.name;
2112
+ },
2113
+
2114
+ getDocumentTemplatePresets() {
2115
+ return documentTemplatePresets.map(cloneDocumentTemplate);
2116
+ },
2117
+
2118
+ compileDocumentTemplate(template = null) {
2119
+ return compileDocumentTemplateCSS(template || this.getDocumentTemplate());
2120
+ },
2121
+
2122
+ serializeDocumentTemplate(template = null, scope = '.markdown-body') {
2123
+ return serializeDocumentTemplateToCss(template || this.getDocumentTemplate(), scope);
2124
+ },
2125
+
2126
+ /**
2127
+ * Serialize the current document template to Pandoc YAML metadata.
2128
+ * @param {object} [template]
2129
+ * @returns {string} YAML string
2130
+ */
2131
+ serializeDocumentTemplatePandoc(template = null) {
2132
+ return serializeDocumentTemplateToPandocYaml(template || this.getDocumentTemplate());
2133
+ },
2134
+
2135
+ /**
2136
+ * Serialize the current document template to a LaTeX preamble.
2137
+ * @param {object} [template]
2138
+ * @returns {string} LaTeX commands
2139
+ */
2140
+ serializeDocumentTemplateLatex(template = null) {
2141
+ return serializeDocumentTemplateToLatexPreamble(template || this.getDocumentTemplate());
2142
+ },
2143
+
2144
+ /**
2145
+ * Serialize the current document template to a standalone HTML wrapper.
2146
+ * @param {object} [template]
2147
+ * @param {object} [options]
2148
+ * @returns {string} HTML document string
2149
+ */
2150
+ serializeDocumentTemplateHtml(template = null, options = {}) {
2151
+ return serializeDocumentTemplateToHtml(template || this.getDocumentTemplate(), options);
2152
+ },
2153
+
2154
+ /**
2155
+ * Get a Word style mapping for the current document template.
2156
+ * @param {object} [template]
2157
+ * @returns {object}
2158
+ */
2159
+ getDocumentTemplateWordStyleMap(template = null) {
2160
+ return serializeDocumentTemplateToWordStyleMap(template || this.getDocumentTemplate());
2161
+ },
2162
+
2163
+ /**
2164
+ * Generate a recommended Pandoc command for the current document template.
2165
+ * @param {object} options - { format, input, output, referenceDoc, preambleFile }
2166
+ * @returns {string}
2167
+ */
2168
+ buildPandocCommand(options = {}) {
2169
+ return buildPandocCommand(this.getDocumentTemplate(), options);
2170
+ },
2171
+
2172
+ bindDocumentTemplate(name) {
2173
+ const result = updateFrontmatterField(this.getContent(), 'template', name);
2174
+ if (!result) return false;
2175
+ view.dispatch({
2176
+ changes: result.changes,
2177
+ userEvent: 'input.document-template-binding',
2178
+ });
2179
+ return true;
2180
+ },
2181
+
2182
+ /**
2183
+ * Update section controls configuration.
2184
+ * @param {{enabled?: boolean, showAi?: boolean, showFormatting?: boolean, mode?: 'full' | 'dots-hover' | 'dots-click'}} updates
2185
+ */
2186
+ setSectionControls(updates = {}) {
2187
+ this.config.sectionControls = {
2188
+ ...this.config.sectionControls,
2189
+ ...updates,
2190
+ };
2191
+ },
2192
+
2193
+ /**
2194
+ * Get current section controls configuration.
2195
+ * @returns {{enabled: boolean, showAi: boolean, showFormatting: boolean, mode: string}}
2196
+ */
2197
+ getSectionControls() {
2198
+ return { ...(this.config.sectionControls || {}) };
2199
+ },
2200
+
2201
+ // ===========================================================================
2202
+ // Source Mode, WYSIWYG Mode & Invisible Characters
2203
+ // ===========================================================================
2204
+
2205
+ /** @private */
2206
+ _sourceMode: false,
2207
+ /** @private */
2208
+ _wysiwygMode: false,
2209
+ /** @private */
2210
+ _showInvisibles: false,
2211
+ /** @private */
2212
+ _documentTemplate: cloneDocumentTemplate(initialDocumentTemplate),
2213
+ /** @private */
2214
+ _documentTemplateName: initialDocumentTemplate.name || 'Default',
2215
+
2216
+ /**
2217
+ * Toggle source mode (show all raw markdown syntax).
2218
+ * Mutually exclusive with WYSIWYG mode.
2219
+ *
2220
+ * @param {boolean} [value] - true=on, false=off. Omit to toggle.
2221
+ * @returns {boolean} The new state
2222
+ */
2223
+ setSourceMode(value) {
2224
+ const newValue = value !== undefined ? !!value : !this._sourceMode;
2225
+ this._sourceMode = newValue;
2226
+ if (newValue) this._wysiwygMode = false;
2227
+ view.dispatch({
2228
+ effects: [
2229
+ sourceModeCompartment.reconfigure(sourceModeFacet.of(newValue)),
2230
+ wysiwygModeCompartment.reconfigure(wysiwygModeFacet.of(false)),
2231
+ ],
2232
+ });
2233
+ return newValue;
2234
+ },
2235
+
2236
+ /**
2237
+ * Get current source mode state.
2238
+ * @returns {boolean}
2239
+ */
2240
+ getSourceMode() {
2241
+ return this._sourceMode;
2242
+ },
2243
+
2244
+ /**
2245
+ * Toggle WYSIWYG mode (fully rendered, syntax-protected editing).
2246
+ * Mutually exclusive with source mode.
2247
+ *
2248
+ * @param {boolean} [value] - true=on, false=off. Omit to toggle.
2249
+ * @returns {boolean} The new state
2250
+ */
2251
+ setWysiwygMode(value) {
2252
+ const newValue = value !== undefined ? !!value : !this._wysiwygMode;
2253
+ this._wysiwygMode = newValue;
2254
+ if (newValue) this._sourceMode = false;
2255
+ view.dispatch({
2256
+ effects: [
2257
+ wysiwygModeCompartment.reconfigure(wysiwygModeFacet.of(newValue)),
2258
+ sourceModeCompartment.reconfigure(sourceModeFacet.of(false)),
2259
+ ],
2260
+ });
2261
+ return newValue;
2262
+ },
2263
+
2264
+ /**
2265
+ * Get current WYSIWYG mode state.
2266
+ * @returns {boolean}
2267
+ */
2268
+ getWysiwygMode() {
2269
+ return this._wysiwygMode;
2270
+ },
2271
+
2272
+ /**
2273
+ * Toggle invisible characters (spaces, tabs, newlines).
2274
+ * When enabled, spaces are shown as dots, tabs as arrows, and
2275
+ * newlines as ¶ symbols.
2276
+ *
2277
+ * @param {boolean} [value] - true=on, false=off. Omit to toggle.
2278
+ * @returns {boolean} The new state
2279
+ */
2280
+ setShowInvisibles(value) {
2281
+ const newValue = value !== undefined ? !!value : !this._showInvisibles;
2282
+ this._showInvisibles = newValue;
2283
+ view.dispatch({
2284
+ effects: invisiblesCompartment.reconfigure(
2285
+ newValue ? createInvisiblesExtension() : []
2286
+ ),
2287
+ });
2288
+ return newValue;
2289
+ },
2290
+
2291
+ /**
2292
+ * Get current invisible characters state.
2293
+ * @returns {boolean}
2294
+ */
2295
+ getShowInvisibles() {
2296
+ return this._showInvisibles;
2297
+ },
2298
+
1384
2299
  // ===========================================================================
1385
2300
  // Wiki-link completion
1386
2301
  // ===========================================================================
@@ -2025,14 +2940,13 @@ function create(target, options = {}) {
2025
2940
  },
2026
2941
 
2027
2942
  /**
2028
- * View source code for symbol at cursor position.
2943
+ * Get source code for symbol at cursor position, without emitting UI callbacks.
2029
2944
  * Calls inspect with detail=2 to get full source code.
2030
- * Triggers registered onViewSource callbacks.
2031
2945
  *
2032
2946
  * @param {number} [pos] - Position (defaults to cursor)
2033
2947
  * @returns {Promise<{found: boolean, name?: string, sourceCode?: string, file?: string, ...}|null>}
2034
2948
  */
2035
- async viewSource(pos) {
2949
+ async getSourceInfo(pos) {
2036
2950
  const position = pos ?? view.state.selection.main.head;
2037
2951
  const content = this.getContent();
2038
2952
  const cell = getCellAtCursor(content, position);
@@ -2047,7 +2961,19 @@ function create(target, options = {}) {
2047
2961
  if (!provider) return null;
2048
2962
 
2049
2963
  const offset = position - cell.codeStart;
2050
- const result = await provider.inspect(cell.code, offset, cell.language, { detail: 2 });
2964
+ return provider.inspect(cell.code, offset, cell.language, { detail: 2 });
2965
+ },
2966
+
2967
+ /**
2968
+ * View source code for symbol at cursor position.
2969
+ * Calls inspect with detail=2 to get full source code.
2970
+ * Triggers registered onViewSource callbacks.
2971
+ *
2972
+ * @param {number} [pos] - Position (defaults to cursor)
2973
+ * @returns {Promise<{found: boolean, name?: string, sourceCode?: string, file?: string, ...}|null>}
2974
+ */
2975
+ async viewSource(pos) {
2976
+ const result = await this.getSourceInfo(pos);
2051
2977
 
2052
2978
  // Trigger callbacks if we got a result
2053
2979
  if (result && result.found) {
@@ -2248,6 +3174,9 @@ function create(target, options = {}) {
2248
3174
 
2249
3175
  destroy() {
2250
3176
  this.execution.cancelAll();
3177
+ if (this.linkedTables?.destroy) {
3178
+ this.linkedTables.destroy();
3179
+ }
2251
3180
  if (cellControls) {
2252
3181
  cellControls.destroy();
2253
3182
  }
@@ -2312,6 +3241,13 @@ function create(target, options = {}) {
2312
3241
  // Create execution manager
2313
3242
  api.execution = createExecutionManager(api, registry);
2314
3243
 
3244
+ // Create linked-table action/job controller
3245
+ api.linkedTables = createLinkedTableController({
3246
+ editor: api,
3247
+ projectRoot: linkedTableHostContext.projectRoot,
3248
+ documentPath: linkedTableHostContext.documentPath,
3249
+ });
3250
+
2315
3251
  // Configure keymap now that api is ready
2316
3252
  // Merge user keybindings with defaults
2317
3253
  const userKeybindings = options.keymap || {};
@@ -2347,6 +3283,16 @@ function create(target, options = {}) {
2347
3283
  return { ...currentKeybindings };
2348
3284
  };
2349
3285
 
3286
+ // Initialize section controls now that API exists
3287
+ const applySectionControlsConfig = () => {
3288
+ const options = reactiveConfig.sectionControls || {};
3289
+ const extension = options.enabled === false ? [] : sectionControls(api, options);
3290
+ view.dispatch({
3291
+ effects: sectionControlsCompartment.reconfigure(extension),
3292
+ });
3293
+ };
3294
+ applySectionControlsConfig();
3295
+
2350
3296
  // Wire execution events to awareness (so execution badges work automatically)
2351
3297
  // This makes the runtime appear as a collaborator executing code
2352
3298
  if (awarenessSystem) {
@@ -2502,6 +3448,11 @@ function create(target, options = {}) {
2502
3448
  });
2503
3449
 
2504
3450
  reactiveConfig._subscribe(configHandler);
3451
+ reactiveConfig._subscribe((event) => {
3452
+ if (event.path[0] === 'sectionControls') {
3453
+ applySectionControlsConfig();
3454
+ }
3455
+ });
2505
3456
 
2506
3457
  // =========================================================================
2507
3458
  // UPDATE DOCUMENT STATE
@@ -2534,6 +3485,21 @@ function create(target, options = {}) {
2534
3485
  stateManager.setDirty(true);
2535
3486
  updateDocumentState();
2536
3487
  }
3488
+
3489
+ if (update.docChanged || update.selectionSet) {
3490
+ const payload = {
3491
+ selection: update.state.selection.main,
3492
+ block: getCurrentBlockTypeInfo(update.view),
3493
+ formatting: getSelectionFormattingInfo(update.view),
3494
+ };
3495
+ selectionHandlers.forEach(fn => {
3496
+ try {
3497
+ fn(payload);
3498
+ } catch (err) {
3499
+ console.warn('[mrmd] selection change handler failed:', err);
3500
+ }
3501
+ });
3502
+ }
2537
3503
  });
2538
3504
 
2539
3505
  // Add update listener extension
@@ -3040,22 +4006,6 @@ const cellControlsExports = {
3040
4006
  };
3041
4007
  // #endregion CELL_CONTROLS_EXPORTS
3042
4008
 
3043
- // #region RUNTIME_CODELENS_EXPORTS
3044
- const runtimeCodeLensExports = {
3045
- createExtensions: createRuntimeCodeLensExtensions,
3046
- rebuild: rebuildRuntimeCodeLens,
3047
- facet: runtimeCodeLensFacet,
3048
- plugin: runtimeCodeLensPlugin,
3049
- rebuildEffect: rebuildRuntimeCodeLensEffect,
3050
- injectStyles: injectRuntimeCodeLensStyles,
3051
- removeStyles: removeRuntimeCodeLensStyles,
3052
- findBlocks: findRuntimeBlocks,
3053
- findYamlConfigBlocks,
3054
- findSessionFrontmatter,
3055
- Widget: RuntimeCodeLensWidget,
3056
- };
3057
- // #endregion RUNTIME_CODELENS_EXPORTS
3058
-
3059
4009
  // #region RUNTIME_LSP_EXPORTS
3060
4010
  const runtimeLspExports = {
3061
4011
  // Adapters
@@ -3083,6 +4033,16 @@ const markdownExports = {
3083
4033
  // Asset resolver facet (for Electron/desktop apps)
3084
4034
  assetResolverFacet,
3085
4035
 
4036
+ // Mode facets
4037
+ sourceModeFacet,
4038
+ wysiwygModeFacet,
4039
+ createInlineEditingExtensions,
4040
+ createWysiwygExtensions,
4041
+ toggleInlineFormat,
4042
+ toggleInlineMark,
4043
+ getSelectionFormattingState,
4044
+ findFencedCodeAt,
4045
+
3086
4046
  // Styles
3087
4047
  markdownStyles,
3088
4048
  injectMarkdownStyles,
@@ -3098,6 +4058,28 @@ const markdownExports = {
3098
4058
  isTableDelimiter,
3099
4059
  generateTableId,
3100
4060
  AlertTitleWidget,
4061
+
4062
+ // Page view pagination
4063
+ pageViewPagination,
4064
+ };
4065
+
4066
+ const documentTemplateExports = {
4067
+ defaultDocumentTemplate,
4068
+ documentTemplatePresets,
4069
+ normalizeDocumentTemplate,
4070
+ cloneDocumentTemplate,
4071
+ createDocumentTemplateExtension,
4072
+ compileDocumentTemplateCSS,
4073
+ serializeDocumentTemplateToCss,
4074
+ findDocumentTemplatePreset,
4075
+ // Multi-format export
4076
+ resolveFontForExport,
4077
+ serializeDocumentTemplateToPandocMeta,
4078
+ serializeDocumentTemplateToPandocYaml,
4079
+ serializeDocumentTemplateToLatexPreamble,
4080
+ serializeDocumentTemplateToHtml,
4081
+ serializeDocumentTemplateToWordStyleMap,
4082
+ buildPandocCommand,
3101
4083
  };
3102
4084
  // #endregion MARKDOWN_EXPORTS
3103
4085
 
@@ -3132,14 +4114,22 @@ const mrmd = {
3132
4114
  stateUtils: stateExports,
3133
4115
  // Cell controls (run buttons, queue, status)
3134
4116
  cellControls: cellControlsExports,
3135
- // Runtime CodeLens (inline session controls above yaml config blocks)
3136
- runtimeCodeLens: runtimeCodeLensExports,
3137
4117
  // Runtime LSP (hover, completions, variables)
3138
4118
  runtimeLsp: runtimeLspExports,
3139
4119
  // Wiki-link completion ([[internal-links]])
3140
4120
  wikiLink: wikiLinkExports,
4121
+ // Linked tables
4122
+ linkedTables,
3141
4123
  // Markdown rendering (blur→render, focus→source)
3142
4124
  markdown: markdownExports,
4125
+ // Frontmatter utilities
4126
+ frontmatter: {
4127
+ parseFrontmatter,
4128
+ readFrontmatterValue,
4129
+ updateFrontmatterField,
4130
+ },
4131
+ // Document templates (semantic content styling)
4132
+ documentTemplates: documentTemplateExports,
3143
4133
  // Shell (status bar, file management, studio layout)
3144
4134
  shell: shellModule,
3145
4135
  // AI Integration (decorations, state, widgets)
@@ -3184,7 +4174,6 @@ export {
3184
4174
  codemirror,
3185
4175
  terminal,
3186
4176
  awarenessExports as awareness,
3187
- runtimeCodeLensExports as runtimeCodeLens,
3188
4177
  RuntimeRegistry,
3189
4178
  createRuntimeRegistry,
3190
4179
  createJavaScriptRuntime,
@@ -3268,23 +4257,26 @@ export {
3268
4257
  cellControlsExports,
3269
4258
  createCellControls,
3270
4259
  CellControlsSystem,
3271
- // Runtime CodeLens exports
3272
- runtimeCodeLensExports,
3273
- createRuntimeCodeLensExtensions,
3274
- rebuildRuntimeCodeLens,
3275
- runtimeCodeLensFacet,
3276
- runtimeCodeLensPlugin,
3277
- rebuildRuntimeCodeLensEffect,
3278
- injectRuntimeCodeLensStyles,
3279
- removeRuntimeCodeLensStyles,
3280
- findRuntimeBlocks,
3281
- findYamlConfigBlocks,
3282
- findSessionFrontmatter,
3283
- RuntimeCodeLensWidget,
3284
4260
  // Monitor coordination exports
3285
4261
  MonitorCoordination,
3286
4262
  EXECUTION_STATUS,
3287
4263
  createMonitorCoordination,
4264
+ // Linked table exports
4265
+ linkedTables as linkedTablesModule,
4266
+ LINKED_TABLE_EVENT,
4267
+ dispatchLinkedTableAction,
4268
+ openLinkedTableWorkspace,
4269
+ canImportLinkedTableFromHost,
4270
+ normalizeLinkedTableBlockInsertion,
4271
+ insertLinkedTableBlock,
4272
+ importLinkedTableFromHost,
4273
+ TableJobsClient,
4274
+ TABLE_JOB_STATUS,
4275
+ createTableJobsClient,
4276
+ LinkedTableController,
4277
+ createLinkedTableController,
4278
+ createLinkedTableBlockAnchor,
4279
+ resolveLinkedTableBlockAnchor,
3288
4280
  // Markdown rendering exports
3289
4281
  markdownExports,
3290
4282
  markdownRendering as markdown,
@@ -3302,6 +4294,33 @@ export {
3302
4294
  isTableDelimiter,
3303
4295
  generateTableId,
3304
4296
  AlertTitleWidget,
4297
+ // Document template exports
4298
+ documentTemplateExports,
4299
+ defaultDocumentTemplate,
4300
+ documentTemplatePresets,
4301
+ normalizeDocumentTemplate,
4302
+ cloneDocumentTemplate,
4303
+ createDocumentTemplateExtension,
4304
+ compileDocumentTemplateCSS,
4305
+ serializeDocumentTemplateToCss,
4306
+ findDocumentTemplatePreset,
4307
+ resolveFontForExport,
4308
+ serializeDocumentTemplateToPandocMeta,
4309
+ serializeDocumentTemplateToPandocYaml,
4310
+ serializeDocumentTemplateToLatexPreamble,
4311
+ serializeDocumentTemplateToHtml,
4312
+ serializeDocumentTemplateToWordStyleMap,
4313
+ buildPandocCommand,
4314
+ // Spellcheck exports
4315
+ createSpellcheckExtensions,
4316
+ // Grammar exports
4317
+ createLanguageToolDiagnosticsExtension,
4318
+ collectVisibleProseFragments,
4319
+ forceLanguageToolRefresh,
4320
+ refreshLanguageToolDiagnostics,
4321
+ applyFirstLanguageToolSuggestion,
4322
+ getLanguageToolSuggestionMenu,
4323
+ applyLanguageToolSuggestionAt,
3305
4324
  // Wiki-link completion exports
3306
4325
  wikiLinkExports,
3307
4326
  projectFilesFacet,
@@ -3318,5 +4337,5 @@ export const { createStudio, OrchestratorClient, Drive, createDrive, ShellStateM
3318
4337
 
3319
4338
  // Document language detection and frontmatter updater
3320
4339
  export { getDocumentLanguages, getLanguageDisplay, isExecutableLanguage } from './document-languages.js';
3321
- export { parseFrontmatter, updateFrontmatterSession, readFrontmatterSession, getEffectiveSessionConfig } from './frontmatter-updater.js';
4340
+ export { parseFrontmatter, readFrontmatterSession, getEffectiveSessionConfig, readFrontmatterValue, updateFrontmatterField } from './frontmatter-updater.js';
3322
4341
  // #endregion EXPORTS