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.
- package/package.json +3 -1
- package/src/commands.js +112 -4
- package/src/comment-syntax.js +364 -39
- package/src/config/handlers.js +1 -2
- package/src/config/schema.js +46 -4
- package/src/document-template.js +2236 -0
- package/src/frontmatter-updater.js +204 -74
- package/src/grammar.js +758 -0
- package/src/index.js +1074 -55
- package/src/keymap.js +11 -2
- package/src/markdown/block-decorations.js +108 -5
- package/src/markdown/facets.js +37 -0
- package/src/markdown/html-inline.js +9 -5
- package/src/markdown/index.js +13 -3
- package/src/markdown/inline-commands.js +256 -0
- package/src/markdown/inline-model.js +578 -0
- package/src/markdown/inline-state.js +103 -0
- package/src/markdown/renderer.js +219 -12
- package/src/markdown/styles.js +290 -3
- package/src/markdown/widgets/alert-title.js +10 -8
- package/src/markdown/widgets/frontmatter.js +0 -6
- package/src/markdown/widgets/index.js +1 -0
- package/src/markdown/widgets/list-marker.js +29 -0
- package/src/markdown/wysiwyg.js +1158 -0
- package/src/mrp-types.js +2 -0
- package/src/output-widget.js +532 -18
- package/src/page-view-pagination.js +127 -0
- package/src/runtime-lsp.js +1757 -150
- package/src/section-controls/commands.js +617 -0
- package/src/section-controls/index.js +63 -0
- package/src/section-controls/plugin.js +165 -0
- package/src/section-controls/widgets.js +936 -0
- package/src/shell/ai-menu.js +11 -0
- package/src/shell/components/context-panel.js +572 -0
- package/src/shell/components/status-bar.js +10 -2
- package/src/shell/layouts/studio.js +206 -14
- package/src/shell/orchestrator-client.js +69 -0
- package/src/spellcheck.js +166 -0
- package/src/tables/README.md +97 -0
- package/src/tables/commands/insert-linked-table.js +122 -0
- package/src/tables/commands/open-table-workspace.js +43 -0
- package/src/tables/index.js +24 -0
- package/src/tables/jobs/client.js +158 -0
- package/src/tables/parsing/anchors.js +82 -0
- package/src/tables/parsing/linked-table-blocks.js +61 -0
- package/src/tables/state/linked-table-state.js +68 -0
- package/src/tables/widgets/linked-table-source-banner.js +77 -0
- package/src/tables/widgets/linked-table-widget.js +256 -0
- package/src/tables/workspace/controller.js +616 -0
- package/src/term-pty-client.js +51 -2
- package/src/term-widget.js +43 -3
- package/src/widgets/theme-utils.js +24 -16
- package/src/widgets/theme.js +1015 -1
- package/src/runtime-codelens/detector.js +0 -279
- package/src/runtime-codelens/index.js +0 -76
- package/src/runtime-codelens/plugin.js +0 -142
- package/src/runtime-codelens/styles.js +0 -184
- package/src/runtime-codelens/widgets.js +0 -216
|
@@ -35,19 +35,17 @@ export function parseFrontmatter(content) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const rawYaml = content.slice(4, endIdx); // Skip opening ---\n
|
|
38
|
-
const endOfClosing = endIdx + 4; // Include \n---
|
|
39
|
-
|
|
40
38
|
try {
|
|
41
39
|
const parsed = yaml.parse(rawYaml) || {};
|
|
42
40
|
return {
|
|
43
41
|
exists: true,
|
|
44
42
|
yaml: parsed,
|
|
45
|
-
range: { start: 0, end:
|
|
43
|
+
range: { start: 0, end: endIdx + 4 },
|
|
46
44
|
raw: rawYaml,
|
|
47
45
|
};
|
|
48
46
|
} catch (e) {
|
|
49
47
|
console.warn('[frontmatter-updater] Failed to parse YAML:', e.message);
|
|
50
|
-
return { exists: true, yaml: null, range: { start: 0, end:
|
|
48
|
+
return { exists: true, yaml: null, range: { start: 0, end: endIdx + 4 }, raw: rawYaml };
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
|
|
@@ -57,8 +55,7 @@ export function parseFrontmatter(content) {
|
|
|
57
55
|
* @param {object} data - Frontmatter data
|
|
58
56
|
* @returns {string} Complete frontmatter block including --- delimiters
|
|
59
57
|
*/
|
|
60
|
-
function buildFrontmatter(data) {
|
|
61
|
-
// Remove empty/null values
|
|
58
|
+
export function buildFrontmatter(data) {
|
|
62
59
|
const cleaned = cleanObject(data);
|
|
63
60
|
|
|
64
61
|
if (!cleaned || Object.keys(cleaned).length === 0) {
|
|
@@ -67,7 +64,7 @@ function buildFrontmatter(data) {
|
|
|
67
64
|
|
|
68
65
|
const yamlStr = yaml.stringify(cleaned, {
|
|
69
66
|
indent: 2,
|
|
70
|
-
lineWidth: 0,
|
|
67
|
+
lineWidth: 0,
|
|
71
68
|
}).trimEnd();
|
|
72
69
|
|
|
73
70
|
return `---\n${yamlStr}\n---`;
|
|
@@ -75,12 +72,14 @@ function buildFrontmatter(data) {
|
|
|
75
72
|
|
|
76
73
|
/**
|
|
77
74
|
* Recursively remove null, undefined, and empty object values.
|
|
75
|
+
*
|
|
78
76
|
* @param {any} obj
|
|
79
77
|
* @returns {any}
|
|
80
78
|
*/
|
|
81
79
|
function cleanObject(obj) {
|
|
82
80
|
if (obj === null || obj === undefined) return undefined;
|
|
83
|
-
if (
|
|
81
|
+
if (Array.isArray(obj)) return obj.map(cloneValue).filter(v => v !== undefined);
|
|
82
|
+
if (typeof obj !== 'object') return obj;
|
|
84
83
|
|
|
85
84
|
const result = {};
|
|
86
85
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -90,6 +89,11 @@ function cleanObject(obj) {
|
|
|
90
89
|
if (cleaned && Object.keys(cleaned).length > 0) {
|
|
91
90
|
result[key] = cleaned;
|
|
92
91
|
}
|
|
92
|
+
} else if (Array.isArray(value)) {
|
|
93
|
+
const cleanedArray = value.map(cloneValue).filter(v => v !== undefined);
|
|
94
|
+
if (cleanedArray.length > 0) {
|
|
95
|
+
result[key] = cleanedArray;
|
|
96
|
+
}
|
|
93
97
|
} else {
|
|
94
98
|
result[key] = value;
|
|
95
99
|
}
|
|
@@ -97,88 +101,152 @@ function cleanObject(obj) {
|
|
|
97
101
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
function isPlainObject(value) {
|
|
105
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cloneValue(value) {
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
return value.map(cloneValue);
|
|
111
|
+
}
|
|
112
|
+
if (isPlainObject(value)) {
|
|
113
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneValue(v)]));
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function mergeTemplateWithExisting(templateValue, existingValue) {
|
|
119
|
+
if (existingValue === undefined || existingValue === null) {
|
|
120
|
+
return cloneValue(templateValue);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(existingValue)) {
|
|
124
|
+
return cloneValue(existingValue);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isPlainObject(templateValue) && isPlainObject(existingValue)) {
|
|
128
|
+
const result = {};
|
|
129
|
+
const keys = new Set([...Object.keys(templateValue), ...Object.keys(existingValue)]);
|
|
130
|
+
for (const key of keys) {
|
|
131
|
+
result[key] = mergeTemplateWithExisting(templateValue?.[key], existingValue?.[key]);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return cloneValue(existingValue);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatLocalDate(date = new Date()) {
|
|
140
|
+
const year = date.getFullYear();
|
|
141
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
142
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
143
|
+
return `${year}-${month}-${day}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createTitleSelection(frontmatterBlock) {
|
|
147
|
+
const prefix = 'title: ';
|
|
148
|
+
const start = frontmatterBlock.indexOf(prefix);
|
|
149
|
+
if (start === -1) return null;
|
|
150
|
+
const from = start + prefix.length;
|
|
151
|
+
const lineEnd = frontmatterBlock.indexOf('\n', from);
|
|
152
|
+
return {
|
|
153
|
+
from,
|
|
154
|
+
to: lineEnd === -1 ? from : lineEnd,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
100
158
|
/**
|
|
101
|
-
*
|
|
159
|
+
* Create a scholarly frontmatter template.
|
|
102
160
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
161
|
+
* @param {Date} [now]
|
|
162
|
+
* @returns {object}
|
|
163
|
+
*/
|
|
164
|
+
export function createArticleFrontmatterTemplate(now = new Date()) {
|
|
165
|
+
return {
|
|
166
|
+
title: 'Untitled',
|
|
167
|
+
date: formatLocalDate(now),
|
|
168
|
+
author: [
|
|
169
|
+
{
|
|
170
|
+
name: 'Your Name',
|
|
171
|
+
id: 'your-id',
|
|
172
|
+
orcid: '0000-0000-0000-0000',
|
|
173
|
+
email: 'you@example.com',
|
|
174
|
+
affiliation: [
|
|
175
|
+
{
|
|
176
|
+
name: 'Your Institution',
|
|
177
|
+
city: 'City',
|
|
178
|
+
state: 'State',
|
|
179
|
+
url: 'https://example.org',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
abstract: 'Write your abstract here.\n',
|
|
185
|
+
keywords: ['Keyword 1', 'Keyword 2'],
|
|
186
|
+
license: 'CC BY',
|
|
187
|
+
copyright: {
|
|
188
|
+
holder: 'Your Name',
|
|
189
|
+
year: now.getFullYear(),
|
|
190
|
+
},
|
|
191
|
+
citation: {
|
|
192
|
+
'container-title': 'Journal or Venue',
|
|
193
|
+
volume: 1,
|
|
194
|
+
issue: 1,
|
|
195
|
+
doi: '10.0000/example',
|
|
196
|
+
},
|
|
197
|
+
funding: 'Add funding information here.',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build a document edit that inserts or augments frontmatter with a template.
|
|
203
|
+
* Existing values win, while missing keys are added from the template.
|
|
105
204
|
*
|
|
106
|
-
* @param {
|
|
107
|
-
* @param {
|
|
108
|
-
* @
|
|
109
|
-
* @param {SessionConfig} [projectDefaults] - Defaults from mrmd.md to diff against
|
|
205
|
+
* @param {string} content - Full document content
|
|
206
|
+
* @param {object} [templateData] - Template frontmatter data
|
|
207
|
+
* @returns {{ changes: {from: number, to: number, insert: string}, selection: {from: number, to: number}|null, data: object }|null}
|
|
110
208
|
*/
|
|
111
|
-
export function
|
|
112
|
-
const doc = view.state.doc;
|
|
113
|
-
const content = doc.toString();
|
|
209
|
+
export function applyFrontmatterTemplate(content, templateData = createArticleFrontmatterTemplate()) {
|
|
114
210
|
const fm = parseFrontmatter(content);
|
|
115
211
|
|
|
116
|
-
|
|
117
|
-
|
|
212
|
+
if (fm.exists && !fm.yaml) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
118
215
|
|
|
119
|
-
|
|
120
|
-
|
|
216
|
+
const merged = fm.exists
|
|
217
|
+
? mergeTemplateWithExisting(templateData, fm.yaml || {})
|
|
218
|
+
: cloneValue(templateData);
|
|
121
219
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (config.venv !== undefined && config.venv !== (projectDefaults.venv || '.venv')) {
|
|
126
|
-
sessionConfig.venv = config.venv;
|
|
127
|
-
}
|
|
128
|
-
if (config.cwd !== undefined && config.cwd !== (projectDefaults.cwd || '.')) {
|
|
129
|
-
sessionConfig.cwd = config.cwd;
|
|
130
|
-
}
|
|
131
|
-
if (config.auto_start !== undefined && config.auto_start !== true) {
|
|
132
|
-
sessionConfig.auto_start = config.auto_start;
|
|
133
|
-
}
|
|
220
|
+
const frontmatterBlock = buildFrontmatter(merged);
|
|
221
|
+
const selection = createTitleSelection(frontmatterBlock);
|
|
134
222
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
223
|
+
if (!fm.exists) {
|
|
224
|
+
return {
|
|
225
|
+
changes: {
|
|
226
|
+
from: 0,
|
|
227
|
+
to: 0,
|
|
228
|
+
insert: `${frontmatterBlock}${content.length > 0 ? '\n\n' : '\n'}`,
|
|
229
|
+
},
|
|
230
|
+
selection,
|
|
231
|
+
data: merged,
|
|
141
232
|
};
|
|
142
|
-
} else {
|
|
143
|
-
// All values match defaults — remove the language key if it exists
|
|
144
|
-
if (data.session && data.session[language]) {
|
|
145
|
-
delete data.session[language];
|
|
146
|
-
if (Object.keys(data.session).length === 0) {
|
|
147
|
-
delete data.session;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
233
|
}
|
|
151
234
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
// Remove frontmatter entirely (and trailing newline if present)
|
|
164
|
-
let removeEnd = fm.range.end;
|
|
165
|
-
if (content[removeEnd] === '\n') removeEnd++;
|
|
166
|
-
view.dispatch({
|
|
167
|
-
changes: { from: fm.range.start, to: removeEnd, insert: '' },
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
} else if (newFrontmatter) {
|
|
171
|
-
// Insert new frontmatter at the top
|
|
172
|
-
view.dispatch({
|
|
173
|
-
changes: { from: 0, to: 0, insert: newFrontmatter + '\n\n' },
|
|
174
|
-
});
|
|
175
|
-
}
|
|
235
|
+
return {
|
|
236
|
+
changes: {
|
|
237
|
+
from: fm.range?.start ?? 0,
|
|
238
|
+
to: fm.range?.end ?? 0,
|
|
239
|
+
insert: frontmatterBlock,
|
|
240
|
+
},
|
|
241
|
+
selection,
|
|
242
|
+
data: merged,
|
|
243
|
+
};
|
|
176
244
|
}
|
|
177
245
|
|
|
178
246
|
/**
|
|
179
247
|
* Read current session configuration from document frontmatter.
|
|
180
248
|
*
|
|
181
|
-
* @param {string} content -
|
|
249
|
+
* @param {string} content - Full document content
|
|
182
250
|
* @param {string} language - Runtime language
|
|
183
251
|
* @returns {SessionConfig} Current session config (may be empty object if using defaults)
|
|
184
252
|
*/
|
|
@@ -208,7 +276,7 @@ export function readFrontmatterSession(content, language) {
|
|
|
208
276
|
* Get the effective session configuration for a document,
|
|
209
277
|
* merging project defaults with document-level overrides.
|
|
210
278
|
*
|
|
211
|
-
* @param {string} content -
|
|
279
|
+
* @param {string} content - Full document content
|
|
212
280
|
* @param {string} language - Runtime language
|
|
213
281
|
* @param {SessionConfig} projectDefaults - Defaults from mrmd.md
|
|
214
282
|
* @returns {SessionConfig} Effective configuration
|
|
@@ -222,3 +290,65 @@ export function getEffectiveSessionConfig(content, language, projectDefaults = {
|
|
|
222
290
|
auto_start: docConfig.auto_start ?? projectDefaults.auto_start ?? true,
|
|
223
291
|
};
|
|
224
292
|
}
|
|
293
|
+
|
|
294
|
+
function setPathValue(target, path, value) {
|
|
295
|
+
const parts = Array.isArray(path) ? path : String(path).split('.').filter(Boolean);
|
|
296
|
+
if (!parts.length) return target;
|
|
297
|
+
|
|
298
|
+
let obj = target;
|
|
299
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
300
|
+
const key = parts[i];
|
|
301
|
+
if (!obj[key] || typeof obj[key] !== 'object' || Array.isArray(obj[key])) {
|
|
302
|
+
obj[key] = {};
|
|
303
|
+
}
|
|
304
|
+
obj = obj[key];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const leaf = parts[parts.length - 1];
|
|
308
|
+
if (value === undefined || value === null || value === '') {
|
|
309
|
+
delete obj[leaf];
|
|
310
|
+
} else {
|
|
311
|
+
obj[leaf] = value;
|
|
312
|
+
}
|
|
313
|
+
return target;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function readFrontmatterValue(content, path, fallback = undefined) {
|
|
317
|
+
const fm = parseFrontmatter(content);
|
|
318
|
+
const parts = Array.isArray(path) ? path : String(path).split('.').filter(Boolean);
|
|
319
|
+
let value = fm.yaml || {};
|
|
320
|
+
for (const part of parts) {
|
|
321
|
+
if (value == null || typeof value !== 'object') return fallback;
|
|
322
|
+
value = value[part];
|
|
323
|
+
}
|
|
324
|
+
return value === undefined ? fallback : value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function updateFrontmatterField(content, path, value) {
|
|
328
|
+
const fm = parseFrontmatter(content);
|
|
329
|
+
if (fm.exists && !fm.yaml) return null;
|
|
330
|
+
|
|
331
|
+
const data = fm.exists ? cloneValue(fm.yaml || {}) : {};
|
|
332
|
+
setPathValue(data, path, value);
|
|
333
|
+
const frontmatterBlock = buildFrontmatter(data);
|
|
334
|
+
|
|
335
|
+
if (!fm.exists) {
|
|
336
|
+
return {
|
|
337
|
+
changes: {
|
|
338
|
+
from: 0,
|
|
339
|
+
to: 0,
|
|
340
|
+
insert: `${frontmatterBlock}${content.length > 0 ? '\n\n' : '\n'}`,
|
|
341
|
+
},
|
|
342
|
+
data,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
changes: {
|
|
348
|
+
from: fm.range?.start ?? 0,
|
|
349
|
+
to: fm.range?.end ?? 0,
|
|
350
|
+
insert: frontmatterBlock,
|
|
351
|
+
},
|
|
352
|
+
data,
|
|
353
|
+
};
|
|
354
|
+
}
|