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
@@ -15,6 +15,7 @@
15
15
  * @property {string} [cwd] - Working directory
16
16
  * @property {string} [venv] - Virtual environment path
17
17
  * @property {string} [filePath] - Associated file path
18
+ * @property {string} [shell] - Preferred shell (e.g. powershell, wsl, cmd)
18
19
  * @property {Function} [onData] - Callback for data from PTY
19
20
  * @property {Function} [onConnect] - Callback when connected
20
21
  * @property {Function} [onDisconnect] - Callback when disconnected
@@ -36,6 +37,7 @@ export class PtyClient {
36
37
  cwd: config.cwd || null,
37
38
  venv: config.venv || null,
38
39
  filePath: config.filePath || null,
40
+ shell: config.shell || null,
39
41
  onData: config.onData || (() => {}),
40
42
  onConnect: config.onConnect || (() => {}),
41
43
  onDisconnect: config.onDisconnect || (() => {}),
@@ -56,6 +58,10 @@ export class PtyClient {
56
58
  this.maxReconnectAttempts = 10;
57
59
  this.baseReconnectDelay = 1000;
58
60
  this.reconnectTimeout = null;
61
+
62
+ // Small-input batching (typing) to reduce WS frame overhead on tunneled/mobile connections.
63
+ this._writeBuffer = '';
64
+ this._writeTimer = null;
59
65
  }
60
66
 
61
67
  /**
@@ -88,6 +94,9 @@ export class PtyClient {
88
94
  if (this.config.filePath) {
89
95
  params.set('file_path', this.config.filePath);
90
96
  }
97
+ if (this.config.shell) {
98
+ params.set('shell', this.config.shell);
99
+ }
91
100
 
92
101
  return `${protocol}//${host}/api/pty?${params.toString()}`;
93
102
  }
@@ -124,7 +133,20 @@ export class PtyClient {
124
133
  };
125
134
 
126
135
  this.ws.onmessage = (event) => {
127
- this.config.onData(event.data);
136
+ // Handle both text and binary messages. Text is the normal case
137
+ // (PTY sends terminal output as text). Binary may arrive if the
138
+ // tunnel proxy incorrectly marks a frame as binary — convert to
139
+ // string so xterm.js can render it (xterm doesn't handle Blobs).
140
+ const data = event.data;
141
+ if (typeof data === 'string') {
142
+ this.config.onData(data);
143
+ } else if (data instanceof Blob) {
144
+ data.text().then(text => this.config.onData(text));
145
+ } else if (data instanceof ArrayBuffer) {
146
+ this.config.onData(new TextDecoder().decode(data));
147
+ } else {
148
+ this.config.onData(data);
149
+ }
128
150
  };
129
151
 
130
152
  this.ws.onerror = (event) => {
@@ -136,6 +158,11 @@ export class PtyClient {
136
158
  console.log('[PtyClient] Disconnected, code:', event.code);
137
159
  this.connected = false;
138
160
  this.ws = null;
161
+ if (this._writeTimer) {
162
+ clearTimeout(this._writeTimer);
163
+ this._writeTimer = null;
164
+ }
165
+ this._writeBuffer = '';
139
166
  this.config.onDisconnect(event.code, event.reason);
140
167
 
141
168
  // Attempt reconnection if not intentionally closed
@@ -172,16 +199,87 @@ export class PtyClient {
172
199
  }
173
200
 
174
201
  /**
175
- * Send data to the PTY (user input)
202
+ * Flush any buffered small-input writes.
203
+ */
204
+ _flushWriteBuffer() {
205
+ if (!this._writeBuffer) return;
206
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
207
+ this._writeBuffer = '';
208
+ return;
209
+ }
210
+
211
+ const buffered = this._writeBuffer;
212
+ this._writeBuffer = '';
213
+ this.ws.send(JSON.stringify({ type: 'input', data: buffered }));
214
+ }
215
+
216
+ /**
217
+ * Send data to the PTY (user input).
218
+ * Large payloads (pastes) are chunked to avoid overwhelming the terminal
219
+ * renderer and WebSocket tunnel on markco.dev.
220
+ *
176
221
  * @param {string} data - Input data
177
222
  */
178
223
  write(data) {
179
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
180
- this.ws.send(JSON.stringify({
181
- type: 'input',
182
- data: data,
183
- }));
224
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
225
+
226
+ // Small inputs (normal typing/control sequences): buffer briefly so
227
+ // rapid keypresses are batched into fewer WS frames.
228
+ if (data.length <= 4096) {
229
+ this._writeBuffer += data;
230
+ if (!this._writeTimer) {
231
+ this._writeTimer = setTimeout(() => {
232
+ this._writeTimer = null;
233
+ this._flushWriteBuffer();
234
+ }, 8);
235
+ }
236
+ return;
237
+ }
238
+
239
+ // Preserve input ordering: flush any pending small-input batch first.
240
+ if (this._writeTimer) {
241
+ clearTimeout(this._writeTimer);
242
+ this._writeTimer = null;
243
+ }
244
+ this._flushWriteBuffer();
245
+
246
+ // Large paste: chunk it to prevent UI freeze.
247
+ // xterm.js may wrap pastes in bracketed paste markers (\x1b[200~ ... \x1b[201~).
248
+ // We preserve those: send a leading marker once, chunk the body, send trailing marker.
249
+ const BPS = '\x1b[200~';
250
+ const BPE = '\x1b[201~';
251
+ let body = data;
252
+ let hasBracket = false;
253
+
254
+ if (body.startsWith(BPS) && body.endsWith(BPE)) {
255
+ hasBracket = true;
256
+ body = body.slice(BPS.length, -BPE.length);
184
257
  }
258
+
259
+ const CHUNK_SIZE = 2048;
260
+ const CHUNK_DELAY_MS = 6;
261
+ let offset = 0;
262
+ const ws = this.ws;
263
+
264
+ if (hasBracket) {
265
+ ws.send(JSON.stringify({ type: 'input', data: BPS }));
266
+ }
267
+
268
+ const sendNext = () => {
269
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
270
+ if (offset >= body.length) {
271
+ if (hasBracket) {
272
+ ws.send(JSON.stringify({ type: 'input', data: BPE }));
273
+ }
274
+ return;
275
+ }
276
+ const chunk = body.slice(offset, offset + CHUNK_SIZE);
277
+ offset += CHUNK_SIZE;
278
+ ws.send(JSON.stringify({ type: 'input', data: chunk }));
279
+ setTimeout(sendNext, CHUNK_DELAY_MS);
280
+ };
281
+
282
+ sendNext();
185
283
  }
186
284
 
187
285
  /**
@@ -211,6 +309,12 @@ export class PtyClient {
211
309
  this.reconnectTimeout = null;
212
310
  }
213
311
 
312
+ if (this._writeTimer) {
313
+ clearTimeout(this._writeTimer);
314
+ this._writeTimer = null;
315
+ }
316
+ this._writeBuffer = '';
317
+
214
318
  if (this.ws) {
215
319
  this.ws.close();
216
320
  this.ws = null;
@@ -146,15 +146,23 @@ class TerminalWidget extends WidgetType {
146
146
 
147
147
  // Get theme from CSS variables
148
148
  const theme = this._getThemeFromCSS();
149
+ const isMobile = /iPhone|iPad|Android|Mobile/i.test(navigator.userAgent);
149
150
 
150
151
  // Create terminal
151
152
  const term = new Terminal({
152
153
  cursorBlink: true,
153
- fontSize: 14,
154
- fontFamily: '"SF Mono", "Fira Code", "Monaco", "Inconsolata", monospace',
155
- scrollback: 10000,
154
+ cursorStyle: 'block',
155
+ cursorInactiveStyle: 'outline',
156
+ fontSize: isMobile ? 13 : 14,
157
+ fontFamily: '"Monaspace Neon Var", "SF Mono", "Fira Code", "Monaco", "Inconsolata", monospace',
158
+ scrollback: isMobile ? 5000 : 50000,
156
159
  convertEol: true,
157
160
  theme: theme,
161
+ drawBoldTextInBrightColors: !isMobile,
162
+ allowTransparency: false,
163
+ smoothScrollDuration: 0,
164
+ fastScrollModifier: 'alt',
165
+ fastScrollSensitivity: isMobile ? 3 : 5,
158
166
  });
159
167
 
160
168
  this.xtermInstance = term;
@@ -176,6 +184,37 @@ class TerminalWidget extends WidgetType {
176
184
  // Open terminal
177
185
  term.open(container);
178
186
 
187
+ // Renderer addons: prefer WebGL on desktop, fallback to Canvas, then DOM.
188
+ let rendererLoaded = false;
189
+ if (!isMobile && typeof WebglAddon !== 'undefined') {
190
+ try {
191
+ const webgl = new WebglAddon.WebglAddon();
192
+ webgl.onContextLoss(() => {
193
+ try { webgl.dispose(); } catch (e) {}
194
+ console.warn('[term] WebGL context lost, falling back to canvas/DOM');
195
+ if (typeof CanvasAddon !== 'undefined') {
196
+ try {
197
+ term.loadAddon(new CanvasAddon.CanvasAddon());
198
+ } catch (canvasErr) {
199
+ console.warn('[term] Canvas addon failed, using DOM fallback:', canvasErr);
200
+ }
201
+ }
202
+ });
203
+ term.loadAddon(webgl);
204
+ rendererLoaded = true;
205
+ } catch (e) {
206
+ console.warn('[term] WebGL failed:', e);
207
+ }
208
+ }
209
+
210
+ if (!rendererLoaded && typeof CanvasAddon !== 'undefined') {
211
+ try {
212
+ term.loadAddon(new CanvasAddon.CanvasAddon());
213
+ } catch (e) {
214
+ console.warn('[term] Canvas addon failed, using DOM fallback:', e);
215
+ }
216
+ }
217
+
179
218
  // Fit to container
180
219
  if (this.fitAddon) {
181
220
  setTimeout(() => {
@@ -266,6 +305,7 @@ class TerminalWidget extends WidgetType {
266
305
  cwd: this.config.cwd,
267
306
  venv: this.config.venv,
268
307
  filePath: this.block.filePath,
308
+ shell: this.config.shell,
269
309
  onData: (data) => {
270
310
  term.write(data);
271
311
  },
@@ -9,7 +9,7 @@
9
9
  * 1. Explicit config (config.appearance.widgetTheme)
10
10
  * 2. CodeMirror theme class (.cm-theme-dark)
11
11
  * 3. System preference (prefers-color-scheme)
12
- * 4. Default: 'midnight' (dark)
12
+ * 4. Default: 'plain-light'
13
13
  *
14
14
  * ## Watching for Changes
15
15
  *
@@ -20,7 +20,7 @@
20
20
  * @module widgets/theme-utils
21
21
  */
22
22
 
23
- import { getTheme, midnightTheme, daylightTheme } from './theme.js';
23
+ import { getTheme, getDefaultTokens } from './theme.js';
24
24
 
25
25
  // #region DETECTION
26
26
 
@@ -31,12 +31,12 @@ import { getTheme, midnightTheme, daylightTheme } from './theme.js';
31
31
  * 1. Explicit theme name passed as parameter
32
32
  * 2. CodeMirror theme class on the editor element
33
33
  * 3. System color scheme preference
34
- * 4. Default: 'midnight'
34
+ * 4. Default: 'plain-light'
35
35
  *
36
36
  * @param {Object} [options]
37
37
  * @param {string} [options.themeName] - Explicit theme name (highest priority)
38
38
  * @param {HTMLElement} [options.editorElement] - Editor DOM element to check for .cm-theme-dark
39
- * @returns {string} Theme name ('midnight', 'daylight', etc.)
39
+ * @returns {string} Theme name ('plain-light', 'plain-dark', etc.)
40
40
  *
41
41
  * @example
42
42
  * // Detect based on CodeMirror and system preference
@@ -57,25 +57,25 @@ export function detectTheme({ themeName, editorElement } = {}) {
57
57
  // CodeMirror adds this class when using oneDark or similar dark themes
58
58
  const hasDarkTheme = editorElement.closest('.cm-theme-dark') !== null;
59
59
  if (hasDarkTheme) {
60
- return 'midnight';
60
+ return 'plain-dark';
61
61
  }
62
62
 
63
63
  // If there's any cm-theme class but not dark, assume light
64
64
  const hasAnyTheme = editorElement.matches('[class*="cm-theme"]') ||
65
65
  editorElement.closest('[class*="cm-theme"]') !== null;
66
66
  if (hasAnyTheme) {
67
- return 'daylight';
67
+ return 'plain-light';
68
68
  }
69
69
  }
70
70
 
71
71
  // 3. System preference
72
72
  if (typeof window !== 'undefined' && window.matchMedia) {
73
73
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
74
- return prefersDark ? 'midnight' : 'daylight';
74
+ return prefersDark ? 'plain-dark' : 'plain-light';
75
75
  }
76
76
 
77
77
  // 4. Default
78
- return 'midnight';
78
+ return 'plain-light';
79
79
  }
80
80
 
81
81
  /**
@@ -208,7 +208,7 @@ const THEME_FONTS_ID = 'mrmd-widget-theme-fonts';
208
208
  *
209
209
  * @example
210
210
  * // Apply by name
211
- * applyTheme('midnight');
211
+ * applyTheme('plain-light');
212
212
  *
213
213
  * // Apply custom theme object
214
214
  * applyTheme({
@@ -224,12 +224,14 @@ export function applyTheme(themeOrName, { target, useStyleTag = true } = {}) {
224
224
  : themeOrName;
225
225
 
226
226
  if (!theme) {
227
- console.warn(`Theme "${themeOrName}" not found, using midnight`);
228
- return applyTheme('midnight', { target, useStyleTag });
227
+ console.warn(`Theme "${themeOrName}" not found, using plain-light`);
228
+ return applyTheme('plain-light', { target, useStyleTag });
229
229
  }
230
230
 
231
231
  // Get token values (exclude name, description, fontFace, isDark)
232
- const tokens = {};
232
+ // Merge with token defaults so newly added tokens are always present,
233
+ // even for older themes that don't define them yet.
234
+ const tokens = getDefaultTokens();
233
235
  for (const [key, value] of Object.entries(theme)) {
234
236
  if (key.startsWith('--')) {
235
237
  tokens[key] = value;
@@ -330,7 +332,7 @@ export function removeThemeStyles() {
330
332
  * @returns {string} CSS string with :root variables and optional font-face
331
333
  *
332
334
  * @example
333
- * const css = generateThemeCSS('midnight');
335
+ * const css = generateThemeCSS('plain-light');
334
336
  * // :root {
335
337
  * // --widget-surface: rgba(0, 0, 0, 0.35);
336
338
  * // ...
@@ -346,11 +348,17 @@ export function generateThemeCSS(themeOrName, { includeFontFace = true } = {}) {
346
348
  : themeOrName;
347
349
 
348
350
  if (!theme) {
349
- return generateThemeCSS('midnight', { includeFontFace });
351
+ return generateThemeCSS('plain-light', { includeFontFace });
350
352
  }
351
353
 
352
- const vars = Object.entries(theme)
353
- .filter(([k]) => k.startsWith('--'))
354
+ const tokens = getDefaultTokens();
355
+ for (const [key, value] of Object.entries(theme)) {
356
+ if (key.startsWith('--')) {
357
+ tokens[key] = value;
358
+ }
359
+ }
360
+
361
+ const vars = Object.entries(tokens)
354
362
  .map(([k, v]) => ` ${k}: ${v};`)
355
363
  .join('\n');
356
364