lat.md 0.4.3 → 0.5.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/dist/src/cli/check.js +19 -7
- package/dist/src/cli/context.js +1 -0
- package/dist/src/lattice.js +89 -16
- package/package.json +1 -1
package/dist/src/cli/check.js
CHANGED
|
@@ -127,19 +127,26 @@ function immediateEntries(walkedPaths) {
|
|
|
127
127
|
}
|
|
128
128
|
return [...entries].sort();
|
|
129
129
|
}
|
|
130
|
-
/** Parse bullet items from an index file. Matches `-
|
|
130
|
+
/** Parse bullet items from an index file. Matches `- [[name]] — description` */
|
|
131
131
|
function parseIndexEntries(content) {
|
|
132
132
|
const names = new Set();
|
|
133
|
-
const re = /^-
|
|
133
|
+
const re = /^- \[\[([^\]]+?)(?:\|[^\]]+)?\]\]/gm;
|
|
134
134
|
let match;
|
|
135
135
|
while ((match = re.exec(content)) !== null) {
|
|
136
136
|
names.add(match[1]);
|
|
137
137
|
}
|
|
138
138
|
return names;
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Convert a filesystem entry name to its wiki link stem.
|
|
142
|
+
* Strips `.md` extension from files; directories stay as-is.
|
|
143
|
+
*/
|
|
144
|
+
function entryToStem(name) {
|
|
145
|
+
return name.endsWith('.md') ? name.slice(0, -3) : name;
|
|
146
|
+
}
|
|
140
147
|
/** Generate a bullet-list snippet for the given entry names. */
|
|
141
148
|
function indexSnippet(entries) {
|
|
142
|
-
return entries.map((e) => `-
|
|
149
|
+
return entries.map((e) => `- [[${entryToStem(e)}]] — <describe>`).join('\n');
|
|
143
150
|
}
|
|
144
151
|
export async function checkIndex(latticeDir) {
|
|
145
152
|
const errors = [];
|
|
@@ -183,12 +190,16 @@ export async function checkIndex(latticeDir) {
|
|
|
183
190
|
});
|
|
184
191
|
continue;
|
|
185
192
|
}
|
|
186
|
-
// Parse existing entries and validate
|
|
193
|
+
// Parse existing entries and validate.
|
|
194
|
+
// Listed entries are wiki link stems (no .md extension).
|
|
195
|
+
// Children are filesystem names (with .md for files, bare for dirs).
|
|
187
196
|
const listed = parseIndexEntries(content);
|
|
197
|
+
const childStems = new Set(children.map(entryToStem));
|
|
198
|
+
const stemToChild = new Map(children.map((c) => [entryToStem(c), c]));
|
|
188
199
|
const relDir = dir === '' ? basename(latticeDir) + '/' : dir + '/';
|
|
189
200
|
const missing = [];
|
|
190
201
|
for (const child of children) {
|
|
191
|
-
if (!listed.has(child)) {
|
|
202
|
+
if (!listed.has(entryToStem(child))) {
|
|
192
203
|
missing.push(child);
|
|
193
204
|
}
|
|
194
205
|
}
|
|
@@ -199,11 +210,12 @@ export async function checkIndex(latticeDir) {
|
|
|
199
210
|
snippet: indexSnippet(missing),
|
|
200
211
|
});
|
|
201
212
|
}
|
|
213
|
+
const indexStem = entryToStem(indexFileName);
|
|
202
214
|
for (const name of listed) {
|
|
203
|
-
if (!
|
|
215
|
+
if (!childStems.has(name) && name !== indexStem) {
|
|
204
216
|
errors.push({
|
|
205
217
|
dir: relDir,
|
|
206
|
-
message: `"${indexRelPath}" lists "${name}" but it does not exist`,
|
|
218
|
+
message: `"${indexRelPath}" lists "[[${name}]]" but it does not exist`,
|
|
207
219
|
});
|
|
208
220
|
}
|
|
209
221
|
}
|
package/dist/src/cli/context.js
CHANGED
|
@@ -8,6 +8,7 @@ export function resolveContext(opts) {
|
|
|
8
8
|
const latDir = findLatticeDir(opts.dir) ?? '';
|
|
9
9
|
if (!latDir) {
|
|
10
10
|
console.error(chalk.red('No lat.md directory found'));
|
|
11
|
+
console.error(chalk.dim('Run `lat init` to create one.'));
|
|
11
12
|
process.exit(1);
|
|
12
13
|
}
|
|
13
14
|
return { latDir, color, chalk };
|
package/dist/src/lattice.js
CHANGED
|
@@ -81,7 +81,7 @@ export function parseSections(filePath, content, latticeDir) {
|
|
|
81
81
|
stack.pop();
|
|
82
82
|
}
|
|
83
83
|
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
84
|
-
const id = parent ? `${parent.id}#${heading}` : file
|
|
84
|
+
const id = parent ? `${parent.id}#${heading}` : `${file}#${heading}`;
|
|
85
85
|
const section = {
|
|
86
86
|
id,
|
|
87
87
|
heading,
|
|
@@ -218,17 +218,39 @@ export function resolveRef(target, sectionIds, fileIndex) {
|
|
|
218
218
|
const hashIdx = target.indexOf('#');
|
|
219
219
|
const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
220
220
|
const rest = hashIdx === -1 ? '' : target.slice(hashIdx);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
// Try resolving the file part: either it's a full path or a bare stem
|
|
222
|
+
const filePaths = fileIndex.has(filePart)
|
|
223
|
+
? fileIndex.get(filePart)
|
|
224
|
+
: [filePart];
|
|
225
|
+
if (filePaths.length === 1) {
|
|
226
|
+
const fp = filePaths[0];
|
|
227
|
+
const expanded = fp + rest;
|
|
224
228
|
if (sectionIds.has(expanded.toLowerCase())) {
|
|
225
229
|
return { resolved: expanded, ambiguous: null, suggested: null };
|
|
226
230
|
}
|
|
231
|
+
// Try inserting root headings between file and rest.
|
|
232
|
+
// Handles Obsidian-style file#heading refs where the h1 is implicit.
|
|
233
|
+
const rootHeadings = findRootHeadings(fp, sectionIds);
|
|
234
|
+
for (const h1 of rootHeadings) {
|
|
235
|
+
const withRoot = rest ? `${fp}#${h1}${rest}` : `${fp}#${h1}`;
|
|
236
|
+
if (sectionIds.has(withRoot.toLowerCase())) {
|
|
237
|
+
return { resolved: withRoot, ambiguous: null, suggested: null };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
227
240
|
}
|
|
228
|
-
else if (
|
|
241
|
+
else if (filePaths.length > 1) {
|
|
229
242
|
// Multiple files share this stem — ambiguous at the filename level
|
|
230
|
-
const all =
|
|
231
|
-
const valid =
|
|
243
|
+
const all = filePaths.map((c) => c + rest);
|
|
244
|
+
const valid = filePaths.filter((c) => {
|
|
245
|
+
if (sectionIds.has((c + rest).toLowerCase()))
|
|
246
|
+
return true;
|
|
247
|
+
// Also try with root heading insertion
|
|
248
|
+
const rootHeadings = findRootHeadings(c, sectionIds);
|
|
249
|
+
return rootHeadings.some((h1) => {
|
|
250
|
+
const withRoot = rest ? `${c}#${h1}${rest}` : `${c}#${h1}`;
|
|
251
|
+
return sectionIds.has(withRoot.toLowerCase());
|
|
252
|
+
});
|
|
253
|
+
});
|
|
232
254
|
return {
|
|
233
255
|
resolved: target,
|
|
234
256
|
ambiguous: all,
|
|
@@ -237,6 +259,20 @@ export function resolveRef(target, sectionIds, fileIndex) {
|
|
|
237
259
|
}
|
|
238
260
|
return { resolved: target, ambiguous: null, suggested: null };
|
|
239
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Find root (h1) headings for a file by scanning sectionIds for entries
|
|
264
|
+
* that have exactly the pattern `file#heading` (no further # segments).
|
|
265
|
+
*/
|
|
266
|
+
function findRootHeadings(file, sectionIds) {
|
|
267
|
+
const prefix = file.toLowerCase() + '#';
|
|
268
|
+
const headings = [];
|
|
269
|
+
for (const id of sectionIds) {
|
|
270
|
+
if (id.startsWith(prefix) && !id.includes('#', prefix.length)) {
|
|
271
|
+
headings.push(id.slice(prefix.length));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return headings;
|
|
275
|
+
}
|
|
240
276
|
const MAX_DISTANCE_RATIO = 0.4;
|
|
241
277
|
export function findSections(sections, query) {
|
|
242
278
|
const flat = flattenSections(sections);
|
|
@@ -252,6 +288,16 @@ export function findSections(sections, query) {
|
|
|
252
288
|
}));
|
|
253
289
|
if (exactMatches.length > 0 && isFullPath)
|
|
254
290
|
return exactMatches;
|
|
291
|
+
// Tier 1a: bare name matches file — return root sections of that file
|
|
292
|
+
if (!isFullPath && exactMatches.length === 0) {
|
|
293
|
+
const fileRoots = flat.filter((s) => s.file.toLowerCase() === q && !s.id.includes('#', s.file.length + 1));
|
|
294
|
+
if (fileRoots.length > 0) {
|
|
295
|
+
return fileRoots.map((s) => ({
|
|
296
|
+
section: s,
|
|
297
|
+
reason: 'exact match',
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
255
301
|
// Tier 1b: file stem expansion
|
|
256
302
|
// For bare names: "locate" → matches root section of "tests/locate.md"
|
|
257
303
|
// For paths with #: "setup#Install" → expands to "guides/setup#Install"
|
|
@@ -262,26 +308,53 @@ export function findSections(sections, query) {
|
|
|
262
308
|
const hashIdx = normalized.indexOf('#');
|
|
263
309
|
const filePart = normalized.slice(0, hashIdx);
|
|
264
310
|
const rest = normalized.slice(hashIdx);
|
|
265
|
-
const
|
|
266
|
-
|
|
311
|
+
const stemPaths = fileIndex.get(filePart) ?? [];
|
|
312
|
+
// Also try filePart as a direct file path (for root-level files not in index)
|
|
313
|
+
const allPaths = stemPaths.length > 0 ? stemPaths : filePart ? [filePart] : [];
|
|
314
|
+
for (const p of allPaths) {
|
|
267
315
|
const expanded = (p + rest).toLowerCase();
|
|
268
316
|
const s = flat.find((s) => s.id.toLowerCase() === expanded && !exact.includes(s));
|
|
269
|
-
if (s)
|
|
317
|
+
if (s) {
|
|
270
318
|
stemMatches.push({
|
|
271
319
|
section: s,
|
|
272
|
-
reason:
|
|
320
|
+
reason: stemPaths.length > 0
|
|
321
|
+
? `file stem expanded: ${filePart} → ${p}`
|
|
322
|
+
: 'exact match',
|
|
273
323
|
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
// Try inserting root headings: file#rest → file#h1#rest
|
|
327
|
+
const rootsOfFile = flat.filter((s) => s.file.toLowerCase() === p.toLowerCase() &&
|
|
328
|
+
!s.id.includes('#', s.file.length + 1));
|
|
329
|
+
for (const root of rootsOfFile) {
|
|
330
|
+
const withRoot = (root.id + rest).toLowerCase();
|
|
331
|
+
const match = flat.find((s) => s.id.toLowerCase() === withRoot && !exact.includes(s));
|
|
332
|
+
if (match) {
|
|
333
|
+
stemMatches.push({
|
|
334
|
+
section: match,
|
|
335
|
+
reason: stemPaths.length > 0
|
|
336
|
+
? `file stem expanded: ${filePart} → ${p}`
|
|
337
|
+
: 'exact match',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
274
341
|
}
|
|
275
342
|
if (stemMatches.length > 0)
|
|
276
343
|
return [...exactMatches, ...stemMatches];
|
|
277
344
|
}
|
|
278
345
|
else {
|
|
279
|
-
// Bare name: match
|
|
346
|
+
// Bare name: match root sections of files via stem index
|
|
280
347
|
const paths = fileIndex.get(normalized) ?? [];
|
|
281
348
|
for (const p of paths) {
|
|
282
|
-
const s
|
|
283
|
-
|
|
284
|
-
|
|
349
|
+
for (const s of flat) {
|
|
350
|
+
if (exact.includes(s))
|
|
351
|
+
continue;
|
|
352
|
+
// Root sections have id = "file#heading" (exactly 2 segments)
|
|
353
|
+
if (s.file.toLowerCase() === p.toLowerCase() &&
|
|
354
|
+
!s.id.includes('#', s.file.length + 1)) {
|
|
355
|
+
stemMatches.push({ section: s, reason: 'file stem match' });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
285
358
|
}
|
|
286
359
|
}
|
|
287
360
|
// Tier 2: exact match on trailing segments (subsection name match)
|
|
@@ -423,7 +496,7 @@ export function extractRefs(filePath, content, latticeDir) {
|
|
|
423
496
|
stack.pop();
|
|
424
497
|
}
|
|
425
498
|
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
426
|
-
const id = parent ? `${parent.id}#${heading}` : file
|
|
499
|
+
const id = parent ? `${parent.id}#${heading}` : `${file}#${heading}`;
|
|
427
500
|
flat[idx].id = id;
|
|
428
501
|
stack.push({ id, depth });
|
|
429
502
|
idx++;
|