lens-content-processor 0.1.0

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.
Files changed (103) hide show
  1. package/dist/bundler/article.d.ts +41 -0
  2. package/dist/bundler/article.js +240 -0
  3. package/dist/bundler/article.js.map +1 -0
  4. package/dist/bundler/article.test.d.ts +1 -0
  5. package/dist/bundler/article.test.js +212 -0
  6. package/dist/bundler/article.test.js.map +1 -0
  7. package/dist/bundler/video.d.ts +50 -0
  8. package/dist/bundler/video.js +213 -0
  9. package/dist/bundler/video.js.map +1 -0
  10. package/dist/bundler/video.test.d.ts +1 -0
  11. package/dist/bundler/video.test.js +95 -0
  12. package/dist/bundler/video.test.js.map +1 -0
  13. package/dist/cli.d.ts +10 -0
  14. package/dist/cli.js +80 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/cli.test.d.ts +1 -0
  17. package/dist/cli.test.js +45 -0
  18. package/dist/cli.test.js.map +1 -0
  19. package/dist/flattener/index.d.ts +20 -0
  20. package/dist/flattener/index.js +707 -0
  21. package/dist/flattener/index.js.map +1 -0
  22. package/dist/flattener/index.test.d.ts +1 -0
  23. package/dist/flattener/index.test.js +547 -0
  24. package/dist/flattener/index.test.js.map +1 -0
  25. package/dist/fs/read-vault.d.ts +4 -0
  26. package/dist/fs/read-vault.js +28 -0
  27. package/dist/fs/read-vault.js.map +1 -0
  28. package/dist/fs/read-vault.test.d.ts +1 -0
  29. package/dist/fs/read-vault.test.js +37 -0
  30. package/dist/fs/read-vault.test.js.map +1 -0
  31. package/dist/index.d.ts +75 -0
  32. package/dist/index.js +137 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/parser/course.d.ts +17 -0
  35. package/dist/parser/course.js +145 -0
  36. package/dist/parser/course.js.map +1 -0
  37. package/dist/parser/course.test.d.ts +1 -0
  38. package/dist/parser/course.test.js +117 -0
  39. package/dist/parser/course.test.js.map +1 -0
  40. package/dist/parser/frontmatter.d.ts +8 -0
  41. package/dist/parser/frontmatter.js +58 -0
  42. package/dist/parser/frontmatter.js.map +1 -0
  43. package/dist/parser/frontmatter.test.d.ts +1 -0
  44. package/dist/parser/frontmatter.test.js +54 -0
  45. package/dist/parser/frontmatter.test.js.map +1 -0
  46. package/dist/parser/learning-outcome.d.ts +21 -0
  47. package/dist/parser/learning-outcome.js +116 -0
  48. package/dist/parser/learning-outcome.js.map +1 -0
  49. package/dist/parser/learning-outcome.test.d.ts +1 -0
  50. package/dist/parser/learning-outcome.test.js +75 -0
  51. package/dist/parser/learning-outcome.test.js.map +1 -0
  52. package/dist/parser/lens.d.ts +51 -0
  53. package/dist/parser/lens.js +328 -0
  54. package/dist/parser/lens.js.map +1 -0
  55. package/dist/parser/lens.test.d.ts +1 -0
  56. package/dist/parser/lens.test.js +453 -0
  57. package/dist/parser/lens.test.js.map +1 -0
  58. package/dist/parser/module.d.ts +21 -0
  59. package/dist/parser/module.js +128 -0
  60. package/dist/parser/module.js.map +1 -0
  61. package/dist/parser/module.test.d.ts +1 -0
  62. package/dist/parser/module.test.js +99 -0
  63. package/dist/parser/module.test.js.map +1 -0
  64. package/dist/parser/sections.d.ts +18 -0
  65. package/dist/parser/sections.js +128 -0
  66. package/dist/parser/sections.js.map +1 -0
  67. package/dist/parser/sections.test.d.ts +1 -0
  68. package/dist/parser/sections.test.js +297 -0
  69. package/dist/parser/sections.test.js.map +1 -0
  70. package/dist/parser/wikilink.d.ts +12 -0
  71. package/dist/parser/wikilink.js +35 -0
  72. package/dist/parser/wikilink.js.map +1 -0
  73. package/dist/parser/wikilink.test.d.ts +1 -0
  74. package/dist/parser/wikilink.test.js +76 -0
  75. package/dist/parser/wikilink.test.js.map +1 -0
  76. package/dist/validator/field-typos.d.ts +10 -0
  77. package/dist/validator/field-typos.js +96 -0
  78. package/dist/validator/field-typos.js.map +1 -0
  79. package/dist/validator/field-typos.test.d.ts +1 -0
  80. package/dist/validator/field-typos.test.js +41 -0
  81. package/dist/validator/field-typos.test.js.map +1 -0
  82. package/dist/validator/field-values.d.ts +11 -0
  83. package/dist/validator/field-values.js +37 -0
  84. package/dist/validator/field-values.js.map +1 -0
  85. package/dist/validator/field-values.test.d.ts +1 -0
  86. package/dist/validator/field-values.test.js +45 -0
  87. package/dist/validator/field-values.test.js.map +1 -0
  88. package/dist/validator/segment-fields.d.ts +12 -0
  89. package/dist/validator/segment-fields.js +55 -0
  90. package/dist/validator/segment-fields.js.map +1 -0
  91. package/dist/validator/segment-fields.test.d.ts +1 -0
  92. package/dist/validator/segment-fields.test.js +73 -0
  93. package/dist/validator/segment-fields.test.js.map +1 -0
  94. package/dist/validator/standalone.test.d.ts +1 -0
  95. package/dist/validator/standalone.test.js +190 -0
  96. package/dist/validator/standalone.test.js.map +1 -0
  97. package/dist/validator/uuid.d.ts +17 -0
  98. package/dist/validator/uuid.js +46 -0
  99. package/dist/validator/uuid.js.map +1 -0
  100. package/dist/validator/uuid.test.d.ts +1 -0
  101. package/dist/validator/uuid.test.js +229 -0
  102. package/dist/validator/uuid.test.js.map +1 -0
  103. package/package.json +45 -0
@@ -0,0 +1,37 @@
1
+ // src/fs/read-vault.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { readVaultFiles } from './read-vault.js';
4
+ import { join } from 'path';
5
+ import { mkdtempSync, rmSync } from 'fs';
6
+ import { tmpdir } from 'os';
7
+ describe('readVaultFiles', () => {
8
+ it('reads all .md files from fixture directory', async () => {
9
+ const fixturesDir = join(import.meta.dirname, '../../fixtures/valid/minimal-module/input');
10
+ const files = await readVaultFiles(fixturesDir);
11
+ expect(files.has('modules/intro.md')).toBe(true);
12
+ expect(files.get('modules/intro.md')).toContain('slug: intro');
13
+ });
14
+ it('reads files from nested directories with correct relative paths', async () => {
15
+ const fixturesDir = join(import.meta.dirname, '../../fixtures/golden/actual-content/input');
16
+ const files = await readVaultFiles(fixturesDir);
17
+ // Should have files in nested paths like "modules/intro.md", "Lenses/..."
18
+ const paths = Array.from(files.keys());
19
+ expect(paths.some(p => p.includes('/'))).toBe(true);
20
+ // Paths should NOT start with leading slash
21
+ expect(paths.every(p => !p.startsWith('/'))).toBe(true);
22
+ });
23
+ it('returns empty map for directory with no .md files', async () => {
24
+ const tempDir = mkdtempSync(join(tmpdir(), 'empty-vault-'));
25
+ try {
26
+ const files = await readVaultFiles(tempDir);
27
+ expect(files.size).toBe(0);
28
+ }
29
+ finally {
30
+ rmSync(tempDir, { recursive: true });
31
+ }
32
+ });
33
+ it('throws descriptive error for non-existent directory', async () => {
34
+ await expect(readVaultFiles('/nonexistent/path')).rejects.toThrow(/ENOENT|no such file/i);
35
+ });
36
+ });
37
+ //# sourceMappingURL=read-vault.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-vault.test.js","sourceRoot":"","sources":["../../src/fs/read-vault.test.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAE5B,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,2CAA2C,CAAC,CAAC;QAE3F,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;QAEhD,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,4CAA4C,CAAC,CAAC;QAE5F,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;QAEhD,0EAA0E;QAC1E,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpD,4CAA4C;QAC5C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;QAC5D,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;YAC5C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC5F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,75 @@
1
+ export interface ProcessResult {
2
+ modules: FlattenedModule[];
3
+ courses: Course[];
4
+ errors: ContentError[];
5
+ }
6
+ export interface FlattenedModule {
7
+ slug: string;
8
+ title: string;
9
+ contentId: string | null;
10
+ sections: Section[];
11
+ error?: string;
12
+ warnings?: string[];
13
+ }
14
+ export interface Course {
15
+ slug: string;
16
+ title: string;
17
+ progression: ProgressionItem[];
18
+ error?: string;
19
+ }
20
+ export interface Section {
21
+ type: 'page' | 'lens-video' | 'lens-article';
22
+ meta: SectionMeta;
23
+ segments: Segment[];
24
+ optional?: boolean;
25
+ contentId: string | null;
26
+ learningOutcomeId: string | null;
27
+ videoId: string | null;
28
+ }
29
+ export interface SectionMeta {
30
+ title?: string;
31
+ author?: string;
32
+ sourceUrl?: string;
33
+ channel?: string;
34
+ }
35
+ export interface ProgressionItem {
36
+ type: 'module' | 'meeting';
37
+ slug?: string;
38
+ number?: number;
39
+ optional?: boolean;
40
+ }
41
+ export interface ContentError {
42
+ file: string;
43
+ line?: number;
44
+ message: string;
45
+ suggestion?: string;
46
+ severity: 'error' | 'warning';
47
+ }
48
+ export interface TextSegment {
49
+ type: 'text';
50
+ content: string;
51
+ optional?: boolean;
52
+ }
53
+ export interface ChatSegment {
54
+ type: 'chat';
55
+ instructions?: string;
56
+ hidePreviousContentFromUser?: boolean;
57
+ hidePreviousContentFromTutor?: boolean;
58
+ optional?: boolean;
59
+ }
60
+ export interface ArticleExcerptSegment {
61
+ type: 'article-excerpt';
62
+ content: string;
63
+ collapsed_before?: string;
64
+ collapsed_after?: string;
65
+ optional?: boolean;
66
+ }
67
+ export interface VideoExcerptSegment {
68
+ type: 'video-excerpt';
69
+ from: number;
70
+ to: number | null;
71
+ transcript: string;
72
+ optional?: boolean;
73
+ }
74
+ export type Segment = TextSegment | ChatSegment | ArticleExcerptSegment | VideoExcerptSegment;
75
+ export declare function processContent(files: Map<string, string>): ProcessResult;
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ import { flattenModule } from './flattener/index.js';
2
+ import { parseCourse } from './parser/course.js';
3
+ import { parseLearningOutcome } from './parser/learning-outcome.js';
4
+ import { parseLens } from './parser/lens.js';
5
+ import { parseWikilink, resolveWikilinkPath, findFileWithExtension } from './parser/wikilink.js';
6
+ import { validateUuids } from './validator/uuid.js';
7
+ import { extractArticleExcerpt } from './bundler/article.js';
8
+ import { extractVideoExcerpt } from './bundler/video.js';
9
+ /**
10
+ * Validate lens excerpts by checking if source files exist and anchors/timestamps are valid.
11
+ */
12
+ function validateLensExcerpts(lens, lensPath, files) {
13
+ const errors = [];
14
+ for (const section of lens.sections) {
15
+ // Skip sections without source (e.g., Text sections)
16
+ if (!section.source)
17
+ continue;
18
+ // Resolve the source wikilink to get the actual file path
19
+ const wikilink = parseWikilink(section.source);
20
+ if (!wikilink)
21
+ continue;
22
+ const resolvedPath = resolveWikilinkPath(wikilink.path, lensPath);
23
+ const actualPath = findFileWithExtension(resolvedPath, files);
24
+ if (!actualPath) {
25
+ errors.push({
26
+ file: lensPath,
27
+ line: section.line,
28
+ message: `Source file not found: ${resolvedPath}`,
29
+ suggestion: 'Check that the file exists and the path is correct',
30
+ severity: 'error',
31
+ });
32
+ continue;
33
+ }
34
+ const sourceContent = files.get(actualPath);
35
+ // Validate article excerpts
36
+ if (section.type === 'article' || section.type === 'lens-article') {
37
+ for (const segment of section.segments) {
38
+ if (segment.type === 'article-excerpt') {
39
+ const result = extractArticleExcerpt(sourceContent, segment.fromAnchor, segment.toAnchor, actualPath);
40
+ if (result.error) {
41
+ errors.push({ ...result.error, file: lensPath });
42
+ }
43
+ }
44
+ }
45
+ }
46
+ // Validate video excerpts
47
+ if (section.type === 'video' || section.type === 'lens-video') {
48
+ // Look for corresponding .timestamps.json file
49
+ const timestampsPath = actualPath.replace(/\.md$/, '.timestamps.json');
50
+ let timestamps;
51
+ if (files.has(timestampsPath)) {
52
+ try {
53
+ timestamps = JSON.parse(files.get(timestampsPath));
54
+ }
55
+ catch {
56
+ // JSON parse error - will fall back to inline timestamps
57
+ }
58
+ }
59
+ for (const segment of section.segments) {
60
+ if (segment.type === 'video-excerpt') {
61
+ const result = extractVideoExcerpt(sourceContent, segment.fromTimeStr, segment.toTimeStr, actualPath, timestamps);
62
+ if (result.error) {
63
+ errors.push({ ...result.error, file: lensPath });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return errors;
70
+ }
71
+ export function processContent(files) {
72
+ const modules = [];
73
+ const courses = [];
74
+ const errors = [];
75
+ const uuidEntries = [];
76
+ // Identify file types by path
77
+ for (const [path, content] of files.entries()) {
78
+ if (path.startsWith('modules/')) {
79
+ const result = flattenModule(path, files);
80
+ if (result.module) {
81
+ modules.push(result.module);
82
+ // Collect module contentId for UUID validation
83
+ if (result.module.contentId) {
84
+ uuidEntries.push({
85
+ uuid: result.module.contentId,
86
+ file: path,
87
+ field: 'contentId',
88
+ });
89
+ }
90
+ }
91
+ errors.push(...result.errors);
92
+ }
93
+ else if (path.startsWith('courses/')) {
94
+ const result = parseCourse(content, path);
95
+ if (result.course) {
96
+ courses.push(result.course);
97
+ }
98
+ errors.push(...result.errors);
99
+ }
100
+ else if (path.startsWith('Learning Outcomes/') || path.includes('/Learning Outcomes/')) {
101
+ // Fully validate Learning Outcome (structure, fields, wikilink syntax)
102
+ const result = parseLearningOutcome(content, path);
103
+ errors.push(...result.errors);
104
+ // Collect id for UUID validation
105
+ if (result.learningOutcome?.id) {
106
+ uuidEntries.push({
107
+ uuid: result.learningOutcome.id,
108
+ file: path,
109
+ field: 'id',
110
+ });
111
+ }
112
+ }
113
+ else if (path.startsWith('Lenses/') || path.includes('/Lenses/')) {
114
+ // Fully validate Lens (structure, segments, fields)
115
+ const result = parseLens(content, path);
116
+ errors.push(...result.errors);
117
+ // Validate excerpts (source files exist, anchors/timestamps valid)
118
+ if (result.lens) {
119
+ const excerptErrors = validateLensExcerpts(result.lens, path, files);
120
+ errors.push(...excerptErrors);
121
+ }
122
+ // Collect id for UUID validation
123
+ if (result.lens?.id) {
124
+ uuidEntries.push({
125
+ uuid: result.lens.id,
126
+ file: path,
127
+ field: 'id',
128
+ });
129
+ }
130
+ }
131
+ }
132
+ // Validate all collected UUIDs
133
+ const uuidValidation = validateUuids(uuidEntries);
134
+ errors.push(...uuidValidation.errors);
135
+ return { modules, courses, errors };
136
+ }
137
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ContentError, Course } from '../index.js';
2
+ export declare const COURSE_SECTION_TYPES: Set<string>;
3
+ export interface CourseParseResult {
4
+ course: Course | null;
5
+ errors: ContentError[];
6
+ }
7
+ /**
8
+ * Parses a course file and extracts its structure.
9
+ *
10
+ * Course files use H1 sections:
11
+ * - `# Module: [[../modules/name.md|Display]]` - references a module
12
+ * - `# Meeting: 1` - marks a meeting point
13
+ *
14
+ * The module slug is extracted from the filename in the wikilink path.
15
+ * Reference validation (checking if modules exist) happens in processContent.
16
+ */
17
+ export declare function parseCourse(content: string, file: string): CourseParseResult;
@@ -0,0 +1,145 @@
1
+ import { parseFrontmatter } from './frontmatter.js';
2
+ import { parseSections } from './sections.js';
3
+ import { parseWikilink } from './wikilink.js';
4
+ import { basename } from 'path';
5
+ // Valid section types for course files
6
+ export const COURSE_SECTION_TYPES = new Set(['module', 'meeting']);
7
+ /**
8
+ * Extracts the slug from a module path (filename without extension)
9
+ * e.g., '../modules/intro.md' -> 'intro'
10
+ */
11
+ function extractSlugFromPath(path) {
12
+ const filename = basename(path);
13
+ return filename.replace(/\.md$/, '');
14
+ }
15
+ /**
16
+ * Parses a Module section title to extract the wikilink and module slug
17
+ * Title format: [[../modules/intro.md|Display Text]]
18
+ */
19
+ function parseModuleSection(section, file) {
20
+ // The title should be a wikilink
21
+ const wikilink = parseWikilink(section.title);
22
+ if (!wikilink) {
23
+ return {
24
+ error: {
25
+ file,
26
+ line: section.line,
27
+ message: `Module section must have a wikilink in title, got: "${section.title}"`,
28
+ suggestion: 'Use format: # Module: [[../modules/module-name.md|Display Text]]',
29
+ severity: 'error',
30
+ },
31
+ };
32
+ }
33
+ const slug = extractSlugFromPath(wikilink.path);
34
+ const optional = section.fields.optional === 'true';
35
+ return { slug, optional };
36
+ }
37
+ /**
38
+ * Parses a Meeting section title to extract the meeting number
39
+ * Title format: 1, 2, 3, etc.
40
+ */
41
+ function parseMeetingSection(section, file) {
42
+ const meetingNumber = parseInt(section.title, 10);
43
+ if (isNaN(meetingNumber)) {
44
+ return {
45
+ error: {
46
+ file,
47
+ line: section.line,
48
+ message: `Meeting section must have a number in title, got: "${section.title}"`,
49
+ suggestion: 'Use format: # Meeting: 1',
50
+ severity: 'error',
51
+ },
52
+ };
53
+ }
54
+ return { number: meetingNumber };
55
+ }
56
+ /**
57
+ * Parses a course file and extracts its structure.
58
+ *
59
+ * Course files use H1 sections:
60
+ * - `# Module: [[../modules/name.md|Display]]` - references a module
61
+ * - `# Meeting: 1` - marks a meeting point
62
+ *
63
+ * The module slug is extracted from the filename in the wikilink path.
64
+ * Reference validation (checking if modules exist) happens in processContent.
65
+ */
66
+ export function parseCourse(content, file) {
67
+ const errors = [];
68
+ // Parse frontmatter
69
+ const frontmatterResult = parseFrontmatter(content, file);
70
+ if (frontmatterResult.error) {
71
+ errors.push(frontmatterResult.error);
72
+ return { course: null, errors };
73
+ }
74
+ const { frontmatter, body, bodyStartLine } = frontmatterResult;
75
+ // Validate required frontmatter fields
76
+ if (!frontmatter.slug) {
77
+ errors.push({
78
+ file,
79
+ line: 2,
80
+ message: 'Missing required field: slug',
81
+ suggestion: "Add 'slug: your-course-slug' to frontmatter",
82
+ severity: 'error',
83
+ });
84
+ }
85
+ if (!frontmatter.title) {
86
+ errors.push({
87
+ file,
88
+ line: 2,
89
+ message: 'Missing required field: title',
90
+ suggestion: "Add 'title: Your Course Title' to frontmatter",
91
+ severity: 'error',
92
+ });
93
+ }
94
+ if (errors.length > 0) {
95
+ return { course: null, errors };
96
+ }
97
+ // Parse sections (H1 headers for course files)
98
+ const sectionsResult = parseSections(body, 1, COURSE_SECTION_TYPES, file);
99
+ // Adjust line numbers to account for frontmatter
100
+ for (const error of sectionsResult.errors) {
101
+ if (error.line) {
102
+ error.line += bodyStartLine - 1;
103
+ }
104
+ }
105
+ errors.push(...sectionsResult.errors);
106
+ for (const section of sectionsResult.sections) {
107
+ section.line += bodyStartLine - 1;
108
+ }
109
+ // Build progression array from parsed sections
110
+ const progression = [];
111
+ for (const section of sectionsResult.sections) {
112
+ if (section.type === 'module') {
113
+ const result = parseModuleSection(section, file);
114
+ if ('error' in result) {
115
+ errors.push(result.error);
116
+ }
117
+ else {
118
+ progression.push({
119
+ type: 'module',
120
+ slug: result.slug,
121
+ optional: result.optional,
122
+ });
123
+ }
124
+ }
125
+ else if (section.type === 'meeting') {
126
+ const result = parseMeetingSection(section, file);
127
+ if ('error' in result) {
128
+ errors.push(result.error);
129
+ }
130
+ else {
131
+ progression.push({
132
+ type: 'meeting',
133
+ number: result.number,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ const course = {
139
+ slug: frontmatter.slug,
140
+ title: frontmatter.title,
141
+ progression,
142
+ };
143
+ return { course, errors };
144
+ }
145
+ //# sourceMappingURL=course.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"course.js","sourceRoot":"","sources":["../../src/parser/course.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ // src/parser/course.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { parseCourse } from './course.js';
4
+ describe('parseCourse', () => {
5
+ it('parses course with module references', () => {
6
+ const content = `---
7
+ slug: intro-course
8
+ title: Introduction to AI Safety
9
+ ---
10
+
11
+ # Module: [[../modules/intro.md|Introduction]]
12
+
13
+ # Module: [[../modules/advanced.md|Advanced Topics]]
14
+ optional:: true
15
+
16
+ # Meeting: 1
17
+
18
+ # Module: [[../modules/conclusion.md|Conclusion]]
19
+ `;
20
+ const result = parseCourse(content, 'courses/intro.md');
21
+ expect(result.course?.slug).toBe('intro-course');
22
+ expect(result.course?.title).toBe('Introduction to AI Safety');
23
+ expect(result.course?.progression).toHaveLength(4);
24
+ expect(result.course?.progression[0].type).toBe('module');
25
+ expect(result.course?.progression[0].slug).toBe('intro');
26
+ expect(result.course?.progression[0].optional).toBe(false);
27
+ expect(result.course?.progression[1].type).toBe('module');
28
+ expect(result.course?.progression[1].slug).toBe('advanced');
29
+ expect(result.course?.progression[1].optional).toBe(true);
30
+ expect(result.course?.progression[2].type).toBe('meeting');
31
+ expect(result.course?.progression[2].number).toBe(1);
32
+ expect(result.course?.progression[3].type).toBe('module');
33
+ expect(result.course?.progression[3].slug).toBe('conclusion');
34
+ expect(result.errors).toHaveLength(0);
35
+ });
36
+ it('validates module references exist', () => {
37
+ const content = `---
38
+ slug: broken-course
39
+ title: Broken Course
40
+ ---
41
+
42
+ # Module: [[../modules/nonexistent.md|Missing]]
43
+ `;
44
+ // Note: Reference validation happens in processContent, not parseCourse
45
+ // parseCourse only parses the structure, it does not validate file existence
46
+ const result = parseCourse(content, 'courses/broken.md');
47
+ expect(result.course).toBeDefined();
48
+ expect(result.course?.slug).toBe('broken-course');
49
+ expect(result.course?.progression).toHaveLength(1);
50
+ expect(result.course?.progression[0].type).toBe('module');
51
+ expect(result.course?.progression[0].slug).toBe('nonexistent');
52
+ // Errors about missing modules are added during processContent, not parseCourse
53
+ });
54
+ it('handles missing frontmatter', () => {
55
+ const content = `# Module: [[../modules/intro.md|Introduction]]`;
56
+ const result = parseCourse(content, 'courses/bad.md');
57
+ expect(result.course).toBeNull();
58
+ expect(result.errors.length).toBeGreaterThan(0);
59
+ expect(result.errors[0].message).toContain('frontmatter');
60
+ });
61
+ it('requires slug in frontmatter', () => {
62
+ const content = `---
63
+ title: Course Without Slug
64
+ ---
65
+
66
+ # Module: [[../modules/intro.md|Introduction]]
67
+ `;
68
+ const result = parseCourse(content, 'courses/no-slug.md');
69
+ expect(result.course).toBeNull();
70
+ expect(result.errors.some(e => e.message.includes('slug'))).toBe(true);
71
+ });
72
+ it('requires title in frontmatter', () => {
73
+ const content = `---
74
+ slug: no-title-course
75
+ ---
76
+
77
+ # Module: [[../modules/intro.md|Introduction]]
78
+ `;
79
+ const result = parseCourse(content, 'courses/no-title.md');
80
+ expect(result.course).toBeNull();
81
+ expect(result.errors.some(e => e.message.includes('title'))).toBe(true);
82
+ });
83
+ it('reports error for unknown section types', () => {
84
+ const content = `---
85
+ slug: test-course
86
+ title: Test Course
87
+ ---
88
+
89
+ # Unknown: Bad Section
90
+ `;
91
+ const result = parseCourse(content, 'courses/unknown.md');
92
+ expect(result.errors.some(e => e.message.includes('Unknown section type'))).toBe(true);
93
+ });
94
+ it('reports error for Module section without wikilink', () => {
95
+ const content = `---
96
+ slug: test-course
97
+ title: Test Course
98
+ ---
99
+
100
+ # Module: No Wikilink Here
101
+ `;
102
+ const result = parseCourse(content, 'courses/no-link.md');
103
+ expect(result.errors.some(e => e.message.includes('wikilink'))).toBe(true);
104
+ });
105
+ it('reports error for Meeting section without number', () => {
106
+ const content = `---
107
+ slug: test-course
108
+ title: Test Course
109
+ ---
110
+
111
+ # Meeting: not a number
112
+ `;
113
+ const result = parseCourse(content, 'courses/bad-meeting.md');
114
+ expect(result.errors.some(e => e.message.includes('number'))).toBe(true);
115
+ });
116
+ });
117
+ //# sourceMappingURL=course.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"course.test.js","sourceRoot":"","sources":["../../src/parser/course.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ContentError } from '../index.js';
2
+ export interface FrontmatterResult {
3
+ frontmatter: Record<string, unknown>;
4
+ body: string;
5
+ bodyStartLine: number;
6
+ error?: ContentError;
7
+ }
8
+ export declare function parseFrontmatter(content: string, file?: string): FrontmatterResult;
@@ -0,0 +1,58 @@
1
+ // src/parser/frontmatter.ts
2
+ import { parse as parseYaml } from 'yaml';
3
+ // Matches: ---\n<yaml>\n---\n<body>
4
+ const FRONTMATTER_PATTERN = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
5
+ export function parseFrontmatter(content, file = '') {
6
+ const match = content.match(FRONTMATTER_PATTERN);
7
+ if (!match) {
8
+ // Check if it starts with --- but doesn't close
9
+ if (content.startsWith('---\n') && !content.includes('\n---\n')) {
10
+ return {
11
+ frontmatter: {},
12
+ body: content,
13
+ bodyStartLine: 1,
14
+ error: {
15
+ file,
16
+ line: 1,
17
+ message: 'Unclosed frontmatter - missing closing ---',
18
+ suggestion: 'Add --- on its own line after frontmatter fields',
19
+ severity: 'error',
20
+ },
21
+ };
22
+ }
23
+ return {
24
+ frontmatter: {},
25
+ body: content,
26
+ bodyStartLine: 1,
27
+ error: {
28
+ file,
29
+ line: 1,
30
+ message: 'Missing frontmatter',
31
+ suggestion: "Add YAML frontmatter with 'slug' and 'title' fields",
32
+ severity: 'error',
33
+ },
34
+ };
35
+ }
36
+ const yamlContent = match[1];
37
+ const body = match[2];
38
+ const bodyStartLine = yamlContent.split('\n').length + 3; // 1 for opening ---, N for yaml, 1 for closing ---
39
+ try {
40
+ const frontmatter = parseYaml(yamlContent) ?? {};
41
+ return { frontmatter, body, bodyStartLine };
42
+ }
43
+ catch (e) {
44
+ return {
45
+ frontmatter: {},
46
+ body,
47
+ bodyStartLine,
48
+ error: {
49
+ file,
50
+ line: 2,
51
+ message: `Invalid YAML: ${e instanceof Error ? e.message : String(e)}`,
52
+ suggestion: 'Check YAML syntax - colons, indentation, quoting',
53
+ severity: 'error',
54
+ },
55
+ };
56
+ }
57
+ }
58
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/parser/frontmatter.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAU1C,oCAAoC;AACpC,MAAM,mBAAmB,GAAG,oCAAoC,CAAC;AAEjE,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAAe,EAAE;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAEjD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,gDAAgD;QAChD,IAAI,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAChE,OAAO;gBACL,WAAW,EAAE,EAAE;gBACf,IAAI,EAAE,OAAO;gBACb,aAAa,EAAE,CAAC;gBAChB,KAAK,EAAE;oBACL,IAAI;oBACJ,IAAI,EAAE,CAAC;oBACP,OAAO,EAAE,4CAA4C;oBACrD,UAAU,EAAE,kDAAkD;oBAC9D,QAAQ,EAAE,OAAO;iBAClB;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,WAAW,EAAE,EAAE;YACf,IAAI,EAAE,OAAO;YACb,aAAa,EAAE,CAAC;YAChB,KAAK,EAAE;gBACL,IAAI;gBACJ,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,qBAAqB;gBAC9B,UAAU,EAAE,qDAAqD;gBACjE,QAAQ,EAAE,OAAO;aAClB;SACF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACtB,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,mDAAmD;IAE7G,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QACjD,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IAC9C,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO;YACL,WAAW,EAAE,EAAE;YACf,IAAI;YACJ,aAAa;YACb,KAAK,EAAE;gBACL,IAAI;gBACJ,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,iBAAiB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;gBACtE,UAAU,EAAE,kDAAkD;gBAC9D,QAAQ,EAAE,OAAO;aAClB;SACF,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ export {};