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.
- 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/execution.js +69 -15
- package/src/frontmatter-updater.js +204 -74
- package/src/grammar.js +758 -0
- package/src/index.js +1120 -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 +218 -8
- package/src/shell/dialogs/file-picker.js +211 -0
- package/src/shell/layouts/studio.js +229 -14
- package/src/shell/orchestrator-client.js +114 -0
- package/src/shell/styles.js +62 -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 +111 -7
- package/src/term-widget.js +43 -3
- package/src/widgets/theme-utils.js +24 -16
- package/src/widgets/theme.js +1535 -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
package/src/execution.js
CHANGED
|
@@ -737,6 +737,9 @@ export class ExecutionManager {
|
|
|
737
737
|
// Emit start event
|
|
738
738
|
this._emit('cellRun', index, cell, execId);
|
|
739
739
|
|
|
740
|
+
// Direct execution output write throttling (reduces CRDT churn for progress bars)
|
|
741
|
+
let clearChunkFlushTimer = null;
|
|
742
|
+
|
|
740
743
|
try {
|
|
741
744
|
// Prepare output position
|
|
742
745
|
// Re-read content as it may have changed
|
|
@@ -815,22 +818,11 @@ export class ExecutionManager {
|
|
|
815
818
|
|
|
816
819
|
// Track current output length in document for replacement
|
|
817
820
|
let currentDocOutputLen = 0;
|
|
821
|
+
const chunkFlushMs = 100;
|
|
822
|
+
let chunkFlushTimer = null;
|
|
823
|
+
let latestProcessedOutput = '';
|
|
818
824
|
|
|
819
|
-
const
|
|
820
|
-
if (controller.signal.aborted) return;
|
|
821
|
-
|
|
822
|
-
// Process through terminal buffer (handles \r, cursor movement, ANSI)
|
|
823
|
-
buffer.write(chunk);
|
|
824
|
-
|
|
825
|
-
// Get processed output with ANSI codes preserved
|
|
826
|
-
let processedOutput = buffer.toAnsi();
|
|
827
|
-
|
|
828
|
-
// Ensure output ends with newline so closing ``` stays on its own line
|
|
829
|
-
// This is critical for maintaining valid markdown structure
|
|
830
|
-
if (processedOutput && !processedOutput.endsWith('\n')) {
|
|
831
|
-
processedOutput += '\n';
|
|
832
|
-
}
|
|
833
|
-
|
|
825
|
+
const applyProcessedOutput = (processedOutput) => {
|
|
834
826
|
// Get current position - prefer finding by execId for robustness
|
|
835
827
|
let currentOutputStart = outputContentStart;
|
|
836
828
|
|
|
@@ -865,6 +857,61 @@ export class ExecutionManager {
|
|
|
865
857
|
});
|
|
866
858
|
|
|
867
859
|
currentDocOutputLen = processedOutput.length;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const flushChunkOutputNow = () => {
|
|
863
|
+
if (chunkFlushTimer) {
|
|
864
|
+
clearTimeout(chunkFlushTimer);
|
|
865
|
+
chunkFlushTimer = null;
|
|
866
|
+
}
|
|
867
|
+
applyProcessedOutput(latestProcessedOutput);
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
clearChunkFlushTimer = () => {
|
|
871
|
+
if (chunkFlushTimer) {
|
|
872
|
+
clearTimeout(chunkFlushTimer);
|
|
873
|
+
chunkFlushTimer = null;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const scheduleChunkOutputFlush = () => {
|
|
878
|
+
if (chunkFlushMs === 0) {
|
|
879
|
+
flushChunkOutputNow();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (chunkFlushTimer) return;
|
|
883
|
+
chunkFlushTimer = setTimeout(() => {
|
|
884
|
+
chunkFlushTimer = null;
|
|
885
|
+
applyProcessedOutput(latestProcessedOutput);
|
|
886
|
+
}, chunkFlushMs);
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
controller.signal.addEventListener('abort', () => {
|
|
890
|
+
clearChunkFlushTimer?.();
|
|
891
|
+
}, { once: true });
|
|
892
|
+
|
|
893
|
+
const onChunk = (chunk, accumulatedRaw, done) => {
|
|
894
|
+
if (controller.signal.aborted) return;
|
|
895
|
+
|
|
896
|
+
// Process through terminal buffer (handles \r, cursor movement, ANSI)
|
|
897
|
+
buffer.write(chunk);
|
|
898
|
+
|
|
899
|
+
// Get processed output with ANSI codes preserved
|
|
900
|
+
let processedOutput = buffer.toAnsi();
|
|
901
|
+
|
|
902
|
+
// Ensure output ends with newline so closing ``` stays on its own line
|
|
903
|
+
// This is critical for maintaining valid markdown structure
|
|
904
|
+
if (processedOutput && !processedOutput.endsWith('\n')) {
|
|
905
|
+
processedOutput += '\n';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
latestProcessedOutput = processedOutput;
|
|
909
|
+
|
|
910
|
+
if (done) {
|
|
911
|
+
flushChunkOutputNow();
|
|
912
|
+
} else {
|
|
913
|
+
scheduleChunkOutputFlush();
|
|
914
|
+
}
|
|
868
915
|
|
|
869
916
|
this._emit('cellOutput', index, chunk, processedOutput, execId);
|
|
870
917
|
};
|
|
@@ -883,6 +930,9 @@ export class ExecutionManager {
|
|
|
883
930
|
return;
|
|
884
931
|
}
|
|
885
932
|
|
|
933
|
+
// Flush pending output so prompt context is visible immediately
|
|
934
|
+
flushChunkOutputNow();
|
|
935
|
+
|
|
886
936
|
const stdinExecId = execId;
|
|
887
937
|
|
|
888
938
|
// Wait for any pending output to be written to the document
|
|
@@ -1009,6 +1059,9 @@ export class ExecutionManager {
|
|
|
1009
1059
|
onAsset,
|
|
1010
1060
|
});
|
|
1011
1061
|
|
|
1062
|
+
// Flush any pending throttled output before final normalization
|
|
1063
|
+
flushChunkOutputNow();
|
|
1064
|
+
|
|
1012
1065
|
// Final update - find output block by execId for robustness
|
|
1013
1066
|
content = this.editor.getContent();
|
|
1014
1067
|
const finalOutputBlock = findOutputBlockByExecId(content, execId);
|
|
@@ -1127,6 +1180,7 @@ export class ExecutionManager {
|
|
|
1127
1180
|
}
|
|
1128
1181
|
return execId;
|
|
1129
1182
|
} finally {
|
|
1183
|
+
clearChunkFlushTimer?.();
|
|
1130
1184
|
this.running.delete(execId);
|
|
1131
1185
|
this.buffers.delete(execId);
|
|
1132
1186
|
|
|
@@ -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
|
+
}
|