@fragno-dev/corpus 0.0.3 → 0.0.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/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +141 -91
- package/dist/index.js.map +1 -1
- package/dist/subjects/client-state-management.md +305 -0
- package/dist/subjects/database-adapters.md +88 -0
- package/dist/subjects/database-querying.md +316 -0
- package/dist/subjects/defining-routes.md +272 -0
- package/dist/subjects/drizzle-adapter.md +60 -0
- package/dist/subjects/fragment-instantiation.md +113 -0
- package/dist/subjects/fragment-services.md +178 -0
- package/dist/subjects/kysely-adapter.md +59 -0
- package/package.json +11 -4
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ interface Example {
|
|
|
14
14
|
explanation: string;
|
|
15
15
|
testName?: string;
|
|
16
16
|
id?: string;
|
|
17
|
+
typesOnly?: boolean;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* A code block with optional ID
|
|
@@ -50,7 +51,7 @@ interface Subject {
|
|
|
50
51
|
*/
|
|
51
52
|
declare function getSubjectParent(subjectId: string): string | null;
|
|
52
53
|
/**
|
|
53
|
-
* Gets the children of a subject
|
|
54
|
+
* Gets the direct children of a subject
|
|
54
55
|
*/
|
|
55
56
|
declare function getSubjectChildren(subjectId: string): string[];
|
|
56
57
|
/**
|
|
@@ -59,14 +60,22 @@ declare function getSubjectChildren(subjectId: string): string[];
|
|
|
59
60
|
*/
|
|
60
61
|
declare function orderSubjects(subjectIds: string[]): string[];
|
|
61
62
|
/**
|
|
62
|
-
* Expands a subject ID to include its
|
|
63
|
+
* Expands a subject ID to include all its descendants recursively
|
|
63
64
|
* Useful for when a user requests a parent topic and wants to see all related content
|
|
64
65
|
*/
|
|
65
66
|
declare function expandSubjectWithChildren(subjectId: string): string[];
|
|
66
67
|
/**
|
|
67
|
-
* Gets all subject IDs in tree order
|
|
68
|
+
* Gets all subject IDs in tree order (depth-first traversal)
|
|
68
69
|
*/
|
|
69
70
|
declare function getAllSubjectIdsInOrder(): string[];
|
|
71
|
+
/**
|
|
72
|
+
* Checks if a subject ID is a category (has no markdown file)
|
|
73
|
+
*/
|
|
74
|
+
declare function isCategory(subjectId: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Gets the category title for display purposes
|
|
77
|
+
*/
|
|
78
|
+
declare function getCategoryTitle(categoryId: string): string;
|
|
70
79
|
//#endregion
|
|
71
80
|
//#region src/index.d.ts
|
|
72
81
|
/**
|
|
@@ -94,5 +103,5 @@ declare function getSubject(...ids: string[]): Subject[];
|
|
|
94
103
|
*/
|
|
95
104
|
declare function getAllSubjects(): Subject[];
|
|
96
105
|
//#endregion
|
|
97
|
-
export { type CodeBlock, type Example, type Section, type Subject, type SubjectInfo, expandSubjectWithChildren, getAllSubjectIdsInOrder, getAllSubjects, getSubject, getSubjectChildren, getSubjectParent, getSubjects, orderSubjects };
|
|
106
|
+
export { type CodeBlock, type Example, type Section, type Subject, type SubjectInfo, expandSubjectWithChildren, getAllSubjectIdsInOrder, getAllSubjects, getCategoryTitle, getSubject, getSubjectChildren, getSubjectParent, getSubjects, isCategory, orderSubjects };
|
|
98
107
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/parser.ts","../src/subject-tree.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/parser.ts","../src/subject-tree.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;AAcA;AAQA;AAWiB,UAnBA,WAAA,CAmBS;EAQT,EAAA,EAAA,MAAO;EASP,KAAA,EAAA,MAAO;;;;;AAQL,UApCF,OAAA,CAoCE;;;;ECuBH,EAAA,CAAA,EAAA,MAAA;EAOA,SAAA,CAAA,EAAA,OAAA;AAQhB;AAYA;AAoBA;AAsBA;AAOgB,UD5HC,SAAA,CC4He;;;;AC/IhC;AA6BA;AAUA;UFZiB,OAAA;;;;;;;;UASA,OAAA;;;;;WAKN;YACC;YACA;YACA;;;;;;;iBCuBI,gBAAA;;AAAhB;AAOA;AAQgB,iBARA,kBAAA,CAQa,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA;AAY7B;AAoBA;AAsBA;AAOA;iBA7DgB,aAAA;;;AClFhB;AA6BA;AAUgB,iBDuDA,yBAAA,CCvDyB,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA;;;;iBD2EzB,uBAAA,CAAA;;;;iBAsBA,UAAA;;;;iBAOA,gBAAA;;;AD/IhB;AAQA;AAWA;AAQA;AASiB,iBEpCD,WAAA,CAAA,CFoCQ,EEpCO,WFoCP,EAAA;;;;;;;;;AC+BxB;AAOA;AAQA;AAYA;AAoBA;AAsBgB,iBC3GA,UAAA,CD2GU,GAAA,GAAA,EAAA,MAAA,EAAA,CAAA,EC3GoB,OD2GpB,EAAA;AAO1B;;;;AC/IgB,iBAuCA,cAAA,CAAA,CAvCe,EAuCG,OAvCQ,EAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,130 @@
|
|
|
1
|
-
import { readFileSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
//#region src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
//#region src/subject-tree.ts
|
|
6
|
+
/**
|
|
7
|
+
* Tree structure defining subject hierarchy and ordering
|
|
8
|
+
* - Root-level subjects are listed in order
|
|
9
|
+
* - Children can be arbitrarily nested
|
|
10
|
+
* - Organized by audience: users, fragment authors, and general topics
|
|
11
|
+
*/
|
|
12
|
+
const SUBJECT_TREE = [
|
|
13
|
+
{
|
|
14
|
+
id: "for-users",
|
|
15
|
+
category: true,
|
|
16
|
+
children: [{ id: "fragment-instantiation" }, { id: "client-state-management" }]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "for-fragment-authors",
|
|
20
|
+
category: true,
|
|
21
|
+
children: [
|
|
22
|
+
{ id: "defining-routes" },
|
|
23
|
+
{ id: "fragment-services" },
|
|
24
|
+
{ id: "database-querying" },
|
|
25
|
+
{
|
|
26
|
+
id: "database-adapters",
|
|
27
|
+
children: [{ id: "kysely-adapter" }, { id: "drizzle-adapter" }]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "general",
|
|
33
|
+
category: true,
|
|
34
|
+
children: []
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Flattened map of all subjects and their parent relationships
|
|
39
|
+
*/
|
|
40
|
+
const SUBJECT_PARENT_MAP = /* @__PURE__ */ new Map();
|
|
41
|
+
const SUBJECT_ORDER_MAP = /* @__PURE__ */ new Map();
|
|
42
|
+
const SUBJECT_CHILDREN_MAP = /* @__PURE__ */ new Map();
|
|
43
|
+
const SUBJECT_CATEGORY_MAP = /* @__PURE__ */ new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Recursively processes a node and its children, building parent/order/category maps
|
|
46
|
+
*/
|
|
47
|
+
function processNode(node, parent, orderIndexRef$1) {
|
|
48
|
+
SUBJECT_PARENT_MAP.set(node.id, parent);
|
|
49
|
+
SUBJECT_ORDER_MAP.set(node.id, orderIndexRef$1.value++);
|
|
50
|
+
if (node.category) SUBJECT_CATEGORY_MAP.set(node.id, true);
|
|
51
|
+
if (node.children) {
|
|
52
|
+
const childIds = node.children.map((child) => child.id);
|
|
53
|
+
SUBJECT_CHILDREN_MAP.set(node.id, childIds);
|
|
54
|
+
for (const childNode of node.children) processNode(childNode, node.id, orderIndexRef$1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const orderIndexRef = { value: 0 };
|
|
58
|
+
for (const node of SUBJECT_TREE) processNode(node, null, orderIndexRef);
|
|
59
|
+
/**
|
|
60
|
+
* Gets the parent of a subject, or null if it's a root subject
|
|
61
|
+
*/
|
|
62
|
+
function getSubjectParent(subjectId) {
|
|
63
|
+
return SUBJECT_PARENT_MAP.get(subjectId) ?? null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Gets the direct children of a subject
|
|
67
|
+
*/
|
|
68
|
+
function getSubjectChildren(subjectId) {
|
|
69
|
+
return SUBJECT_CHILDREN_MAP.get(subjectId) ?? [];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Orders an array of subject IDs according to the tree structure
|
|
73
|
+
* This ensures deterministic ordering regardless of input order
|
|
74
|
+
*/
|
|
75
|
+
function orderSubjects(subjectIds) {
|
|
76
|
+
return [...subjectIds].sort((a, b) => {
|
|
77
|
+
return (SUBJECT_ORDER_MAP.get(a) ?? Number.MAX_SAFE_INTEGER) - (SUBJECT_ORDER_MAP.get(b) ?? Number.MAX_SAFE_INTEGER);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Expands a subject ID to include all its descendants recursively
|
|
82
|
+
* Useful for when a user requests a parent topic and wants to see all related content
|
|
83
|
+
*/
|
|
84
|
+
function expandSubjectWithChildren(subjectId) {
|
|
85
|
+
const result = [subjectId];
|
|
86
|
+
function collectDescendants(id) {
|
|
87
|
+
const children = SUBJECT_CHILDREN_MAP.get(id);
|
|
88
|
+
if (children) for (const childId of children) {
|
|
89
|
+
result.push(childId);
|
|
90
|
+
collectDescendants(childId);
|
|
91
|
+
}
|
|
14
92
|
}
|
|
15
|
-
|
|
93
|
+
collectDescendants(subjectId);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gets all subject IDs in tree order (depth-first traversal)
|
|
98
|
+
*/
|
|
99
|
+
function getAllSubjectIdsInOrder() {
|
|
100
|
+
const ids = [];
|
|
101
|
+
function traverse(node) {
|
|
102
|
+
ids.push(node.id);
|
|
103
|
+
if (node.children) for (const childNode of node.children) traverse(childNode);
|
|
104
|
+
}
|
|
105
|
+
for (const node of SUBJECT_TREE) traverse(node);
|
|
106
|
+
return ids;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Checks if a subject ID is a category (has no markdown file)
|
|
110
|
+
*/
|
|
111
|
+
function isCategory(subjectId) {
|
|
112
|
+
return SUBJECT_CATEGORY_MAP.get(subjectId) ?? false;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Gets the category title for display purposes
|
|
116
|
+
*/
|
|
117
|
+
function getCategoryTitle(categoryId) {
|
|
118
|
+
return {
|
|
119
|
+
"for-users": "For Users",
|
|
120
|
+
"for-fragment-authors": "For Fragment Authors",
|
|
121
|
+
general: "General"
|
|
122
|
+
}[categoryId] ?? categoryId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/parser.ts
|
|
127
|
+
const SUBJECTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "subjects");
|
|
16
128
|
/**
|
|
17
129
|
* Helper function to extract code blocks with optional IDs from a directive
|
|
18
130
|
*/
|
|
@@ -40,21 +152,23 @@ function parseMarkdownFile(content) {
|
|
|
40
152
|
const imports = importsMatch ? importsMatch[1].trim() : "";
|
|
41
153
|
const prelude = extractCodeBlocks(content, "prelude");
|
|
42
154
|
const testInit = extractCodeBlocks(content, "test-init");
|
|
43
|
-
const testBlockRegex = /```typescript @fragno-test(?::(\w+(?:-\w+)*))?\n([\s\S]*?)```([\s\S]*?)(?=```typescript @fragno-test|$)/g;
|
|
155
|
+
const testBlockRegex = /```typescript @fragno-test(?::(\w+(?:-\w+)*))?\s*(types-only)?\n([\s\S]*?)```([\s\S]*?)(?=```typescript @fragno-test|$)/g;
|
|
44
156
|
const testBlocks = [];
|
|
45
157
|
let match;
|
|
46
158
|
while ((match = testBlockRegex.exec(content)) !== null) {
|
|
47
159
|
const id = match[1] || void 0;
|
|
48
|
-
const
|
|
160
|
+
const typesOnly = match[2] === "types-only";
|
|
161
|
+
const code = match[3].trim();
|
|
49
162
|
const lines = code.split("\n");
|
|
50
163
|
let testName;
|
|
51
164
|
if (lines[0]?.trim().startsWith("//")) testName = lines[0].replace(/^\/\/\s*/, "").trim();
|
|
52
|
-
const explanation = match[
|
|
165
|
+
const explanation = match[4].split(/```/)[0].trim();
|
|
53
166
|
testBlocks.push({
|
|
54
167
|
code,
|
|
55
168
|
explanation,
|
|
56
169
|
testName,
|
|
57
|
-
id
|
|
170
|
+
id,
|
|
171
|
+
typesOnly
|
|
58
172
|
});
|
|
59
173
|
}
|
|
60
174
|
const descriptionMatch = content.substring(content.indexOf(title) + title.length).match(/\n\n([\s\S]*?)(?=```|##|$)/);
|
|
@@ -92,7 +206,8 @@ function markdownToSubject(id, parsed) {
|
|
|
92
206
|
code: block.code,
|
|
93
207
|
explanation: block.explanation,
|
|
94
208
|
testName: block.testName,
|
|
95
|
-
id: block.id
|
|
209
|
+
id: block.id,
|
|
210
|
+
typesOnly: block.typesOnly
|
|
96
211
|
}));
|
|
97
212
|
return {
|
|
98
213
|
id,
|
|
@@ -107,9 +222,13 @@ function markdownToSubject(id, parsed) {
|
|
|
107
222
|
}
|
|
108
223
|
/**
|
|
109
224
|
* Loads and parses a subject file by ID
|
|
225
|
+
* Returns null for category nodes (which have no markdown file)
|
|
110
226
|
*/
|
|
111
227
|
function loadSubject(id) {
|
|
112
|
-
|
|
228
|
+
if (isCategory(id)) return null;
|
|
229
|
+
const filePath = join(SUBJECTS_DIR, `${id}.md`);
|
|
230
|
+
if (!existsSync(filePath)) throw new Error(`Subject file not found: ${filePath}`);
|
|
231
|
+
return markdownToSubject(id, parseMarkdownFile(readFileSync(filePath, "utf-8")));
|
|
113
232
|
}
|
|
114
233
|
/**
|
|
115
234
|
* Gets all available subject IDs from the subjects directory
|
|
@@ -119,9 +238,10 @@ function getAvailableSubjectIds() {
|
|
|
119
238
|
}
|
|
120
239
|
/**
|
|
121
240
|
* Loads multiple subjects by their IDs
|
|
241
|
+
* Skips category nodes (which have no markdown files)
|
|
122
242
|
*/
|
|
123
243
|
function loadSubjects(ids) {
|
|
124
|
-
return ids.map((id) => loadSubject(id));
|
|
244
|
+
return ids.map((id) => loadSubject(id)).filter((s) => s !== null);
|
|
125
245
|
}
|
|
126
246
|
/**
|
|
127
247
|
* Loads all available subjects
|
|
@@ -130,77 +250,6 @@ function loadAllSubjects() {
|
|
|
130
250
|
return loadSubjects(getAvailableSubjectIds());
|
|
131
251
|
}
|
|
132
252
|
|
|
133
|
-
//#endregion
|
|
134
|
-
//#region src/subject-tree.ts
|
|
135
|
-
/**
|
|
136
|
-
* Tree structure defining subject hierarchy and ordering
|
|
137
|
-
* - Root-level subjects are listed in order
|
|
138
|
-
* - Children are indented under their parents
|
|
139
|
-
*/
|
|
140
|
-
const SUBJECT_TREE = [
|
|
141
|
-
{ id: "defining-routes" },
|
|
142
|
-
{ id: "database-querying" },
|
|
143
|
-
{
|
|
144
|
-
id: "database-adapters",
|
|
145
|
-
children: ["kysely-adapter", "drizzle-adapter"]
|
|
146
|
-
}
|
|
147
|
-
];
|
|
148
|
-
/**
|
|
149
|
-
* Flattened map of all subjects and their parent relationships
|
|
150
|
-
*/
|
|
151
|
-
const SUBJECT_PARENT_MAP = /* @__PURE__ */ new Map();
|
|
152
|
-
const SUBJECT_ORDER_MAP = /* @__PURE__ */ new Map();
|
|
153
|
-
let orderIndex = 0;
|
|
154
|
-
for (const node of SUBJECT_TREE) {
|
|
155
|
-
SUBJECT_PARENT_MAP.set(node.id, null);
|
|
156
|
-
SUBJECT_ORDER_MAP.set(node.id, orderIndex++);
|
|
157
|
-
if (node.children) for (const childId of node.children) {
|
|
158
|
-
SUBJECT_PARENT_MAP.set(childId, node.id);
|
|
159
|
-
SUBJECT_ORDER_MAP.set(childId, orderIndex++);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Gets the parent of a subject, or null if it's a root subject
|
|
164
|
-
*/
|
|
165
|
-
function getSubjectParent(subjectId) {
|
|
166
|
-
return SUBJECT_PARENT_MAP.get(subjectId) ?? null;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Gets the children of a subject
|
|
170
|
-
*/
|
|
171
|
-
function getSubjectChildren(subjectId) {
|
|
172
|
-
return SUBJECT_TREE.find((n) => n.id === subjectId)?.children ?? [];
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Orders an array of subject IDs according to the tree structure
|
|
176
|
-
* This ensures deterministic ordering regardless of input order
|
|
177
|
-
*/
|
|
178
|
-
function orderSubjects(subjectIds) {
|
|
179
|
-
return [...subjectIds].sort((a, b) => {
|
|
180
|
-
return (SUBJECT_ORDER_MAP.get(a) ?? Number.MAX_SAFE_INTEGER) - (SUBJECT_ORDER_MAP.get(b) ?? Number.MAX_SAFE_INTEGER);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Expands a subject ID to include its children if it has any
|
|
185
|
-
* Useful for when a user requests a parent topic and wants to see all related content
|
|
186
|
-
*/
|
|
187
|
-
function expandSubjectWithChildren(subjectId) {
|
|
188
|
-
const children = getSubjectChildren(subjectId);
|
|
189
|
-
if (children.length > 0) return [subjectId, ...children];
|
|
190
|
-
return [subjectId];
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Gets all subject IDs in tree order
|
|
194
|
-
*/
|
|
195
|
-
function getAllSubjectIdsInOrder() {
|
|
196
|
-
const ids = [];
|
|
197
|
-
for (const node of SUBJECT_TREE) {
|
|
198
|
-
ids.push(node.id);
|
|
199
|
-
if (node.children) ids.push(...node.children);
|
|
200
|
-
}
|
|
201
|
-
return ids;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
253
|
//#endregion
|
|
205
254
|
//#region src/index.ts
|
|
206
255
|
/**
|
|
@@ -210,11 +259,12 @@ function getAllSubjectIdsInOrder() {
|
|
|
210
259
|
function getSubjects() {
|
|
211
260
|
return getAvailableSubjectIds().map((id) => {
|
|
212
261
|
const subject = loadSubject(id);
|
|
262
|
+
if (!subject) return null;
|
|
213
263
|
return {
|
|
214
264
|
id: subject.id,
|
|
215
265
|
title: subject.title
|
|
216
266
|
};
|
|
217
|
-
});
|
|
267
|
+
}).filter((s) => s !== null);
|
|
218
268
|
}
|
|
219
269
|
/**
|
|
220
270
|
* Get one or more subjects by their IDs
|
|
@@ -241,5 +291,5 @@ function getAllSubjects() {
|
|
|
241
291
|
}
|
|
242
292
|
|
|
243
293
|
//#endregion
|
|
244
|
-
export { expandSubjectWithChildren, getAllSubjectIdsInOrder, getAllSubjects, getSubject, getSubjectChildren, getSubjectParent, getSubjects, orderSubjects };
|
|
294
|
+
export { expandSubjectWithChildren, getAllSubjectIdsInOrder, getAllSubjects, getCategoryTitle, getSubject, getSubjectChildren, getSubjectParent, getSubjects, isCategory, orderSubjects };
|
|
245
295
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["blocks: CodeBlock[]","testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n }>","testName: string | undefined","sections: Section[]","match","examples: Example[]","SUBJECT_TREE: SubjectNode[]","ids: string[]"],"sources":["../src/parser.ts","../src/subject-tree.ts","../src/index.ts"],"sourcesContent":["import { readFileSync, readdirSync } from \"node:fs\";\nimport { join, basename, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Basic information about a subject\n */\nexport interface SubjectInfo {\n id: string;\n title: string;\n}\n\n/**\n * A single example within a subject\n */\nexport interface Example {\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n}\n\n/**\n * A code block with optional ID\n */\nexport interface CodeBlock {\n code: string;\n id?: string;\n}\n\n/**\n * A markdown section with heading and content\n */\nexport interface Section {\n heading: string;\n content: string;\n lineNumber?: number;\n}\n\n/**\n * Complete subject with all examples and metadata\n */\nexport interface Subject {\n id: string;\n title: string;\n description: string;\n imports: string;\n prelude: CodeBlock[];\n testInit: CodeBlock[];\n examples: Example[];\n sections: Section[];\n}\n\n/**\n * Raw parsed data from markdown before processing\n */\nexport interface ParsedMarkdown {\n title: string;\n description: string;\n imports: string;\n prelude: CodeBlock[];\n testInit: CodeBlock[];\n testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n }>;\n sections: Section[];\n}\n\n// Look for subjects directory in source or relative to built dist\nconst SUBJECTS_DIR = (() => {\n // Try dist/../src/subjects (when running from built code)\n const distRelative = join(__dirname, \"..\", \"src\", \"subjects\");\n try {\n readdirSync(distRelative);\n return distRelative;\n } catch {\n // Fall back to ./subjects (when running from source)\n return join(__dirname, \"subjects\");\n }\n})();\n\n/**\n * Helper function to extract code blocks with optional IDs from a directive\n */\nfunction extractCodeBlocks(content: string, directive: string): CodeBlock[] {\n const regex = new RegExp(\n `\\`\\`\\`typescript @fragno-${directive}(?::(\\\\w+(?:-\\\\w+)*))?\\\\n([\\\\s\\\\S]*?)\\`\\`\\``,\n \"g\",\n );\n const blocks: CodeBlock[] = [];\n\n let match;\n while ((match = regex.exec(content)) !== null) {\n const id = match[1] || undefined;\n const code = match[2].trim();\n blocks.push({ code, id });\n }\n\n return blocks;\n}\n\n/**\n * Parses a markdown file and extracts structured content\n */\nexport function parseMarkdownFile(content: string): ParsedMarkdown {\n // Extract title (first # heading)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"Untitled\";\n\n // Extract imports block\n const importsMatch = content.match(/```typescript @fragno-imports\\n([\\s\\S]*?)```/);\n const imports = importsMatch ? importsMatch[1].trim() : \"\";\n\n // Extract prelude blocks\n const prelude = extractCodeBlocks(content, \"prelude\");\n\n // Extract test-init blocks\n const testInit = extractCodeBlocks(content, \"test-init\");\n\n // Extract all test blocks with their explanations and optional IDs\n const testBlockRegex =\n /```typescript @fragno-test(?::(\\w+(?:-\\w+)*))?\\n([\\s\\S]*?)```([\\s\\S]*?)(?=```typescript @fragno-test|$)/g;\n const testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n }> = [];\n\n let match;\n while ((match = testBlockRegex.exec(content)) !== null) {\n const id = match[1] || undefined;\n const code = match[2].trim();\n\n // Extract test name from first line if it's a comment\n const lines = code.split(\"\\n\");\n let testName: string | undefined;\n if (lines[0]?.trim().startsWith(\"//\")) {\n testName = lines[0].replace(/^\\/\\/\\s*/, \"\").trim();\n }\n\n // Get explanation text after the code block until next code block or end\n const afterBlock = match[3];\n const explanation = afterBlock\n .split(/```/)[0] // Stop at next code block\n .trim();\n\n testBlocks.push({ code, explanation, testName, id });\n }\n\n // Extract description (everything between title and first code block or ## heading)\n const afterTitle = content.substring(content.indexOf(title) + title.length);\n const descriptionMatch = afterTitle.match(/\\n\\n([\\s\\S]*?)(?=```|##|$)/);\n const description = descriptionMatch ? descriptionMatch[1].trim() : \"\";\n\n // Extract all sections (## headings and their content)\n const sections: Section[] = [];\n const sectionRegex = /^##\\s+(.+)$/gm;\n const matches = [...content.matchAll(sectionRegex)];\n\n for (let i = 0; i < matches.length; i++) {\n const match = matches[i];\n const heading = match[1].trim();\n const sectionStart = match.index! + match[0].length;\n const nextSectionStart = matches[i + 1]?.index ?? content.length;\n let sectionContent = content.substring(sectionStart, nextSectionStart).trim();\n\n // Convert @fragno directive code blocks to regular typescript blocks for display\n sectionContent = sectionContent.replace(\n /```typescript @fragno-\\w+(?::\\w+(?:-\\w+)*)?/g,\n \"```typescript\",\n );\n sectionContent = sectionContent.trim();\n\n if (sectionContent) {\n sections.push({ heading, content: sectionContent });\n }\n }\n\n return {\n title,\n description,\n imports,\n prelude,\n testInit,\n testBlocks,\n sections,\n };\n}\n\n/**\n * Converts parsed markdown to a Subject\n */\nexport function markdownToSubject(id: string, parsed: ParsedMarkdown): Subject {\n const examples: Example[] = parsed.testBlocks.map((block) => ({\n code: block.code,\n explanation: block.explanation,\n testName: block.testName,\n id: block.id,\n }));\n\n return {\n id,\n title: parsed.title,\n description: parsed.description,\n imports: parsed.imports,\n prelude: parsed.prelude,\n testInit: parsed.testInit,\n examples,\n sections: parsed.sections,\n };\n}\n\n/**\n * Loads and parses a subject file by ID\n */\nexport function loadSubject(id: string): Subject {\n const filePath = join(SUBJECTS_DIR, `${id}.md`);\n const content = readFileSync(filePath, \"utf-8\");\n const parsed = parseMarkdownFile(content);\n return markdownToSubject(id, parsed);\n}\n\n/**\n * Gets all available subject IDs from the subjects directory\n */\nexport function getAvailableSubjectIds(): string[] {\n const files = readdirSync(SUBJECTS_DIR);\n return files.filter((file) => file.endsWith(\".md\")).map((file) => basename(file, \".md\"));\n}\n\n/**\n * Loads multiple subjects by their IDs\n */\nexport function loadSubjects(ids: string[]): Subject[] {\n return ids.map((id) => loadSubject(id));\n}\n\n/**\n * Loads all available subjects\n */\nexport function loadAllSubjects(): Subject[] {\n const ids = getAvailableSubjectIds();\n return loadSubjects(ids);\n}\n","/**\n * Subject tree structure defining relationships and ordering\n */\n\nexport interface SubjectNode {\n id: string;\n children?: string[];\n}\n\n/**\n * Tree structure defining subject hierarchy and ordering\n * - Root-level subjects are listed in order\n * - Children are indented under their parents\n */\nconst SUBJECT_TREE: SubjectNode[] = [\n { id: \"defining-routes\" },\n { id: \"database-querying\" },\n {\n id: \"database-adapters\",\n children: [\"kysely-adapter\", \"drizzle-adapter\"],\n },\n];\n\n/**\n * Flattened map of all subjects and their parent relationships\n */\nconst SUBJECT_PARENT_MAP = new Map<string, string | null>();\nconst SUBJECT_ORDER_MAP = new Map<string, number>();\n\n// Build the parent and order maps\nlet orderIndex = 0;\nfor (const node of SUBJECT_TREE) {\n SUBJECT_PARENT_MAP.set(node.id, null);\n SUBJECT_ORDER_MAP.set(node.id, orderIndex++);\n\n if (node.children) {\n for (const childId of node.children) {\n SUBJECT_PARENT_MAP.set(childId, node.id);\n SUBJECT_ORDER_MAP.set(childId, orderIndex++);\n }\n }\n}\n\n/**\n * Gets the parent of a subject, or null if it's a root subject\n */\nexport function getSubjectParent(subjectId: string): string | null {\n return SUBJECT_PARENT_MAP.get(subjectId) ?? null;\n}\n\n/**\n * Gets the children of a subject\n */\nexport function getSubjectChildren(subjectId: string): string[] {\n const node = SUBJECT_TREE.find((n) => n.id === subjectId);\n return node?.children ?? [];\n}\n\n/**\n * Orders an array of subject IDs according to the tree structure\n * This ensures deterministic ordering regardless of input order\n */\nexport function orderSubjects(subjectIds: string[]): string[] {\n return [...subjectIds].sort((a, b) => {\n const orderA = SUBJECT_ORDER_MAP.get(a) ?? Number.MAX_SAFE_INTEGER;\n const orderB = SUBJECT_ORDER_MAP.get(b) ?? Number.MAX_SAFE_INTEGER;\n return orderA - orderB;\n });\n}\n\n/**\n * Expands a subject ID to include its children if it has any\n * Useful for when a user requests a parent topic and wants to see all related content\n */\nexport function expandSubjectWithChildren(subjectId: string): string[] {\n const children = getSubjectChildren(subjectId);\n if (children.length > 0) {\n return [subjectId, ...children];\n }\n return [subjectId];\n}\n\n/**\n * Gets all subject IDs in tree order\n */\nexport function getAllSubjectIdsInOrder(): string[] {\n const ids: string[] = [];\n for (const node of SUBJECT_TREE) {\n ids.push(node.id);\n if (node.children) {\n ids.push(...node.children);\n }\n }\n return ids;\n}\n","import {\n getAvailableSubjectIds,\n loadSubject,\n loadSubjects,\n loadAllSubjects,\n type SubjectInfo,\n type Subject,\n} from \"./parser\";\nimport { orderSubjects } from \"./subject-tree\";\n\n/**\n * Get basic information about all available subjects\n * @returns Array of subject info (id and title)\n */\nexport function getSubjects(): SubjectInfo[] {\n const ids = getAvailableSubjectIds();\n return ids.map((id) => {\n const subject = loadSubject(id);\n return {\n id: subject.id,\n title: subject.title,\n };\n });\n}\n\n/**\n * Get one or more subjects by their IDs\n * @param ids Subject IDs to load\n * @returns Array of complete subject data ordered by the subject tree\n * @example\n * ```ts\n * // Get single subject\n * const [routes] = getSubject(\"defining-routes\");\n *\n * // Get multiple subjects for combined context\n * const [adapters, kysely] = getSubject(\"database-adapters\", \"kysely-adapter\");\n * ```\n */\nexport function getSubject(...ids: string[]): Subject[] {\n // Order subjects deterministically according to the tree structure\n const orderedIds = orderSubjects(ids);\n return loadSubjects(orderedIds);\n}\n\n/**\n * Get all available subjects\n * @returns Array of all subjects with complete data\n */\nexport function getAllSubjects(): Subject[] {\n return loadAllSubjects();\n}\n\n// Re-export types\nexport type { Subject, SubjectInfo, Example, Section, CodeBlock } from \"./parser.js\";\n\n// Re-export subject tree utilities\nexport {\n orderSubjects,\n getSubjectParent,\n getSubjectChildren,\n expandSubjectWithChildren,\n getAllSubjectIdsInOrder,\n} from \"./subject-tree.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["SUBJECT_TREE: SubjectNode[]","orderIndexRef","result: string[]","ids: string[]","blocks: CodeBlock[]","testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n typesOnly?: boolean;\n }>","testName: string | undefined","sections: Section[]","match","examples: Example[]"],"sources":["../src/subject-tree.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["/**\n * Subject tree structure defining relationships and ordering\n */\n\nexport interface SubjectNode {\n id: string;\n children?: SubjectNode[];\n /** If true, this is a category node without a markdown file */\n category?: boolean;\n}\n\n/**\n * Tree structure defining subject hierarchy and ordering\n * - Root-level subjects are listed in order\n * - Children can be arbitrarily nested\n * - Organized by audience: users, fragment authors, and general topics\n */\nconst SUBJECT_TREE: SubjectNode[] = [\n {\n id: \"for-users\",\n category: true,\n children: [{ id: \"fragment-instantiation\" }, { id: \"client-state-management\" }],\n },\n {\n id: \"for-fragment-authors\",\n category: true,\n children: [\n { id: \"defining-routes\" },\n { id: \"fragment-services\" },\n { id: \"database-querying\" },\n {\n id: \"database-adapters\",\n children: [{ id: \"kysely-adapter\" }, { id: \"drizzle-adapter\" }],\n },\n ],\n },\n {\n id: \"general\",\n category: true,\n children: [],\n },\n];\n\n/**\n * Flattened map of all subjects and their parent relationships\n */\nconst SUBJECT_PARENT_MAP = new Map<string, string | null>();\nconst SUBJECT_ORDER_MAP = new Map<string, number>();\nconst SUBJECT_CHILDREN_MAP = new Map<string, string[]>();\nconst SUBJECT_CATEGORY_MAP = new Map<string, boolean>();\n\n/**\n * Recursively processes a node and its children, building parent/order/category maps\n */\nfunction processNode(node: SubjectNode, parent: string | null, orderIndexRef: { value: number }) {\n SUBJECT_PARENT_MAP.set(node.id, parent);\n SUBJECT_ORDER_MAP.set(node.id, orderIndexRef.value++);\n\n if (node.category) {\n SUBJECT_CATEGORY_MAP.set(node.id, true);\n }\n\n if (node.children) {\n const childIds = node.children.map((child) => child.id);\n SUBJECT_CHILDREN_MAP.set(node.id, childIds);\n\n for (const childNode of node.children) {\n processNode(childNode, node.id, orderIndexRef);\n }\n }\n}\n\n// Build the parent and order maps\nconst orderIndexRef = { value: 0 };\nfor (const node of SUBJECT_TREE) {\n processNode(node, null, orderIndexRef);\n}\n\n/**\n * Gets the parent of a subject, or null if it's a root subject\n */\nexport function getSubjectParent(subjectId: string): string | null {\n return SUBJECT_PARENT_MAP.get(subjectId) ?? null;\n}\n\n/**\n * Gets the direct children of a subject\n */\nexport function getSubjectChildren(subjectId: string): string[] {\n return SUBJECT_CHILDREN_MAP.get(subjectId) ?? [];\n}\n\n/**\n * Orders an array of subject IDs according to the tree structure\n * This ensures deterministic ordering regardless of input order\n */\nexport function orderSubjects(subjectIds: string[]): string[] {\n return [...subjectIds].sort((a, b) => {\n const orderA = SUBJECT_ORDER_MAP.get(a) ?? Number.MAX_SAFE_INTEGER;\n const orderB = SUBJECT_ORDER_MAP.get(b) ?? Number.MAX_SAFE_INTEGER;\n return orderA - orderB;\n });\n}\n\n/**\n * Expands a subject ID to include all its descendants recursively\n * Useful for when a user requests a parent topic and wants to see all related content\n */\nexport function expandSubjectWithChildren(subjectId: string): string[] {\n const result: string[] = [subjectId];\n\n function collectDescendants(id: string) {\n const children = SUBJECT_CHILDREN_MAP.get(id);\n if (children) {\n for (const childId of children) {\n result.push(childId);\n collectDescendants(childId);\n }\n }\n }\n\n collectDescendants(subjectId);\n return result;\n}\n\n/**\n * Gets all subject IDs in tree order (depth-first traversal)\n */\nexport function getAllSubjectIdsInOrder(): string[] {\n const ids: string[] = [];\n\n function traverse(node: SubjectNode) {\n ids.push(node.id);\n if (node.children) {\n for (const childNode of node.children) {\n traverse(childNode);\n }\n }\n }\n\n for (const node of SUBJECT_TREE) {\n traverse(node);\n }\n\n return ids;\n}\n\n/**\n * Checks if a subject ID is a category (has no markdown file)\n */\nexport function isCategory(subjectId: string): boolean {\n return SUBJECT_CATEGORY_MAP.get(subjectId) ?? false;\n}\n\n/**\n * Gets the category title for display purposes\n */\nexport function getCategoryTitle(categoryId: string): string {\n const titles: Record<string, string> = {\n \"for-users\": \"For Users\",\n \"for-fragment-authors\": \"For Fragment Authors\",\n general: \"General\",\n };\n return titles[categoryId] ?? categoryId;\n}\n","import { readFileSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, basename, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isCategory } from \"./subject-tree.js\";\n\n// __filename will be:\n// - In development: /path/to/packages/corpus/src/parser.ts\n// - In production: /path/to/node_modules/@fragno-dev/corpus/dist/parser.js\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Basic information about a subject\n */\nexport interface SubjectInfo {\n id: string;\n title: string;\n}\n\n/**\n * A single example within a subject\n */\nexport interface Example {\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n typesOnly?: boolean;\n}\n\n/**\n * A code block with optional ID\n */\nexport interface CodeBlock {\n code: string;\n id?: string;\n}\n\n/**\n * A markdown section with heading and content\n */\nexport interface Section {\n heading: string;\n content: string;\n lineNumber?: number;\n}\n\n/**\n * Complete subject with all examples and metadata\n */\nexport interface Subject {\n id: string;\n title: string;\n description: string;\n imports: string;\n prelude: CodeBlock[];\n testInit: CodeBlock[];\n examples: Example[];\n sections: Section[];\n}\n\n/**\n * Raw parsed data from markdown before processing\n */\nexport interface ParsedMarkdown {\n title: string;\n description: string;\n imports: string;\n prelude: CodeBlock[];\n testInit: CodeBlock[];\n testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n typesOnly?: boolean;\n }>;\n sections: Section[];\n}\n\n// SUBJECTS_DIR resolution:\n// - In development: join(__dirname, \"subjects\") = /path/to/packages/corpus/src/subjects\n// - In production: join(__dirname, \"subjects\") = /path/to/node_modules/@fragno-dev/corpus/dist/subjects\n//\n// The tsdown build copies src/subjects/ to dist/subjects/, ensuring the directory exists in both cases.\nconst SUBJECTS_DIR = join(__dirname, \"subjects\");\n\n/**\n * Helper function to extract code blocks with optional IDs from a directive\n */\nfunction extractCodeBlocks(content: string, directive: string): CodeBlock[] {\n const regex = new RegExp(\n `\\`\\`\\`typescript @fragno-${directive}(?::(\\\\w+(?:-\\\\w+)*))?\\\\n([\\\\s\\\\S]*?)\\`\\`\\``,\n \"g\",\n );\n const blocks: CodeBlock[] = [];\n\n let match;\n while ((match = regex.exec(content)) !== null) {\n const id = match[1] || undefined;\n const code = match[2].trim();\n blocks.push({ code, id });\n }\n\n return blocks;\n}\n\n/**\n * Parses a markdown file and extracts structured content\n */\nexport function parseMarkdownFile(content: string): ParsedMarkdown {\n // Extract title (first # heading)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"Untitled\";\n\n // Extract imports block\n const importsMatch = content.match(/```typescript @fragno-imports\\n([\\s\\S]*?)```/);\n const imports = importsMatch ? importsMatch[1].trim() : \"\";\n\n // Extract prelude blocks\n const prelude = extractCodeBlocks(content, \"prelude\");\n\n // Extract test-init blocks\n const testInit = extractCodeBlocks(content, \"test-init\");\n\n // Extract all test blocks with their explanations and optional IDs\n // Pattern: ```typescript @fragno-test[:id] [types-only]\n const testBlockRegex =\n /```typescript @fragno-test(?::(\\w+(?:-\\w+)*))?\\s*(types-only)?\\n([\\s\\S]*?)```([\\s\\S]*?)(?=```typescript @fragno-test|$)/g;\n const testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n typesOnly?: boolean;\n }> = [];\n\n let match;\n while ((match = testBlockRegex.exec(content)) !== null) {\n const id = match[1] || undefined;\n const typesOnly = match[2] === \"types-only\";\n const code = match[3].trim();\n\n // Extract test name from first line if it's a comment\n const lines = code.split(\"\\n\");\n let testName: string | undefined;\n if (lines[0]?.trim().startsWith(\"//\")) {\n testName = lines[0].replace(/^\\/\\/\\s*/, \"\").trim();\n }\n\n // Get explanation text after the code block until next code block or end\n const afterBlock = match[4];\n const explanation = afterBlock\n .split(/```/)[0] // Stop at next code block\n .trim();\n\n testBlocks.push({ code, explanation, testName, id, typesOnly });\n }\n\n // Extract description (everything between title and first code block or ## heading)\n const afterTitle = content.substring(content.indexOf(title) + title.length);\n const descriptionMatch = afterTitle.match(/\\n\\n([\\s\\S]*?)(?=```|##|$)/);\n const description = descriptionMatch ? descriptionMatch[1].trim() : \"\";\n\n // Extract all sections (## headings and their content)\n const sections: Section[] = [];\n const sectionRegex = /^##\\s+(.+)$/gm;\n const matches = [...content.matchAll(sectionRegex)];\n\n for (let i = 0; i < matches.length; i++) {\n const match = matches[i];\n const heading = match[1].trim();\n const sectionStart = match.index! + match[0].length;\n const nextSectionStart = matches[i + 1]?.index ?? content.length;\n let sectionContent = content.substring(sectionStart, nextSectionStart).trim();\n\n // Convert @fragno directive code blocks to regular typescript blocks for display\n sectionContent = sectionContent.replace(\n /```typescript @fragno-\\w+(?::\\w+(?:-\\w+)*)?/g,\n \"```typescript\",\n );\n sectionContent = sectionContent.trim();\n\n if (sectionContent) {\n sections.push({ heading, content: sectionContent });\n }\n }\n\n return {\n title,\n description,\n imports,\n prelude,\n testInit,\n testBlocks,\n sections,\n };\n}\n\n/**\n * Converts parsed markdown to a Subject\n */\nexport function markdownToSubject(id: string, parsed: ParsedMarkdown): Subject {\n const examples: Example[] = parsed.testBlocks.map((block) => ({\n code: block.code,\n explanation: block.explanation,\n testName: block.testName,\n id: block.id,\n typesOnly: block.typesOnly,\n }));\n\n return {\n id,\n title: parsed.title,\n description: parsed.description,\n imports: parsed.imports,\n prelude: parsed.prelude,\n testInit: parsed.testInit,\n examples,\n sections: parsed.sections,\n };\n}\n\n/**\n * Loads and parses a subject file by ID\n * Returns null for category nodes (which have no markdown file)\n */\nexport function loadSubject(id: string): Subject | null {\n // Categories don't have markdown files\n if (isCategory(id)) {\n return null;\n }\n\n const filePath = join(SUBJECTS_DIR, `${id}.md`);\n\n // Check if file exists before trying to read\n if (!existsSync(filePath)) {\n throw new Error(`Subject file not found: ${filePath}`);\n }\n\n const content = readFileSync(filePath, \"utf-8\");\n const parsed = parseMarkdownFile(content);\n return markdownToSubject(id, parsed);\n}\n\n/**\n * Gets all available subject IDs from the subjects directory\n */\nexport function getAvailableSubjectIds(): string[] {\n const files = readdirSync(SUBJECTS_DIR);\n return files.filter((file) => file.endsWith(\".md\")).map((file) => basename(file, \".md\"));\n}\n\n/**\n * Loads multiple subjects by their IDs\n * Skips category nodes (which have no markdown files)\n */\nexport function loadSubjects(ids: string[]): Subject[] {\n return ids.map((id) => loadSubject(id)).filter((s): s is Subject => s !== null);\n}\n\n/**\n * Loads all available subjects\n */\nexport function loadAllSubjects(): Subject[] {\n const ids = getAvailableSubjectIds();\n return loadSubjects(ids);\n}\n","import {\n getAvailableSubjectIds,\n loadSubject,\n loadSubjects,\n loadAllSubjects,\n type SubjectInfo,\n type Subject,\n} from \"./parser\";\nimport { orderSubjects } from \"./subject-tree\";\n\n/**\n * Get basic information about all available subjects\n * @returns Array of subject info (id and title)\n */\nexport function getSubjects(): SubjectInfo[] {\n const ids = getAvailableSubjectIds();\n return ids\n .map((id) => {\n const subject = loadSubject(id);\n if (!subject) {\n return null;\n }\n return {\n id: subject.id,\n title: subject.title,\n };\n })\n .filter((s): s is SubjectInfo => s !== null);\n}\n\n/**\n * Get one or more subjects by their IDs\n * @param ids Subject IDs to load\n * @returns Array of complete subject data ordered by the subject tree\n * @example\n * ```ts\n * // Get single subject\n * const [routes] = getSubject(\"defining-routes\");\n *\n * // Get multiple subjects for combined context\n * const [adapters, kysely] = getSubject(\"database-adapters\", \"kysely-adapter\");\n * ```\n */\nexport function getSubject(...ids: string[]): Subject[] {\n // Order subjects deterministically according to the tree structure\n const orderedIds = orderSubjects(ids);\n return loadSubjects(orderedIds);\n}\n\n/**\n * Get all available subjects\n * @returns Array of all subjects with complete data\n */\nexport function getAllSubjects(): Subject[] {\n return loadAllSubjects();\n}\n\n// Re-export types\nexport type { Subject, SubjectInfo, Example, Section, CodeBlock } from \"./parser.js\";\n\n// Re-export subject tree utilities\nexport {\n orderSubjects,\n getSubjectParent,\n getSubjectChildren,\n expandSubjectWithChildren,\n getAllSubjectIdsInOrder,\n isCategory,\n getCategoryTitle,\n} from \"./subject-tree.js\";\n"],"mappings":"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"}
|