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 +335 -0
- package/package.json +46 -0
- package/spec.md +1056 -0
- package/src/assets.js +142 -0
- package/src/fsml.js +371 -0
- package/src/index.js +65 -0
- package/src/links.js +198 -0
- package/src/project.js +293 -0
- package/src/scaffold.js +136 -0
- package/src/search.js +177 -0
package/src/assets.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assets module - Asset path computation and refactoring
|
|
3
|
+
*
|
|
4
|
+
* Handles relative paths between documents and assets in _assets/.
|
|
5
|
+
*
|
|
6
|
+
* @module Assets
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute the relative path from a document to an asset
|
|
11
|
+
*
|
|
12
|
+
* @param {string} documentPath - Path of the document (relative to project root)
|
|
13
|
+
* @param {string} assetPath - Path of the asset (relative to project root)
|
|
14
|
+
* @returns {string} Relative path from document to asset
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* Assets.computeRelativePath('01-intro.md', '_assets/img.png')
|
|
18
|
+
* // Returns '_assets/img.png'
|
|
19
|
+
*
|
|
20
|
+
* Assets.computeRelativePath('02-section/01-doc.md', '_assets/img.png')
|
|
21
|
+
* // Returns '../_assets/img.png'
|
|
22
|
+
*/
|
|
23
|
+
export function computeRelativePath(documentPath, assetPath) {
|
|
24
|
+
if (!documentPath || !assetPath) return assetPath || '';
|
|
25
|
+
|
|
26
|
+
// Get document directory (remove filename)
|
|
27
|
+
const docDir = documentPath.includes('/')
|
|
28
|
+
? documentPath.split('/').slice(0, -1).join('/')
|
|
29
|
+
: '';
|
|
30
|
+
|
|
31
|
+
// If document is at root, return asset path as-is
|
|
32
|
+
if (!docDir) {
|
|
33
|
+
return assetPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Count how many levels up we need to go
|
|
37
|
+
const docDepth = docDir.split('/').length;
|
|
38
|
+
|
|
39
|
+
// Build the relative path
|
|
40
|
+
const upPath = '../'.repeat(docDepth);
|
|
41
|
+
return upPath + assetPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update asset paths in content when a document moves
|
|
46
|
+
*
|
|
47
|
+
* @param {string} content - Document content
|
|
48
|
+
* @param {string} oldDocPath - Old document path
|
|
49
|
+
* @param {string} newDocPath - New document path
|
|
50
|
+
* @param {string} assetsDir - Assets directory name (default '_assets')
|
|
51
|
+
* @returns {string} Content with updated asset paths
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* Assets.refactorPaths(
|
|
55
|
+
* '',
|
|
56
|
+
* '01-intro.md',
|
|
57
|
+
* '02-section/01-intro.md',
|
|
58
|
+
* '_assets'
|
|
59
|
+
* )
|
|
60
|
+
* // Returns ''
|
|
61
|
+
*/
|
|
62
|
+
export function refactorPaths(content, oldDocPath, newDocPath, assetsDir = '_assets') {
|
|
63
|
+
if (!content) return content;
|
|
64
|
+
|
|
65
|
+
// Calculate old and new depths
|
|
66
|
+
const oldDir = oldDocPath.includes('/') ? oldDocPath.split('/').slice(0, -1).join('/') : '';
|
|
67
|
+
const newDir = newDocPath.includes('/') ? newDocPath.split('/').slice(0, -1).join('/') : '';
|
|
68
|
+
|
|
69
|
+
const oldDepth = oldDir ? oldDir.split('/').length : 0;
|
|
70
|
+
const newDepth = newDir ? newDir.split('/').length : 0;
|
|
71
|
+
|
|
72
|
+
// Find all asset references and update them
|
|
73
|
+
const regex = /(!?)\[([^\]]*)\]\(([^)]+)\)/g;
|
|
74
|
+
|
|
75
|
+
return content.replace(regex, (match, bang, alt, path) => {
|
|
76
|
+
// Check if this path references assets
|
|
77
|
+
if (!path.includes(assetsDir)) {
|
|
78
|
+
return match;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract the asset path relative to project root
|
|
82
|
+
// Old path might be: _assets/img.png or ../_assets/img.png or ../../_assets/img.png
|
|
83
|
+
let assetRelativePath;
|
|
84
|
+
|
|
85
|
+
if (path.startsWith(assetsDir)) {
|
|
86
|
+
// Direct path from root: _assets/img.png
|
|
87
|
+
assetRelativePath = path;
|
|
88
|
+
} else if (path.includes('/' + assetsDir)) {
|
|
89
|
+
// Relative path: ../_assets/img.png
|
|
90
|
+
const idx = path.indexOf(assetsDir);
|
|
91
|
+
assetRelativePath = path.slice(idx);
|
|
92
|
+
} else {
|
|
93
|
+
// Can't parse, leave as-is
|
|
94
|
+
return match;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Compute new relative path
|
|
98
|
+
const newRelativePath = computeRelativePath(newDocPath, assetRelativePath);
|
|
99
|
+
|
|
100
|
+
return `${bang}[${alt}](${newRelativePath})`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract all asset references from content
|
|
106
|
+
*
|
|
107
|
+
* Finds both image syntax  and link syntax [](path) that reference assets.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} content - Document content
|
|
110
|
+
* @returns {object[]} Array of asset references
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* Assets.extractPaths('')
|
|
114
|
+
* // Returns [{ path: '../_assets/img.png', start: 6, end: 26, type: 'image' }]
|
|
115
|
+
*/
|
|
116
|
+
export function extractPaths(content) {
|
|
117
|
+
if (!content) return [];
|
|
118
|
+
|
|
119
|
+
const refs = [];
|
|
120
|
+
|
|
121
|
+
// Match both images  and links [text](path)
|
|
122
|
+
// that reference _assets
|
|
123
|
+
const regex = /(!?)\[([^\]]*)\]\(([^)]+)\)/g;
|
|
124
|
+
let match;
|
|
125
|
+
|
|
126
|
+
while ((match = regex.exec(content)) !== null) {
|
|
127
|
+
const isImage = match[1] === '!';
|
|
128
|
+
const path = match[3];
|
|
129
|
+
|
|
130
|
+
// Only include paths that look like assets (contain _assets or relative paths to _assets)
|
|
131
|
+
if (path.includes('_assets') || path.includes('assets')) {
|
|
132
|
+
refs.push({
|
|
133
|
+
path,
|
|
134
|
+
start: match.index + match[1].length + match[2].length + 3, // Position of path start
|
|
135
|
+
end: match.index + match[0].length - 1, // Position before closing )
|
|
136
|
+
type: isImage ? 'image' : 'link',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return refs;
|
|
142
|
+
}
|
package/src/fsml.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FSML module - Filesystem Markup Language utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles path parsing, sorting, and navigation tree building
|
|
5
|
+
* according to FSML conventions.
|
|
6
|
+
*
|
|
7
|
+
* @module FSML
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a relative path into FSML components
|
|
12
|
+
*
|
|
13
|
+
* @param {string} relativePath - Path relative to project root
|
|
14
|
+
* @returns {object} Parsed path information
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* FSML.parsePath('02-getting-started/01-installation.md')
|
|
18
|
+
* // Returns {
|
|
19
|
+
* // path: '02-getting-started/01-installation.md',
|
|
20
|
+
* // order: 1,
|
|
21
|
+
* // name: 'installation',
|
|
22
|
+
* // title: 'Installation',
|
|
23
|
+
* // extension: '.md',
|
|
24
|
+
* // isFolder: false,
|
|
25
|
+
* // isHidden: false,
|
|
26
|
+
* // isSystem: false,
|
|
27
|
+
* // depth: 1,
|
|
28
|
+
* // parent: '02-getting-started'
|
|
29
|
+
* // }
|
|
30
|
+
*/
|
|
31
|
+
export function parsePath(relativePath) {
|
|
32
|
+
if (!relativePath) {
|
|
33
|
+
return {
|
|
34
|
+
path: '',
|
|
35
|
+
order: null,
|
|
36
|
+
name: '',
|
|
37
|
+
title: '',
|
|
38
|
+
extension: '',
|
|
39
|
+
isFolder: false,
|
|
40
|
+
isHidden: false,
|
|
41
|
+
isSystem: false,
|
|
42
|
+
depth: 0,
|
|
43
|
+
parent: '',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Normalize path (remove trailing slash)
|
|
48
|
+
const path = relativePath.replace(/\/+$/, '');
|
|
49
|
+
|
|
50
|
+
// Get segments
|
|
51
|
+
const segments = path.split('/').filter(Boolean);
|
|
52
|
+
const depth = segments.length - 1;
|
|
53
|
+
const parent = segments.slice(0, -1).join('/');
|
|
54
|
+
|
|
55
|
+
// Get filename (last segment)
|
|
56
|
+
const filename = segments[segments.length - 1] || '';
|
|
57
|
+
|
|
58
|
+
// Check if it's a folder (no extension or explicitly ends with /)
|
|
59
|
+
const hasExtension = /\.[^./]+$/.test(filename);
|
|
60
|
+
const isFolder = !hasExtension;
|
|
61
|
+
|
|
62
|
+
// Get extension
|
|
63
|
+
const extMatch = filename.match(/(\.[^.]+)$/);
|
|
64
|
+
const extension = extMatch ? extMatch[1] : '';
|
|
65
|
+
|
|
66
|
+
// Get name without extension
|
|
67
|
+
const nameWithoutExt = filename.replace(/\.[^.]+$/, '');
|
|
68
|
+
|
|
69
|
+
// Parse numeric prefix (e.g., "01-" or "02_")
|
|
70
|
+
const prefixMatch = nameWithoutExt.match(/^(\d+)[-_]/);
|
|
71
|
+
const order = prefixMatch ? parseInt(prefixMatch[1], 10) : null;
|
|
72
|
+
|
|
73
|
+
// Get name without prefix
|
|
74
|
+
const name = prefixMatch ? nameWithoutExt.replace(/^\d+[-_]/, '') : nameWithoutExt;
|
|
75
|
+
|
|
76
|
+
// Derive title
|
|
77
|
+
const title = titleFromFilename(filename);
|
|
78
|
+
|
|
79
|
+
// Check hidden/system status based on first segment
|
|
80
|
+
const firstSegment = segments[0] || filename;
|
|
81
|
+
const isHidden = firstSegment.startsWith('_');
|
|
82
|
+
const isSystem = firstSegment.startsWith('.');
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
path,
|
|
86
|
+
order,
|
|
87
|
+
name,
|
|
88
|
+
title,
|
|
89
|
+
extension,
|
|
90
|
+
isFolder,
|
|
91
|
+
isHidden,
|
|
92
|
+
isSystem,
|
|
93
|
+
depth,
|
|
94
|
+
parent,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sort paths according to FSML rules
|
|
100
|
+
*
|
|
101
|
+
* Rules:
|
|
102
|
+
* 1. Numbered items first, in numeric order
|
|
103
|
+
* 2. Unnumbered items after, alphabetically
|
|
104
|
+
* 3. Folders and files interleaved by their order
|
|
105
|
+
*
|
|
106
|
+
* @param {string[]} paths - Array of relative paths
|
|
107
|
+
* @returns {string[]} Sorted paths
|
|
108
|
+
*/
|
|
109
|
+
export function sortPaths(paths) {
|
|
110
|
+
if (!paths || paths.length === 0) return [];
|
|
111
|
+
|
|
112
|
+
// Parse all paths
|
|
113
|
+
const parsed = paths.map(p => ({
|
|
114
|
+
path: p,
|
|
115
|
+
...parsePath(p),
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// Sort function
|
|
119
|
+
return parsed
|
|
120
|
+
.sort((a, b) => {
|
|
121
|
+
// First, compare by parent path to keep hierarchy
|
|
122
|
+
const aParts = a.path.split('/');
|
|
123
|
+
const bParts = b.path.split('/');
|
|
124
|
+
|
|
125
|
+
// Compare common ancestor parts
|
|
126
|
+
const minLen = Math.min(aParts.length, bParts.length);
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < minLen; i++) {
|
|
129
|
+
const aPart = aParts[i];
|
|
130
|
+
const bPart = bParts[i];
|
|
131
|
+
|
|
132
|
+
if (aPart === bPart) continue;
|
|
133
|
+
|
|
134
|
+
// Parse parts for comparison
|
|
135
|
+
const aPartParsed = parsePath(aPart);
|
|
136
|
+
const bPartParsed = parsePath(bPart);
|
|
137
|
+
|
|
138
|
+
// Numbered items first
|
|
139
|
+
if (aPartParsed.order !== null && bPartParsed.order !== null) {
|
|
140
|
+
return aPartParsed.order - bPartParsed.order;
|
|
141
|
+
}
|
|
142
|
+
if (aPartParsed.order !== null) return -1;
|
|
143
|
+
if (bPartParsed.order !== null) return 1;
|
|
144
|
+
|
|
145
|
+
// Alphabetical for unnumbered
|
|
146
|
+
return aPart.localeCompare(bPart);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Shorter paths (parents) come before longer paths (children) -- actually not, children should come after parent
|
|
150
|
+
return aParts.length - bParts.length;
|
|
151
|
+
})
|
|
152
|
+
.map(p => p.path);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build a navigation tree from sorted paths
|
|
157
|
+
*
|
|
158
|
+
* @param {string[]} paths - Array of relative paths
|
|
159
|
+
* @returns {object[]} Navigation tree nodes
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* FSML.buildNavTree(['01-intro.md', '02-methods/01-setup.md', '02-methods/02-analysis.md'])
|
|
163
|
+
* // Returns [
|
|
164
|
+
* // { path: '01-intro.md', title: 'Intro', isFolder: false, children: [] },
|
|
165
|
+
* // { path: '02-methods', title: 'Methods', isFolder: true, hasIndex: false, children: [...] }
|
|
166
|
+
* // ]
|
|
167
|
+
*/
|
|
168
|
+
export function buildNavTree(paths) {
|
|
169
|
+
if (!paths || paths.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
// Filter out mrmd.md, hidden (_), and system (.) paths
|
|
172
|
+
const filtered = paths.filter(p => {
|
|
173
|
+
const parsed = parsePath(p);
|
|
174
|
+
if (parsed.isHidden || parsed.isSystem) return false;
|
|
175
|
+
// Exclude mrmd.md at root
|
|
176
|
+
if (p === 'mrmd.md') return false;
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Sort the paths
|
|
181
|
+
const sorted = sortPaths(filtered);
|
|
182
|
+
|
|
183
|
+
// Build folder structure first - collect all unique folders
|
|
184
|
+
const folders = new Map(); // folder path -> { hasIndex, children: [] }
|
|
185
|
+
const rootChildren = [];
|
|
186
|
+
|
|
187
|
+
// First pass: identify folders and track index.md
|
|
188
|
+
for (const path of sorted) {
|
|
189
|
+
const segments = path.split('/');
|
|
190
|
+
|
|
191
|
+
// Track folder hierarchy
|
|
192
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
193
|
+
const folderPath = segments.slice(0, i + 1).join('/');
|
|
194
|
+
if (!folders.has(folderPath)) {
|
|
195
|
+
const parsed = parsePath(folderPath);
|
|
196
|
+
folders.set(folderPath, {
|
|
197
|
+
path: folderPath,
|
|
198
|
+
title: titleFromFilename(segments[i]),
|
|
199
|
+
order: parsed.order,
|
|
200
|
+
isFolder: true,
|
|
201
|
+
hasIndex: false,
|
|
202
|
+
children: [],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if this is an index.md
|
|
208
|
+
const filename = segments[segments.length - 1];
|
|
209
|
+
if (filename === 'index.md' && segments.length > 1) {
|
|
210
|
+
const parentPath = segments.slice(0, -1).join('/');
|
|
211
|
+
if (folders.has(parentPath)) {
|
|
212
|
+
folders.get(parentPath).hasIndex = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Second pass: build tree
|
|
218
|
+
for (const path of sorted) {
|
|
219
|
+
const segments = path.split('/');
|
|
220
|
+
const filename = segments[segments.length - 1];
|
|
221
|
+
|
|
222
|
+
// Skip index.md files (they're represented by the folder itself)
|
|
223
|
+
if (filename === 'index.md') continue;
|
|
224
|
+
|
|
225
|
+
const parsed = parsePath(path);
|
|
226
|
+
|
|
227
|
+
const node = {
|
|
228
|
+
path,
|
|
229
|
+
title: parsed.title,
|
|
230
|
+
order: parsed.order,
|
|
231
|
+
isFolder: false,
|
|
232
|
+
hasIndex: false,
|
|
233
|
+
children: [],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (segments.length === 1) {
|
|
237
|
+
// Top-level file
|
|
238
|
+
rootChildren.push(node);
|
|
239
|
+
} else {
|
|
240
|
+
// File in a folder
|
|
241
|
+
const parentPath = segments.slice(0, -1).join('/');
|
|
242
|
+
if (folders.has(parentPath)) {
|
|
243
|
+
folders.get(parentPath).children.push(node);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Third pass: add folders to tree
|
|
249
|
+
// Sort folders by depth (shallow first) to build hierarchy
|
|
250
|
+
const folderList = Array.from(folders.values());
|
|
251
|
+
folderList.sort((a, b) => a.path.split('/').length - b.path.split('/').length);
|
|
252
|
+
|
|
253
|
+
for (const folder of folderList) {
|
|
254
|
+
const segments = folder.path.split('/');
|
|
255
|
+
|
|
256
|
+
if (segments.length === 1) {
|
|
257
|
+
// Top-level folder
|
|
258
|
+
rootChildren.push(folder);
|
|
259
|
+
} else {
|
|
260
|
+
// Nested folder
|
|
261
|
+
const parentPath = segments.slice(0, -1).join('/');
|
|
262
|
+
if (folders.has(parentPath)) {
|
|
263
|
+
folders.get(parentPath).children.push(folder);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Sort root children
|
|
269
|
+
rootChildren.sort((a, b) => {
|
|
270
|
+
if (a.order !== null && b.order !== null) return a.order - b.order;
|
|
271
|
+
if (a.order !== null) return -1;
|
|
272
|
+
if (b.order !== null) return 1;
|
|
273
|
+
return a.title.localeCompare(b.title);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Sort children of each folder
|
|
277
|
+
for (const folder of folders.values()) {
|
|
278
|
+
folder.children.sort((a, b) => {
|
|
279
|
+
if (a.order !== null && b.order !== null) return a.order - b.order;
|
|
280
|
+
if (a.order !== null) return -1;
|
|
281
|
+
if (b.order !== null) return 1;
|
|
282
|
+
return a.title.localeCompare(b.title);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return rootChildren;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Derive a human-readable title from a filename
|
|
291
|
+
*
|
|
292
|
+
* @param {string} filename - Filename (with or without extension)
|
|
293
|
+
* @returns {string} Human-readable title
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* FSML.titleFromFilename('01-getting-started.md') // 'Getting Started'
|
|
297
|
+
* FSML.titleFromFilename('my_cool_doc.md') // 'My Cool Doc'
|
|
298
|
+
*/
|
|
299
|
+
export function titleFromFilename(filename) {
|
|
300
|
+
if (!filename) return '';
|
|
301
|
+
|
|
302
|
+
// Remove extension
|
|
303
|
+
let name = filename.replace(/\.[^.]+$/, '');
|
|
304
|
+
|
|
305
|
+
// Remove numeric prefix (e.g., "01-" or "02_")
|
|
306
|
+
name = name.replace(/^\d+-/, '');
|
|
307
|
+
|
|
308
|
+
// Replace hyphens and underscores with spaces
|
|
309
|
+
name = name.replace(/[-_]/g, ' ');
|
|
310
|
+
|
|
311
|
+
// Title case: capitalize first letter of each word
|
|
312
|
+
name = name
|
|
313
|
+
.split(' ')
|
|
314
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
315
|
+
.join(' ');
|
|
316
|
+
|
|
317
|
+
return name;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Compute new paths when reordering files
|
|
322
|
+
*
|
|
323
|
+
* @param {string} sourcePath - Path being moved
|
|
324
|
+
* @param {string} targetPath - Path to move relative to
|
|
325
|
+
* @param {'before' | 'after' | 'inside'} position - Where to place
|
|
326
|
+
* @returns {object} New path and required renames
|
|
327
|
+
*/
|
|
328
|
+
export function computeNewPath(sourcePath, targetPath, position) {
|
|
329
|
+
const source = parsePath(sourcePath);
|
|
330
|
+
const target = parsePath(targetPath);
|
|
331
|
+
|
|
332
|
+
// Get target directory
|
|
333
|
+
let targetDir = '';
|
|
334
|
+
if (position === 'inside') {
|
|
335
|
+
// Moving inside a folder
|
|
336
|
+
targetDir = targetPath;
|
|
337
|
+
} else {
|
|
338
|
+
// Moving before/after a file or folder
|
|
339
|
+
targetDir = target.parent;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Determine the new order number
|
|
343
|
+
let newOrder;
|
|
344
|
+
if (position === 'before') {
|
|
345
|
+
// Take the target's order
|
|
346
|
+
newOrder = target.order || 1;
|
|
347
|
+
} else if (position === 'after') {
|
|
348
|
+
newOrder = (target.order || 0) + 1;
|
|
349
|
+
} else {
|
|
350
|
+
// Inside: use order 1
|
|
351
|
+
newOrder = 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Build new filename
|
|
355
|
+
const paddedOrder = String(newOrder).padStart(2, '0');
|
|
356
|
+
const newFilename = `${paddedOrder}-${source.name}${source.extension}`;
|
|
357
|
+
const newPath = targetDir ? `${targetDir}/${newFilename}` : newFilename;
|
|
358
|
+
|
|
359
|
+
// Calculate renames needed (simplified - just returns the main rename)
|
|
360
|
+
const renames = [];
|
|
361
|
+
|
|
362
|
+
// The source file rename
|
|
363
|
+
if (sourcePath !== newPath) {
|
|
364
|
+
renames.push({ from: sourcePath, to: newPath });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
newPath,
|
|
369
|
+
renames,
|
|
370
|
+
};
|
|
371
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mrmd-project
|
|
3
|
+
*
|
|
4
|
+
* Pure logic for understanding mrmd project structure and conventions.
|
|
5
|
+
*
|
|
6
|
+
* This package has NO side effects - no file I/O, no process spawning.
|
|
7
|
+
* All functions are pure: same input → same output.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { Project, FSML, Links, Assets, Scaffold, Search } from 'mrmd-project';
|
|
11
|
+
*
|
|
12
|
+
* // Find and parse project
|
|
13
|
+
* const root = Project.findRoot('/path/to/file.md', hasFile);
|
|
14
|
+
* const config = Project.parseConfig(mrmdMdContent);
|
|
15
|
+
*
|
|
16
|
+
* // Build navigation tree
|
|
17
|
+
* const tree = FSML.buildNavTree(files);
|
|
18
|
+
*
|
|
19
|
+
* // Resolve internal links
|
|
20
|
+
* const resolved = Links.resolve('installation', 'index.md', projectFiles);
|
|
21
|
+
*
|
|
22
|
+
* // Compute asset paths
|
|
23
|
+
* const relativePath = Assets.computeRelativePath('chapter/doc.md', '_assets/img.png');
|
|
24
|
+
*
|
|
25
|
+
* // Generate scaffolding
|
|
26
|
+
* const scaffold = Scaffold.project('my-project');
|
|
27
|
+
*
|
|
28
|
+
* // Fuzzy search
|
|
29
|
+
* const results = Search.files('thesis readme', allFiles);
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Project configuration
|
|
33
|
+
export * as Project from './project.js';
|
|
34
|
+
|
|
35
|
+
// Filesystem Markup Language
|
|
36
|
+
export * as FSML from './fsml.js';
|
|
37
|
+
|
|
38
|
+
// Internal links
|
|
39
|
+
export * as Links from './links.js';
|
|
40
|
+
|
|
41
|
+
// Asset management
|
|
42
|
+
export * as Assets from './assets.js';
|
|
43
|
+
|
|
44
|
+
// Scaffolding templates
|
|
45
|
+
export * as Scaffold from './scaffold.js';
|
|
46
|
+
|
|
47
|
+
// Search utilities
|
|
48
|
+
export * as Search from './search.js';
|
|
49
|
+
|
|
50
|
+
// Re-export everything as default for convenience
|
|
51
|
+
import * as Project from './project.js';
|
|
52
|
+
import * as FSML from './fsml.js';
|
|
53
|
+
import * as Links from './links.js';
|
|
54
|
+
import * as Assets from './assets.js';
|
|
55
|
+
import * as Scaffold from './scaffold.js';
|
|
56
|
+
import * as Search from './search.js';
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
Project,
|
|
60
|
+
FSML,
|
|
61
|
+
Links,
|
|
62
|
+
Assets,
|
|
63
|
+
Scaffold,
|
|
64
|
+
Search,
|
|
65
|
+
};
|