mrmd-project 0.1.0 → 0.1.1
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 +2 -2
- package/src/fsml.js +205 -2
- package/src/project.js +136 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mrmd-project",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Pure logic for understanding mrmd project structure and conventions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
32
|
-
"url": "https://github.com/anthropics/mrmd.git",
|
|
32
|
+
"url": "git+https://github.com/anthropics/mrmd.git",
|
|
33
33
|
"directory": "packages/mrmd-project"
|
|
34
34
|
},
|
|
35
35
|
"bugs": {
|
package/src/fsml.js
CHANGED
|
@@ -318,12 +318,13 @@ export function titleFromFilename(filename) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
|
-
* Compute new paths when reordering files
|
|
321
|
+
* Compute new paths when reordering files (simple version)
|
|
322
322
|
*
|
|
323
323
|
* @param {string} sourcePath - Path being moved
|
|
324
324
|
* @param {string} targetPath - Path to move relative to
|
|
325
325
|
* @param {'before' | 'after' | 'inside'} position - Where to place
|
|
326
|
-
* @returns {object} New path and required renames
|
|
326
|
+
* @returns {object} New path and required renames (source only, no sibling shifts)
|
|
327
|
+
* @deprecated Use computeReorder() for full sibling shifting support
|
|
327
328
|
*/
|
|
328
329
|
export function computeNewPath(sourcePath, targetPath, position) {
|
|
329
330
|
const source = parsePath(sourcePath);
|
|
@@ -369,3 +370,205 @@ export function computeNewPath(sourcePath, targetPath, position) {
|
|
|
369
370
|
renames,
|
|
370
371
|
};
|
|
371
372
|
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Compute all renames needed when reordering files with proper sibling shifting.
|
|
376
|
+
*
|
|
377
|
+
* This is the main function for drag-drop reordering. It:
|
|
378
|
+
* 1. Computes the new path for the source
|
|
379
|
+
* 2. Shifts siblings to make room (updates their order prefixes)
|
|
380
|
+
* 3. Returns all renames in the correct execution order
|
|
381
|
+
*
|
|
382
|
+
* @param {string} sourcePath - Path being moved
|
|
383
|
+
* @param {string} targetPath - Path to move relative to (drop target)
|
|
384
|
+
* @param {'before' | 'after' | 'inside'} position - Where to place relative to target
|
|
385
|
+
* @param {string[]} siblings - All paths in the target directory (for shift calculation)
|
|
386
|
+
* @returns {{ newPath: string, renames: Array<{ from: string, to: string }> }}
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* // Drag 02-config.md before 01-intro.md
|
|
390
|
+
* computeReorder('02-config.md', '01-intro.md', 'before', ['01-intro.md', '02-config.md'])
|
|
391
|
+
* // Returns {
|
|
392
|
+
* // newPath: '01-config.md',
|
|
393
|
+
* // renames: [
|
|
394
|
+
* // { from: '01-intro.md', to: '02-intro.md' }, // shift
|
|
395
|
+
* // { from: '02-config.md', to: '01-config.md' } // move
|
|
396
|
+
* // ]
|
|
397
|
+
* // }
|
|
398
|
+
*/
|
|
399
|
+
export function computeReorder(sourcePath, targetPath, position, siblings = []) {
|
|
400
|
+
const source = parsePath(sourcePath);
|
|
401
|
+
const target = parsePath(targetPath);
|
|
402
|
+
|
|
403
|
+
// Determine target directory
|
|
404
|
+
let targetDir = '';
|
|
405
|
+
if (position === 'inside') {
|
|
406
|
+
targetDir = targetPath;
|
|
407
|
+
} else {
|
|
408
|
+
targetDir = target.parent;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Filter siblings to only those in the target directory
|
|
412
|
+
// and parse them for order information
|
|
413
|
+
const siblingsInDir = siblings
|
|
414
|
+
.filter(p => {
|
|
415
|
+
const parsed = parsePath(p);
|
|
416
|
+
return parsed.parent === targetDir;
|
|
417
|
+
})
|
|
418
|
+
.map(p => ({
|
|
419
|
+
path: p,
|
|
420
|
+
...parsePath(p),
|
|
421
|
+
}))
|
|
422
|
+
.filter(s => s.order !== null) // Only numbered items participate in reordering
|
|
423
|
+
.sort((a, b) => a.order - b.order);
|
|
424
|
+
|
|
425
|
+
// Determine what order number the source should get
|
|
426
|
+
let insertOrder;
|
|
427
|
+
if (position === 'inside') {
|
|
428
|
+
// Moving inside a folder - find max order and add 1, or use 1 if empty
|
|
429
|
+
const maxOrder = siblingsInDir.reduce((max, s) => Math.max(max, s.order || 0), 0);
|
|
430
|
+
insertOrder = maxOrder + 1;
|
|
431
|
+
} else if (position === 'before') {
|
|
432
|
+
insertOrder = target.order || 1;
|
|
433
|
+
} else {
|
|
434
|
+
// after
|
|
435
|
+
insertOrder = (target.order || 0) + 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Build new path for source
|
|
439
|
+
const paddedOrder = String(insertOrder).padStart(2, '0');
|
|
440
|
+
const sourceExt = source.isFolder ? '' : source.extension;
|
|
441
|
+
const newFilename = `${paddedOrder}-${source.name}${sourceExt}`;
|
|
442
|
+
const newPath = targetDir ? `${targetDir}/${newFilename}` : newFilename;
|
|
443
|
+
|
|
444
|
+
// Now compute all renames needed
|
|
445
|
+
const renames = [];
|
|
446
|
+
|
|
447
|
+
// Is source currently in the same directory?
|
|
448
|
+
const sourceInSameDir = source.parent === targetDir;
|
|
449
|
+
|
|
450
|
+
// Determine which siblings need to shift
|
|
451
|
+
// We need to shift siblings at or after insertOrder to make room
|
|
452
|
+
// But if source is in same dir and moving "down", logic is different
|
|
453
|
+
|
|
454
|
+
if (sourceInSameDir && source.order !== null) {
|
|
455
|
+
// Source is moving within same directory
|
|
456
|
+
const sourceOrder = source.order;
|
|
457
|
+
|
|
458
|
+
if (sourceOrder < insertOrder) {
|
|
459
|
+
// Moving DOWN (e.g., 01 -> position after 03)
|
|
460
|
+
// Items between old position and new position shift UP (decrease order)
|
|
461
|
+
// insertOrder needs adjustment since source is leaving
|
|
462
|
+
const adjustedInsertOrder = insertOrder - 1;
|
|
463
|
+
|
|
464
|
+
for (const sibling of siblingsInDir) {
|
|
465
|
+
if (sibling.path === sourcePath) continue;
|
|
466
|
+
|
|
467
|
+
if (sibling.order > sourceOrder && sibling.order <= adjustedInsertOrder) {
|
|
468
|
+
// This sibling shifts up (decreases order)
|
|
469
|
+
const newSiblingOrder = sibling.order - 1;
|
|
470
|
+
const siblingExt = sibling.isFolder ? '' : sibling.extension;
|
|
471
|
+
const newSiblingFilename = `${String(newSiblingOrder).padStart(2, '0')}-${sibling.name}${siblingExt}`;
|
|
472
|
+
const newSiblingPath = targetDir ? `${targetDir}/${newSiblingFilename}` : newSiblingFilename;
|
|
473
|
+
|
|
474
|
+
if (sibling.path !== newSiblingPath) {
|
|
475
|
+
renames.push({ from: sibling.path, to: newSiblingPath });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Source gets the adjusted insert position
|
|
481
|
+
const finalSourceOrder = adjustedInsertOrder;
|
|
482
|
+
const finalSourceFilename = `${String(finalSourceOrder).padStart(2, '0')}-${source.name}${sourceExt}`;
|
|
483
|
+
const finalSourcePath = targetDir ? `${targetDir}/${finalSourceFilename}` : finalSourceFilename;
|
|
484
|
+
|
|
485
|
+
if (sourcePath !== finalSourcePath) {
|
|
486
|
+
renames.push({ from: sourcePath, to: finalSourcePath });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { newPath: finalSourcePath, renames: sortRenamesForExecution(renames, 'down') };
|
|
490
|
+
|
|
491
|
+
} else if (sourceOrder > insertOrder) {
|
|
492
|
+
// Moving UP (e.g., 03 -> position before 01)
|
|
493
|
+
// Items at or after insertOrder (up to source's old position) shift DOWN (increase order)
|
|
494
|
+
|
|
495
|
+
for (const sibling of siblingsInDir) {
|
|
496
|
+
if (sibling.path === sourcePath) continue;
|
|
497
|
+
|
|
498
|
+
if (sibling.order >= insertOrder && sibling.order < sourceOrder) {
|
|
499
|
+
// This sibling shifts down (increases order)
|
|
500
|
+
const newSiblingOrder = sibling.order + 1;
|
|
501
|
+
const siblingExt = sibling.isFolder ? '' : sibling.extension;
|
|
502
|
+
const newSiblingFilename = `${String(newSiblingOrder).padStart(2, '0')}-${sibling.name}${siblingExt}`;
|
|
503
|
+
const newSiblingPath = targetDir ? `${targetDir}/${newSiblingFilename}` : newSiblingFilename;
|
|
504
|
+
|
|
505
|
+
if (sibling.path !== newSiblingPath) {
|
|
506
|
+
renames.push({ from: sibling.path, to: newSiblingPath });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Source gets insertOrder
|
|
512
|
+
if (sourcePath !== newPath) {
|
|
513
|
+
renames.push({ from: sourcePath, to: newPath });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { newPath, renames: sortRenamesForExecution(renames, 'up') };
|
|
517
|
+
|
|
518
|
+
} else {
|
|
519
|
+
// Same position - no change needed
|
|
520
|
+
return { newPath: sourcePath, renames: [] };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
} else {
|
|
524
|
+
// Source is moving from different directory (or has no order)
|
|
525
|
+
// All siblings at or after insertOrder need to shift down (increase order)
|
|
526
|
+
|
|
527
|
+
for (const sibling of siblingsInDir) {
|
|
528
|
+
if (sibling.order >= insertOrder) {
|
|
529
|
+
const newSiblingOrder = sibling.order + 1;
|
|
530
|
+
const siblingExt = sibling.isFolder ? '' : sibling.extension;
|
|
531
|
+
const newSiblingFilename = `${String(newSiblingOrder).padStart(2, '0')}-${sibling.name}${siblingExt}`;
|
|
532
|
+
const newSiblingPath = targetDir ? `${targetDir}/${newSiblingFilename}` : newSiblingFilename;
|
|
533
|
+
|
|
534
|
+
if (sibling.path !== newSiblingPath) {
|
|
535
|
+
renames.push({ from: sibling.path, to: newSiblingPath });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Source gets insertOrder
|
|
541
|
+
if (sourcePath !== newPath) {
|
|
542
|
+
renames.push({ from: sourcePath, to: newPath });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return { newPath, renames: sortRenamesForExecution(renames, 'up') };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Sort renames to avoid conflicts during execution.
|
|
551
|
+
*
|
|
552
|
+
* When shifting DOWN (increasing order numbers), rename from highest to lowest
|
|
553
|
+
* to avoid collisions (e.g., rename 02->03 before 01->02).
|
|
554
|
+
*
|
|
555
|
+
* When shifting UP (decreasing order numbers), rename from lowest to highest.
|
|
556
|
+
*
|
|
557
|
+
* @param {Array<{ from: string, to: string }>} renames
|
|
558
|
+
* @param {'up' | 'down'} direction - 'up' = orders increasing, 'down' = orders decreasing
|
|
559
|
+
* @returns {Array<{ from: string, to: string }>}
|
|
560
|
+
*/
|
|
561
|
+
function sortRenamesForExecution(renames, direction) {
|
|
562
|
+
return renames.sort((a, b) => {
|
|
563
|
+
const aOrder = parsePath(a.from).order || 0;
|
|
564
|
+
const bOrder = parsePath(b.from).order || 0;
|
|
565
|
+
|
|
566
|
+
if (direction === 'up') {
|
|
567
|
+
// Shifting down (orders increasing) - rename highest first
|
|
568
|
+
return bOrder - aOrder;
|
|
569
|
+
} else {
|
|
570
|
+
// Shifting up (orders decreasing) - rename lowest first
|
|
571
|
+
return aOrder - bOrder;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
package/src/project.js
CHANGED
|
@@ -17,6 +17,27 @@ const DEFAULTS = {
|
|
|
17
17
|
name: 'default',
|
|
18
18
|
auto_start: true,
|
|
19
19
|
},
|
|
20
|
+
bash: {
|
|
21
|
+
cwd: '.',
|
|
22
|
+
name: 'default',
|
|
23
|
+
auto_start: true,
|
|
24
|
+
},
|
|
25
|
+
r: {
|
|
26
|
+
cwd: '.',
|
|
27
|
+
name: 'default',
|
|
28
|
+
auto_start: true,
|
|
29
|
+
},
|
|
30
|
+
julia: {
|
|
31
|
+
cwd: '.',
|
|
32
|
+
name: 'default',
|
|
33
|
+
auto_start: true,
|
|
34
|
+
},
|
|
35
|
+
term: {
|
|
36
|
+
venv: null, // optional, for venv activation in terminals
|
|
37
|
+
cwd: '.',
|
|
38
|
+
name: 'default',
|
|
39
|
+
auto_start: true,
|
|
40
|
+
},
|
|
20
41
|
},
|
|
21
42
|
assets: {
|
|
22
43
|
directory: '_assets',
|
|
@@ -145,7 +166,8 @@ export function parseFrontmatter(content) {
|
|
|
145
166
|
try {
|
|
146
167
|
const parsed = YAML.parse(yamlContent);
|
|
147
168
|
if (parsed && typeof parsed === 'object') {
|
|
148
|
-
|
|
169
|
+
// Normalize shorthand syntax to full config structure
|
|
170
|
+
return normalizeFrontmatter(parsed);
|
|
149
171
|
}
|
|
150
172
|
return null;
|
|
151
173
|
} catch (e) {
|
|
@@ -154,6 +176,60 @@ export function parseFrontmatter(content) {
|
|
|
154
176
|
}
|
|
155
177
|
}
|
|
156
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Normalize shorthand frontmatter syntax to full config structure.
|
|
181
|
+
*
|
|
182
|
+
* Supports minimal syntax like:
|
|
183
|
+
* python: .venv -> session: { python: { venv: '.venv' } }
|
|
184
|
+
* python: { venv: .venv } -> session: { python: { venv: '.venv' } }
|
|
185
|
+
* bash: { cwd: ./src } -> session: { bash: { cwd: './src' } }
|
|
186
|
+
*
|
|
187
|
+
* This enables a cleaner, more readable frontmatter format while
|
|
188
|
+
* maintaining backwards compatibility with the full verbose syntax.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} frontmatter - Parsed frontmatter object
|
|
191
|
+
* @returns {object} Normalized frontmatter with full structure
|
|
192
|
+
*/
|
|
193
|
+
function normalizeFrontmatter(frontmatter) {
|
|
194
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
195
|
+
return frontmatter;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const normalized = { ...frontmatter };
|
|
199
|
+
|
|
200
|
+
// Languages that support shorthand syntax
|
|
201
|
+
const languages = ['python', 'bash', 'term', 'node', 'julia', 'r'];
|
|
202
|
+
|
|
203
|
+
for (const lang of languages) {
|
|
204
|
+
if (lang in normalized && !normalized.session?.[lang]) {
|
|
205
|
+
const value = normalized[lang];
|
|
206
|
+
|
|
207
|
+
// Ensure session object exists
|
|
208
|
+
if (!normalized.session) {
|
|
209
|
+
normalized.session = {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof value === 'string') {
|
|
213
|
+
// Simple form: python: .venv
|
|
214
|
+
// For python, string is venv path; for others, it's cwd
|
|
215
|
+
if (lang === 'python') {
|
|
216
|
+
normalized.session[lang] = { venv: value };
|
|
217
|
+
} else {
|
|
218
|
+
normalized.session[lang] = { cwd: value };
|
|
219
|
+
}
|
|
220
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
221
|
+
// Object form: python: { venv: .venv, cwd: . }
|
|
222
|
+
normalized.session[lang] = value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Remove the shorthand key
|
|
226
|
+
delete normalized[lang];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
|
|
157
233
|
/**
|
|
158
234
|
* Deep merge project config with document frontmatter
|
|
159
235
|
*
|
|
@@ -171,9 +247,10 @@ export function mergeConfig(projectConfig, frontmatter) {
|
|
|
171
247
|
}
|
|
172
248
|
|
|
173
249
|
/**
|
|
174
|
-
* Resolve full session configuration for a document
|
|
250
|
+
* Resolve full session configuration for a document (Python)
|
|
175
251
|
*
|
|
176
252
|
* Computes absolute paths and full session name.
|
|
253
|
+
* Uses legacy naming format {project}:{name} for backwards compatibility.
|
|
177
254
|
*
|
|
178
255
|
* @param {string} documentPath - Absolute path to document
|
|
179
256
|
* @param {string} projectRoot - Absolute path to project root
|
|
@@ -191,26 +268,82 @@ export function mergeConfig(projectConfig, frontmatter) {
|
|
|
191
268
|
export function resolveSession(documentPath, projectRoot, mergedConfig) {
|
|
192
269
|
const pythonConfig = mergedConfig?.session?.python || {};
|
|
193
270
|
const defaults = DEFAULTS.session.python;
|
|
271
|
+
const projectName = mergedConfig?.name || 'unnamed';
|
|
194
272
|
|
|
195
273
|
// Get values with defaults
|
|
196
274
|
const venvRelative = pythonConfig.venv || defaults.venv;
|
|
197
275
|
const cwdRelative = pythonConfig.cwd || defaults.cwd;
|
|
198
276
|
const sessionName = pythonConfig.name || defaults.name;
|
|
199
277
|
const autoStart = pythonConfig.auto_start !== undefined ? pythonConfig.auto_start : defaults.auto_start;
|
|
200
|
-
const projectName = mergedConfig?.name || 'unnamed';
|
|
201
278
|
|
|
202
279
|
// Resolve paths relative to project root
|
|
203
280
|
const venv = resolvePath(projectRoot, venvRelative);
|
|
204
281
|
const cwd = resolvePath(projectRoot, cwdRelative);
|
|
205
282
|
|
|
283
|
+
// Legacy naming format: {project}:{name} (no language in name)
|
|
206
284
|
return {
|
|
207
285
|
name: `${projectName}:${sessionName}`,
|
|
208
286
|
venv,
|
|
209
287
|
cwd,
|
|
210
288
|
autoStart,
|
|
289
|
+
language: 'python',
|
|
211
290
|
};
|
|
212
291
|
}
|
|
213
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Resolve session configuration for a specific language
|
|
295
|
+
*
|
|
296
|
+
* @param {'python' | 'bash' | 'term'} language - Language to resolve session for
|
|
297
|
+
* @param {string} documentPath - Absolute path to document
|
|
298
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
299
|
+
* @param {object} mergedConfig - Merged configuration
|
|
300
|
+
* @returns {object} Resolved session config with absolute paths
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* const session = Project.resolveSessionForLanguage(
|
|
304
|
+
* 'bash',
|
|
305
|
+
* '/home/user/thesis/chapter/doc.md',
|
|
306
|
+
* '/home/user/thesis',
|
|
307
|
+
* { name: 'thesis', session: { bash: { cwd: '.', name: 'default' } } }
|
|
308
|
+
* );
|
|
309
|
+
* // Returns { name: 'thesis:bash:default', cwd: '/home/user/thesis', autoStart: true }
|
|
310
|
+
*/
|
|
311
|
+
export function resolveSessionForLanguage(language, documentPath, projectRoot, mergedConfig) {
|
|
312
|
+
const langConfig = mergedConfig?.session?.[language] || {};
|
|
313
|
+
const defaults = DEFAULTS.session[language] || {};
|
|
314
|
+
const projectName = mergedConfig?.name || 'unnamed';
|
|
315
|
+
|
|
316
|
+
// Common fields for all languages
|
|
317
|
+
const cwdRelative = langConfig.cwd || defaults.cwd || '.';
|
|
318
|
+
const sessionName = langConfig.name || defaults.name || 'default';
|
|
319
|
+
const autoStart = langConfig.auto_start !== undefined ? langConfig.auto_start : (defaults.auto_start !== undefined ? defaults.auto_start : true);
|
|
320
|
+
|
|
321
|
+
// Resolve cwd relative to project root
|
|
322
|
+
const cwd = resolvePath(projectRoot, cwdRelative);
|
|
323
|
+
|
|
324
|
+
// Base session info
|
|
325
|
+
const session = {
|
|
326
|
+
name: `${projectName}:${language}:${sessionName}`,
|
|
327
|
+
cwd,
|
|
328
|
+
autoStart,
|
|
329
|
+
language,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Language-specific fields
|
|
333
|
+
if (language === 'python') {
|
|
334
|
+
const venvRelative = langConfig.venv || defaults.venv || '.venv';
|
|
335
|
+
session.venv = resolvePath(projectRoot, venvRelative);
|
|
336
|
+
} else if (language === 'term') {
|
|
337
|
+
// Term supports optional venv for activation in terminals
|
|
338
|
+
const venvRelative = langConfig.venv || defaults.venv;
|
|
339
|
+
if (venvRelative) {
|
|
340
|
+
session.venv = resolvePath(projectRoot, venvRelative);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return session;
|
|
345
|
+
}
|
|
346
|
+
|
|
214
347
|
/**
|
|
215
348
|
* Resolve a potentially relative path against a base directory
|
|
216
349
|
* @private
|