mrmd-editor 0.5.0 → 0.7.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.
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Frontmatter Widget
3
+ *
4
+ * Renders YAML frontmatter as a styled document header.
5
+ * Shows title, subtitle, author, date, and abstract.
6
+ * Session/runtime config keys are skipped (handled by runtime-codelens).
7
+ *
8
+ * @module markdown/widgets/frontmatter
9
+ */
10
+
11
+ import { WidgetType } from '@codemirror/view';
12
+ import yaml from 'yaml';
13
+
14
+ // Keys that are handled by runtime-codelens (not rendered here)
15
+ const RUNTIME_KEYS = new Set([
16
+ 'session', 'python', 'bash', 'node', 'julia', 'r', 'shell', 'term',
17
+ ]);
18
+
19
+ const TITLE_COMMIT_EVENT = 'mrmd:frontmatter-title-commit';
20
+
21
+ let stylesInjected = false;
22
+
23
+ function injectStyles() {
24
+ if (stylesInjected) return;
25
+ stylesInjected = true;
26
+
27
+ const css = `
28
+ .cm-frontmatter-widget {
29
+ padding: 20px 0 16px;
30
+ margin-bottom: 4px;
31
+ border-bottom: 1px solid var(--frontmatter-border, rgba(128, 128, 128, 0.15));
32
+ line-height: 1.4;
33
+ }
34
+
35
+ .cm-frontmatter-title-row {
36
+ display: flex;
37
+ align-items: baseline;
38
+ gap: 10px;
39
+ margin: 0 0 4px;
40
+ }
41
+
42
+ .cm-frontmatter-title-input {
43
+ flex: 1;
44
+ font-size: 2em;
45
+ font-weight: 700;
46
+ color: var(--text-primary, var(--text, #e0e0e0));
47
+ margin: 0;
48
+ padding: 0;
49
+ width: 100%;
50
+ border: none;
51
+ outline: none;
52
+ background: transparent;
53
+ line-height: 1.15;
54
+ letter-spacing: -0.02em;
55
+ font-family: inherit;
56
+ }
57
+
58
+ .cm-frontmatter-title-input::placeholder {
59
+ color: var(--text-dim, #808080);
60
+ opacity: 0.7;
61
+ }
62
+
63
+ .cm-frontmatter-title-input:focus {
64
+ text-decoration: underline;
65
+ text-underline-offset: 5px;
66
+ text-decoration-color: var(--accent, #4a9eff);
67
+ }
68
+
69
+ .cm-frontmatter-title-hint {
70
+ font-size: 0.7em;
71
+ color: var(--text-dim, #808080);
72
+ white-space: nowrap;
73
+ opacity: 0;
74
+ transition: opacity 120ms ease;
75
+ }
76
+
77
+ .cm-frontmatter-title-row:focus-within .cm-frontmatter-title-hint {
78
+ opacity: 0.8;
79
+ }
80
+
81
+ .cm-frontmatter-subtitle {
82
+ font-size: 1.25em;
83
+ font-weight: 400;
84
+ color: var(--text-secondary, var(--text-muted, #a0a0a0));
85
+ margin: 0 0 12px;
86
+ line-height: 1.3;
87
+ }
88
+
89
+ .cm-frontmatter-authors {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ gap: 6px 16px;
93
+ margin-bottom: 6px;
94
+ }
95
+
96
+ .cm-frontmatter-author {
97
+ font-size: 0.9em;
98
+ color: var(--text, #e0e0e0);
99
+ }
100
+
101
+ .cm-frontmatter-author-affiliation {
102
+ font-size: 0.8em;
103
+ color: var(--text-dim, #808080);
104
+ margin-left: 2px;
105
+ }
106
+
107
+ .cm-frontmatter-date {
108
+ font-size: 0.85em;
109
+ color: var(--text-dim, #808080);
110
+ margin-bottom: 4px;
111
+ }
112
+
113
+ .cm-frontmatter-abstract {
114
+ margin-top: 12px;
115
+ padding: 10px 14px;
116
+ border-left: 3px solid var(--accent, #4a9eff);
117
+ font-size: 0.9em;
118
+ color: var(--text-secondary, var(--text-muted, #a0a0a0));
119
+ line-height: 1.6;
120
+ background: var(--frontmatter-abstract-bg, rgba(128, 128, 128, 0.04));
121
+ border-radius: 0 4px 4px 0;
122
+ }
123
+
124
+ .cm-frontmatter-keywords {
125
+ margin-top: 10px;
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ gap: 6px;
129
+ }
130
+
131
+ .cm-frontmatter-keyword {
132
+ display: inline-block;
133
+ padding: 2px 10px;
134
+ border-radius: 12px;
135
+ font-size: 0.78em;
136
+ background: var(--frontmatter-keyword-bg, rgba(128, 128, 128, 0.12));
137
+ color: var(--text-dim, #808080);
138
+ }
139
+
140
+ .cm-frontmatter-empty {
141
+ padding: 4px 0;
142
+ }
143
+ `;
144
+
145
+ const style = document.createElement('style');
146
+ style.textContent = css;
147
+ document.head.appendChild(style);
148
+ }
149
+
150
+ /**
151
+ * Extract author name(s) from Quarto's various author formats.
152
+ * Supports: string, array of strings, array of objects with name/given/family.
153
+ */
154
+ function extractAuthors(authorVal) {
155
+ if (!authorVal) return [];
156
+ if (typeof authorVal === 'string') return [{ name: authorVal }];
157
+ if (Array.isArray(authorVal)) {
158
+ return authorVal.map(a => {
159
+ if (typeof a === 'string') return { name: a };
160
+ if (a && typeof a === 'object') {
161
+ const name = a.name || [a.given, a.family].filter(Boolean).join(' ') || 'Unknown';
162
+ const affiliation = extractAffiliation(a.affiliation);
163
+ return { name, affiliation };
164
+ }
165
+ return { name: String(a) };
166
+ });
167
+ }
168
+ if (typeof authorVal === 'object') {
169
+ const name = authorVal.name || [authorVal.given, authorVal.family].filter(Boolean).join(' ');
170
+ const affiliation = extractAffiliation(authorVal.affiliation);
171
+ return [{ name, affiliation }];
172
+ }
173
+ return [];
174
+ }
175
+
176
+ function extractAffiliation(aff) {
177
+ if (!aff) return '';
178
+ if (typeof aff === 'string') return aff;
179
+ if (Array.isArray(aff)) {
180
+ return aff.map(a => typeof a === 'string' ? a : a?.name || '').filter(Boolean).join(', ');
181
+ }
182
+ if (typeof aff === 'object') return aff.name || '';
183
+ return '';
184
+ }
185
+
186
+ /**
187
+ * Format a date string for display.
188
+ */
189
+ function formatDate(dateVal) {
190
+ if (!dateVal) return '';
191
+ const str = String(dateVal);
192
+ if (str.toLowerCase() === 'today') {
193
+ return new Date().toLocaleDateString('en-US', {
194
+ year: 'numeric', month: 'long', day: 'numeric',
195
+ });
196
+ }
197
+ const d = new Date(str);
198
+ if (!isNaN(d.getTime())) {
199
+ return d.toLocaleDateString('en-US', {
200
+ year: 'numeric', month: 'long', day: 'numeric',
201
+ });
202
+ }
203
+ return str;
204
+ }
205
+
206
+ /**
207
+ * Extract keywords from various formats.
208
+ */
209
+ function extractKeywords(kw) {
210
+ if (!kw) return [];
211
+ if (Array.isArray(kw)) return kw.map(String);
212
+ if (typeof kw === 'string') return kw.split(/[,;]\s*/).filter(Boolean);
213
+ return [];
214
+ }
215
+
216
+ function escapeYamlDoubleQuoted(value) {
217
+ return String(value)
218
+ .replace(/\\/g, '\\\\')
219
+ .replace(/"/g, '\\"');
220
+ }
221
+
222
+ function replaceFrontmatterTitleInBlock(frontmatterBlock, title) {
223
+ if (!frontmatterBlock || typeof frontmatterBlock !== 'string') return frontmatterBlock;
224
+ const eol = frontmatterBlock.includes('\r\n') ? '\r\n' : '\n';
225
+ if (!frontmatterBlock.startsWith(`---${eol}`) || !frontmatterBlock.endsWith(`${eol}---`)) {
226
+ return frontmatterBlock;
227
+ }
228
+
229
+ const openingLen = `---${eol}`.length;
230
+ const closingLen = `${eol}---`.length;
231
+ const body = frontmatterBlock.slice(openingLen, frontmatterBlock.length - closingLen);
232
+ const escapedTitle = escapeYamlDoubleQuoted(title.trim());
233
+
234
+ let nextBody;
235
+ if (/^\s*title\s*:/mi.test(body)) {
236
+ nextBody = body.replace(/^\s*title\s*:\s*.*$/mi, `title: "${escapedTitle}"`);
237
+ } else if (!body.trim()) {
238
+ nextBody = `title: "${escapedTitle}"`;
239
+ } else {
240
+ nextBody = `title: "${escapedTitle}"${eol}${body}`;
241
+ }
242
+
243
+ return `---${eol}${nextBody}${eol}---`;
244
+ }
245
+
246
+ /**
247
+ * Frontmatter WidgetType for CodeMirror 6.
248
+ * Renders document metadata as a styled header block.
249
+ */
250
+ export class FrontmatterWidget extends WidgetType {
251
+ constructor(yamlContent, contentHash, sourceFrom = null, sourceTo = null) {
252
+ super();
253
+ this.yamlContent = yamlContent;
254
+ this.contentHash = contentHash;
255
+ this.sourceFrom = sourceFrom;
256
+ this.sourceTo = sourceTo;
257
+ this.parsed = null;
258
+
259
+ try {
260
+ this.parsed = yaml.parse(yamlContent);
261
+ } catch (e) {
262
+ // Invalid YAML — will render empty
263
+ }
264
+ }
265
+
266
+ eq(other) {
267
+ return other.contentHash === this.contentHash;
268
+ }
269
+
270
+ commitTitle(view, nextTitle) {
271
+ if (
272
+ !view ||
273
+ typeof this.sourceFrom !== 'number' ||
274
+ typeof this.sourceTo !== 'number'
275
+ ) {
276
+ return false;
277
+ }
278
+
279
+ const trimmed = String(nextTitle || '').trim();
280
+ if (!trimmed) return false;
281
+
282
+ const currentBlock = view.state.doc.sliceString(this.sourceFrom, this.sourceTo);
283
+ const nextBlock = replaceFrontmatterTitleInBlock(currentBlock, trimmed);
284
+ if (!nextBlock || nextBlock === currentBlock) return false;
285
+
286
+ view.dispatch({
287
+ changes: {
288
+ from: this.sourceFrom,
289
+ to: this.sourceTo,
290
+ insert: nextBlock,
291
+ },
292
+ });
293
+
294
+ view.dom.dispatchEvent(new CustomEvent(TITLE_COMMIT_EVENT, {
295
+ bubbles: true,
296
+ detail: { title: trimmed },
297
+ }));
298
+
299
+ return true;
300
+ }
301
+
302
+ toDOM(view) {
303
+ injectStyles();
304
+
305
+ const container = document.createElement('div');
306
+ container.className = 'cm-frontmatter-widget';
307
+
308
+ const p = this.parsed;
309
+ if (!p || typeof p !== 'object') {
310
+ container.classList.add('cm-frontmatter-empty');
311
+ return container;
312
+ }
313
+
314
+ // Check if there's any renderable metadata (skip runtime-only frontmatter)
315
+ const hasMetadata = p.title || p.subtitle || p.author || p.date || p.abstract || p.keywords;
316
+ if (!hasMetadata) {
317
+ container.classList.add('cm-frontmatter-empty');
318
+ return container;
319
+ }
320
+
321
+ const titleRow = document.createElement('div');
322
+ titleRow.className = 'cm-frontmatter-title-row';
323
+
324
+ const titleInput = document.createElement('input');
325
+ titleInput.className = 'cm-frontmatter-title-input';
326
+ titleInput.type = 'text';
327
+ titleInput.placeholder = 'Untitled';
328
+ titleInput.value = p.title ? String(p.title) : '';
329
+ titleInput.setAttribute('aria-label', 'Frontmatter title');
330
+
331
+ const commitTitle = () => {
332
+ const nextTitle = titleInput.value.trim();
333
+ if (!nextTitle) {
334
+ titleInput.value = p.title ? String(p.title) : '';
335
+ return;
336
+ }
337
+ if (nextTitle === (p.title ? String(p.title).trim() : '')) return;
338
+ const committed = this.commitTitle(view, nextTitle);
339
+ if (!committed) {
340
+ titleInput.value = p.title ? String(p.title) : '';
341
+ }
342
+ };
343
+
344
+ titleInput.addEventListener('keydown', (event) => {
345
+ event.stopPropagation();
346
+ if (event.key === 'Enter') {
347
+ event.preventDefault();
348
+ commitTitle();
349
+ view.focus();
350
+ } else if (event.key === 'Escape') {
351
+ event.preventDefault();
352
+ titleInput.value = p.title ? String(p.title) : '';
353
+ titleInput.blur();
354
+ view.focus();
355
+ }
356
+ });
357
+
358
+ titleInput.addEventListener('blur', commitTitle);
359
+ titleInput.addEventListener('mousedown', (event) => event.stopPropagation());
360
+ titleInput.addEventListener('click', (event) => event.stopPropagation());
361
+ titleInput.addEventListener('focus', () => titleInput.select());
362
+
363
+ const hint = document.createElement('span');
364
+ hint.className = 'cm-frontmatter-title-hint';
365
+ hint.textContent = 'Enter to commit';
366
+
367
+ titleRow.appendChild(titleInput);
368
+ titleRow.appendChild(hint);
369
+ container.appendChild(titleRow);
370
+
371
+ // Subtitle
372
+ if (p.subtitle) {
373
+ const el = document.createElement('div');
374
+ el.className = 'cm-frontmatter-subtitle';
375
+ el.textContent = String(p.subtitle);
376
+ container.appendChild(el);
377
+ }
378
+
379
+ // Authors
380
+ const authors = extractAuthors(p.author);
381
+ if (authors.length > 0) {
382
+ const authorsDiv = document.createElement('div');
383
+ authorsDiv.className = 'cm-frontmatter-authors';
384
+
385
+ for (const author of authors) {
386
+ const span = document.createElement('span');
387
+ span.className = 'cm-frontmatter-author';
388
+ span.textContent = author.name;
389
+
390
+ if (author.affiliation) {
391
+ const aff = document.createElement('span');
392
+ aff.className = 'cm-frontmatter-author-affiliation';
393
+ aff.textContent = `(${author.affiliation})`;
394
+ span.appendChild(aff);
395
+ }
396
+
397
+ authorsDiv.appendChild(span);
398
+ }
399
+ container.appendChild(authorsDiv);
400
+ }
401
+
402
+ // Date
403
+ if (p.date) {
404
+ const el = document.createElement('div');
405
+ el.className = 'cm-frontmatter-date';
406
+ el.textContent = formatDate(p.date);
407
+ container.appendChild(el);
408
+ }
409
+
410
+ // Abstract
411
+ if (p.abstract) {
412
+ const el = document.createElement('div');
413
+ el.className = 'cm-frontmatter-abstract';
414
+ el.textContent = String(p.abstract);
415
+ container.appendChild(el);
416
+ }
417
+
418
+ // Keywords
419
+ const keywords = extractKeywords(p.keywords);
420
+ if (keywords.length > 0) {
421
+ const kwContainer = document.createElement('div');
422
+ kwContainer.className = 'cm-frontmatter-keywords';
423
+ for (const kw of keywords) {
424
+ const badge = document.createElement('span');
425
+ badge.className = 'cm-frontmatter-keyword';
426
+ badge.textContent = kw.trim();
427
+ kwContainer.appendChild(badge);
428
+ }
429
+ container.appendChild(kwContainer);
430
+ }
431
+
432
+ return container;
433
+ }
434
+
435
+ ignoreEvent() {
436
+ return false;
437
+ }
438
+ }
@@ -99,11 +99,10 @@ export class MonitorCoordination {
99
99
  * @param {string} options.code - Code to execute
100
100
  * @param {string} options.language - Language identifier
101
101
  * @param {string} options.runtimeUrl - MRP runtime URL
102
- * @param {string} [options.session] - MRP session ID
103
102
  * @param {string} [options.cellId] - Cell ID for tracking
104
103
  * @returns {string} execId
105
104
  */
106
- requestExecution({ code, language, runtimeUrl, session = 'default', cellId }) {
105
+ requestExecution({ code, language, runtimeUrl, cellId }) {
107
106
  const execId = MonitorCoordination.generateExecId();
108
107
 
109
108
  this.executions.set(execId, {
@@ -112,7 +111,6 @@ export class MonitorCoordination {
112
111
  code,
113
112
  language,
114
113
  runtimeUrl,
115
- session,
116
114
  status: EXECUTION_STATUS.REQUESTED,
117
115
  requestedBy: this.clientId,
118
116
  requestedAt: Date.now(),