mrmd-project 0.1.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/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # mrmd-project
2
+
3
+ Pure logic for understanding mrmd project structure and conventions.
4
+
5
+ This package provides zero-side-effect functions for parsing project configuration, building navigation trees, resolving internal links, and managing asset paths in [mrmd](https://github.com/anthropics/mrmd) markdown notebook projects.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install mrmd-project
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Zero I/O** - All functions are pure; no file system access
16
+ - **Project Config** - Parse `mrmd.md` files with `yaml config` blocks
17
+ - **FSML** - Filesystem Markup Language for navigation trees
18
+ - **Internal Links** - Parse and resolve `[[wiki-style]]` links
19
+ - **Asset Paths** - Compute relative paths between documents and assets
20
+ - **Search** - Fuzzy matching for file picker functionality
21
+
22
+ ## Usage
23
+
24
+ ```javascript
25
+ import { Project, FSML, Links, Assets, Scaffold, Search } from 'mrmd-project';
26
+ ```
27
+
28
+ ### Project Configuration
29
+
30
+ Parse project config from `mrmd.md` files:
31
+
32
+ ```javascript
33
+ const mrmdContent = `# My Project
34
+
35
+ \`\`\`yaml config
36
+ name: "My Thesis"
37
+ session:
38
+ python:
39
+ venv: ".venv"
40
+ \`\`\`
41
+ `;
42
+
43
+ const config = Project.parseConfig(mrmdContent);
44
+ // { name: 'My Thesis', session: { python: { venv: '.venv' } } }
45
+ ```
46
+
47
+ Parse document frontmatter:
48
+
49
+ ```javascript
50
+ const docContent = `---
51
+ title: "Chapter 1"
52
+ session:
53
+ python:
54
+ name: "gpu-session"
55
+ ---
56
+
57
+ # Content here
58
+ `;
59
+
60
+ const frontmatter = Project.parseFrontmatter(docContent);
61
+ // { title: 'Chapter 1', session: { python: { name: 'gpu-session' } } }
62
+ ```
63
+
64
+ Merge configs (document overrides project):
65
+
66
+ ```javascript
67
+ const merged = Project.mergeConfig(projectConfig, frontmatter);
68
+ ```
69
+
70
+ Resolve session configuration:
71
+
72
+ ```javascript
73
+ const session = Project.resolveSession(
74
+ '/home/user/thesis/chapter.md', // document path
75
+ '/home/user/thesis', // project root
76
+ merged // merged config
77
+ );
78
+ // {
79
+ // name: 'my-thesis:gpu-session',
80
+ // venv: '/home/user/thesis/.venv',
81
+ // cwd: '/home/user/thesis',
82
+ // autoStart: true
83
+ // }
84
+ ```
85
+
86
+ Find project root:
87
+
88
+ ```javascript
89
+ const root = Project.findRoot(
90
+ '/home/user/thesis/chapter/doc.md',
91
+ (path) => fs.existsSync(path + '/mrmd.md')
92
+ );
93
+ // '/home/user/thesis'
94
+ ```
95
+
96
+ ### FSML (Filesystem Markup Language)
97
+
98
+ Parse paths into components:
99
+
100
+ ```javascript
101
+ const parsed = FSML.parsePath('02-getting-started/01-installation.md');
102
+ // {
103
+ // path: '02-getting-started/01-installation.md',
104
+ // order: 1,
105
+ // name: 'installation',
106
+ // title: 'Installation',
107
+ // extension: '.md',
108
+ // isFolder: false,
109
+ // isHidden: false,
110
+ // isSystem: false,
111
+ // depth: 1,
112
+ // parent: '02-getting-started'
113
+ // }
114
+ ```
115
+
116
+ Sort paths by FSML rules:
117
+
118
+ ```javascript
119
+ const sorted = FSML.sortPaths([
120
+ 'appendix.md',
121
+ '03-results.md',
122
+ '01-intro.md',
123
+ '02-methods.md',
124
+ ]);
125
+ // ['01-intro.md', '02-methods.md', '03-results.md', 'appendix.md']
126
+ ```
127
+
128
+ Build navigation tree:
129
+
130
+ ```javascript
131
+ const tree = FSML.buildNavTree([
132
+ '01-intro.md',
133
+ '02-getting-started/index.md',
134
+ '02-getting-started/01-install.md',
135
+ '02-getting-started/02-config.md',
136
+ ]);
137
+ // [
138
+ // { path: '01-intro.md', title: 'Intro', isFolder: false, children: [] },
139
+ // { path: '02-getting-started', title: 'Getting Started', isFolder: true, hasIndex: true, children: [...] }
140
+ // ]
141
+ ```
142
+
143
+ Derive titles from filenames:
144
+
145
+ ```javascript
146
+ FSML.titleFromFilename('01-getting-started.md'); // 'Getting Started'
147
+ FSML.titleFromFilename('my_cool_doc.md'); // 'My Cool Doc'
148
+ ```
149
+
150
+ ### Internal Links
151
+
152
+ Parse wiki-style links:
153
+
154
+ ```javascript
155
+ const links = Links.parse('See [[installation]] and [[config#advanced|advanced config]].');
156
+ // [
157
+ // { raw: '[[installation]]', target: 'installation', anchor: null, display: null, ... },
158
+ // { raw: '[[config#advanced|advanced config]]', target: 'config', anchor: 'advanced', display: 'advanced config', ... }
159
+ // ]
160
+ ```
161
+
162
+ Resolve links to files:
163
+
164
+ ```javascript
165
+ const files = ['01-intro.md', '02-getting-started/01-installation.md'];
166
+ const resolved = Links.resolve('installation', '01-intro.md', files);
167
+ // '02-getting-started/01-installation.md'
168
+
169
+ // Special links
170
+ Links.resolve('next', '01-intro.md', files); // next document in order
171
+ Links.resolve('prev', '02-methods.md', files); // previous document
172
+ Links.resolve('home', 'any.md', files); // first document
173
+ ```
174
+
175
+ Refactor links when files move:
176
+
177
+ ```javascript
178
+ const updated = Links.refactor(
179
+ 'See [[old-name]] for details.',
180
+ [{ from: 'old-name.md', to: 'new-name.md' }],
181
+ 'index.md'
182
+ );
183
+ // 'See [[new-name]] for details.'
184
+ ```
185
+
186
+ ### Asset Paths
187
+
188
+ Compute relative paths from documents to assets:
189
+
190
+ ```javascript
191
+ Assets.computeRelativePath('01-intro.md', '_assets/img.png');
192
+ // '_assets/img.png'
193
+
194
+ Assets.computeRelativePath('02-section/01-doc.md', '_assets/img.png');
195
+ // '../_assets/img.png'
196
+
197
+ Assets.computeRelativePath('02-section/sub/deep/doc.md', '_assets/img.png');
198
+ // '../../../_assets/img.png'
199
+ ```
200
+
201
+ Refactor paths when documents move:
202
+
203
+ ```javascript
204
+ const content = '![Screenshot](_assets/screenshot.png)';
205
+ const updated = Assets.refactorPaths(
206
+ content,
207
+ '01-intro.md', // old location
208
+ '02-section/01-intro.md', // new location
209
+ '_assets'
210
+ );
211
+ // '![Screenshot](../_assets/screenshot.png)'
212
+ ```
213
+
214
+ Extract asset references:
215
+
216
+ ```javascript
217
+ const refs = Assets.extractPaths('![Alt](../_assets/img.png)');
218
+ // [{ path: '../_assets/img.png', type: 'image', start: 6, end: 25 }]
219
+ ```
220
+
221
+ ### Scaffolding
222
+
223
+ Generate new project scaffold:
224
+
225
+ ```javascript
226
+ const scaffold = Scaffold.project('my-research');
227
+ // {
228
+ // files: [
229
+ // { path: 'mrmd.md', content: '...' },
230
+ // { path: '01-index.md', content: '...' },
231
+ // { path: '_assets/.gitkeep', content: '' }
232
+ // ],
233
+ // venvPath: '.venv'
234
+ // }
235
+ ```
236
+
237
+ Generate standalone file frontmatter:
238
+
239
+ ```javascript
240
+ const frontmatter = Scaffold.standaloneFrontmatter({
241
+ venv: '/home/user/.venv',
242
+ cwd: '/home/user/work',
243
+ title: 'Quick Analysis'
244
+ });
245
+ // '---\ntitle: "Quick Analysis"\nsession:\n python:\n venv: "/home/user/.venv"\n cwd: "/home/user/work"\n---\n'
246
+ ```
247
+
248
+ ### Search
249
+
250
+ Fuzzy match strings:
251
+
252
+ ```javascript
253
+ const result = Search.fuzzyMatch('instal', 'installation');
254
+ // { score: 15, matches: [0, 1, 2, 3, 4, 5] }
255
+ ```
256
+
257
+ Search files by path:
258
+
259
+ ```javascript
260
+ const results = Search.files('thesis readme', [
261
+ '/home/user/thesis/README.md',
262
+ '/home/user/other/README.md',
263
+ ]);
264
+ // Ranked by match quality, thesis/README.md first
265
+ ```
266
+
267
+ ## FSML Conventions
268
+
269
+ FSML (Filesystem Markup Language) treats the filesystem as markup:
270
+
271
+ | Pattern | Example | Meaning |
272
+ |---------|---------|---------|
273
+ | `NN-name` | `01-intro.md` | Ordered item (position 1) |
274
+ | No prefix | `appendix.md` | Unordered (sorted alphabetically after numbered) |
275
+ | `_folder/` | `_assets/` | Hidden from navigation (author-only) |
276
+ | `.folder/` | `.git/` | System folder (ignored) |
277
+ | `index.md` | `02-section/index.md` | Section landing page |
278
+ | `mrmd.md` | Root `mrmd.md` | Project configuration file |
279
+
280
+ ## API Reference
281
+
282
+ ### Project Module
283
+
284
+ | Function | Description |
285
+ |----------|-------------|
286
+ | `parseConfig(content)` | Extract and merge `yaml config` blocks |
287
+ | `parseFrontmatter(content)` | Extract YAML frontmatter |
288
+ | `mergeConfig(project, doc)` | Deep merge configs (doc wins) |
289
+ | `findRoot(path, hasFile)` | Walk up to find project root |
290
+ | `resolveSession(doc, root, config)` | Compute session with absolute paths |
291
+ | `getDefaults()` | Get default configuration values |
292
+
293
+ ### FSML Module
294
+
295
+ | Function | Description |
296
+ |----------|-------------|
297
+ | `parsePath(path)` | Parse path into FSML components |
298
+ | `sortPaths(paths)` | Sort by FSML rules |
299
+ | `buildNavTree(paths)` | Build nested navigation structure |
300
+ | `titleFromFilename(name)` | Derive human-readable title |
301
+ | `computeNewPath(src, target, pos)` | Compute path for reordering |
302
+
303
+ ### Links Module
304
+
305
+ | Function | Description |
306
+ |----------|-------------|
307
+ | `parse(content)` | Extract all `[[links]]` |
308
+ | `resolve(target, from, files)` | Resolve link to file path |
309
+ | `refactor(content, moves, doc)` | Update links after file moves |
310
+
311
+ ### Assets Module
312
+
313
+ | Function | Description |
314
+ |----------|-------------|
315
+ | `computeRelativePath(doc, asset)` | Get relative path to asset |
316
+ | `refactorPaths(content, old, new, dir)` | Update paths after doc moves |
317
+ | `extractPaths(content)` | Find all asset references |
318
+
319
+ ### Scaffold Module
320
+
321
+ | Function | Description |
322
+ |----------|-------------|
323
+ | `project(name)` | Generate project scaffold |
324
+ | `standaloneFrontmatter(config)` | Generate standalone frontmatter |
325
+
326
+ ### Search Module
327
+
328
+ | Function | Description |
329
+ |----------|-------------|
330
+ | `fuzzyMatch(query, target)` | Fuzzy match with scoring |
331
+ | `files(query, paths)` | Search and rank file paths |
332
+
333
+ ## License
334
+
335
+ MIT
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "mrmd-project",
3
+ "version": "0.1.0",
4
+ "description": "Pure logic for understanding mrmd project structure and conventions",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "spec.md",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test test/*.test.js"
17
+ },
18
+ "keywords": [
19
+ "mrmd",
20
+ "markdown",
21
+ "project",
22
+ "fsml",
23
+ "session",
24
+ "configuration",
25
+ "notebook",
26
+ "wiki-links",
27
+ "navigation"
28
+ ],
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/anthropics/mrmd.git",
33
+ "directory": "packages/mrmd-project"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/anthropics/mrmd/issues"
37
+ },
38
+ "homepage": "https://github.com/anthropics/mrmd/tree/main/packages/mrmd-project#readme",
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "dependencies": {
43
+ "yaml": "^2.3.0"
44
+ },
45
+ "devDependencies": {}
46
+ }