busy-cli 0.1.2
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 +129 -0
- package/dist/builders/context.d.ts +50 -0
- package/dist/builders/context.d.ts.map +1 -0
- package/dist/builders/context.js +190 -0
- package/dist/cache/index.d.ts +100 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +270 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +463 -0
- package/dist/commands/package.d.ts +96 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +361 -0
- package/dist/merge.d.ts +16 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +102 -0
- package/dist/package/manifest.d.ts +59 -0
- package/dist/package/manifest.d.ts.map +1 -0
- package/dist/package/manifest.js +265 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +220 -0
- package/dist/parsers/frontmatter.d.ts +14 -0
- package/dist/parsers/frontmatter.d.ts.map +1 -0
- package/dist/parsers/frontmatter.js +110 -0
- package/dist/parsers/imports.d.ts +48 -0
- package/dist/parsers/imports.d.ts.map +1 -0
- package/dist/parsers/imports.js +147 -0
- package/dist/parsers/links.d.ts +12 -0
- package/dist/parsers/links.d.ts.map +1 -0
- package/dist/parsers/links.js +79 -0
- package/dist/parsers/localdefs.d.ts +6 -0
- package/dist/parsers/localdefs.d.ts.map +1 -0
- package/dist/parsers/localdefs.js +132 -0
- package/dist/parsers/operations.d.ts +32 -0
- package/dist/parsers/operations.d.ts.map +1 -0
- package/dist/parsers/operations.js +313 -0
- package/dist/parsers/sections.d.ts +15 -0
- package/dist/parsers/sections.d.ts.map +1 -0
- package/dist/parsers/sections.js +173 -0
- package/dist/parsers/tools.d.ts +30 -0
- package/dist/parsers/tools.d.ts.map +1 -0
- package/dist/parsers/tools.js +178 -0
- package/dist/parsers/triggers.d.ts +35 -0
- package/dist/parsers/triggers.d.ts.map +1 -0
- package/dist/parsers/triggers.js +219 -0
- package/dist/providers/base.d.ts +60 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +34 -0
- package/dist/providers/github.d.ts +18 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/gitlab.d.ts +18 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +101 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/local.d.ts +31 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +116 -0
- package/dist/providers/url.d.ts +16 -0
- package/dist/providers/url.d.ts.map +1 -0
- package/dist/providers/url.js +45 -0
- package/dist/registry/index.d.ts +99 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +320 -0
- package/dist/types/schema.d.ts +3259 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +258 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/slugify.d.ts +14 -0
- package/dist/utils/slugify.d.ts.map +1 -0
- package/dist/utils/slugify.js +28 -0
- package/package.json +61 -0
- package/src/__tests__/cache.test.ts +393 -0
- package/src/__tests__/cli-package.test.ts +667 -0
- package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
- package/src/__tests__/fixtures/concept.busy.md +30 -0
- package/src/__tests__/fixtures/document.busy.md +44 -0
- package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
- package/src/__tests__/fixtures/tool-document.busy.md +71 -0
- package/src/__tests__/fixtures/tool.busy.md +54 -0
- package/src/__tests__/imports.test.ts +244 -0
- package/src/__tests__/integration.test.ts +432 -0
- package/src/__tests__/operations.test.ts +408 -0
- package/src/__tests__/package-manifest.test.ts +455 -0
- package/src/__tests__/providers.test.ts +672 -0
- package/src/__tests__/registry.test.ts +402 -0
- package/src/__tests__/schema.test.ts +467 -0
- package/src/__tests__/tools.test.ts +376 -0
- package/src/__tests__/triggers.test.ts +312 -0
- package/src/builders/context.ts +294 -0
- package/src/cache/index.ts +312 -0
- package/src/cli/index.ts +514 -0
- package/src/commands/package.ts +392 -0
- package/src/index.ts +46 -0
- package/src/loader.ts +474 -0
- package/src/merge.ts +126 -0
- package/src/package/manifest.ts +349 -0
- package/src/parser.ts +278 -0
- package/src/parsers/frontmatter.ts +135 -0
- package/src/parsers/imports.ts +196 -0
- package/src/parsers/links.ts +108 -0
- package/src/parsers/localdefs.ts +166 -0
- package/src/parsers/operations.ts +404 -0
- package/src/parsers/sections.ts +230 -0
- package/src/parsers/tools.ts +215 -0
- package/src/parsers/triggers.ts +252 -0
- package/src/providers/base.ts +77 -0
- package/src/providers/github.ts +129 -0
- package/src/providers/gitlab.ts +121 -0
- package/src/providers/index.ts +25 -0
- package/src/providers/local.ts +129 -0
- package/src/providers/url.ts +56 -0
- package/src/registry/index.ts +408 -0
- package/src/types/schema.ts +369 -0
- package/src/utils/logger.ts +25 -0
- package/src/utils/slugify.ts +31 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import matter from 'gray-matter';
|
|
2
|
+
import { FrontMatter, FrontMatterSchema, ConceptBase } from '../types/schema.js';
|
|
3
|
+
import { normalizeDocId, getBasename } from '../utils/slugify.js';
|
|
4
|
+
import { debug, warn } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
export interface ParsedFrontMatter {
|
|
7
|
+
frontmatter: FrontMatter;
|
|
8
|
+
content: string;
|
|
9
|
+
docId: string;
|
|
10
|
+
kind: ConceptBase['kind'];
|
|
11
|
+
types: string[];
|
|
12
|
+
extends: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse front-matter from markdown content
|
|
17
|
+
*/
|
|
18
|
+
export function parseFrontMatter(
|
|
19
|
+
fileContent: string,
|
|
20
|
+
filePath: string
|
|
21
|
+
): ParsedFrontMatter {
|
|
22
|
+
let data: any;
|
|
23
|
+
let content: string;
|
|
24
|
+
|
|
25
|
+
// Extract only the first frontmatter block to avoid "multiple documents" error
|
|
26
|
+
// when the body contains --- horizontal rules
|
|
27
|
+
const frontmatterMatch = fileContent.match(/^---\n([\s\S]*?)\n---/);
|
|
28
|
+
if (frontmatterMatch) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = matter(frontmatterMatch[0]);
|
|
31
|
+
data = parsed.data;
|
|
32
|
+
content = fileContent.slice(frontmatterMatch[0].length);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
warn(`Failed to parse YAML frontmatter in ${filePath}: ${err}`, { file: filePath });
|
|
35
|
+
data = {};
|
|
36
|
+
content = fileContent;
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// No frontmatter found
|
|
40
|
+
data = {};
|
|
41
|
+
content = fileContent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
debug.frontmatter('Parsing frontmatter for %s', filePath);
|
|
45
|
+
|
|
46
|
+
// Validate frontmatter
|
|
47
|
+
let frontmatter: FrontMatter;
|
|
48
|
+
try {
|
|
49
|
+
frontmatter = FrontMatterSchema.parse(data);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
warn(`Invalid frontmatter schema in ${filePath}`, { file: filePath });
|
|
52
|
+
// Provide defaults if validation fails
|
|
53
|
+
frontmatter = {
|
|
54
|
+
Name: getBasename(filePath),
|
|
55
|
+
Type: [],
|
|
56
|
+
Description: undefined,
|
|
57
|
+
Tags: [],
|
|
58
|
+
Extends: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Normalize docId from Name or filename
|
|
63
|
+
const docId = frontmatter.Name
|
|
64
|
+
? normalizeDocId(frontmatter.Name)
|
|
65
|
+
: normalizeDocId(getBasename(filePath));
|
|
66
|
+
|
|
67
|
+
// Normalize types - strip markdown link brackets like [Document] -> Document
|
|
68
|
+
const types = (frontmatter.Type ?? []).map(stripMarkdownBrackets);
|
|
69
|
+
|
|
70
|
+
// Infer kind from types
|
|
71
|
+
const kind = inferKind(types);
|
|
72
|
+
|
|
73
|
+
// Normalize extends - also strip brackets
|
|
74
|
+
const extendsFromFm = (frontmatter.Extends ?? []).map(stripMarkdownBrackets);
|
|
75
|
+
const extendsFromTypes = inferExtendsFromTypes(types);
|
|
76
|
+
const extends_ = Array.from(new Set([...extendsFromFm, ...extendsFromTypes]));
|
|
77
|
+
|
|
78
|
+
// Normalize tags
|
|
79
|
+
const tags = (frontmatter.Tags ?? []).map(stripMarkdownBrackets);
|
|
80
|
+
|
|
81
|
+
debug.frontmatter(
|
|
82
|
+
'Parsed: docId=%s, kind=%s, types=%o, extends=%o',
|
|
83
|
+
docId,
|
|
84
|
+
kind,
|
|
85
|
+
types,
|
|
86
|
+
extends_
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
frontmatter: {
|
|
91
|
+
...frontmatter,
|
|
92
|
+
Type: types,
|
|
93
|
+
Extends: extends_,
|
|
94
|
+
Tags: tags,
|
|
95
|
+
},
|
|
96
|
+
content,
|
|
97
|
+
docId,
|
|
98
|
+
kind,
|
|
99
|
+
types,
|
|
100
|
+
extends: extends_,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Strip markdown link brackets from a string
|
|
106
|
+
* [Document] -> Document
|
|
107
|
+
* [[Document]] -> Document
|
|
108
|
+
*/
|
|
109
|
+
function stripMarkdownBrackets(str: string): string {
|
|
110
|
+
return str.replace(/^\[+|\]+$/g, '');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Infer kind from types array
|
|
115
|
+
*/
|
|
116
|
+
function inferKind(types: string[]): ConceptBase['kind'] {
|
|
117
|
+
const typesLower = types.map((t) => t.toLowerCase());
|
|
118
|
+
|
|
119
|
+
if (typesLower.includes('document')) return 'document';
|
|
120
|
+
if (typesLower.includes('operation')) return 'operation';
|
|
121
|
+
if (typesLower.includes('checklist')) return 'checklist';
|
|
122
|
+
if (typesLower.includes('tool')) return 'tool';
|
|
123
|
+
if (typesLower.includes('playbook')) return 'playbook';
|
|
124
|
+
|
|
125
|
+
return 'concept';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Infer extends from types
|
|
130
|
+
* All Type references should be included in extends
|
|
131
|
+
*/
|
|
132
|
+
function inferExtendsFromTypes(types: string[]): string[] {
|
|
133
|
+
// All types are also extends - they define what this concept is based on
|
|
134
|
+
return types;
|
|
135
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
4
|
+
import { visit } from 'unist-util-visit';
|
|
5
|
+
import type { Root, Definition } from 'mdast';
|
|
6
|
+
import { ImportDef, DocId, Import } from '../types/schema.js';
|
|
7
|
+
import { debug } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// NEW PARSER FUNCTIONS - busy-python compatible
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse reference-style imports from markdown content
|
|
15
|
+
* Matches busy-python format: [ConceptName]: path/to/file.md[#anchor]
|
|
16
|
+
*
|
|
17
|
+
* @returns Object with imports array and symbols table
|
|
18
|
+
*/
|
|
19
|
+
export function parseImports(
|
|
20
|
+
content: string
|
|
21
|
+
): { imports: Import[]; symbols: Record<string, { docId?: string; slug?: string }> } {
|
|
22
|
+
const imports: Import[] = [];
|
|
23
|
+
const symbols: Record<string, { docId?: string; slug?: string }> = {};
|
|
24
|
+
|
|
25
|
+
// Regex pattern matching busy-python: r"\[([^\]]+)\]:\s*([^\s#]+)(?:#([^\s]+))?"
|
|
26
|
+
const importPattern = /^\[([^\]]+)\]:\s*([^\s#]+)(?:#([^\s]+))?$/gm;
|
|
27
|
+
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
30
|
+
const [, conceptName, path, anchor] = match;
|
|
31
|
+
|
|
32
|
+
const importDef: Import = {
|
|
33
|
+
conceptName,
|
|
34
|
+
path,
|
|
35
|
+
anchor: anchor || undefined,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
imports.push(importDef);
|
|
39
|
+
|
|
40
|
+
// Add to symbol table for later resolution
|
|
41
|
+
symbols[conceptName] = {
|
|
42
|
+
docId: undefined,
|
|
43
|
+
slug: anchor || undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { imports, symbols };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve an import path to docId and optional slug
|
|
52
|
+
* Used for import resolution against a file map
|
|
53
|
+
*/
|
|
54
|
+
export function resolveImportTarget(
|
|
55
|
+
target: string,
|
|
56
|
+
fileMap: Map<string, { docId: string; path: string }>
|
|
57
|
+
): { docId?: string; slug?: string } {
|
|
58
|
+
// Parse target: "file.md", "./file.md", "../core/file.md", or "file.md#slug"
|
|
59
|
+
const hashIndex = target.indexOf('#');
|
|
60
|
+
let filePath = target;
|
|
61
|
+
let slug: string | undefined;
|
|
62
|
+
|
|
63
|
+
if (hashIndex !== -1) {
|
|
64
|
+
filePath = target.slice(0, hashIndex);
|
|
65
|
+
slug = target.slice(hashIndex + 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Remove leading ./ if present
|
|
69
|
+
filePath = filePath.replace(/^\.\//, '');
|
|
70
|
+
|
|
71
|
+
// Try multiple resolution strategies:
|
|
72
|
+
// 1. Full path as provided
|
|
73
|
+
let fileInfo = fileMap.get(filePath);
|
|
74
|
+
|
|
75
|
+
// 2. Try just the basename
|
|
76
|
+
if (!fileInfo) {
|
|
77
|
+
const basename = filePath.split('/').pop() || filePath;
|
|
78
|
+
fileInfo = fileMap.get(basename);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Try without extension
|
|
82
|
+
if (!fileInfo) {
|
|
83
|
+
const basename = filePath.split('/').pop() || filePath;
|
|
84
|
+
const withoutExt = basename.replace(/\.busy\.md$/, '').replace(/\.md$/, '');
|
|
85
|
+
fileInfo = fileMap.get(withoutExt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!fileInfo) {
|
|
89
|
+
return slug ? { slug } : {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
docId: fileInfo.docId,
|
|
94
|
+
slug,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// LEGACY FUNCTIONS - kept for backward compatibility
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract reference-style imports from markdown content (LEGACY)
|
|
104
|
+
* Returns ImportDef objects for graph-based representation
|
|
105
|
+
*/
|
|
106
|
+
export function extractImports(
|
|
107
|
+
content: string,
|
|
108
|
+
docId: DocId
|
|
109
|
+
): { imports: ImportDef[]; symbols: Record<string, { docId?: string; slug?: string }> } {
|
|
110
|
+
debug.imports('Extracting imports for %s', docId);
|
|
111
|
+
|
|
112
|
+
const processor = unified().use(remarkParse).use(remarkFrontmatter, ['yaml']);
|
|
113
|
+
|
|
114
|
+
const tree = processor.parse(content) as Root;
|
|
115
|
+
|
|
116
|
+
const imports: ImportDef[] = [];
|
|
117
|
+
const symbols: Record<string, { docId?: string; slug?: string }> = {};
|
|
118
|
+
|
|
119
|
+
// Visit definition nodes (reference-style links)
|
|
120
|
+
visit(tree, 'definition', (node: Definition) => {
|
|
121
|
+
const label = node.label ?? node.identifier;
|
|
122
|
+
const target = node.url;
|
|
123
|
+
|
|
124
|
+
debug.imports('Found import: [%s]: %s', label, target);
|
|
125
|
+
|
|
126
|
+
const importDef: ImportDef = {
|
|
127
|
+
kind: 'importdef',
|
|
128
|
+
id: `${docId}::import::${label}`,
|
|
129
|
+
docId,
|
|
130
|
+
slug: label.toLowerCase(),
|
|
131
|
+
name: label,
|
|
132
|
+
content: '', // Imports don't have content
|
|
133
|
+
types: [],
|
|
134
|
+
extends: [],
|
|
135
|
+
sectionRef: '', // Imports don't belong to a section
|
|
136
|
+
label,
|
|
137
|
+
target,
|
|
138
|
+
resolved: undefined, // Will be resolved later
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
imports.push(importDef);
|
|
142
|
+
|
|
143
|
+
// Add to symbol table (will be fully resolved later)
|
|
144
|
+
symbols[label] = {
|
|
145
|
+
docId: undefined,
|
|
146
|
+
slug: undefined,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
debug.imports('Found %d imports', imports.length);
|
|
151
|
+
|
|
152
|
+
return { imports, symbols };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve an import target to {docId, slug} (LEGACY)
|
|
157
|
+
* This version requires currentDocId parameter for backward compatibility
|
|
158
|
+
*/
|
|
159
|
+
export function legacyResolveImportTarget(
|
|
160
|
+
target: string,
|
|
161
|
+
currentDocId: DocId,
|
|
162
|
+
fileMap: Map<string, { docId: string; path: string }>
|
|
163
|
+
): { docId?: string; slug?: string } {
|
|
164
|
+
// Parse target: "./file.md", "../core/file.md", or "./file.md#slug"
|
|
165
|
+
const match = target.match(/^\.{1,2}\/(.+?)(#(.+))?$/);
|
|
166
|
+
|
|
167
|
+
if (!match) {
|
|
168
|
+
debug.imports('Invalid import target: %s', target);
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const [, filePath, , slug] = match;
|
|
173
|
+
|
|
174
|
+
// Try multiple resolution strategies:
|
|
175
|
+
// 1. Full relative path
|
|
176
|
+
// 2. Just the basename
|
|
177
|
+
// 3. Basename with different extensions
|
|
178
|
+
|
|
179
|
+
let fileInfo = fileMap.get(filePath);
|
|
180
|
+
|
|
181
|
+
if (!fileInfo) {
|
|
182
|
+
// Try just the basename
|
|
183
|
+
const basename = filePath.split('/').pop() || filePath;
|
|
184
|
+
fileInfo = fileMap.get(basename);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!fileInfo) {
|
|
188
|
+
debug.imports('File not found in map: %s', filePath);
|
|
189
|
+
return { slug };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
docId: fileInfo.docId,
|
|
194
|
+
slug,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
4
|
+
import { visit } from 'unist-util-visit';
|
|
5
|
+
import type { Root, Link, LinkReference } from 'mdast';
|
|
6
|
+
import { Section, Edge, EdgeRole } from '../types/schema.js';
|
|
7
|
+
import { debug } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract links from a section and create edges
|
|
12
|
+
*/
|
|
13
|
+
export function extractLinksFromSection(
|
|
14
|
+
section: Section,
|
|
15
|
+
content: string,
|
|
16
|
+
symbols: Record<string, { docId?: string; slug?: string }>,
|
|
17
|
+
fileMap: Map<string, { docId: string; path: string }>
|
|
18
|
+
): Edge[] {
|
|
19
|
+
debug.links('Extracting links from section %s', section.id);
|
|
20
|
+
|
|
21
|
+
const processor = unified().use(remarkParse).use(remarkFrontmatter, ['yaml']);
|
|
22
|
+
|
|
23
|
+
const tree = processor.parse(content) as Root;
|
|
24
|
+
|
|
25
|
+
const edges: Edge[] = [];
|
|
26
|
+
|
|
27
|
+
// Visit link nodes
|
|
28
|
+
visit(tree, 'link', (node: Link) => {
|
|
29
|
+
const href = node.url;
|
|
30
|
+
|
|
31
|
+
const resolved = resolveLink(href, section.docId, fileMap);
|
|
32
|
+
|
|
33
|
+
if (resolved) {
|
|
34
|
+
// Temporarily mark as 'ref', will be reclassified in loader
|
|
35
|
+
edges.push({
|
|
36
|
+
from: section.id,
|
|
37
|
+
to: resolved,
|
|
38
|
+
role: 'ref',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Visit link reference nodes
|
|
44
|
+
visit(tree, 'linkReference', (node: LinkReference) => {
|
|
45
|
+
const label = node.label ?? node.identifier;
|
|
46
|
+
|
|
47
|
+
// Resolve via symbol table
|
|
48
|
+
const symbol = symbols[label];
|
|
49
|
+
if (symbol) {
|
|
50
|
+
let resolved: string | undefined;
|
|
51
|
+
|
|
52
|
+
if (symbol.docId && symbol.slug) {
|
|
53
|
+
resolved = `${symbol.docId}#${symbol.slug}`;
|
|
54
|
+
} else if (symbol.docId) {
|
|
55
|
+
resolved = symbol.docId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (resolved) {
|
|
59
|
+
// Temporarily mark as 'ref', will be reclassified in loader
|
|
60
|
+
edges.push({
|
|
61
|
+
from: section.id,
|
|
62
|
+
to: resolved,
|
|
63
|
+
role: 'ref',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
debug.links('Found %d edges from section', edges.length);
|
|
70
|
+
|
|
71
|
+
return edges;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a link href to a node ID
|
|
76
|
+
*/
|
|
77
|
+
function resolveLink(
|
|
78
|
+
href: string,
|
|
79
|
+
currentDocId: string,
|
|
80
|
+
fileMap: Map<string, { docId: string; path: string }>
|
|
81
|
+
): string | undefined {
|
|
82
|
+
// Handle internal anchors: #slug
|
|
83
|
+
if (href.startsWith('#')) {
|
|
84
|
+
const slug = href.slice(1);
|
|
85
|
+
return `${currentDocId}#${slug}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle relative file links: ./file.md or ./file.md#slug
|
|
89
|
+
const match = href.match(/^\.\/(.+?)(#(.+))?$/);
|
|
90
|
+
if (match) {
|
|
91
|
+
const [, filePath, , slug] = match;
|
|
92
|
+
|
|
93
|
+
const fileInfo = fileMap.get(filePath);
|
|
94
|
+
if (!fileInfo) {
|
|
95
|
+
debug.links('File not found: %s', filePath);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (slug) {
|
|
100
|
+
return `${fileInfo.docId}#${slug}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return fileInfo.docId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// External link - return as-is or skip
|
|
107
|
+
return href.startsWith('http') ? href : undefined;
|
|
108
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Section, LocalDef, DocId } from '../types/schema.js';
|
|
2
|
+
import { createSlug } from '../utils/slugify.js';
|
|
3
|
+
import { findSection, getAllSections, getSectionExtends } from './sections.js';
|
|
4
|
+
import { debug } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const LOCAL_DEFS_ALIASES = [
|
|
7
|
+
'local definitions',
|
|
8
|
+
'definitions',
|
|
9
|
+
'glossary',
|
|
10
|
+
'local-definitions-section',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract Local Definitions from sections
|
|
15
|
+
*/
|
|
16
|
+
export function extractLocalDefs(
|
|
17
|
+
sections: Section[],
|
|
18
|
+
docId: DocId,
|
|
19
|
+
filePath: string
|
|
20
|
+
): LocalDef[] {
|
|
21
|
+
debug.localdefs('Extracting local definitions for %s', docId);
|
|
22
|
+
|
|
23
|
+
// Find the Local Definitions section
|
|
24
|
+
const localDefsSection = findLocalDefinitionsSection(sections);
|
|
25
|
+
|
|
26
|
+
if (!localDefsSection) {
|
|
27
|
+
debug.localdefs('No Local Definitions section found');
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
debug.localdefs(
|
|
32
|
+
'Found Local Definitions section: %s',
|
|
33
|
+
localDefsSection.title
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Extract all subheadings as LocalDefs
|
|
37
|
+
const localdefs: LocalDef[] = [];
|
|
38
|
+
|
|
39
|
+
for (const child of getAllSections(localDefsSection.children)) {
|
|
40
|
+
const localdef = createLocalDef(child, docId, filePath);
|
|
41
|
+
localdefs.push(localdef);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
debug.localdefs('Extracted %d local definitions', localdefs.length);
|
|
45
|
+
|
|
46
|
+
return localdefs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find the Local Definitions section (case-insensitive)
|
|
51
|
+
*/
|
|
52
|
+
function findLocalDefinitionsSection(sections: Section[]): Section | undefined {
|
|
53
|
+
for (const alias of LOCAL_DEFS_ALIASES) {
|
|
54
|
+
const section = findSection(sections, alias);
|
|
55
|
+
if (section) {
|
|
56
|
+
return section;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a LocalDef from a section
|
|
64
|
+
*/
|
|
65
|
+
function createLocalDef(
|
|
66
|
+
section: Section,
|
|
67
|
+
docId: DocId,
|
|
68
|
+
filePath: string
|
|
69
|
+
): LocalDef {
|
|
70
|
+
const slug = section.slug;
|
|
71
|
+
const id = `${docId}::${slug}`;
|
|
72
|
+
|
|
73
|
+
// Parse extends from content
|
|
74
|
+
const { extends: extendsFromContent } = parseLocalDefAttrs(section.content);
|
|
75
|
+
|
|
76
|
+
// Get extends from section heading (e.g., ## [MyDef][Type])
|
|
77
|
+
const extendsFromHeading = getSectionExtends(section.id);
|
|
78
|
+
|
|
79
|
+
// Combine both sources of extends
|
|
80
|
+
const extends_ = Array.from(new Set([...extendsFromContent, ...extendsFromHeading]));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
kind: 'localdef',
|
|
84
|
+
id,
|
|
85
|
+
docId,
|
|
86
|
+
slug,
|
|
87
|
+
name: section.title,
|
|
88
|
+
content: section.content,
|
|
89
|
+
types: [],
|
|
90
|
+
extends: extends_,
|
|
91
|
+
sectionRef: section.id,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse attributes from local def content
|
|
97
|
+
* Looks for:
|
|
98
|
+
* 1. Fenced blocks with "yaml busy" or "json busy"
|
|
99
|
+
* 2. Inline patterns like "Extends: [...]" or "_Extends:_"
|
|
100
|
+
*/
|
|
101
|
+
function parseLocalDefAttrs(content: string): {
|
|
102
|
+
attrs: Record<string, unknown>;
|
|
103
|
+
extends: string[];
|
|
104
|
+
} {
|
|
105
|
+
const attrs: Record<string, unknown> = {};
|
|
106
|
+
let extends_: string[] = [];
|
|
107
|
+
|
|
108
|
+
// Check for fenced blocks first
|
|
109
|
+
const yamlBusyMatch = content.match(/```(?:yaml|json)\s+busy\s*\n([\s\S]*?)```/);
|
|
110
|
+
if (yamlBusyMatch) {
|
|
111
|
+
try {
|
|
112
|
+
// Simple parsing - just look for key: value pairs
|
|
113
|
+
const block = yamlBusyMatch[1];
|
|
114
|
+
const lines = block.split('\n');
|
|
115
|
+
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const match = line.match(/^\s*(\w+):\s*(.+)$/);
|
|
118
|
+
if (match) {
|
|
119
|
+
const [, key, value] = match;
|
|
120
|
+
// Try to parse as JSON
|
|
121
|
+
try {
|
|
122
|
+
attrs[key] = JSON.parse(value);
|
|
123
|
+
} catch {
|
|
124
|
+
attrs[key] = value.trim();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (attrs.Extends) {
|
|
130
|
+
extends_ = Array.isArray(attrs.Extends)
|
|
131
|
+
? attrs.Extends
|
|
132
|
+
: [attrs.Extends];
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
debug.localdefs('Failed to parse fenced block: %s', err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Look for inline Extends patterns
|
|
140
|
+
const extendsPatterns = [
|
|
141
|
+
/^Extends:\s*\[(.*?)\]/im,
|
|
142
|
+
/^_Extends:_\s*\[(.*?)\]/im,
|
|
143
|
+
/^Extends:\s*(.+)$/im,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
for (const pattern of extendsPatterns) {
|
|
147
|
+
const match = content.match(pattern);
|
|
148
|
+
if (match) {
|
|
149
|
+
const value = match[1].trim();
|
|
150
|
+
// Parse as comma-separated list or JSON array
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(`[${value}]`);
|
|
153
|
+
extends_ = [...extends_, ...parsed];
|
|
154
|
+
} catch {
|
|
155
|
+
// Try comma-separated
|
|
156
|
+
extends_ = [
|
|
157
|
+
...extends_,
|
|
158
|
+
...value.split(',').map((s) => s.trim().replace(/['"]/g, '')),
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { attrs, extends: Array.from(new Set(extends_)) };
|
|
166
|
+
}
|