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.
- package/dist/bundler/article.d.ts +41 -0
- package/dist/bundler/article.js +240 -0
- package/dist/bundler/article.js.map +1 -0
- package/dist/bundler/article.test.d.ts +1 -0
- package/dist/bundler/article.test.js +212 -0
- package/dist/bundler/article.test.js.map +1 -0
- package/dist/bundler/video.d.ts +50 -0
- package/dist/bundler/video.js +213 -0
- package/dist/bundler/video.js.map +1 -0
- package/dist/bundler/video.test.d.ts +1 -0
- package/dist/bundler/video.test.js +95 -0
- package/dist/bundler/video.test.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +80 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +45 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/flattener/index.d.ts +20 -0
- package/dist/flattener/index.js +707 -0
- package/dist/flattener/index.js.map +1 -0
- package/dist/flattener/index.test.d.ts +1 -0
- package/dist/flattener/index.test.js +547 -0
- package/dist/flattener/index.test.js.map +1 -0
- package/dist/fs/read-vault.d.ts +4 -0
- package/dist/fs/read-vault.js +28 -0
- package/dist/fs/read-vault.js.map +1 -0
- package/dist/fs/read-vault.test.d.ts +1 -0
- package/dist/fs/read-vault.test.js +37 -0
- package/dist/fs/read-vault.test.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/course.d.ts +17 -0
- package/dist/parser/course.js +145 -0
- package/dist/parser/course.js.map +1 -0
- package/dist/parser/course.test.d.ts +1 -0
- package/dist/parser/course.test.js +117 -0
- package/dist/parser/course.test.js.map +1 -0
- package/dist/parser/frontmatter.d.ts +8 -0
- package/dist/parser/frontmatter.js +58 -0
- package/dist/parser/frontmatter.js.map +1 -0
- package/dist/parser/frontmatter.test.d.ts +1 -0
- package/dist/parser/frontmatter.test.js +54 -0
- package/dist/parser/frontmatter.test.js.map +1 -0
- package/dist/parser/learning-outcome.d.ts +21 -0
- package/dist/parser/learning-outcome.js +116 -0
- package/dist/parser/learning-outcome.js.map +1 -0
- package/dist/parser/learning-outcome.test.d.ts +1 -0
- package/dist/parser/learning-outcome.test.js +75 -0
- package/dist/parser/learning-outcome.test.js.map +1 -0
- package/dist/parser/lens.d.ts +51 -0
- package/dist/parser/lens.js +328 -0
- package/dist/parser/lens.js.map +1 -0
- package/dist/parser/lens.test.d.ts +1 -0
- package/dist/parser/lens.test.js +453 -0
- package/dist/parser/lens.test.js.map +1 -0
- package/dist/parser/module.d.ts +21 -0
- package/dist/parser/module.js +128 -0
- package/dist/parser/module.js.map +1 -0
- package/dist/parser/module.test.d.ts +1 -0
- package/dist/parser/module.test.js +99 -0
- package/dist/parser/module.test.js.map +1 -0
- package/dist/parser/sections.d.ts +18 -0
- package/dist/parser/sections.js +128 -0
- package/dist/parser/sections.js.map +1 -0
- package/dist/parser/sections.test.d.ts +1 -0
- package/dist/parser/sections.test.js +297 -0
- package/dist/parser/sections.test.js.map +1 -0
- package/dist/parser/wikilink.d.ts +12 -0
- package/dist/parser/wikilink.js +35 -0
- package/dist/parser/wikilink.js.map +1 -0
- package/dist/parser/wikilink.test.d.ts +1 -0
- package/dist/parser/wikilink.test.js +76 -0
- package/dist/parser/wikilink.test.js.map +1 -0
- package/dist/validator/field-typos.d.ts +10 -0
- package/dist/validator/field-typos.js +96 -0
- package/dist/validator/field-typos.js.map +1 -0
- package/dist/validator/field-typos.test.d.ts +1 -0
- package/dist/validator/field-typos.test.js +41 -0
- package/dist/validator/field-typos.test.js.map +1 -0
- package/dist/validator/field-values.d.ts +11 -0
- package/dist/validator/field-values.js +37 -0
- package/dist/validator/field-values.js.map +1 -0
- package/dist/validator/field-values.test.d.ts +1 -0
- package/dist/validator/field-values.test.js +45 -0
- package/dist/validator/field-values.test.js.map +1 -0
- package/dist/validator/segment-fields.d.ts +12 -0
- package/dist/validator/segment-fields.js +55 -0
- package/dist/validator/segment-fields.js.map +1 -0
- package/dist/validator/segment-fields.test.d.ts +1 -0
- package/dist/validator/segment-fields.test.js +73 -0
- package/dist/validator/segment-fields.test.js.map +1 -0
- package/dist/validator/standalone.test.d.ts +1 -0
- package/dist/validator/standalone.test.js +190 -0
- package/dist/validator/standalone.test.js.map +1 -0
- package/dist/validator/uuid.d.ts +17 -0
- package/dist/validator/uuid.js +46 -0
- package/dist/validator/uuid.js.map +1 -0
- package/dist/validator/uuid.test.d.ts +1 -0
- package/dist/validator/uuid.test.js +229 -0
- package/dist/validator/uuid.test.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { parseModule, parsePageTextSegments } from '../parser/module.js';
|
|
2
|
+
import { parseLearningOutcome } from '../parser/learning-outcome.js';
|
|
3
|
+
import { parseLens } from '../parser/lens.js';
|
|
4
|
+
import { parseWikilink, resolveWikilinkPath, findFileWithExtension } from '../parser/wikilink.js';
|
|
5
|
+
import { parseFrontmatter } from '../parser/frontmatter.js';
|
|
6
|
+
import { extractArticleExcerpt } from '../bundler/article.js';
|
|
7
|
+
import { extractVideoExcerpt } from '../bundler/video.js';
|
|
8
|
+
/**
|
|
9
|
+
* Extract YouTube video ID from a YouTube URL.
|
|
10
|
+
*
|
|
11
|
+
* Supported formats:
|
|
12
|
+
* - https://www.youtube.com/watch?v=VIDEO_ID
|
|
13
|
+
* - https://youtube.com/watch?v=VIDEO_ID
|
|
14
|
+
* - https://youtu.be/VIDEO_ID
|
|
15
|
+
* - https://www.youtube.com/embed/VIDEO_ID
|
|
16
|
+
*
|
|
17
|
+
* @returns Video ID string, or null if URL format is not recognized
|
|
18
|
+
*/
|
|
19
|
+
function extractVideoIdFromUrl(url) {
|
|
20
|
+
const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]+)/);
|
|
21
|
+
return match ? match[1] : null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Flatten a module by resolving all references to Learning Outcomes, Lenses, and content.
|
|
25
|
+
*
|
|
26
|
+
* This function:
|
|
27
|
+
* 1. Parses the module file
|
|
28
|
+
* 2. For each Learning Outcome section, resolves the LO file
|
|
29
|
+
* 3. For each Lens in the LO, resolves the lens file
|
|
30
|
+
* 4. For each segment, extracts content from articles/videos as needed
|
|
31
|
+
*
|
|
32
|
+
* @param modulePath - Path to the module file within the files Map
|
|
33
|
+
* @param files - Map of all file paths to their content
|
|
34
|
+
* @param visitedPaths - Optional set of already-visited paths for cycle detection
|
|
35
|
+
* @returns Flattened module with resolved sections and segments, plus any errors
|
|
36
|
+
*/
|
|
37
|
+
export function flattenModule(modulePath, files, visitedPaths = new Set()) {
|
|
38
|
+
// Check for circular reference
|
|
39
|
+
if (visitedPaths.has(modulePath)) {
|
|
40
|
+
return {
|
|
41
|
+
module: null,
|
|
42
|
+
errors: [{
|
|
43
|
+
file: modulePath,
|
|
44
|
+
message: `Circular reference detected: ${modulePath}`,
|
|
45
|
+
severity: 'error',
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
visitedPaths.add(modulePath);
|
|
50
|
+
const errors = [];
|
|
51
|
+
let moduleError;
|
|
52
|
+
// Get module content
|
|
53
|
+
const moduleContent = files.get(modulePath);
|
|
54
|
+
if (!moduleContent) {
|
|
55
|
+
errors.push({
|
|
56
|
+
file: modulePath,
|
|
57
|
+
message: `Module file not found: ${modulePath}`,
|
|
58
|
+
severity: 'error',
|
|
59
|
+
});
|
|
60
|
+
return { module: null, errors };
|
|
61
|
+
}
|
|
62
|
+
// Parse the module
|
|
63
|
+
const moduleResult = parseModule(moduleContent, modulePath);
|
|
64
|
+
errors.push(...moduleResult.errors);
|
|
65
|
+
if (!moduleResult.module) {
|
|
66
|
+
return { module: null, errors };
|
|
67
|
+
}
|
|
68
|
+
const parsedModule = moduleResult.module;
|
|
69
|
+
const flattenedSections = [];
|
|
70
|
+
// Process each section in the module
|
|
71
|
+
for (const section of parsedModule.sections) {
|
|
72
|
+
if (section.type === 'learning-outcome') {
|
|
73
|
+
// Resolve the Learning Outcome reference
|
|
74
|
+
// Create a copy of visitedPaths for this section's reference chain
|
|
75
|
+
// This allows the same file to be referenced in different sections
|
|
76
|
+
// while still detecting cycles within a single chain
|
|
77
|
+
const sectionVisitedPaths = new Set(visitedPaths);
|
|
78
|
+
const result = flattenLearningOutcomeSection(section, modulePath, files, sectionVisitedPaths);
|
|
79
|
+
errors.push(...result.errors);
|
|
80
|
+
flattenedSections.push(...result.sections);
|
|
81
|
+
}
|
|
82
|
+
else if (section.type === 'page') {
|
|
83
|
+
// Page sections don't have LO references, they have inline content
|
|
84
|
+
// Parse the section body for ## Text subsections
|
|
85
|
+
const textSegments = parsePageTextSegments(section.body);
|
|
86
|
+
const pageSection = {
|
|
87
|
+
type: 'page',
|
|
88
|
+
meta: { title: section.title },
|
|
89
|
+
segments: textSegments,
|
|
90
|
+
optional: section.fields.optional === 'true',
|
|
91
|
+
contentId: section.fields.id ?? null,
|
|
92
|
+
learningOutcomeId: null,
|
|
93
|
+
videoId: null,
|
|
94
|
+
};
|
|
95
|
+
flattenedSections.push(pageSection);
|
|
96
|
+
}
|
|
97
|
+
else if (section.type === 'uncategorized') {
|
|
98
|
+
// Uncategorized sections can contain ## Lens: references
|
|
99
|
+
// Each lens becomes its own section (lens-video, lens-article, or page)
|
|
100
|
+
const sectionVisitedPaths = new Set(visitedPaths);
|
|
101
|
+
const result = flattenUncategorizedSection(section, modulePath, files, sectionVisitedPaths);
|
|
102
|
+
errors.push(...result.errors);
|
|
103
|
+
flattenedSections.push(...result.sections);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const flattenedModule = {
|
|
107
|
+
slug: parsedModule.slug,
|
|
108
|
+
title: parsedModule.title,
|
|
109
|
+
contentId: parsedModule.contentId,
|
|
110
|
+
sections: flattenedSections,
|
|
111
|
+
};
|
|
112
|
+
if (moduleError) {
|
|
113
|
+
flattenedModule.error = moduleError;
|
|
114
|
+
}
|
|
115
|
+
return { module: flattenedModule, errors };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Flatten a Learning Outcome section by resolving its LO file and all referenced lenses.
|
|
119
|
+
* Each lens in the LO becomes its own section.
|
|
120
|
+
*/
|
|
121
|
+
function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
const sections = [];
|
|
124
|
+
// Get the source wikilink
|
|
125
|
+
const source = section.fields.source;
|
|
126
|
+
if (!source) {
|
|
127
|
+
errors.push({
|
|
128
|
+
file: modulePath,
|
|
129
|
+
line: section.line,
|
|
130
|
+
message: 'Learning Outcome section missing source:: field',
|
|
131
|
+
suggestion: "Add 'source:: [[../Learning Outcomes/filename.md|Display]]'",
|
|
132
|
+
severity: 'error',
|
|
133
|
+
});
|
|
134
|
+
return { sections: [], errors };
|
|
135
|
+
}
|
|
136
|
+
// Parse and resolve the wikilink
|
|
137
|
+
const wikilink = parseWikilink(source);
|
|
138
|
+
if (!wikilink) {
|
|
139
|
+
errors.push({
|
|
140
|
+
file: modulePath,
|
|
141
|
+
line: section.line,
|
|
142
|
+
message: `Invalid wikilink format: ${source}`,
|
|
143
|
+
suggestion: 'Use format [[../Learning Outcomes/filename.md|Display Text]]',
|
|
144
|
+
severity: 'error',
|
|
145
|
+
});
|
|
146
|
+
return { sections: [], errors };
|
|
147
|
+
}
|
|
148
|
+
const loPathResolved = resolveWikilinkPath(wikilink.path, modulePath);
|
|
149
|
+
const loPath = findFileWithExtension(loPathResolved, files);
|
|
150
|
+
// Get the LO file content
|
|
151
|
+
if (!loPath) {
|
|
152
|
+
errors.push({
|
|
153
|
+
file: modulePath,
|
|
154
|
+
line: section.line,
|
|
155
|
+
message: `Referenced file not found: ${loPathResolved}`,
|
|
156
|
+
suggestion: 'Check the file path in the wiki-link',
|
|
157
|
+
severity: 'error',
|
|
158
|
+
});
|
|
159
|
+
return { sections: [], errors };
|
|
160
|
+
}
|
|
161
|
+
// Check for circular reference
|
|
162
|
+
if (visitedPaths.has(loPath)) {
|
|
163
|
+
errors.push({
|
|
164
|
+
file: modulePath,
|
|
165
|
+
line: section.line,
|
|
166
|
+
message: `Circular reference detected: ${loPath}`,
|
|
167
|
+
severity: 'error',
|
|
168
|
+
});
|
|
169
|
+
return { sections: [], errors };
|
|
170
|
+
}
|
|
171
|
+
visitedPaths.add(loPath);
|
|
172
|
+
const loContent = files.get(loPath);
|
|
173
|
+
// Parse the Learning Outcome
|
|
174
|
+
const loResult = parseLearningOutcome(loContent, loPath);
|
|
175
|
+
errors.push(...loResult.errors);
|
|
176
|
+
if (!loResult.learningOutcome) {
|
|
177
|
+
return { sections: [], errors };
|
|
178
|
+
}
|
|
179
|
+
const lo = loResult.learningOutcome;
|
|
180
|
+
// Each lens becomes its own section
|
|
181
|
+
for (const lensRef of lo.lenses) {
|
|
182
|
+
const lensPath = findFileWithExtension(lensRef.resolvedPath, files);
|
|
183
|
+
if (!lensPath) {
|
|
184
|
+
errors.push({
|
|
185
|
+
file: loPath,
|
|
186
|
+
message: `Referenced lens file not found: ${lensRef.resolvedPath}`,
|
|
187
|
+
suggestion: 'Check the file path in the wiki-link',
|
|
188
|
+
severity: 'error',
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// Check for circular reference
|
|
193
|
+
if (visitedPaths.has(lensPath)) {
|
|
194
|
+
errors.push({
|
|
195
|
+
file: loPath,
|
|
196
|
+
message: `Circular reference detected: ${lensPath}`,
|
|
197
|
+
severity: 'error',
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
visitedPaths.add(lensPath);
|
|
202
|
+
const lensContent = files.get(lensPath);
|
|
203
|
+
// Parse the lens
|
|
204
|
+
const lensResult = parseLens(lensContent, lensPath);
|
|
205
|
+
errors.push(...lensResult.errors);
|
|
206
|
+
if (!lensResult.lens) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const lens = lensResult.lens;
|
|
210
|
+
// Each lens becomes its own section
|
|
211
|
+
let sectionType = 'page';
|
|
212
|
+
const meta = { title: section.title };
|
|
213
|
+
const segments = [];
|
|
214
|
+
let videoId;
|
|
215
|
+
// Process each section in the lens
|
|
216
|
+
for (const lensSection of lens.sections) {
|
|
217
|
+
// Determine section type from lens section
|
|
218
|
+
if (lensSection.type === 'lens-article') {
|
|
219
|
+
sectionType = 'lens-article';
|
|
220
|
+
// Extract article metadata from the article file's frontmatter
|
|
221
|
+
if (lensSection.source) {
|
|
222
|
+
const articleWikilink = parseWikilink(lensSection.source);
|
|
223
|
+
if (articleWikilink) {
|
|
224
|
+
const articlePathResolved = resolveWikilinkPath(articleWikilink.path, lensPath);
|
|
225
|
+
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
226
|
+
if (articlePath) {
|
|
227
|
+
const articleContent = files.get(articlePath);
|
|
228
|
+
const articleFrontmatter = parseFrontmatter(articleContent, articlePath);
|
|
229
|
+
// Extract metadata fields
|
|
230
|
+
if (articleFrontmatter.frontmatter.title) {
|
|
231
|
+
meta.title = articleFrontmatter.frontmatter.title;
|
|
232
|
+
}
|
|
233
|
+
if (articleFrontmatter.frontmatter.author) {
|
|
234
|
+
meta.author = articleFrontmatter.frontmatter.author;
|
|
235
|
+
}
|
|
236
|
+
if (articleFrontmatter.frontmatter.sourceUrl) {
|
|
237
|
+
meta.sourceUrl = articleFrontmatter.frontmatter.sourceUrl;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (lensSection.type === 'lens-video') {
|
|
244
|
+
sectionType = 'lens-video';
|
|
245
|
+
// Extract video metadata from the video transcript file's frontmatter
|
|
246
|
+
if (lensSection.source) {
|
|
247
|
+
const videoWikilink = parseWikilink(lensSection.source);
|
|
248
|
+
if (videoWikilink) {
|
|
249
|
+
const videoPathResolved = resolveWikilinkPath(videoWikilink.path, lensPath);
|
|
250
|
+
const videoPath = findFileWithExtension(videoPathResolved, files);
|
|
251
|
+
if (videoPath) {
|
|
252
|
+
const videoContent = files.get(videoPath);
|
|
253
|
+
const videoFrontmatter = parseFrontmatter(videoContent, videoPath);
|
|
254
|
+
// Extract metadata fields
|
|
255
|
+
if (videoFrontmatter.frontmatter.title) {
|
|
256
|
+
meta.title = videoFrontmatter.frontmatter.title;
|
|
257
|
+
}
|
|
258
|
+
if (videoFrontmatter.frontmatter.channel) {
|
|
259
|
+
meta.channel = videoFrontmatter.frontmatter.channel;
|
|
260
|
+
}
|
|
261
|
+
// Extract video ID from YouTube URL
|
|
262
|
+
if (videoFrontmatter.frontmatter.url) {
|
|
263
|
+
const extractedVideoId = extractVideoIdFromUrl(videoFrontmatter.frontmatter.url);
|
|
264
|
+
if (extractedVideoId) {
|
|
265
|
+
videoId = extractedVideoId;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Process segments
|
|
273
|
+
for (const parsedSegment of lensSection.segments) {
|
|
274
|
+
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths);
|
|
275
|
+
errors.push(...segmentResult.errors);
|
|
276
|
+
if (segmentResult.segment) {
|
|
277
|
+
segments.push(segmentResult.segment);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Create a section for this lens
|
|
282
|
+
const resultSection = {
|
|
283
|
+
type: sectionType,
|
|
284
|
+
meta,
|
|
285
|
+
segments,
|
|
286
|
+
optional: section.fields.optional === 'true',
|
|
287
|
+
learningOutcomeId: lo.id ?? null,
|
|
288
|
+
contentId: lens.id ?? null,
|
|
289
|
+
videoId: videoId ?? null,
|
|
290
|
+
};
|
|
291
|
+
sections.push(resultSection);
|
|
292
|
+
}
|
|
293
|
+
return { sections, errors };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Flatten an Uncategorized section by parsing its ## Lens: references.
|
|
297
|
+
* Each lens becomes its own section with the appropriate type (lens-video, lens-article, or page).
|
|
298
|
+
*/
|
|
299
|
+
function flattenUncategorizedSection(section, modulePath, files, visitedPaths) {
|
|
300
|
+
const errors = [];
|
|
301
|
+
const sections = [];
|
|
302
|
+
// Parse the section body for ## Lens: subsections
|
|
303
|
+
const lensRefs = parseUncategorizedLensRefs(section.body, modulePath);
|
|
304
|
+
// If no lens refs found, return empty array
|
|
305
|
+
if (lensRefs.length === 0) {
|
|
306
|
+
return { sections: [], errors };
|
|
307
|
+
}
|
|
308
|
+
// Process each lens reference as a separate section
|
|
309
|
+
for (const lensRef of lensRefs) {
|
|
310
|
+
const lensPath = findFileWithExtension(lensRef.resolvedPath, files);
|
|
311
|
+
if (!lensPath) {
|
|
312
|
+
errors.push({
|
|
313
|
+
file: modulePath,
|
|
314
|
+
message: `Referenced lens file not found: ${lensRef.resolvedPath}`,
|
|
315
|
+
suggestion: 'Check the file path in the wiki-link',
|
|
316
|
+
severity: 'error',
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// Check for circular reference
|
|
321
|
+
if (visitedPaths.has(lensPath)) {
|
|
322
|
+
errors.push({
|
|
323
|
+
file: modulePath,
|
|
324
|
+
message: `Circular reference detected: ${lensPath}`,
|
|
325
|
+
severity: 'error',
|
|
326
|
+
});
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
visitedPaths.add(lensPath);
|
|
330
|
+
const lensContent = files.get(lensPath);
|
|
331
|
+
// Parse the lens
|
|
332
|
+
const lensResult = parseLens(lensContent, lensPath);
|
|
333
|
+
errors.push(...lensResult.errors);
|
|
334
|
+
if (!lensResult.lens) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const lens = lensResult.lens;
|
|
338
|
+
// Each lens becomes its own section
|
|
339
|
+
let sectionType = 'page';
|
|
340
|
+
const meta = { title: section.title };
|
|
341
|
+
const segments = [];
|
|
342
|
+
let videoId;
|
|
343
|
+
// Process each section in the lens
|
|
344
|
+
for (const lensSection of lens.sections) {
|
|
345
|
+
// Determine section type from lens section
|
|
346
|
+
if (lensSection.type === 'lens-article') {
|
|
347
|
+
sectionType = 'lens-article';
|
|
348
|
+
// Extract article metadata from the article file's frontmatter
|
|
349
|
+
if (lensSection.source) {
|
|
350
|
+
const articleWikilink = parseWikilink(lensSection.source);
|
|
351
|
+
if (articleWikilink) {
|
|
352
|
+
const articlePathResolved = resolveWikilinkPath(articleWikilink.path, lensPath);
|
|
353
|
+
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
354
|
+
if (articlePath) {
|
|
355
|
+
const articleContent = files.get(articlePath);
|
|
356
|
+
const articleFrontmatter = parseFrontmatter(articleContent, articlePath);
|
|
357
|
+
// Extract metadata fields
|
|
358
|
+
if (articleFrontmatter.frontmatter.title) {
|
|
359
|
+
meta.title = articleFrontmatter.frontmatter.title;
|
|
360
|
+
}
|
|
361
|
+
if (articleFrontmatter.frontmatter.author) {
|
|
362
|
+
meta.author = articleFrontmatter.frontmatter.author;
|
|
363
|
+
}
|
|
364
|
+
if (articleFrontmatter.frontmatter.sourceUrl) {
|
|
365
|
+
meta.sourceUrl = articleFrontmatter.frontmatter.sourceUrl;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else if (lensSection.type === 'lens-video') {
|
|
372
|
+
sectionType = 'lens-video';
|
|
373
|
+
// Extract video metadata from the video transcript file's frontmatter
|
|
374
|
+
if (lensSection.source) {
|
|
375
|
+
const videoWikilink = parseWikilink(lensSection.source);
|
|
376
|
+
if (videoWikilink) {
|
|
377
|
+
const videoPathResolved = resolveWikilinkPath(videoWikilink.path, lensPath);
|
|
378
|
+
const videoPath = findFileWithExtension(videoPathResolved, files);
|
|
379
|
+
if (videoPath) {
|
|
380
|
+
const videoContent = files.get(videoPath);
|
|
381
|
+
const videoFrontmatter = parseFrontmatter(videoContent, videoPath);
|
|
382
|
+
// Extract metadata fields
|
|
383
|
+
if (videoFrontmatter.frontmatter.title) {
|
|
384
|
+
meta.title = videoFrontmatter.frontmatter.title;
|
|
385
|
+
}
|
|
386
|
+
if (videoFrontmatter.frontmatter.channel) {
|
|
387
|
+
meta.channel = videoFrontmatter.frontmatter.channel;
|
|
388
|
+
}
|
|
389
|
+
// Extract video ID from YouTube URL
|
|
390
|
+
if (videoFrontmatter.frontmatter.url) {
|
|
391
|
+
const extractedVideoId = extractVideoIdFromUrl(videoFrontmatter.frontmatter.url);
|
|
392
|
+
if (extractedVideoId) {
|
|
393
|
+
videoId = extractedVideoId;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Process segments
|
|
401
|
+
for (const parsedSegment of lensSection.segments) {
|
|
402
|
+
const segmentResult = convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths);
|
|
403
|
+
errors.push(...segmentResult.errors);
|
|
404
|
+
if (segmentResult.segment) {
|
|
405
|
+
segments.push(segmentResult.segment);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Create a section for this lens
|
|
410
|
+
const resultSection = {
|
|
411
|
+
type: sectionType,
|
|
412
|
+
meta,
|
|
413
|
+
segments,
|
|
414
|
+
optional: lensRef.optional,
|
|
415
|
+
learningOutcomeId: null,
|
|
416
|
+
contentId: lens.id ?? null,
|
|
417
|
+
videoId: videoId ?? null,
|
|
418
|
+
};
|
|
419
|
+
sections.push(resultSection);
|
|
420
|
+
}
|
|
421
|
+
return { sections, errors };
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Parse ## Lens: subsections from an Uncategorized section's body.
|
|
425
|
+
* Returns an array of lens references with resolved paths.
|
|
426
|
+
*/
|
|
427
|
+
function parseUncategorizedLensRefs(body, parentPath) {
|
|
428
|
+
const lensRefs = [];
|
|
429
|
+
const lines = body.split('\n');
|
|
430
|
+
let inLensSection = false;
|
|
431
|
+
let currentFields = {};
|
|
432
|
+
let currentField = null;
|
|
433
|
+
let currentValue = [];
|
|
434
|
+
const LENS_HEADER_PATTERN = /^##\s+Lens:\s*(.*)$/i;
|
|
435
|
+
const FIELD_PATTERN = /^(\w+)::\s*(.*)$/;
|
|
436
|
+
for (let i = 0; i < lines.length; i++) {
|
|
437
|
+
const line = lines[i];
|
|
438
|
+
// Check for ## Lens: header
|
|
439
|
+
const lensMatch = line.match(LENS_HEADER_PATTERN);
|
|
440
|
+
if (lensMatch) {
|
|
441
|
+
// Save previous lens if we were in one
|
|
442
|
+
if (inLensSection) {
|
|
443
|
+
// First, finalize current field if we were collecting one
|
|
444
|
+
// This must happen BEFORE checking currentFields.source
|
|
445
|
+
if (currentField) {
|
|
446
|
+
currentFields[currentField] = currentValue.join('\n').trim();
|
|
447
|
+
}
|
|
448
|
+
if (currentFields.source) {
|
|
449
|
+
const wikilink = parseWikilink(currentFields.source);
|
|
450
|
+
if (wikilink) {
|
|
451
|
+
const resolvedPath = resolveWikilinkPath(wikilink.path, parentPath);
|
|
452
|
+
lensRefs.push({
|
|
453
|
+
source: currentFields.source,
|
|
454
|
+
resolvedPath,
|
|
455
|
+
optional: currentFields.optional === 'true',
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
inLensSection = true;
|
|
461
|
+
currentFields = {};
|
|
462
|
+
currentField = null;
|
|
463
|
+
currentValue = [];
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
// Check for another ## header (end of Lens section)
|
|
467
|
+
if (line.match(/^##\s+\S/) && inLensSection) {
|
|
468
|
+
// Save current lens
|
|
469
|
+
if (currentField) {
|
|
470
|
+
currentFields[currentField] = currentValue.join('\n').trim();
|
|
471
|
+
}
|
|
472
|
+
if (currentFields.source) {
|
|
473
|
+
const wikilink = parseWikilink(currentFields.source);
|
|
474
|
+
if (wikilink) {
|
|
475
|
+
const resolvedPath = resolveWikilinkPath(wikilink.path, parentPath);
|
|
476
|
+
lensRefs.push({
|
|
477
|
+
source: currentFields.source,
|
|
478
|
+
resolvedPath,
|
|
479
|
+
optional: currentFields.optional === 'true',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
inLensSection = false;
|
|
484
|
+
currentFields = {};
|
|
485
|
+
currentField = null;
|
|
486
|
+
currentValue = [];
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (inLensSection) {
|
|
490
|
+
// Parse fields
|
|
491
|
+
const fieldMatch = line.match(FIELD_PATTERN);
|
|
492
|
+
if (fieldMatch) {
|
|
493
|
+
// Save previous field
|
|
494
|
+
if (currentField) {
|
|
495
|
+
currentFields[currentField] = currentValue.join('\n').trim();
|
|
496
|
+
}
|
|
497
|
+
currentField = fieldMatch[1];
|
|
498
|
+
const inlineValue = fieldMatch[2].trim();
|
|
499
|
+
currentValue = inlineValue ? [inlineValue] : [];
|
|
500
|
+
}
|
|
501
|
+
else if (currentField) {
|
|
502
|
+
// Check if line starts a new section header
|
|
503
|
+
if (line.match(/^#/)) {
|
|
504
|
+
currentFields[currentField] = currentValue.join('\n').trim();
|
|
505
|
+
currentField = null;
|
|
506
|
+
currentValue = [];
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
currentValue.push(line);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Don't forget the last lens section
|
|
515
|
+
if (inLensSection) {
|
|
516
|
+
if (currentField) {
|
|
517
|
+
currentFields[currentField] = currentValue.join('\n').trim();
|
|
518
|
+
}
|
|
519
|
+
if (currentFields.source) {
|
|
520
|
+
const wikilink = parseWikilink(currentFields.source);
|
|
521
|
+
if (wikilink) {
|
|
522
|
+
const resolvedPath = resolveWikilinkPath(wikilink.path, parentPath);
|
|
523
|
+
lensRefs.push({
|
|
524
|
+
source: currentFields.source,
|
|
525
|
+
resolvedPath,
|
|
526
|
+
optional: currentFields.optional === 'true',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return lensRefs;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Convert a parsed lens segment into a final flattened segment.
|
|
535
|
+
* For article-excerpt and video-excerpt, this involves extracting content from source files.
|
|
536
|
+
*/
|
|
537
|
+
function convertSegment(parsedSegment, lensSection, lensPath, files, visitedPaths) {
|
|
538
|
+
const errors = [];
|
|
539
|
+
switch (parsedSegment.type) {
|
|
540
|
+
case 'text': {
|
|
541
|
+
const segment = {
|
|
542
|
+
type: 'text',
|
|
543
|
+
content: parsedSegment.content,
|
|
544
|
+
};
|
|
545
|
+
if (parsedSegment.optional) {
|
|
546
|
+
segment.optional = true;
|
|
547
|
+
}
|
|
548
|
+
return { segment, errors };
|
|
549
|
+
}
|
|
550
|
+
case 'chat': {
|
|
551
|
+
const segment = {
|
|
552
|
+
type: 'chat',
|
|
553
|
+
};
|
|
554
|
+
if (parsedSegment.instructions) {
|
|
555
|
+
segment.instructions = parsedSegment.instructions;
|
|
556
|
+
}
|
|
557
|
+
if (parsedSegment.hidePreviousContentFromUser) {
|
|
558
|
+
segment.hidePreviousContentFromUser = true;
|
|
559
|
+
}
|
|
560
|
+
if (parsedSegment.hidePreviousContentFromTutor) {
|
|
561
|
+
segment.hidePreviousContentFromTutor = true;
|
|
562
|
+
}
|
|
563
|
+
if (parsedSegment.optional) {
|
|
564
|
+
segment.optional = true;
|
|
565
|
+
}
|
|
566
|
+
return { segment, errors };
|
|
567
|
+
}
|
|
568
|
+
case 'article-excerpt': {
|
|
569
|
+
// Need to resolve the article path from the lens section's source field
|
|
570
|
+
if (!lensSection.source) {
|
|
571
|
+
errors.push({
|
|
572
|
+
file: lensPath,
|
|
573
|
+
message: 'Article section missing source:: field for article-excerpt',
|
|
574
|
+
severity: 'error',
|
|
575
|
+
});
|
|
576
|
+
return { segment: null, errors };
|
|
577
|
+
}
|
|
578
|
+
const wikilink = parseWikilink(lensSection.source);
|
|
579
|
+
if (!wikilink) {
|
|
580
|
+
errors.push({
|
|
581
|
+
file: lensPath,
|
|
582
|
+
message: `Invalid wikilink in article source: ${lensSection.source}`,
|
|
583
|
+
severity: 'error',
|
|
584
|
+
});
|
|
585
|
+
return { segment: null, errors };
|
|
586
|
+
}
|
|
587
|
+
const articlePathResolved = resolveWikilinkPath(wikilink.path, lensPath);
|
|
588
|
+
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
589
|
+
if (!articlePath) {
|
|
590
|
+
errors.push({
|
|
591
|
+
file: lensPath,
|
|
592
|
+
message: `Referenced article file not found: ${articlePathResolved}`,
|
|
593
|
+
suggestion: 'Check the file path in the wiki-link',
|
|
594
|
+
severity: 'error',
|
|
595
|
+
});
|
|
596
|
+
return { segment: null, errors };
|
|
597
|
+
}
|
|
598
|
+
// Check if the article path points back to an already-visited structural file
|
|
599
|
+
// This would indicate a circular reference (e.g., a lens source pointing back to an LO)
|
|
600
|
+
// Note: We only check, we don't add article paths to visitedPaths since
|
|
601
|
+
// multiple segments can legitimately reference the same article
|
|
602
|
+
if (visitedPaths.has(articlePath)) {
|
|
603
|
+
errors.push({
|
|
604
|
+
file: lensPath,
|
|
605
|
+
message: `Circular reference detected: ${articlePath}`,
|
|
606
|
+
severity: 'error',
|
|
607
|
+
});
|
|
608
|
+
return { segment: null, errors };
|
|
609
|
+
}
|
|
610
|
+
const articleContent = files.get(articlePath);
|
|
611
|
+
// Extract the excerpt
|
|
612
|
+
const excerptResult = extractArticleExcerpt(articleContent, parsedSegment.fromAnchor, parsedSegment.toAnchor, articlePath);
|
|
613
|
+
if (excerptResult.error) {
|
|
614
|
+
errors.push(excerptResult.error);
|
|
615
|
+
return { segment: null, errors };
|
|
616
|
+
}
|
|
617
|
+
const segment = {
|
|
618
|
+
type: 'article-excerpt',
|
|
619
|
+
content: excerptResult.content,
|
|
620
|
+
};
|
|
621
|
+
if (parsedSegment.optional) {
|
|
622
|
+
segment.optional = true;
|
|
623
|
+
}
|
|
624
|
+
return { segment, errors };
|
|
625
|
+
}
|
|
626
|
+
case 'video-excerpt': {
|
|
627
|
+
// Need to resolve the video/transcript path from the lens section's source field
|
|
628
|
+
if (!lensSection.source) {
|
|
629
|
+
errors.push({
|
|
630
|
+
file: lensPath,
|
|
631
|
+
message: 'Video section missing source:: field for video-excerpt',
|
|
632
|
+
severity: 'error',
|
|
633
|
+
});
|
|
634
|
+
return { segment: null, errors };
|
|
635
|
+
}
|
|
636
|
+
const wikilink = parseWikilink(lensSection.source);
|
|
637
|
+
if (!wikilink) {
|
|
638
|
+
errors.push({
|
|
639
|
+
file: lensPath,
|
|
640
|
+
message: `Invalid wikilink in video source: ${lensSection.source}`,
|
|
641
|
+
severity: 'error',
|
|
642
|
+
});
|
|
643
|
+
return { segment: null, errors };
|
|
644
|
+
}
|
|
645
|
+
const videoPathResolved = resolveWikilinkPath(wikilink.path, lensPath);
|
|
646
|
+
const videoPath = findFileWithExtension(videoPathResolved, files);
|
|
647
|
+
if (!videoPath) {
|
|
648
|
+
errors.push({
|
|
649
|
+
file: lensPath,
|
|
650
|
+
message: `Referenced video transcript file not found: ${videoPathResolved}`,
|
|
651
|
+
suggestion: 'Check the file path in the wiki-link',
|
|
652
|
+
severity: 'error',
|
|
653
|
+
});
|
|
654
|
+
return { segment: null, errors };
|
|
655
|
+
}
|
|
656
|
+
// Check if the video path points back to an already-visited structural file
|
|
657
|
+
// This would indicate a circular reference
|
|
658
|
+
// Note: We only check, we don't add video paths to visitedPaths since
|
|
659
|
+
// multiple segments can legitimately reference the same video
|
|
660
|
+
if (visitedPaths.has(videoPath)) {
|
|
661
|
+
errors.push({
|
|
662
|
+
file: lensPath,
|
|
663
|
+
message: `Circular reference detected: ${videoPath}`,
|
|
664
|
+
severity: 'error',
|
|
665
|
+
});
|
|
666
|
+
return { segment: null, errors };
|
|
667
|
+
}
|
|
668
|
+
const transcriptContent = files.get(videoPath);
|
|
669
|
+
// Look for corresponding .timestamps.json file
|
|
670
|
+
// e.g., video_transcripts/foo.md -> video_transcripts/foo.timestamps.json
|
|
671
|
+
const timestampsPath = videoPath.replace(/\.md$/, '.timestamps.json');
|
|
672
|
+
let timestamps;
|
|
673
|
+
// Debug: Log path resolution (uncomment to debug)
|
|
674
|
+
// console.log('DEBUG videoPath:', videoPath);
|
|
675
|
+
// console.log('DEBUG timestampsPath:', timestampsPath);
|
|
676
|
+
// console.log('DEBUG files has timestamps:', files.has(timestampsPath));
|
|
677
|
+
if (files.has(timestampsPath)) {
|
|
678
|
+
try {
|
|
679
|
+
timestamps = JSON.parse(files.get(timestampsPath));
|
|
680
|
+
// console.log('DEBUG loaded timestamps count:', timestamps.length);
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
// JSON parse error - will fall back to inline timestamps
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Extract the video excerpt
|
|
687
|
+
const excerptResult = extractVideoExcerpt(transcriptContent, parsedSegment.fromTimeStr, parsedSegment.toTimeStr, videoPath, timestamps);
|
|
688
|
+
if (excerptResult.error) {
|
|
689
|
+
errors.push(excerptResult.error);
|
|
690
|
+
return { segment: null, errors };
|
|
691
|
+
}
|
|
692
|
+
const segment = {
|
|
693
|
+
type: 'video-excerpt',
|
|
694
|
+
from: excerptResult.from,
|
|
695
|
+
to: excerptResult.to,
|
|
696
|
+
transcript: excerptResult.transcript,
|
|
697
|
+
};
|
|
698
|
+
if (parsedSegment.optional) {
|
|
699
|
+
segment.optional = true;
|
|
700
|
+
}
|
|
701
|
+
return { segment, errors };
|
|
702
|
+
}
|
|
703
|
+
default:
|
|
704
|
+
return { segment: null, errors };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
//# sourceMappingURL=index.js.map
|