mrmd-editor 0.7.0 → 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 (61) 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/execution.js +69 -15
  8. package/src/frontmatter-updater.js +204 -74
  9. package/src/grammar.js +758 -0
  10. package/src/index.js +1120 -55
  11. package/src/keymap.js +11 -2
  12. package/src/markdown/block-decorations.js +108 -5
  13. package/src/markdown/facets.js +37 -0
  14. package/src/markdown/html-inline.js +9 -5
  15. package/src/markdown/index.js +13 -3
  16. package/src/markdown/inline-commands.js +256 -0
  17. package/src/markdown/inline-model.js +578 -0
  18. package/src/markdown/inline-state.js +103 -0
  19. package/src/markdown/renderer.js +219 -12
  20. package/src/markdown/styles.js +290 -3
  21. package/src/markdown/widgets/alert-title.js +10 -8
  22. package/src/markdown/widgets/frontmatter.js +0 -6
  23. package/src/markdown/widgets/index.js +1 -0
  24. package/src/markdown/widgets/list-marker.js +29 -0
  25. package/src/markdown/wysiwyg.js +1158 -0
  26. package/src/mrp-types.js +2 -0
  27. package/src/output-widget.js +532 -18
  28. package/src/page-view-pagination.js +127 -0
  29. package/src/runtime-lsp.js +1757 -150
  30. package/src/section-controls/commands.js +617 -0
  31. package/src/section-controls/index.js +63 -0
  32. package/src/section-controls/plugin.js +165 -0
  33. package/src/section-controls/widgets.js +936 -0
  34. package/src/shell/ai-menu.js +11 -0
  35. package/src/shell/components/context-panel.js +572 -0
  36. package/src/shell/components/status-bar.js +218 -8
  37. package/src/shell/dialogs/file-picker.js +211 -0
  38. package/src/shell/layouts/studio.js +229 -14
  39. package/src/shell/orchestrator-client.js +114 -0
  40. package/src/shell/styles.js +62 -0
  41. package/src/spellcheck.js +166 -0
  42. package/src/tables/README.md +97 -0
  43. package/src/tables/commands/insert-linked-table.js +122 -0
  44. package/src/tables/commands/open-table-workspace.js +43 -0
  45. package/src/tables/index.js +24 -0
  46. package/src/tables/jobs/client.js +158 -0
  47. package/src/tables/parsing/anchors.js +82 -0
  48. package/src/tables/parsing/linked-table-blocks.js +61 -0
  49. package/src/tables/state/linked-table-state.js +68 -0
  50. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  51. package/src/tables/widgets/linked-table-widget.js +256 -0
  52. package/src/tables/workspace/controller.js +616 -0
  53. package/src/term-pty-client.js +111 -7
  54. package/src/term-widget.js +43 -3
  55. package/src/widgets/theme-utils.js +24 -16
  56. package/src/widgets/theme.js +1535 -1
  57. package/src/runtime-codelens/detector.js +0 -279
  58. package/src/runtime-codelens/index.js +0 -76
  59. package/src/runtime-codelens/plugin.js +0 -142
  60. package/src/runtime-codelens/styles.js +0 -184
  61. 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.
@@ -791,6 +945,457 @@ class Writer {
791
945
  }
792
946
  // #endregion WRITER
793
947
 
948
+ // #region INITIAL_CURSOR
949
+ /**
950
+ * Find the ideal initial cursor position for a markdown document.
951
+ *
952
+ * When opening a file, placing the cursor at position 0 shows raw frontmatter
953
+ * YAML which looks ugly. Instead, we find the first empty line after any
954
+ * frontmatter block — this causes the frontmatter to render as a nice widget
955
+ * and gives a clean first impression.
956
+ *
957
+ * @param {string} content - Document content
958
+ * @returns {number} Character position for the cursor
959
+ */
960
+ function findInitialCursorPosition(content) {
961
+ if (!content) return 0;
962
+
963
+ const lines = content.split('\n');
964
+ let i = 0;
965
+
966
+ // Skip YAML frontmatter if present (--- ... ---)
967
+ if (lines[0]?.trim() === '---') {
968
+ i = 1;
969
+ while (i < lines.length && lines[i]?.trim() !== '---') {
970
+ i++;
971
+ }
972
+ if (i < lines.length) i++; // skip closing ---
973
+ }
974
+
975
+ // Find first empty line from current position
976
+ while (i < lines.length) {
977
+ if (lines[i]?.trim() === '') {
978
+ // Calculate character position (start of this empty line)
979
+ let pos = 0;
980
+ for (let j = 0; j < i; j++) {
981
+ pos += lines[j].length + 1; // +1 for \n
982
+ }
983
+ return pos;
984
+ }
985
+ i++;
986
+ }
987
+
988
+ return 0; // fallback to start
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
+ }
1397
+ // #endregion INITIAL_CURSOR
1398
+
794
1399
  // #region CREATE
795
1400
  /**
796
1401
  * Create a standalone markdown editor
@@ -825,6 +1430,7 @@ function create(target, options = {}) {
825
1430
  const dark = config.appearance.dark;
826
1431
  const placeholderText = config.appearance.placeholder;
827
1432
  const readonly = config.appearance.readonly;
1433
+ const spellcheck = config.appearance.spellcheck;
828
1434
  const userName = config.user.name;
829
1435
  const userColor = config.user.color;
830
1436
  const userType = config.user.type;
@@ -839,6 +1445,11 @@ function create(target, options = {}) {
839
1445
  awarenessUI = true,
840
1446
  } = options;
841
1447
 
1448
+ const linkedTableHostContext = {
1449
+ projectRoot: options.projectRoot || null,
1450
+ documentPath: options.documentPath || null,
1451
+ };
1452
+
842
1453
  // Runtimes from normalized config
843
1454
  const runtimes = {};
844
1455
  for (const [name, rtConfig] of Object.entries(config.runtimes)) {
@@ -891,10 +1502,16 @@ function create(target, options = {}) {
891
1502
 
892
1503
  // Always read initial content from Yjs (source of truth)
893
1504
  const initialContent = yText.toString();
1505
+ const initialDocumentTemplate = normalizeDocumentTemplate(options.documentTemplate || defaultDocumentTemplate);
894
1506
  const themeCompartment = new Compartment();
1507
+ const documentTemplateCompartment = new Compartment();
895
1508
  const readonlyCompartment = new Compartment();
896
1509
  const keymapCompartment = new Compartment();
897
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();
898
1515
 
899
1516
  // Create UndoManager for undo/redo tracking
900
1517
  // We create it ourselves so we can listen to stack changes
@@ -933,7 +1550,7 @@ function create(target, options = {}) {
933
1550
  // Use explicit theme if set, otherwise auto-select based on dark mode
934
1551
  const resolveThemeName = (theme, isDarkMode) => {
935
1552
  if (theme) return theme;
936
- return isDarkMode ? 'midnight' : 'daylight';
1553
+ return 'plain-light';
937
1554
  };
938
1555
  const initialThemeName = resolveThemeName(config.appearance?.theme, isDark);
939
1556
 
@@ -959,8 +1576,11 @@ function create(target, options = {}) {
959
1576
  documentTheme,
960
1577
  codeBlockBackground, // Add gray background to code blocks
961
1578
  codeBlockStyles,
1579
+ // Spellcheck: enable browser-native spellcheck on prose, disable in code
1580
+ ...(spellcheck !== false ? createSpellcheckExtensions() : []),
962
1581
  EditorView.lineWrapping, // Always wrap markdown text
963
1582
  themeCompartment.of(initialCMTheme),
1583
+ documentTemplateCompartment.of(createDocumentTemplateExtension(initialDocumentTemplate)),
964
1584
  readonlyCompartment.of(readonly ? EditorState.readOnly.of(true) : []),
965
1585
  placeholderText ? placeholder(placeholderText) : [],
966
1586
  // Yjs collaboration - y-codemirror.next handles sync and undo
@@ -972,13 +1592,26 @@ function create(target, options = {}) {
972
1592
  // Initially empty, configured after api is created
973
1593
  keymapCompartment.of([]),
974
1594
  outputWidgetPlugin, // ANSI output rendering
1595
+ ...createInlineEditingExtensions(),
975
1596
  lineHeightTracker, // ViewPlugin: tracks line height for spacer calculations
1597
+ linkedTableMarkdownState,
976
1598
  blockDecorations, // StateField for tables, display math (multi-line)
977
1599
  markdownRenderer, // ViewPlugin for everything else (inline)
1600
+ pageViewPagination, // ViewPlugin: page-view spacers at page boundaries
1601
+ ...createWysiwygExtensions(),
1602
+ ...commentSyntaxModule.createCommentSyntaxExtension(),
978
1603
  // Wiki-link completion - just the facet for project files
979
1604
  // The actual completion is provided by runtime-lsp (via additionalSources)
980
1605
  // or by a standalone autocompletion added below if no runtime providers exist
981
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([]),
982
1615
  ];
983
1616
 
984
1617
  // Inject markdown styles
@@ -1048,6 +1681,7 @@ function create(target, options = {}) {
1048
1681
 
1049
1682
  // Event handlers
1050
1683
  const changeHandlers = [];
1684
+ const selectionHandlers = [];
1051
1685
  const saveHandlers = [];
1052
1686
  const frontmatterTitleCommitHandlers = [];
1053
1687
  const viewSourceHandlers = [];
@@ -1175,6 +1809,12 @@ function create(target, options = {}) {
1175
1809
  });
1176
1810
  runtimeLspExtensions.push(completionExt);
1177
1811
 
1812
+ const signatureHelpExt = createRuntimeSignatureHelpExtension({
1813
+ providers: runtimeLspProviders,
1814
+ getContent: () => view.state.doc.toString(),
1815
+ });
1816
+ runtimeLspExtensions.push(signatureHelpExt);
1817
+
1178
1818
  // Add extensions to the view
1179
1819
  view.dispatch({
1180
1820
  effects: StateEffect.appendConfig.of(runtimeLspExtensions),
@@ -1241,6 +1881,7 @@ function create(target, options = {}) {
1241
1881
  // Runtime
1242
1882
  registry,
1243
1883
  execution: null, // Set below
1884
+ linkedTables: null, // Set below
1244
1885
 
1245
1886
  // Runtime LSP (hover, completions, variables)
1246
1887
  runtimeLspProviders,
@@ -1266,6 +1907,36 @@ function create(target, options = {}) {
1266
1907
  return yText;
1267
1908
  },
1268
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
+
1269
1940
  setContent(text) {
1270
1941
  view.dispatch({
1271
1942
  changes: { from: 0, to: view.state.doc.length, insert: text }
@@ -1291,6 +1962,82 @@ function create(target, options = {}) {
1291
1962
  });
1292
1963
  },
1293
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
+
1294
2041
  // ===========================================================================
1295
2042
  // Streaming Writer
1296
2043
  // ===========================================================================
@@ -1337,6 +2084,218 @@ function create(target, options = {}) {
1337
2084
  return getThemeNames();
1338
2085
  },
1339
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
+
1340
2299
  // ===========================================================================
1341
2300
  // Wiki-link completion
1342
2301
  // ===========================================================================
@@ -1981,14 +2940,13 @@ function create(target, options = {}) {
1981
2940
  },
1982
2941
 
1983
2942
  /**
1984
- * View source code for symbol at cursor position.
2943
+ * Get source code for symbol at cursor position, without emitting UI callbacks.
1985
2944
  * Calls inspect with detail=2 to get full source code.
1986
- * Triggers registered onViewSource callbacks.
1987
2945
  *
1988
2946
  * @param {number} [pos] - Position (defaults to cursor)
1989
2947
  * @returns {Promise<{found: boolean, name?: string, sourceCode?: string, file?: string, ...}|null>}
1990
2948
  */
1991
- async viewSource(pos) {
2949
+ async getSourceInfo(pos) {
1992
2950
  const position = pos ?? view.state.selection.main.head;
1993
2951
  const content = this.getContent();
1994
2952
  const cell = getCellAtCursor(content, position);
@@ -2003,7 +2961,19 @@ function create(target, options = {}) {
2003
2961
  if (!provider) return null;
2004
2962
 
2005
2963
  const offset = position - cell.codeStart;
2006
- 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);
2007
2977
 
2008
2978
  // Trigger callbacks if we got a result
2009
2979
  if (result && result.found) {
@@ -2204,6 +3174,9 @@ function create(target, options = {}) {
2204
3174
 
2205
3175
  destroy() {
2206
3176
  this.execution.cancelAll();
3177
+ if (this.linkedTables?.destroy) {
3178
+ this.linkedTables.destroy();
3179
+ }
2207
3180
  if (cellControls) {
2208
3181
  cellControls.destroy();
2209
3182
  }
@@ -2268,6 +3241,13 @@ function create(target, options = {}) {
2268
3241
  // Create execution manager
2269
3242
  api.execution = createExecutionManager(api, registry);
2270
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
+
2271
3251
  // Configure keymap now that api is ready
2272
3252
  // Merge user keybindings with defaults
2273
3253
  const userKeybindings = options.keymap || {};
@@ -2303,6 +3283,16 @@ function create(target, options = {}) {
2303
3283
  return { ...currentKeybindings };
2304
3284
  };
2305
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
+
2306
3296
  // Wire execution events to awareness (so execution badges work automatically)
2307
3297
  // This makes the runtime appear as a collaborator executing code
2308
3298
  if (awarenessSystem) {
@@ -2458,6 +3448,11 @@ function create(target, options = {}) {
2458
3448
  });
2459
3449
 
2460
3450
  reactiveConfig._subscribe(configHandler);
3451
+ reactiveConfig._subscribe((event) => {
3452
+ if (event.path[0] === 'sectionControls') {
3453
+ applySectionControlsConfig();
3454
+ }
3455
+ });
2461
3456
 
2462
3457
  // =========================================================================
2463
3458
  // UPDATE DOCUMENT STATE
@@ -2490,6 +3485,21 @@ function create(target, options = {}) {
2490
3485
  stateManager.setDirty(true);
2491
3486
  updateDocumentState();
2492
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
+ }
2493
3503
  });
2494
3504
 
2495
3505
  // Add update listener extension
@@ -2996,22 +4006,6 @@ const cellControlsExports = {
2996
4006
  };
2997
4007
  // #endregion CELL_CONTROLS_EXPORTS
2998
4008
 
2999
- // #region RUNTIME_CODELENS_EXPORTS
3000
- const runtimeCodeLensExports = {
3001
- createExtensions: createRuntimeCodeLensExtensions,
3002
- rebuild: rebuildRuntimeCodeLens,
3003
- facet: runtimeCodeLensFacet,
3004
- plugin: runtimeCodeLensPlugin,
3005
- rebuildEffect: rebuildRuntimeCodeLensEffect,
3006
- injectStyles: injectRuntimeCodeLensStyles,
3007
- removeStyles: removeRuntimeCodeLensStyles,
3008
- findBlocks: findRuntimeBlocks,
3009
- findYamlConfigBlocks,
3010
- findSessionFrontmatter,
3011
- Widget: RuntimeCodeLensWidget,
3012
- };
3013
- // #endregion RUNTIME_CODELENS_EXPORTS
3014
-
3015
4009
  // #region RUNTIME_LSP_EXPORTS
3016
4010
  const runtimeLspExports = {
3017
4011
  // Adapters
@@ -3039,6 +4033,16 @@ const markdownExports = {
3039
4033
  // Asset resolver facet (for Electron/desktop apps)
3040
4034
  assetResolverFacet,
3041
4035
 
4036
+ // Mode facets
4037
+ sourceModeFacet,
4038
+ wysiwygModeFacet,
4039
+ createInlineEditingExtensions,
4040
+ createWysiwygExtensions,
4041
+ toggleInlineFormat,
4042
+ toggleInlineMark,
4043
+ getSelectionFormattingState,
4044
+ findFencedCodeAt,
4045
+
3042
4046
  // Styles
3043
4047
  markdownStyles,
3044
4048
  injectMarkdownStyles,
@@ -3054,6 +4058,28 @@ const markdownExports = {
3054
4058
  isTableDelimiter,
3055
4059
  generateTableId,
3056
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,
3057
4083
  };
3058
4084
  // #endregion MARKDOWN_EXPORTS
3059
4085
 
@@ -3076,6 +4102,7 @@ const mrmd = {
3076
4102
  create,
3077
4103
  drive,
3078
4104
  runtime,
4105
+ findInitialCursorPosition,
3079
4106
  yjs,
3080
4107
  codemirror,
3081
4108
  terminal,
@@ -3087,14 +4114,22 @@ const mrmd = {
3087
4114
  stateUtils: stateExports,
3088
4115
  // Cell controls (run buttons, queue, status)
3089
4116
  cellControls: cellControlsExports,
3090
- // Runtime CodeLens (inline session controls above yaml config blocks)
3091
- runtimeCodeLens: runtimeCodeLensExports,
3092
4117
  // Runtime LSP (hover, completions, variables)
3093
4118
  runtimeLsp: runtimeLspExports,
3094
4119
  // Wiki-link completion ([[internal-links]])
3095
4120
  wikiLink: wikiLinkExports,
4121
+ // Linked tables
4122
+ linkedTables,
3096
4123
  // Markdown rendering (blur→render, focus→source)
3097
4124
  markdown: markdownExports,
4125
+ // Frontmatter utilities
4126
+ frontmatter: {
4127
+ parseFrontmatter,
4128
+ readFrontmatterValue,
4129
+ updateFrontmatterField,
4130
+ },
4131
+ // Document templates (semantic content styling)
4132
+ documentTemplates: documentTemplateExports,
3098
4133
  // Shell (status bar, file management, studio layout)
3099
4134
  shell: shellModule,
3100
4135
  // AI Integration (decorations, state, widgets)
@@ -3134,11 +4169,11 @@ export {
3134
4169
  create,
3135
4170
  drive,
3136
4171
  runtime,
4172
+ findInitialCursorPosition,
3137
4173
  yjs,
3138
4174
  codemirror,
3139
4175
  terminal,
3140
4176
  awarenessExports as awareness,
3141
- runtimeCodeLensExports as runtimeCodeLens,
3142
4177
  RuntimeRegistry,
3143
4178
  createRuntimeRegistry,
3144
4179
  createJavaScriptRuntime,
@@ -3222,23 +4257,26 @@ export {
3222
4257
  cellControlsExports,
3223
4258
  createCellControls,
3224
4259
  CellControlsSystem,
3225
- // Runtime CodeLens exports
3226
- runtimeCodeLensExports,
3227
- createRuntimeCodeLensExtensions,
3228
- rebuildRuntimeCodeLens,
3229
- runtimeCodeLensFacet,
3230
- runtimeCodeLensPlugin,
3231
- rebuildRuntimeCodeLensEffect,
3232
- injectRuntimeCodeLensStyles,
3233
- removeRuntimeCodeLensStyles,
3234
- findRuntimeBlocks,
3235
- findYamlConfigBlocks,
3236
- findSessionFrontmatter,
3237
- RuntimeCodeLensWidget,
3238
4260
  // Monitor coordination exports
3239
4261
  MonitorCoordination,
3240
4262
  EXECUTION_STATUS,
3241
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,
3242
4280
  // Markdown rendering exports
3243
4281
  markdownExports,
3244
4282
  markdownRendering as markdown,
@@ -3256,6 +4294,33 @@ export {
3256
4294
  isTableDelimiter,
3257
4295
  generateTableId,
3258
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,
3259
4324
  // Wiki-link completion exports
3260
4325
  wikiLinkExports,
3261
4326
  projectFilesFacet,
@@ -3272,5 +4337,5 @@ export const { createStudio, OrchestratorClient, Drive, createDrive, ShellStateM
3272
4337
 
3273
4338
  // Document language detection and frontmatter updater
3274
4339
  export { getDocumentLanguages, getLanguageDisplay, isExecutableLanguage } from './document-languages.js';
3275
- export { parseFrontmatter, updateFrontmatterSession, readFrontmatterSession, getEffectiveSessionConfig } from './frontmatter-updater.js';
4340
+ export { parseFrontmatter, readFrontmatterSession, getEffectiveSessionConfig, readFrontmatterValue, updateFrontmatterField } from './frontmatter-updater.js';
3276
4341
  // #endregion EXPORTS