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,54 @@
1
+ // src/parser/frontmatter.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { parseFrontmatter } from './frontmatter';
4
+ describe('parseFrontmatter', () => {
5
+ it('extracts frontmatter fields', () => {
6
+ const content = `---
7
+ slug: my-module
8
+ title: My Module Title
9
+ ---
10
+
11
+ # Content here`;
12
+ const result = parseFrontmatter(content);
13
+ expect(result.frontmatter).toEqual({
14
+ slug: 'my-module',
15
+ title: 'My Module Title',
16
+ });
17
+ expect(result.body).toBe('\n# Content here');
18
+ expect(result.bodyStartLine).toBe(5);
19
+ });
20
+ it('returns error for missing frontmatter', () => {
21
+ const content = '# No frontmatter here';
22
+ const result = parseFrontmatter(content);
23
+ expect(result.error).toBeDefined();
24
+ expect(result.error?.message).toBe('Missing frontmatter');
25
+ expect(result.error?.line).toBe(1);
26
+ });
27
+ it('returns error for unclosed frontmatter', () => {
28
+ const content = `---
29
+ slug: broken
30
+ title: Never Closed`;
31
+ const result = parseFrontmatter(content);
32
+ expect(result.error).toBeDefined();
33
+ expect(result.error?.message).toContain('Unclosed frontmatter');
34
+ });
35
+ it('includes file path in error when provided', () => {
36
+ const content = '# No frontmatter here';
37
+ const result = parseFrontmatter(content, 'modules/test.md');
38
+ expect(result.error).toBeDefined();
39
+ expect(result.error?.file).toBe('modules/test.md');
40
+ });
41
+ it('handles invalid YAML syntax', () => {
42
+ const content = `---
43
+ slug: test
44
+ title: [invalid yaml
45
+ ---
46
+
47
+ # Content`;
48
+ const result = parseFrontmatter(content);
49
+ expect(result.error).toBeDefined();
50
+ expect(result.error?.message).toContain('Invalid YAML');
51
+ expect(result.error?.line).toBe(2);
52
+ });
53
+ });
54
+ //# sourceMappingURL=frontmatter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.test.js","sourceRoot":"","sources":["../../src/parser/frontmatter.test.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,OAAO,GAAG;;;;;eAKL,CAAC;QAEZ,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAEzC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC;YACjC,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,iBAAiB;SACzB,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,OAAO,GAAG,uBAAuB,CAAC;QAExC,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAEzC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,OAAO,GAAG;;oBAEA,CAAC;QAEjB,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAEzC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,OAAO,GAAG,uBAAuB,CAAC;QAExC,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QAE5D,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,OAAO,GAAG;;;;;UAKV,CAAC;QAEP,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAEzC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { ContentError } from '../index.js';
2
+ export interface ParsedLensRef {
3
+ source: string;
4
+ resolvedPath: string;
5
+ optional: boolean;
6
+ }
7
+ export interface ParsedTestRef {
8
+ source: string;
9
+ resolvedPath: string;
10
+ }
11
+ export interface ParsedLearningOutcome {
12
+ id: string;
13
+ lenses: ParsedLensRef[];
14
+ test?: ParsedTestRef;
15
+ discussion?: string;
16
+ }
17
+ export interface LearningOutcomeParseResult {
18
+ learningOutcome: ParsedLearningOutcome | null;
19
+ errors: ContentError[];
20
+ }
21
+ export declare function parseLearningOutcome(content: string, file: string): LearningOutcomeParseResult;
@@ -0,0 +1,116 @@
1
+ import { parseFrontmatter } from './frontmatter.js';
2
+ import { parseSections, LO_SECTION_TYPES } from './sections.js';
3
+ import { parseWikilink, resolveWikilinkPath } from './wikilink.js';
4
+ export function parseLearningOutcome(content, file) {
5
+ const errors = [];
6
+ // Step 1: Parse frontmatter and validate id field
7
+ const frontmatterResult = parseFrontmatter(content, file);
8
+ if (frontmatterResult.error) {
9
+ errors.push(frontmatterResult.error);
10
+ return { learningOutcome: null, errors };
11
+ }
12
+ const { frontmatter, body, bodyStartLine } = frontmatterResult;
13
+ // Validate required id field
14
+ if (!frontmatter.id) {
15
+ errors.push({
16
+ file,
17
+ line: 2,
18
+ message: 'Missing required field: id',
19
+ suggestion: "Add 'id: <uuid>' to frontmatter",
20
+ severity: 'error',
21
+ });
22
+ return { learningOutcome: null, errors };
23
+ }
24
+ // Step 2: Parse sections with H2 level and LO_SECTION_TYPES ('lens', 'test')
25
+ const sectionsResult = parseSections(body, 2, LO_SECTION_TYPES, file);
26
+ // Adjust line numbers to account for frontmatter
27
+ for (const error of sectionsResult.errors) {
28
+ if (error.line) {
29
+ error.line += bodyStartLine - 1;
30
+ }
31
+ }
32
+ errors.push(...sectionsResult.errors);
33
+ for (const section of sectionsResult.sections) {
34
+ section.line += bodyStartLine - 1;
35
+ }
36
+ // Step 3: Extract lens refs with source field and optional flag
37
+ const lenses = [];
38
+ let testRef;
39
+ for (const section of sectionsResult.sections) {
40
+ if (section.type === 'lens') {
41
+ const source = section.fields.source;
42
+ if (!source) {
43
+ errors.push({
44
+ file,
45
+ line: section.line,
46
+ message: 'Lens section missing source:: field',
47
+ suggestion: "Add 'source:: [[../Lenses/filename.md|Display]]' to the lens section",
48
+ severity: 'error',
49
+ });
50
+ continue;
51
+ }
52
+ // Parse wikilink and resolve path
53
+ const wikilink = parseWikilink(source);
54
+ if (!wikilink) {
55
+ errors.push({
56
+ file,
57
+ line: section.line,
58
+ message: `Invalid wikilink format in source:: field: ${source}`,
59
+ suggestion: 'Use format [[../Lenses/filename.md|Display Text]]',
60
+ severity: 'error',
61
+ });
62
+ continue;
63
+ }
64
+ const resolvedPath = resolveWikilinkPath(wikilink.path, file);
65
+ const optional = section.fields.optional === 'true';
66
+ lenses.push({
67
+ source,
68
+ resolvedPath,
69
+ optional,
70
+ });
71
+ }
72
+ else if (section.type === 'test') {
73
+ const source = section.fields.source;
74
+ // source:: is optional for test sections (tests not fully implemented yet)
75
+ if (!source) {
76
+ continue;
77
+ }
78
+ // Parse wikilink and resolve path
79
+ const wikilink = parseWikilink(source);
80
+ if (!wikilink) {
81
+ errors.push({
82
+ file,
83
+ line: section.line,
84
+ message: `Invalid wikilink format in source:: field: ${source}`,
85
+ suggestion: 'Use format [[../Tests/filename.md|Display Text]]',
86
+ severity: 'error',
87
+ });
88
+ continue;
89
+ }
90
+ const resolvedPath = resolveWikilinkPath(wikilink.path, file);
91
+ testRef = {
92
+ source,
93
+ resolvedPath,
94
+ };
95
+ }
96
+ }
97
+ // Step 4: Validate at least one lens exists
98
+ if (lenses.length === 0) {
99
+ errors.push({
100
+ file,
101
+ line: bodyStartLine,
102
+ message: 'Learning Outcome must have at least one ## Lens: section',
103
+ suggestion: "Add a '## Lens: <title>' section with a source:: field",
104
+ severity: 'error',
105
+ });
106
+ }
107
+ // Return result even if there are errors (partial success)
108
+ const learningOutcome = {
109
+ id: frontmatter.id,
110
+ lenses,
111
+ test: testRef,
112
+ discussion: frontmatter.discussion,
113
+ };
114
+ return { learningOutcome, errors };
115
+ }
116
+ //# sourceMappingURL=learning-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ // src/parser/learning-outcome.test.ts
2
+ import { describe, it, expect } from 'vitest';
3
+ import { parseLearningOutcome } from './learning-outcome.js';
4
+ describe('parseLearningOutcome', () => {
5
+ it('parses LO with multiple lenses', () => {
6
+ const content = `---
7
+ id: 550e8400-e29b-41d4-a716-446655440001
8
+ ---
9
+
10
+ ## Lens: First Lens
11
+ source:: [[../Lenses/lens1.md|Lens 1]]
12
+
13
+ ## Lens: Second Lens
14
+ source:: [[../Lenses/lens2.md|Lens 2]]
15
+ optional:: true
16
+
17
+ ## Test: Knowledge Check
18
+ source:: [[../Tests/test1.md|Test]]
19
+ `;
20
+ const result = parseLearningOutcome(content, 'Learning Outcomes/lo1.md');
21
+ expect(result.learningOutcome?.id).toBe('550e8400-e29b-41d4-a716-446655440001');
22
+ expect(result.learningOutcome?.lenses).toHaveLength(2);
23
+ expect(result.learningOutcome?.lenses[0].source).toBe('[[../Lenses/lens1.md|Lens 1]]');
24
+ expect(result.learningOutcome?.lenses[0].resolvedPath).toBe('Lenses/lens1.md');
25
+ expect(result.learningOutcome?.lenses[0].optional).toBe(false);
26
+ expect(result.learningOutcome?.lenses[1].source).toBe('[[../Lenses/lens2.md|Lens 2]]');
27
+ expect(result.learningOutcome?.lenses[1].resolvedPath).toBe('Lenses/lens2.md');
28
+ expect(result.learningOutcome?.lenses[1].optional).toBe(true);
29
+ expect(result.learningOutcome?.test?.source).toContain('test1.md');
30
+ expect(result.learningOutcome?.test?.resolvedPath).toBe('Tests/test1.md');
31
+ expect(result.errors).toHaveLength(0);
32
+ });
33
+ it('requires id in frontmatter', () => {
34
+ const content = `---
35
+ title: Missing ID
36
+ ---
37
+
38
+ ## Lens: Test
39
+ source:: [[../Lenses/lens1.md|Lens]]
40
+ `;
41
+ const result = parseLearningOutcome(content, 'Learning Outcomes/bad.md');
42
+ expect(result.errors).toHaveLength(1);
43
+ expect(result.errors[0].message).toContain('id');
44
+ });
45
+ it('requires at least one lens', () => {
46
+ const content = `---
47
+ id: 550e8400-e29b-41d4-a716-446655440001
48
+ ---
49
+
50
+ No lenses here.
51
+ `;
52
+ const result = parseLearningOutcome(content, 'Learning Outcomes/empty.md');
53
+ expect(result.errors.some(e => e.message.includes('Lens'))).toBe(true);
54
+ });
55
+ it('allows test section without source:: field (tests not implemented yet)', () => {
56
+ const content = `---
57
+ id: 550e8400-e29b-41d4-a716-446655440001
58
+ ---
59
+
60
+ ## Lens: Main Content
61
+ source:: [[../Lenses/lens1.md|Lens 1]]
62
+
63
+ ## Test: Knowledge Check
64
+ `;
65
+ const result = parseLearningOutcome(content, 'Learning Outcomes/lo1.md');
66
+ // Should NOT produce an error for missing source:: in test section
67
+ expect(result.errors).toHaveLength(0);
68
+ // The LO should be parsed successfully
69
+ expect(result.learningOutcome?.id).toBe('550e8400-e29b-41d4-a716-446655440001');
70
+ expect(result.learningOutcome?.lenses).toHaveLength(1);
71
+ // Test should be undefined since no source was provided
72
+ expect(result.learningOutcome?.test).toBeUndefined();
73
+ });
74
+ });
75
+ //# sourceMappingURL=learning-outcome.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learning-outcome.test.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { ContentError } from '../index.js';
2
+ export interface ParsedTextSegment {
3
+ type: 'text';
4
+ content: string;
5
+ optional?: boolean;
6
+ }
7
+ export interface ParsedChatSegment {
8
+ type: 'chat';
9
+ title?: string;
10
+ instructions?: string;
11
+ hidePreviousContentFromUser?: boolean;
12
+ hidePreviousContentFromTutor?: boolean;
13
+ optional?: boolean;
14
+ }
15
+ export interface ParsedArticleExcerptSegment {
16
+ type: 'article-excerpt';
17
+ fromAnchor?: string;
18
+ toAnchor?: string;
19
+ optional?: boolean;
20
+ }
21
+ export interface ParsedVideoExcerptSegment {
22
+ type: 'video-excerpt';
23
+ fromTimeStr: string;
24
+ toTimeStr: string;
25
+ optional?: boolean;
26
+ }
27
+ export type ParsedLensSegment = ParsedTextSegment | ParsedChatSegment | ParsedArticleExcerptSegment | ParsedVideoExcerptSegment;
28
+ export interface ParsedLensSection {
29
+ type: string;
30
+ title: string;
31
+ source?: string;
32
+ resolvedPath?: string;
33
+ segments: ParsedLensSegment[];
34
+ line: number;
35
+ }
36
+ export interface ParsedLens {
37
+ id: string;
38
+ sections: ParsedLensSection[];
39
+ }
40
+ export interface LensParseResult {
41
+ lens: ParsedLens | null;
42
+ errors: ContentError[];
43
+ }
44
+ /**
45
+ * Parse a lens file into structured lens data.
46
+ *
47
+ * Lens files use:
48
+ * - H3 (`###`) for sections: Text, Article, Video
49
+ * - H4 (`####`) for segments: Text, Chat, Article-excerpt, Video-excerpt
50
+ */
51
+ export declare function parseLens(content: string, file: string): LensParseResult;
@@ -0,0 +1,328 @@
1
+ import { parseFrontmatter } from './frontmatter.js';
2
+ import { parseSections, LENS_SECTION_TYPES, LENS_OUTPUT_TYPE } from './sections.js';
3
+ import { validateSegmentFields } from '../validator/segment-fields.js';
4
+ import { validateFieldValues } from '../validator/field-values.js';
5
+ // Valid segment types for lens H4 headers
6
+ const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt']);
7
+ // H4 segment header pattern: #### <type> or #### <type>: <title>
8
+ const SEGMENT_HEADER_PATTERN = /^####\s+(\S+)(?::\s*(.*))?$/i;
9
+ // Field pattern: fieldname:: value
10
+ const FIELD_PATTERN = /^(\w+)::\s*(.*)$/;
11
+ /**
12
+ * Parse H4 segments from a section body.
13
+ * Segments are defined by `#### <type>` headers within a section.
14
+ */
15
+ function parseSegments(sectionBody, bodyStartLine, file) {
16
+ const lines = sectionBody.split('\n');
17
+ const segments = [];
18
+ const errors = [];
19
+ let currentSegment = null;
20
+ let currentFieldLines = [];
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ const lineNum = bodyStartLine + i;
24
+ const headerMatch = line.match(SEGMENT_HEADER_PATTERN);
25
+ if (headerMatch) {
26
+ // Save previous segment
27
+ if (currentSegment) {
28
+ parseFieldsIntoSegment(currentSegment, currentFieldLines);
29
+ segments.push(currentSegment);
30
+ }
31
+ const rawType = headerMatch[1].trim();
32
+ const normalizedType = rawType.toLowerCase();
33
+ const title = headerMatch[2]?.trim() || undefined;
34
+ if (!LENS_SEGMENT_TYPES.has(normalizedType)) {
35
+ errors.push({
36
+ file,
37
+ line: lineNum,
38
+ message: `Unknown segment type: ${rawType}`,
39
+ suggestion: `Valid types: ${[...LENS_SEGMENT_TYPES].join(', ')}`,
40
+ severity: 'error',
41
+ });
42
+ }
43
+ currentSegment = {
44
+ type: normalizedType,
45
+ title,
46
+ fields: {},
47
+ line: lineNum,
48
+ };
49
+ currentFieldLines = [];
50
+ }
51
+ else if (currentSegment) {
52
+ currentFieldLines.push(line);
53
+ }
54
+ }
55
+ // Don't forget last segment
56
+ if (currentSegment) {
57
+ parseFieldsIntoSegment(currentSegment, currentFieldLines);
58
+ segments.push(currentSegment);
59
+ }
60
+ return { segments, errors };
61
+ }
62
+ /**
63
+ * Parse fields from lines into a segment, handling multiline values.
64
+ * A field continues until the next field or the end of the lines.
65
+ */
66
+ function parseFieldsIntoSegment(segment, lines) {
67
+ let currentField = null;
68
+ let currentValue = [];
69
+ for (const line of lines) {
70
+ const match = line.match(FIELD_PATTERN);
71
+ if (match) {
72
+ // Save previous field if any
73
+ if (currentField) {
74
+ segment.fields[currentField] = currentValue.join('\n').trim();
75
+ }
76
+ currentField = match[1];
77
+ const inlineValue = match[2].trim();
78
+ currentValue = inlineValue ? [inlineValue] : [];
79
+ }
80
+ else if (currentField) {
81
+ // Continue multiline value
82
+ currentValue.push(line);
83
+ }
84
+ }
85
+ // Save final field
86
+ if (currentField) {
87
+ segment.fields[currentField] = currentValue.join('\n').trim();
88
+ }
89
+ }
90
+ /**
91
+ * Convert a raw segment to a typed ParsedLensSegment.
92
+ */
93
+ function convertSegment(raw, sectionType, file) {
94
+ const errors = [];
95
+ switch (raw.type) {
96
+ case 'text': {
97
+ const hasContentField = 'content' in raw.fields;
98
+ const content = raw.fields.content;
99
+ if (!hasContentField) {
100
+ // Field completely missing - error
101
+ errors.push({
102
+ file,
103
+ line: raw.line,
104
+ message: 'Text segment missing content:: field',
105
+ suggestion: "Add 'content:: Your text here' to the text segment",
106
+ severity: 'error',
107
+ });
108
+ return { segment: null, errors };
109
+ }
110
+ if (!content || content.trim() === '') {
111
+ // Field present but empty - warning
112
+ errors.push({
113
+ file,
114
+ line: raw.line,
115
+ message: 'Text segment has empty content:: field',
116
+ suggestion: 'Add text content after content::',
117
+ severity: 'warning',
118
+ });
119
+ // Still create the segment, just with empty content
120
+ const segment = {
121
+ type: 'text',
122
+ content: '',
123
+ optional: raw.fields.optional === 'true' ? true : undefined,
124
+ };
125
+ return { segment, errors };
126
+ }
127
+ const segment = {
128
+ type: 'text',
129
+ content,
130
+ optional: raw.fields.optional === 'true' ? true : undefined,
131
+ };
132
+ return { segment, errors };
133
+ }
134
+ case 'chat': {
135
+ const segment = {
136
+ type: 'chat',
137
+ title: raw.title,
138
+ instructions: raw.fields.instructions,
139
+ hidePreviousContentFromUser: raw.fields.hidePreviousContentFromUser === 'true' ? true : undefined,
140
+ hidePreviousContentFromTutor: raw.fields.hidePreviousContentFromTutor === 'true' ? true : undefined,
141
+ optional: raw.fields.optional === 'true' ? true : undefined,
142
+ };
143
+ return { segment, errors };
144
+ }
145
+ case 'article-excerpt': {
146
+ const fromField = raw.fields.from;
147
+ const toField = raw.fields.to;
148
+ // Both from:: and to:: are optional for article-excerpt:
149
+ // - Only from:: → extract from anchor to end of article
150
+ // - Only to:: → extract from start to anchor
151
+ // - Neither → extract entire article
152
+ // Strip quotes from anchor text if present
153
+ const fromAnchor = fromField ? stripQuotes(fromField) : undefined;
154
+ const toAnchor = toField ? stripQuotes(toField) : undefined;
155
+ const segment = {
156
+ type: 'article-excerpt',
157
+ fromAnchor,
158
+ toAnchor,
159
+ optional: raw.fields.optional === 'true' ? true : undefined,
160
+ };
161
+ return { segment, errors };
162
+ }
163
+ case 'video-excerpt': {
164
+ const fromField = raw.fields.from;
165
+ const toField = raw.fields.to;
166
+ // to:: is required, from:: defaults to "0:00"
167
+ if (!toField) {
168
+ errors.push({
169
+ file,
170
+ line: raw.line,
171
+ message: 'Video-excerpt segment missing to:: field',
172
+ suggestion: "Add 'to:: M:SS' or 'to:: H:MM:SS' to the segment",
173
+ severity: 'error',
174
+ });
175
+ return { segment: null, errors };
176
+ }
177
+ const segment = {
178
+ type: 'video-excerpt',
179
+ fromTimeStr: fromField || '0:00', // Default to start of video
180
+ toTimeStr: toField,
181
+ optional: raw.fields.optional === 'true' ? true : undefined,
182
+ };
183
+ return { segment, errors };
184
+ }
185
+ default:
186
+ // Unknown segment type - error already reported during parseSegments
187
+ return { segment: null, errors };
188
+ }
189
+ }
190
+ /**
191
+ * Strip surrounding quotes from a string if present.
192
+ */
193
+ function stripQuotes(s) {
194
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
195
+ return s.slice(1, -1);
196
+ }
197
+ return s;
198
+ }
199
+ /**
200
+ * Check if a segment is empty (has no meaningful fields).
201
+ * Returns a warning if the segment is empty.
202
+ *
203
+ * Note: article-excerpt with no fields is valid (means entire article).
204
+ */
205
+ function checkEmptySegment(raw, file) {
206
+ // A segment is empty if it has no fields at all
207
+ const fieldCount = Object.keys(raw.fields).length;
208
+ // article-excerpt with no fields is valid - means "include entire article"
209
+ if (raw.type === 'article-excerpt' && fieldCount === 0) {
210
+ return null;
211
+ }
212
+ if (fieldCount === 0) {
213
+ return {
214
+ file,
215
+ line: raw.line,
216
+ message: `Empty ${raw.type} segment has no fields`,
217
+ suggestion: `Add required fields to the ${raw.type} segment`,
218
+ severity: 'warning',
219
+ };
220
+ }
221
+ return null;
222
+ }
223
+ /**
224
+ * Parse a lens file into structured lens data.
225
+ *
226
+ * Lens files use:
227
+ * - H3 (`###`) for sections: Text, Article, Video
228
+ * - H4 (`####`) for segments: Text, Chat, Article-excerpt, Video-excerpt
229
+ */
230
+ export function parseLens(content, file) {
231
+ const errors = [];
232
+ // Step 1: Parse frontmatter and validate id field
233
+ const frontmatterResult = parseFrontmatter(content, file);
234
+ if (frontmatterResult.error) {
235
+ errors.push(frontmatterResult.error);
236
+ return { lens: null, errors };
237
+ }
238
+ const { frontmatter, body, bodyStartLine } = frontmatterResult;
239
+ // Validate required id field
240
+ if (!frontmatter.id) {
241
+ errors.push({
242
+ file,
243
+ line: 2,
244
+ message: 'Missing required field: id',
245
+ suggestion: "Add 'id: <uuid>' to frontmatter",
246
+ severity: 'error',
247
+ });
248
+ return { lens: null, errors };
249
+ }
250
+ // Step 2: Parse H3 sections (Text, Article, Video)
251
+ const sectionsResult = parseSections(body, 3, LENS_SECTION_TYPES, file);
252
+ // Adjust line numbers to account for frontmatter
253
+ for (const error of sectionsResult.errors) {
254
+ if (error.line) {
255
+ error.line += bodyStartLine - 1;
256
+ }
257
+ }
258
+ errors.push(...sectionsResult.errors);
259
+ for (const section of sectionsResult.sections) {
260
+ section.line += bodyStartLine - 1;
261
+ }
262
+ // Step 3: Convert raw sections to ParsedLensSections with segments
263
+ const parsedSections = [];
264
+ for (const rawSection of sectionsResult.sections) {
265
+ // Map section type: 'article' -> 'lens-article', 'video' -> 'lens-video'
266
+ const outputType = LENS_OUTPUT_TYPE[rawSection.type] ?? rawSection.type;
267
+ // For article/video sections, source field is required
268
+ const needsSource = outputType === 'lens-article' || outputType === 'lens-video';
269
+ const source = rawSection.fields.source;
270
+ if (needsSource && !source) {
271
+ errors.push({
272
+ file,
273
+ line: rawSection.line,
274
+ message: `${rawSection.rawType} section missing source:: field`,
275
+ suggestion: `Add 'source:: [[../path/to/file.md|Display]]' to the ${rawSection.rawType.toLowerCase()} section`,
276
+ severity: 'error',
277
+ });
278
+ }
279
+ // Parse H4 segments within this section
280
+ const { segments: rawSegments, errors: segmentErrors } = parseSegments(rawSection.body, rawSection.line + 1, // Segments start after the section header
281
+ file);
282
+ errors.push(...segmentErrors);
283
+ // Convert raw segments to typed segments
284
+ const segments = [];
285
+ for (const rawSeg of rawSegments) {
286
+ // Check for empty segments
287
+ const emptyWarning = checkEmptySegment(rawSeg, file);
288
+ if (emptyWarning) {
289
+ errors.push(emptyWarning);
290
+ }
291
+ // Validate that fields are appropriate for this segment type
292
+ const fieldWarnings = validateSegmentFields(rawSeg.type, rawSeg.fields, file, rawSeg.line);
293
+ errors.push(...fieldWarnings);
294
+ // Validate field values (e.g., boolean fields should have 'true' or 'false')
295
+ const valueWarnings = validateFieldValues(rawSeg.fields, file, rawSeg.line);
296
+ errors.push(...valueWarnings);
297
+ const { segment, errors: conversionErrors } = convertSegment(rawSeg, outputType, file);
298
+ errors.push(...conversionErrors);
299
+ if (segment) {
300
+ segments.push(segment);
301
+ }
302
+ }
303
+ // Warn if section has no segments
304
+ if (segments.length === 0) {
305
+ errors.push({
306
+ file,
307
+ line: rawSection.line,
308
+ message: `${rawSection.rawType} section has no segments`,
309
+ suggestion: `Add at least one segment (#### Text, #### Chat, etc.) to the ${rawSection.rawType.toLowerCase()} section`,
310
+ severity: 'warning',
311
+ });
312
+ }
313
+ const parsedSection = {
314
+ type: outputType,
315
+ title: rawSection.title,
316
+ source: source,
317
+ segments,
318
+ line: rawSection.line,
319
+ };
320
+ parsedSections.push(parsedSection);
321
+ }
322
+ const lens = {
323
+ id: frontmatter.id,
324
+ sections: parsedSections,
325
+ };
326
+ return { lens, errors };
327
+ }
328
+ //# sourceMappingURL=lens.js.map