@plebeiantech/lat.md 0.12.3-fork.3 → 0.12.3-fork.5
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 +2 -0
- package/dist/src/cli/check-mode.js +5 -1
- package/dist/src/cli/comment-guard.d.ts +14 -0
- package/dist/src/cli/comment-guard.js +10 -33
- package/dist/src/cli/comment-reminder.d.ts +65 -6
- package/dist/src/cli/comment-reminder.js +164 -28
- package/dist/src/cli/index.js +29 -4
- package/dist/src/cli/search.js +6 -4
- package/dist/src/cli/ui-build.d.ts +4 -0
- package/dist/src/cli/ui-build.js +13 -0
- package/dist/src/cli/ui.js +18 -2
- package/dist/src/code-refs.js +25 -4
- package/dist/src/fork/upstream-drift-cli.d.ts +1 -0
- package/dist/src/fork/upstream-drift-cli.js +2 -0
- package/dist/src/fork/upstream-drift.d.ts +26 -0
- package/dist/src/fork/upstream-drift.js +185 -0
- package/dist/src/fork/upstream-guard-cli.d.ts +1 -0
- package/dist/src/fork/upstream-guard-cli.js +2 -0
- package/dist/src/fork/upstream-guard.d.ts +72 -0
- package/dist/src/fork/upstream-guard.js +445 -0
- package/dist/src/graph-export.d.ts +2 -0
- package/dist/src/graph-export.js +31 -12
- package/dist/src/knowledge/claude-memory.d.ts +1 -0
- package/dist/src/knowledge/claude-memory.js +33 -4
- package/dist/src/lattice.d.ts +1 -0
- package/dist/src/search/search.d.ts +1 -0
- package/dist/src/search/search.js +15 -9
- package/dist/src/untrusted.d.ts +0 -6
- package/dist/src/untrusted.js +7 -11
- package/dist/src/view/client/assets/index-VZwPPiFE.js +334 -0
- package/dist/src/view/client/assets/index-W8ktvNj9.css +1 -0
- package/dist/src/view/client/assets/logo-CNF8zs47.svg +182 -0
- package/dist/src/view/client/index.html +2 -2
- package/dist/src/view/git-diff.js +17 -0
- package/dist/src/view/graph.d.ts +6 -0
- package/dist/src/view/graph.js +176 -0
- package/dist/src/view/markdown.d.ts +5 -1
- package/dist/src/view/markdown.js +112 -25
- package/dist/src/view/protocol.d.ts +43 -0
- package/dist/src/view/protocol.js +1 -1
- package/dist/src/view/references.d.ts +2 -0
- package/dist/src/view/references.js +30 -8
- package/dist/src/view/repository.d.ts +5 -2
- package/dist/src/view/repository.js +13 -12
- package/dist/src/view/search.js +1 -0
- package/dist/src/view/server.d.ts +3 -0
- package/dist/src/view/server.js +42 -9
- package/dist/src/view/source-target.d.ts +8 -0
- package/dist/src/view/source-target.js +22 -0
- package/dist/src/view/static-build.d.ts +16 -0
- package/dist/src/view/static-build.js +408 -0
- package/dist/src/view/static-protocol.d.ts +23 -0
- package/dist/src/view/static-protocol.js +10 -0
- package/dist/src/view/store.d.ts +9 -1
- package/dist/src/view/store.js +82 -15
- package/dist/src/view/table-of-contents.d.ts +9 -0
- package/dist/src/view/table-of-contents.js +103 -0
- package/package.json +3 -1
- package/templates/AGENTS.md +2 -0
- package/templates/skill/SKILL.md +2 -0
- package/dist/src/view/client/assets/index-B0n0u03y.css +0 -1
- package/dist/src/view/client/assets/index-xm2_px4q.js +0 -23
|
@@ -10,6 +10,7 @@ const CODE_LINK_CLASSES = [
|
|
|
10
10
|
'wiki-link-code',
|
|
11
11
|
'wiki-link-active',
|
|
12
12
|
'code-link-language',
|
|
13
|
+
'code-link-leading',
|
|
13
14
|
'code-language-ts',
|
|
14
15
|
'code-language-js',
|
|
15
16
|
'code-language-py',
|
|
@@ -18,11 +19,17 @@ const CODE_LINK_CLASSES = [
|
|
|
18
19
|
'code-language-c',
|
|
19
20
|
];
|
|
20
21
|
const ERROR_CLASS = 'markdown-error';
|
|
22
|
+
const EXTERNAL_LINK_CLASS = 'external-link';
|
|
23
|
+
const EXTERNAL_LINK_ICON_CLASS = 'external-link-icon';
|
|
21
24
|
const GIT_CLASSES = ['git-added', 'git-removed'];
|
|
22
25
|
function classAttributes(tag) {
|
|
26
|
+
const attributes = defaultSchema.attributes?.[tag] ?? [];
|
|
27
|
+
const allowedClasses = attributes.flatMap((attribute) => Array.isArray(attribute) && attribute[0] === 'className'
|
|
28
|
+
? attribute.slice(1)
|
|
29
|
+
: []);
|
|
23
30
|
return [
|
|
24
|
-
...(
|
|
25
|
-
['className', ERROR_CLASS, ...GIT_CLASSES],
|
|
31
|
+
...attributes.filter((attribute) => !(Array.isArray(attribute) && attribute[0] === 'className')),
|
|
32
|
+
['className', ...allowedClasses, ERROR_CLASS, ...GIT_CLASSES],
|
|
26
33
|
];
|
|
27
34
|
}
|
|
28
35
|
const sanitizeSchema = {
|
|
@@ -38,6 +45,7 @@ const sanitizeSchema = {
|
|
|
38
45
|
'wiki-link-segmented',
|
|
39
46
|
'wiki-link-code',
|
|
40
47
|
'wiki-link-active',
|
|
48
|
+
EXTERNAL_LINK_CLASS,
|
|
41
49
|
ERROR_CLASS,
|
|
42
50
|
...GIT_CLASSES,
|
|
43
51
|
],
|
|
@@ -55,20 +63,25 @@ const sanitizeSchema = {
|
|
|
55
63
|
img: classAttributes('img'),
|
|
56
64
|
ins: classAttributes('ins'),
|
|
57
65
|
li: classAttributes('li'),
|
|
66
|
+
ol: classAttributes('ol'),
|
|
58
67
|
p: classAttributes('p'),
|
|
59
68
|
pre: classAttributes('pre'),
|
|
60
69
|
span: [
|
|
61
70
|
...(defaultSchema.attributes?.span ?? []),
|
|
62
71
|
'ariaHidden',
|
|
72
|
+
'ariaLabel',
|
|
63
73
|
[
|
|
64
74
|
'className',
|
|
65
75
|
'wiki-link-context',
|
|
66
76
|
'wiki-link-leaf',
|
|
77
|
+
'wiki-link-ref-count',
|
|
78
|
+
EXTERNAL_LINK_ICON_CLASS,
|
|
67
79
|
...CODE_LINK_CLASSES.slice(2),
|
|
68
80
|
ERROR_CLASS,
|
|
69
81
|
...GIT_CLASSES,
|
|
70
82
|
],
|
|
71
83
|
],
|
|
84
|
+
ul: classAttributes('ul'),
|
|
72
85
|
},
|
|
73
86
|
};
|
|
74
87
|
const htmlProcessor = unified()
|
|
@@ -155,6 +168,61 @@ function languageIcon(language) {
|
|
|
155
168
|
children: [{ type: 'text', value: language.label }],
|
|
156
169
|
};
|
|
157
170
|
}
|
|
171
|
+
function codeLinkContent(language, children) {
|
|
172
|
+
const [first, ...rest] = children;
|
|
173
|
+
if (!first)
|
|
174
|
+
return [languageIcon(language)];
|
|
175
|
+
let leading = first;
|
|
176
|
+
let remainder = null;
|
|
177
|
+
if (first.type === 'text') {
|
|
178
|
+
const breakAt = first.value.search(/\s/);
|
|
179
|
+
if (breakAt > 0) {
|
|
180
|
+
leading = { ...first, value: first.value.slice(0, breakAt) };
|
|
181
|
+
remainder = { ...first, value: first.value.slice(breakAt) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [
|
|
185
|
+
{
|
|
186
|
+
type: 'emphasis',
|
|
187
|
+
data: {
|
|
188
|
+
hName: 'span',
|
|
189
|
+
hProperties: { className: ['code-link-leading'] },
|
|
190
|
+
},
|
|
191
|
+
children: [languageIcon(language), leading],
|
|
192
|
+
},
|
|
193
|
+
...(remainder ? [remainder] : []),
|
|
194
|
+
...rest,
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
function externalLinkIcon() {
|
|
198
|
+
return {
|
|
199
|
+
type: 'emphasis',
|
|
200
|
+
data: {
|
|
201
|
+
hName: 'span',
|
|
202
|
+
hProperties: {
|
|
203
|
+
ariaHidden: 'true',
|
|
204
|
+
className: [EXTERNAL_LINK_ICON_CLASS],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
children: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function isExternalSiteUrl(url) {
|
|
211
|
+
return /^(?:https?:)?\/\//i.test(url);
|
|
212
|
+
}
|
|
213
|
+
function referenceCountBadge(count) {
|
|
214
|
+
return {
|
|
215
|
+
type: 'emphasis',
|
|
216
|
+
data: {
|
|
217
|
+
hName: 'span',
|
|
218
|
+
hProperties: {
|
|
219
|
+
className: ['wiki-link-ref-count'],
|
|
220
|
+
ariaLabel: `${count} ${count === 1 ? 'reference' : 'references'}`,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
children: [{ type: 'text', value: String(count) }],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
158
226
|
function targetMatches(node, target) {
|
|
159
227
|
if (node.type === 'wikiLink') {
|
|
160
228
|
return node.value.toLowerCase() === target.toLowerCase();
|
|
@@ -229,24 +297,39 @@ export async function renderMarkdown(markdown, filePath, resolveWikiLink, option
|
|
|
229
297
|
tree.children = tree.children.filter((node) => node.type !== 'yaml');
|
|
230
298
|
if (options.errors)
|
|
231
299
|
markMarkdownErrors(tree, options.errors);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
node.data
|
|
238
|
-
|
|
239
|
-
hProperties
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
300
|
+
visit(tree, 'link', (node) => {
|
|
301
|
+
const authoredUrl = node.url;
|
|
302
|
+
if (options.activeMarkdownLink &&
|
|
303
|
+
authoredUrl === options.activeMarkdownLink) {
|
|
304
|
+
node.data = {
|
|
305
|
+
...node.data,
|
|
306
|
+
hProperties: {
|
|
307
|
+
...(node.data?.hProperties ?? {}),
|
|
308
|
+
className: ['wiki-link-active'],
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (options.rewriteMarkdownLink) {
|
|
313
|
+
node.url = options.rewriteMarkdownLink(authoredUrl);
|
|
314
|
+
}
|
|
315
|
+
if (!isExternalSiteUrl(node.url))
|
|
316
|
+
return;
|
|
317
|
+
const properties = node.data?.hProperties ?? {};
|
|
318
|
+
const currentClasses = properties.className;
|
|
319
|
+
const classes = Array.isArray(currentClasses)
|
|
320
|
+
? currentClasses.map(String)
|
|
321
|
+
: currentClasses
|
|
322
|
+
? [String(currentClasses)]
|
|
323
|
+
: [];
|
|
324
|
+
node.data = {
|
|
325
|
+
...node.data,
|
|
326
|
+
hProperties: {
|
|
327
|
+
...properties,
|
|
328
|
+
className: [...classes, EXTERNAL_LINK_CLASS],
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
node.children.push(externalLinkIcon());
|
|
332
|
+
});
|
|
250
333
|
const firstHeading = tree.children.find((node) => node.type === 'heading');
|
|
251
334
|
const title = firstHeading
|
|
252
335
|
? nodeText(firstHeading)
|
|
@@ -266,9 +349,10 @@ export async function renderMarkdown(markdown, filePath, resolveWikiLink, option
|
|
|
266
349
|
visit(tree, 'wikiLink', (node, index, parent) => {
|
|
267
350
|
if (index === undefined || !parent || !('children' in parent))
|
|
268
351
|
return;
|
|
269
|
-
const
|
|
352
|
+
const resolution = resolvedLinks.get(node);
|
|
270
353
|
const markedProperties = node.data?.hProperties;
|
|
271
|
-
if (
|
|
354
|
+
if (resolution) {
|
|
355
|
+
const { href, referenceCount } = resolution;
|
|
272
356
|
const content = wikiLinkContent(node);
|
|
273
357
|
const language = href.startsWith('/code/')
|
|
274
358
|
? codeLanguage(node.value)
|
|
@@ -296,9 +380,12 @@ export async function renderMarkdown(markdown, filePath, resolveWikiLink, option
|
|
|
296
380
|
},
|
|
297
381
|
}
|
|
298
382
|
: undefined,
|
|
299
|
-
children:
|
|
300
|
-
|
|
301
|
-
|
|
383
|
+
children: [
|
|
384
|
+
...(language
|
|
385
|
+
? codeLinkContent(language, content.children)
|
|
386
|
+
: content.children),
|
|
387
|
+
...(referenceCount > 1 ? [referenceCountBadge(referenceCount)] : []),
|
|
388
|
+
],
|
|
302
389
|
};
|
|
303
390
|
return;
|
|
304
391
|
}
|
|
@@ -7,7 +7,9 @@ export type ViewIndex = {
|
|
|
7
7
|
entry: string;
|
|
8
8
|
errorCounts: Record<string, number>;
|
|
9
9
|
git: ViewGitState | null;
|
|
10
|
+
logoText: string;
|
|
10
11
|
};
|
|
12
|
+
export declare const DEFAULT_VIEW_LOGO_TEXT = "lat.md";
|
|
11
13
|
export type ViewDocumentError = {
|
|
12
14
|
anchor: string;
|
|
13
15
|
line: number;
|
|
@@ -22,6 +24,7 @@ export type ViewSearchResult = {
|
|
|
22
24
|
breadcrumbs: string[];
|
|
23
25
|
description: string;
|
|
24
26
|
url: string;
|
|
27
|
+
score: number;
|
|
25
28
|
};
|
|
26
29
|
export type ViewSearchResponse = {
|
|
27
30
|
query: string;
|
|
@@ -31,11 +34,51 @@ export type ViewProjectChange = {
|
|
|
31
34
|
generation: number;
|
|
32
35
|
markdownGeneration: number;
|
|
33
36
|
};
|
|
37
|
+
export type ViewGraphNodeKind = 'document' | 'source' | 'code-reference';
|
|
38
|
+
export type ViewGraphNode = {
|
|
39
|
+
id: string;
|
|
40
|
+
kind: ViewGraphNodeKind;
|
|
41
|
+
label: string;
|
|
42
|
+
url: string;
|
|
43
|
+
breadcrumbs: string[];
|
|
44
|
+
inDegree: number;
|
|
45
|
+
outDegree: number;
|
|
46
|
+
documentPath?: string;
|
|
47
|
+
sectionId?: string;
|
|
48
|
+
sourcePath?: string;
|
|
49
|
+
symbol?: string;
|
|
50
|
+
line?: number;
|
|
51
|
+
snippet?: string;
|
|
52
|
+
gitStatus?: ViewGitFileStatus;
|
|
53
|
+
errorCount?: number;
|
|
54
|
+
};
|
|
55
|
+
export type ViewGraphEdgeKind = 'wiki' | 'markdown' | 'source' | 'code-mention';
|
|
56
|
+
export type ViewGraphEdge = {
|
|
57
|
+
id: string;
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
kind: ViewGraphEdgeKind;
|
|
61
|
+
weight: number;
|
|
62
|
+
};
|
|
63
|
+
export type ViewGraph = {
|
|
64
|
+
generation: number;
|
|
65
|
+
nodes: ViewGraphNode[];
|
|
66
|
+
edges: ViewGraphEdge[];
|
|
67
|
+
};
|
|
68
|
+
export type ViewDocumentTocItem = {
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
depth: number;
|
|
72
|
+
errorCount: number;
|
|
73
|
+
hasGitChanges: boolean;
|
|
74
|
+
};
|
|
34
75
|
export type ViewDocument = {
|
|
35
76
|
path: string;
|
|
36
77
|
title: string;
|
|
37
78
|
html: string;
|
|
38
79
|
gitHtml: string | null;
|
|
80
|
+
graphNodeIds: Record<string, string>;
|
|
81
|
+
tableOfContents: ViewDocumentTocItem[];
|
|
39
82
|
errors: ViewDocumentError[];
|
|
40
83
|
backReferences: ViewSectionBackReferences[];
|
|
41
84
|
frontmatter: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const DEFAULT_VIEW_LOGO_TEXT = 'lat.md';
|
|
@@ -47,9 +47,11 @@ type IndexedBackReference = IndexedMarkdownReference | ViewCodeBackReference;
|
|
|
47
47
|
export type ViewReferenceIndex = {
|
|
48
48
|
incomingBySection: ReadonlyMap<string, readonly IndexedBackReference[]>;
|
|
49
49
|
sourceByTarget: ReadonlyMap<string, readonly IndexedMarkdownReference[]>;
|
|
50
|
+
sourceReferenceCounts: ReadonlyMap<string, number>;
|
|
50
51
|
};
|
|
51
52
|
/** Parse one Markdown file once into every structure needed by the view store. */
|
|
52
53
|
export declare function parseViewMarkdownFile(absolutePath: string, content: string, latDir: string, projectRoot: string): ViewParsedMarkdownFile;
|
|
54
|
+
export declare function linkedSection(url: string, sourcePath: string, sectionsByPath: ReadonlyMap<string, Section[]>): Section | null;
|
|
53
55
|
/** Resolve cached outgoing occurrences into direct section and source indexes. */
|
|
54
56
|
export declare function buildViewReferenceIndex(markdownFiles: Iterable<ViewParsedMarkdownFile>, codeFiles: Iterable<ViewCodeReferenceFile>, allSections: Section[]): ViewReferenceIndex;
|
|
55
57
|
/** Render direct reverse-index entries for the sections visible in one file. */
|
|
@@ -4,6 +4,7 @@ import { buildFileIndex, buildSectionSlugIndex, extractRefs, extractLinks, flatt
|
|
|
4
4
|
import { parse } from '../parser.js';
|
|
5
5
|
import { toPosix } from '../walk.js';
|
|
6
6
|
import { renderMarkdown } from './markdown.js';
|
|
7
|
+
import { viewSourceTarget } from './source-target.js';
|
|
7
8
|
function inlineText(node) {
|
|
8
9
|
if (node.type === 'wikiLink')
|
|
9
10
|
return node.data.alias ?? node.value;
|
|
@@ -115,7 +116,7 @@ function sourceLineUrl(path, line) {
|
|
|
115
116
|
const encoded = path.split('/').map(encodeURIComponent).join('/');
|
|
116
117
|
return `/code/${encoded}?at=${line}`;
|
|
117
118
|
}
|
|
118
|
-
function linkedSection(url, sourcePath, sectionsByPath) {
|
|
119
|
+
export function linkedSection(url, sourcePath, sectionsByPath) {
|
|
119
120
|
if (url.startsWith('//') || /^[a-z][a-z\d+.-]*:/i.test(url))
|
|
120
121
|
return null;
|
|
121
122
|
const encodedSourcePath = sourcePath
|
|
@@ -184,6 +185,15 @@ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
|
|
|
184
185
|
if (!references.has(key))
|
|
185
186
|
references.set(key, reference);
|
|
186
187
|
};
|
|
188
|
+
const addSourceReference = (target, key, reference) => {
|
|
189
|
+
let references = sourceByTarget.get(target);
|
|
190
|
+
if (!references) {
|
|
191
|
+
references = new Map();
|
|
192
|
+
sourceByTarget.set(target, references);
|
|
193
|
+
}
|
|
194
|
+
if (!references.has(key))
|
|
195
|
+
references.set(key, reference);
|
|
196
|
+
};
|
|
187
197
|
for (const file of files) {
|
|
188
198
|
const fileSections = flattenSections(file.sections).sort((a, b) => a.startLine - b.startLine);
|
|
189
199
|
for (const ref of file.wikiRefs) {
|
|
@@ -198,16 +208,20 @@ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
|
|
|
198
208
|
paragraph: paragraphFor(file, section, ref.line),
|
|
199
209
|
activeWikiLink: ref.target,
|
|
200
210
|
};
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
const locationKey = `markdown:${section.filePath}:${reference.paragraph.startLine}`;
|
|
212
|
+
const source = viewSourceTarget(ref.target);
|
|
213
|
+
if (source) {
|
|
214
|
+
addSourceReference(source.key, locationKey, reference);
|
|
215
|
+
if (source.symbol) {
|
|
216
|
+
addSourceReference(source.fileKey, locationKey, reference);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
205
219
|
const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
|
|
206
220
|
if (resolved.ambiguous ||
|
|
207
221
|
!sectionById.has(resolved.resolved.toLowerCase())) {
|
|
208
222
|
continue;
|
|
209
223
|
}
|
|
210
|
-
addIncoming(resolved.resolved,
|
|
224
|
+
addIncoming(resolved.resolved, locationKey, reference);
|
|
211
225
|
}
|
|
212
226
|
for (const link of file.markdownLinks) {
|
|
213
227
|
if (link.kind !== 'link')
|
|
@@ -252,7 +266,14 @@ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
|
|
|
252
266
|
target,
|
|
253
267
|
[...references.values()],
|
|
254
268
|
])),
|
|
255
|
-
sourceByTarget,
|
|
269
|
+
sourceByTarget: new Map([...sourceByTarget].map(([target, references]) => [
|
|
270
|
+
target,
|
|
271
|
+
[...references.values()],
|
|
272
|
+
])),
|
|
273
|
+
sourceReferenceCounts: new Map([...sourceByTarget].map(([target, references]) => [
|
|
274
|
+
target,
|
|
275
|
+
references.size,
|
|
276
|
+
])),
|
|
256
277
|
};
|
|
257
278
|
}
|
|
258
279
|
async function renderIndexedMarkdownReference(reference, latDir, projectRoot, resolveWikiLink) {
|
|
@@ -305,7 +326,8 @@ export async function renderSectionBackReferences(index, visibleSections, latDir
|
|
|
305
326
|
}
|
|
306
327
|
/** Render cached Markdown occurrences that point to one source target. */
|
|
307
328
|
export async function renderSourceReferenceContext(index, target, origin, latDir, projectRoot, createWikiLinkResolver) {
|
|
308
|
-
const
|
|
329
|
+
const source = viewSourceTarget(target);
|
|
330
|
+
const indexed = source ? (index.sourceByTarget.get(source.key) ?? []) : [];
|
|
309
331
|
const resolverByPath = new Map();
|
|
310
332
|
const located = [];
|
|
311
333
|
for (const reference of indexed) {
|
|
@@ -5,8 +5,11 @@ export declare class ViewDocumentNotFoundError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
export declare class ViewSourceNotFoundError extends Error {
|
|
7
7
|
}
|
|
8
|
-
export declare function createMarkdownWikiLinkResolver(latDir: string, requestedPath: string, loadedSections: Section[]): Promise<(target: string, context: {
|
|
8
|
+
export declare function createMarkdownWikiLinkResolver(latDir: string, requestedPath: string, loadedSections: Section[], referenceIndex?: ViewReferenceIndex): Promise<(target: string, context: {
|
|
9
9
|
line: number;
|
|
10
|
-
}) => Promise<
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
href: string;
|
|
12
|
+
referenceCount: number;
|
|
13
|
+
} | null>>;
|
|
11
14
|
/** Read and highlight a source file after constraining it to the project root. */
|
|
12
15
|
export declare function getViewSource(latDir: string, projectRoot: string, requestedPath: string, requestedSymbol?: string, origin?: SourceReferenceOrigin, requestedLine?: number, allSections?: Section[], referenceIndex?: ViewReferenceIndex): Promise<ViewSourceDocument>;
|
|
@@ -5,6 +5,7 @@ import { resolveSourceSymbol, SOURCE_EXTENSIONS, } from '../source-parser.js';
|
|
|
5
5
|
import { toPosix } from '../walk.js';
|
|
6
6
|
import { highlightSource } from './highlight.js';
|
|
7
7
|
import { renderSourceReferenceContext, } from './references.js';
|
|
8
|
+
import { viewSourceTarget } from './source-target.js';
|
|
8
9
|
export class ViewDocumentNotFoundError extends Error {
|
|
9
10
|
}
|
|
10
11
|
export class ViewSourceNotFoundError extends Error {
|
|
@@ -28,13 +29,6 @@ function sourceUrl(path, symbol, origin) {
|
|
|
28
29
|
const fragment = symbol ? `#${encodeURIComponent(symbol)}` : '';
|
|
29
30
|
return `/code/${encodedPath}${search}${fragment}`;
|
|
30
31
|
}
|
|
31
|
-
function sourceTarget(target) {
|
|
32
|
-
const hash = target.indexOf('#');
|
|
33
|
-
const path = hash === -1 ? target : target.slice(0, hash);
|
|
34
|
-
if (!SOURCE_EXTENSIONS.has(extname(path)))
|
|
35
|
-
return null;
|
|
36
|
-
return { path, symbol: hash === -1 ? '' : target.slice(hash + 1) };
|
|
37
|
-
}
|
|
38
32
|
function matchingSymbol(symbols, symbolPath) {
|
|
39
33
|
const parts = symbolPath.split('#');
|
|
40
34
|
if (parts.length === 1) {
|
|
@@ -45,7 +39,7 @@ function matchingSymbol(symbols, symbolPath) {
|
|
|
45
39
|
}
|
|
46
40
|
return undefined;
|
|
47
41
|
}
|
|
48
|
-
export async function createMarkdownWikiLinkResolver(latDir, requestedPath, loadedSections) {
|
|
42
|
+
export async function createMarkdownWikiLinkResolver(latDir, requestedPath, loadedSections, referenceIndex) {
|
|
49
43
|
const projectRoot = dirname(latDir);
|
|
50
44
|
const flat = flattenSections(loadedSections);
|
|
51
45
|
const sectionIds = new Set(flat.map((section) => section.id.toLowerCase()));
|
|
@@ -67,9 +61,13 @@ export async function createMarkdownWikiLinkResolver(latDir, requestedPath, load
|
|
|
67
61
|
const fragment = target.includes('#') && section.githubSlug
|
|
68
62
|
? `#${encodeURIComponent(section.githubSlug)}`
|
|
69
63
|
: '';
|
|
70
|
-
return
|
|
64
|
+
return {
|
|
65
|
+
href: `${documentUrl(file)}${fragment}`,
|
|
66
|
+
referenceCount: referenceIndex?.incomingBySection.get(section.id.toLowerCase())
|
|
67
|
+
?.length ?? 0,
|
|
68
|
+
};
|
|
71
69
|
}
|
|
72
|
-
const source =
|
|
70
|
+
const source = viewSourceTarget(target);
|
|
73
71
|
if (!source)
|
|
74
72
|
return null;
|
|
75
73
|
try {
|
|
@@ -83,7 +81,10 @@ export async function createMarkdownWikiLinkResolver(latDir, requestedPath, load
|
|
|
83
81
|
const origin = section
|
|
84
82
|
? { sectionId: section.id, line: context.line }
|
|
85
83
|
: undefined;
|
|
86
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
href: sourceUrl(source.path, source.symbol, origin),
|
|
86
|
+
referenceCount: referenceIndex?.sourceReferenceCounts.get(source.key) ?? 0,
|
|
87
|
+
};
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
if (error instanceof ViewSourceNotFoundError)
|
|
@@ -157,7 +158,7 @@ async function readViewSource(projectRoot, requestedPath, requestedSymbol = '',
|
|
|
157
158
|
export async function getViewSource(latDir, projectRoot, requestedPath, requestedSymbol = '', origin, requestedLine = 0, allSections = [], referenceIndex) {
|
|
158
159
|
const source = await readViewSource(projectRoot, requestedPath, requestedSymbol, requestedLine);
|
|
159
160
|
const references = referenceIndex
|
|
160
|
-
? await renderSourceReferenceContext(referenceIndex, `${source.path}${requestedSymbol ? `#${requestedSymbol}` : ''}`, origin, latDir, projectRoot, (path) => createMarkdownWikiLinkResolver(latDir, path, allSections))
|
|
161
|
+
? await renderSourceReferenceContext(referenceIndex, `${source.path}${requestedSymbol ? `#${requestedSymbol}` : ''}`, origin, latDir, projectRoot, (path) => createMarkdownWikiLinkResolver(latDir, path, allSections, referenceIndex))
|
|
161
162
|
: { context: null, otherReferences: [] };
|
|
162
163
|
return {
|
|
163
164
|
...source,
|
package/dist/src/view/search.js
CHANGED
|
@@ -28,6 +28,7 @@ function viewSearchResult(latDir, match) {
|
|
|
28
28
|
breadcrumbs: [...fileBreadcrumbs, ...section.id.split('#').slice(1)],
|
|
29
29
|
description: section.firstParagraph,
|
|
30
30
|
url: documentUrl(path, section.githubSlug),
|
|
31
|
+
score: match.score ?? 0,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
/** Create the lazily indexed semantic search service used by `lat ui`. */
|
|
@@ -2,6 +2,7 @@ import { type Server } from 'node:http';
|
|
|
2
2
|
import type { CmdContext } from '../context.js';
|
|
3
3
|
import { type ViewSearch } from './search.js';
|
|
4
4
|
import { type ViewStore } from './store.js';
|
|
5
|
+
export declare const DEFAULT_VIEW_PORT = 4242;
|
|
5
6
|
export type ViewServer = {
|
|
6
7
|
server: Server;
|
|
7
8
|
store: ViewStore;
|
|
@@ -11,7 +12,9 @@ export type ViewServer = {
|
|
|
11
12
|
export type ViewServerOptions = {
|
|
12
13
|
clientDir?: string;
|
|
13
14
|
git?: boolean;
|
|
15
|
+
gitPollMs?: number;
|
|
14
16
|
host?: string;
|
|
17
|
+
logoText?: string;
|
|
15
18
|
port?: number;
|
|
16
19
|
search?: ViewSearch;
|
|
17
20
|
watch?: boolean;
|
package/dist/src/view/server.js
CHANGED
|
@@ -2,16 +2,18 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { extname, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { DEFAULT_VIEW_LOGO_TEXT, } from './protocol.js';
|
|
5
6
|
import { ViewDocumentNotFoundError, ViewSourceNotFoundError, } from './repository.js';
|
|
6
7
|
import { createViewSearch } from './search.js';
|
|
7
8
|
import { createViewStore } from './store.js';
|
|
8
9
|
const DEFAULT_HOST = '127.0.0.1';
|
|
10
|
+
export const DEFAULT_VIEW_PORT = 4242;
|
|
9
11
|
const defaultClientDir = fileURLToPath(new URL('./client/', import.meta.url));
|
|
10
12
|
function documentUrl(path) {
|
|
11
13
|
return `/docs/${path.split('/').map(encodeURIComponent).join('/')}`;
|
|
12
14
|
}
|
|
13
15
|
function setSecurityHeaders(res) {
|
|
14
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
|
|
16
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
|
|
15
17
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
16
18
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
17
19
|
}
|
|
@@ -69,12 +71,29 @@ async function sendClientFile(res, path, headOnly, immutable = false) {
|
|
|
69
71
|
send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
74
|
+
function listen(server, host, port) {
|
|
75
|
+
return new Promise((resolveListen, reject) => {
|
|
76
|
+
const onError = (error) => {
|
|
77
|
+
server.off('listening', onListening);
|
|
78
|
+
reject(error);
|
|
79
|
+
};
|
|
80
|
+
const onListening = () => {
|
|
81
|
+
server.off('error', onError);
|
|
82
|
+
resolveListen();
|
|
83
|
+
};
|
|
84
|
+
server.once('error', onError);
|
|
85
|
+
server.once('listening', onListening);
|
|
86
|
+
server.listen(port, host);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
72
89
|
/** Start the read-only loopback server used by `lat ui`. */
|
|
73
90
|
export async function startViewServer(ctx, options = {}) {
|
|
74
91
|
const host = options.host ?? DEFAULT_HOST;
|
|
75
92
|
const clientDir = options.clientDir ?? defaultClientDir;
|
|
93
|
+
const logoText = options.logoText ?? DEFAULT_VIEW_LOGO_TEXT;
|
|
76
94
|
const store = await createViewStore(ctx.latDir, ctx.projectRoot, {
|
|
77
95
|
git: options.git,
|
|
96
|
+
gitPollMs: options.gitPollMs,
|
|
78
97
|
watch: options.watch,
|
|
79
98
|
});
|
|
80
99
|
const search = options.search ??
|
|
@@ -109,7 +128,11 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
109
128
|
return;
|
|
110
129
|
}
|
|
111
130
|
if (url.pathname === '/api/index') {
|
|
112
|
-
sendJson(res, 200, store.getIndex(), headOnly);
|
|
131
|
+
sendJson(res, 200, { ...store.getIndex(), logoText }, headOnly);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (url.pathname === '/api/graph') {
|
|
135
|
+
sendJson(res, 200, store.getGraph(), headOnly);
|
|
113
136
|
return;
|
|
114
137
|
}
|
|
115
138
|
if (url.pathname === '/api/events') {
|
|
@@ -180,6 +203,7 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
180
203
|
return;
|
|
181
204
|
}
|
|
182
205
|
if (url.pathname === '/search' ||
|
|
206
|
+
url.pathname === '/graph' ||
|
|
183
207
|
url.pathname.startsWith('/docs/') ||
|
|
184
208
|
url.pathname.startsWith('/code/')) {
|
|
185
209
|
await sendClientFile(res, join(clientDir, 'index.html'), headOnly);
|
|
@@ -195,13 +219,22 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
195
219
|
});
|
|
196
220
|
});
|
|
197
221
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
const requestedPort = options.port;
|
|
223
|
+
let port = requestedPort ?? DEFAULT_VIEW_PORT;
|
|
224
|
+
while (true) {
|
|
225
|
+
try {
|
|
226
|
+
await listen(server, host, port);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (requestedPort !== undefined ||
|
|
231
|
+
error.code !== 'EADDRINUSE' ||
|
|
232
|
+
port === 65_535) {
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
port++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
205
238
|
}
|
|
206
239
|
catch (error) {
|
|
207
240
|
unsubscribeStore();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ViewSourceTarget = {
|
|
2
|
+
path: string;
|
|
3
|
+
symbol: string;
|
|
4
|
+
key: string;
|
|
5
|
+
fileKey: string;
|
|
6
|
+
};
|
|
7
|
+
/** Normalize a supported source wiki-link target for view indexes and routes. */
|
|
8
|
+
export declare function viewSourceTarget(target: string): ViewSourceTarget | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { extname, posix } from 'node:path';
|
|
2
|
+
import { SOURCE_EXTENSIONS } from '../source-parser.js';
|
|
3
|
+
/** Normalize a supported source wiki-link target for view indexes and routes. */
|
|
4
|
+
export function viewSourceTarget(target) {
|
|
5
|
+
const hash = target.indexOf('#');
|
|
6
|
+
const authoredPath = hash === -1 ? target : target.slice(0, hash);
|
|
7
|
+
if (!authoredPath ||
|
|
8
|
+
authoredPath.includes('\\') ||
|
|
9
|
+
posix.isAbsolute(authoredPath) ||
|
|
10
|
+
!SOURCE_EXTENSIONS.has(extname(authoredPath))) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const path = posix.normalize(authoredPath).replace(/^\.\//, '');
|
|
14
|
+
const symbol = hash === -1 ? '' : target.slice(hash + 1);
|
|
15
|
+
const fileKey = path.toLowerCase();
|
|
16
|
+
return {
|
|
17
|
+
path,
|
|
18
|
+
symbol,
|
|
19
|
+
key: `${fileKey}${symbol ? `#${symbol.toLowerCase()}` : ''}`,
|
|
20
|
+
fileKey,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CmdContext } from '../context.js';
|
|
2
|
+
export type StaticViewBuildOptions = {
|
|
3
|
+
basePath?: string;
|
|
4
|
+
clientDir?: string;
|
|
5
|
+
logoText?: string;
|
|
6
|
+
};
|
|
7
|
+
export type StaticViewBuildResult = {
|
|
8
|
+
documents: number;
|
|
9
|
+
outputDir: string;
|
|
10
|
+
sources: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function normalizeStaticViewBasePath(value: string): string;
|
|
13
|
+
/** Convert a live view route to its physical static-directory URL. */
|
|
14
|
+
export declare function staticViewUrl(value: string, basePath: string): string;
|
|
15
|
+
/** Build a serverless snapshot of the current read-only Lat UI. */
|
|
16
|
+
export declare function buildStaticView(ctx: CmdContext, requestedOutput: string, options?: StaticViewBuildOptions): Promise<StaticViewBuildResult>;
|