@intentius/chant 0.0.2 → 0.0.4
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/package.json +2 -1
- package/src/build.ts +14 -1
- package/src/codegen/docs-interpolation.test.ts +77 -0
- package/src/codegen/docs.ts +75 -1
- package/src/discovery/collect.ts +7 -0
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -53,6 +53,8 @@ export function partitionByLexicon(
|
|
|
53
53
|
const partitions = new Map<string, Map<string, Declarable>>();
|
|
54
54
|
|
|
55
55
|
for (const [name, entity] of entities) {
|
|
56
|
+
// LexiconOutput instances are collected separately; skip them here
|
|
57
|
+
if (isLexiconOutput(entity)) continue;
|
|
56
58
|
const lexicon = entity.lexicon;
|
|
57
59
|
if (!partitions.has(lexicon)) {
|
|
58
60
|
partitions.set(lexicon, new Map());
|
|
@@ -100,7 +102,18 @@ export function collectLexiconOutputs(
|
|
|
100
102
|
for (const [name, entity] of entities) {
|
|
101
103
|
if (isLexiconOutput(entity as unknown)) {
|
|
102
104
|
const lexiconOutput = entity as unknown as LexiconOutput;
|
|
103
|
-
|
|
105
|
+
// Resolve source entity name from the WeakRef parent identity
|
|
106
|
+
const parent = lexiconOutput._sourceParent.deref();
|
|
107
|
+
let sourceName = name;
|
|
108
|
+
if (parent) {
|
|
109
|
+
for (const [entityName, e] of entities) {
|
|
110
|
+
if (e === parent) {
|
|
111
|
+
sourceName = entityName;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
lexiconOutput._setSourceEntity(sourceName);
|
|
104
117
|
outputs.push(lexiconOutput);
|
|
105
118
|
continue;
|
|
106
119
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { expandFileMarkers } from "./docs";
|
|
6
|
+
|
|
7
|
+
describe("expandFileMarkers", () => {
|
|
8
|
+
let dir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "docs-interp-"));
|
|
12
|
+
mkdirSync(join(dir, "sub"), { recursive: true });
|
|
13
|
+
writeFileSync(
|
|
14
|
+
join(dir, "example.ts"),
|
|
15
|
+
'import * as _ from "./_";\n\nexport const bucket = new _.Bucket({\n bucketName: "test",\n});\n',
|
|
16
|
+
);
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(dir, "sub", "nested.ts"),
|
|
19
|
+
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("expands full file marker", () => {
|
|
28
|
+
const result = expandFileMarkers("Before\n\n{{file:example.ts}}\n\nAfter", dir);
|
|
29
|
+
expect(result).toContain('```typescript title="example.ts"');
|
|
30
|
+
expect(result).toContain('import * as _ from "./_";');
|
|
31
|
+
expect(result).toContain("```");
|
|
32
|
+
expect(result).toStartWith("Before\n\n");
|
|
33
|
+
expect(result).toEndWith("\n\nAfter");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("expands line range", () => {
|
|
37
|
+
const result = expandFileMarkers("{{file:sub/nested.ts:3-5}}", dir);
|
|
38
|
+
expect(result).toContain('```typescript title="nested.ts"');
|
|
39
|
+
expect(result).toContain("line3\nline4\nline5");
|
|
40
|
+
expect(result).not.toContain("line2");
|
|
41
|
+
expect(result).not.toContain("line6");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("supports title override", () => {
|
|
45
|
+
const result = expandFileMarkers("{{file:example.ts|title=my-file.ts}}", dir);
|
|
46
|
+
expect(result).toContain('```typescript title="my-file.ts"');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("supports line range with title override", () => {
|
|
50
|
+
const result = expandFileMarkers(
|
|
51
|
+
"{{file:sub/nested.ts:2-4|title=snippet.ts}}",
|
|
52
|
+
dir,
|
|
53
|
+
);
|
|
54
|
+
expect(result).toContain('```typescript title="snippet.ts"');
|
|
55
|
+
expect(result).toContain("line2\nline3\nline4");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("throws on missing file", () => {
|
|
59
|
+
expect(() => expandFileMarkers("{{file:nope.ts}}", dir)).toThrow(
|
|
60
|
+
/file not found/,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("leaves content without markers unchanged", () => {
|
|
65
|
+
const input = "No markers here\n\n```typescript\ncode\n```\n";
|
|
66
|
+
expect(expandFileMarkers(input, dir)).toBe(input);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("expands multiple markers", () => {
|
|
70
|
+
const result = expandFileMarkers(
|
|
71
|
+
"{{file:example.ts}}\n\n{{file:sub/nested.ts:1-2}}",
|
|
72
|
+
dir,
|
|
73
|
+
);
|
|
74
|
+
expect(result).toContain('title="example.ts"');
|
|
75
|
+
expect(result).toContain('title="nested.ts"');
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/codegen/docs.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface DocsConfig {
|
|
|
39
39
|
srcDir?: string;
|
|
40
40
|
/** Base path for the generated Astro site (e.g. '/lexicons/aws/') */
|
|
41
41
|
basePath?: string;
|
|
42
|
+
/** Root directory for resolving {{file:...}} markers in extra page content */
|
|
43
|
+
examplesDir?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface DocsResult {
|
|
@@ -84,6 +86,66 @@ interface RuleMeta {
|
|
|
84
86
|
type: "lint" | "post-synth";
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
// ── File marker interpolation ──────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Expand `{{file:path.ts}}` markers in content with fenced code blocks.
|
|
93
|
+
*
|
|
94
|
+
* Supported forms:
|
|
95
|
+
* - `{{file:path.ts}}` — full file
|
|
96
|
+
* - `{{file:path.ts:5-12}}` — lines 5–12 (1-based, inclusive)
|
|
97
|
+
* - `{{file:path.ts|title=custom.ts}}` — override the code block title
|
|
98
|
+
* - `{{file:path.ts:5-12|title=custom.ts}}` — both
|
|
99
|
+
*/
|
|
100
|
+
export function expandFileMarkers(content: string, examplesDir: string): string {
|
|
101
|
+
return content.replace(
|
|
102
|
+
/\{\{file:([^}]+)\}\}/g,
|
|
103
|
+
(_match, spec: string) => {
|
|
104
|
+
// Parse options after |
|
|
105
|
+
let filePart = spec;
|
|
106
|
+
let title: string | undefined;
|
|
107
|
+
const pipeIdx = spec.indexOf("|");
|
|
108
|
+
if (pipeIdx !== -1) {
|
|
109
|
+
filePart = spec.substring(0, pipeIdx);
|
|
110
|
+
const opts = spec.substring(pipeIdx + 1);
|
|
111
|
+
const titleMatch = opts.match(/title=([^\s|]+)/);
|
|
112
|
+
if (titleMatch) title = titleMatch[1];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse line range after :digits-digits at end of filePart
|
|
116
|
+
let lineStart: number | undefined;
|
|
117
|
+
let lineEnd: number | undefined;
|
|
118
|
+
const rangeMatch = filePart.match(/^(.+):(\d+)-(\d+)$/);
|
|
119
|
+
if (rangeMatch) {
|
|
120
|
+
filePart = rangeMatch[1];
|
|
121
|
+
lineStart = parseInt(rangeMatch[2], 10);
|
|
122
|
+
lineEnd = parseInt(rangeMatch[3], 10);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const filePath = join(examplesDir, filePart);
|
|
126
|
+
let fileContent: string;
|
|
127
|
+
try {
|
|
128
|
+
fileContent = readFileSync(filePath, "utf-8");
|
|
129
|
+
} catch {
|
|
130
|
+
throw new Error(`File marker {{file:${spec}}} — file not found: ${filePath}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extract line range if specified
|
|
134
|
+
if (lineStart !== undefined && lineEnd !== undefined) {
|
|
135
|
+
const lines = fileContent.split("\n");
|
|
136
|
+
fileContent = lines.slice(lineStart - 1, lineEnd).join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Determine language from extension
|
|
140
|
+
const ext = filePart.substring(filePart.lastIndexOf(".") + 1);
|
|
141
|
+
const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext;
|
|
142
|
+
const displayTitle = title ?? filePart.substring(filePart.lastIndexOf("/") + 1);
|
|
143
|
+
|
|
144
|
+
return `\`\`\`${lang} title="${displayTitle}"\n${fileContent.trimEnd()}\n\`\`\``;
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
87
149
|
// ── Pipeline ───────────────────────────────────────────────────────
|
|
88
150
|
|
|
89
151
|
/**
|
|
@@ -137,6 +199,10 @@ export function docsPipeline(config: DocsConfig): DocsResult {
|
|
|
137
199
|
// Extra pages from lexicon config
|
|
138
200
|
if (config.extraPages) {
|
|
139
201
|
for (const page of config.extraPages) {
|
|
202
|
+
let content = page.content;
|
|
203
|
+
if (config.examplesDir) {
|
|
204
|
+
content = expandFileMarkers(content, config.examplesDir);
|
|
205
|
+
}
|
|
140
206
|
pages.set(
|
|
141
207
|
`${page.slug}.mdx`,
|
|
142
208
|
[
|
|
@@ -145,7 +211,7 @@ export function docsPipeline(config: DocsConfig): DocsResult {
|
|
|
145
211
|
page.description ? `description: "${page.description}"` : "",
|
|
146
212
|
"---",
|
|
147
213
|
"",
|
|
148
|
-
|
|
214
|
+
content,
|
|
149
215
|
"",
|
|
150
216
|
]
|
|
151
217
|
.filter(Boolean)
|
|
@@ -300,7 +366,15 @@ function buildSidebar(
|
|
|
300
366
|
config: DocsConfig,
|
|
301
367
|
result: DocsResult,
|
|
302
368
|
): Array<Record<string, unknown>> {
|
|
369
|
+
// Starlight prepends basePath to every sidebar `link`, so a site-root-relative
|
|
370
|
+
// path like "/chant/" becomes "/chant/lexicons/aws/chant/" — a 404. Instead
|
|
371
|
+
// we use relative traversal: "../../" is prepended to become
|
|
372
|
+
// "/chant/lexicons/aws/../../" which the browser resolves to "/chant/".
|
|
373
|
+
const segments = (config.basePath ?? "/").replace(/^\/|\/$/g, "").split("/");
|
|
374
|
+
const backLink = segments.length > 1 ? "../".repeat(segments.length - 1) : "/";
|
|
375
|
+
|
|
303
376
|
const items: Array<Record<string, unknown>> = [
|
|
377
|
+
{ label: "← chant docs", link: backLink },
|
|
304
378
|
{ label: "Overview", slug: "index" },
|
|
305
379
|
];
|
|
306
380
|
|
package/src/discovery/collect.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
2
2
|
import { isCompositeInstance, expandComposite } from "../composite";
|
|
3
|
+
import { isLexiconOutput } from "../lexicon-output";
|
|
3
4
|
import { DiscoveryError } from "../errors";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Collects all declarable entities from imported modules.
|
|
7
8
|
* CompositeInstance exports are expanded into individual entities
|
|
8
9
|
* with `{exportName}_{memberName}` naming.
|
|
10
|
+
* LexiconOutput exports are also collected so that build() can
|
|
11
|
+
* extract them and pass them to the serializer.
|
|
9
12
|
*
|
|
10
13
|
* @param modules - Array of module records with their exports
|
|
11
14
|
* @returns Map of export name to Declarable entity
|
|
@@ -43,6 +46,10 @@ export function collectEntities(
|
|
|
43
46
|
}
|
|
44
47
|
entities.set(expandedName, entity);
|
|
45
48
|
}
|
|
49
|
+
} else if (isLexiconOutput(value)) {
|
|
50
|
+
// LexiconOutput is not a Declarable but build() expects to find them
|
|
51
|
+
// in the entities map so it can collect and pass them to serializers
|
|
52
|
+
entities.set(name, value as unknown as Declarable);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
}
|