@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 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 children if it has any
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/parser.ts","../src/subject-tree.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;AAUA;AAQA;AAUiB,UAlBA,WAAA,CAkBS;EAQT,EAAA,EAAA,MAAO;EASP,KAAA,EAAA,MAAO;;;;;AAQL,UAnCF,OAAA,CAmCE;;;;ECPH,EAAA,CAAA,EAAA,MAAA;AAOhB;AASA;AAYA;AAWA;UDzDiB,SAAA;;;AEdjB;AAwBA;AAUA;;UFZiB,OAAA;;;;;;;;UASA,OAAA;;;;;WAKN;YACC;YACA;YACA;;;;;;;AAAO,iBCPH,gBAAA,CDOG,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;;ACPH,iBAOA,kBAAA,CAPgB,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA;AAOhC;AASA;AAYA;AAWA;iBAvBgB,aAAA;;;AChDhB;AAwBA;AAUgB,iBD0BA,yBAAA,CC1ByB,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA;;;;iBDqCzB,uBAAA,CAAA;;;AD3EhB;AAQA;AAUA;AAQA;AASiB,iBE/BD,WAAA,CAAA,CF+BQ,EE/BO,WF+BP,EAAA;;;;;;;;;ACCxB;AAOA;AASA;AAYA;AAWA;iBC/CgB,UAAA,oBAA8B;;;AAxB9C;AAwBA;AAUgB,iBAAA,cAAA,CAAA,CAAyB,EAAP,OAAO,EAAA"}
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/parser.ts
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const SUBJECTS_DIR = (() => {
8
- const distRelative = join(__dirname, "..", "src", "subjects");
9
- try {
10
- readdirSync(distRelative);
11
- return distRelative;
12
- } catch {
13
- return join(__dirname, "subjects");
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 code = match[2].trim();
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[3].split(/```/)[0].trim();
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
- return markdownToSubject(id, parseMarkdownFile(readFileSync(join(SUBJECTS_DIR, `${id}.md`), "utf-8")));
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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"}