lens-content-processor 0.10.0 → 0.12.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/content-schema.js +12 -0
- package/dist/content-schema.js.map +1 -1
- package/dist/flattener/index.d.ts +2 -0
- package/dist/flattener/index.js +159 -200
- package/dist/flattener/index.js.map +1 -1
- package/dist/flattener/test-helpers.d.ts +7 -11
- package/dist/flattener/test-helpers.js +25 -27
- package/dist/flattener/test-helpers.js.map +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.js +2 -11
- package/dist/index.js.map +1 -1
- package/dist/parser/learning-outcome.d.ts +3 -12
- package/dist/parser/learning-outcome.js +83 -69
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/lens.d.ts +22 -3
- package/dist/parser/lens.js +114 -3
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/module.js +3 -1
- package/dist/parser/module.js.map +1 -1
- package/dist/validator/segment-fields.js +1 -1
- package/dist/validator/segment-fields.js.map +1 -1
- package/package.json +1 -1
package/dist/content-schema.js
CHANGED
|
@@ -65,6 +65,16 @@ export const SEGMENT_SCHEMAS = {
|
|
|
65
65
|
"optional",
|
|
66
66
|
"feedback",
|
|
67
67
|
], ["user-customizable", "optional", "feedback"]),
|
|
68
|
+
embed: segmentSchema([], [
|
|
69
|
+
"source",
|
|
70
|
+
"from",
|
|
71
|
+
"to",
|
|
72
|
+
"height",
|
|
73
|
+
"width",
|
|
74
|
+
"aspect-ratio",
|
|
75
|
+
"sandbox",
|
|
76
|
+
"optional",
|
|
77
|
+
], ["optional"]),
|
|
68
78
|
};
|
|
69
79
|
/**
|
|
70
80
|
* Valid fields per segment type, derived from SEGMENT_SCHEMAS.
|
|
@@ -83,6 +93,8 @@ const SECTION_LEVEL_FIELDS = [
|
|
|
83
93
|
"learningOutcomeId",
|
|
84
94
|
"sourceUrl",
|
|
85
95
|
"hide",
|
|
96
|
+
"notes",
|
|
97
|
+
"add_to_ai_context", // submodule-level (on `# Submodule:` markers)
|
|
86
98
|
];
|
|
87
99
|
const SECTION_LEVEL_BOOLEAN_FIELDS = ["hide"];
|
|
88
100
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-schema.js","sourceRoot":"","sources":["../src/content-schema.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"content-schema.js","sourceRoot":"","sources":["../src/content-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ export type BoundaryMarker = {
|
|
|
10
10
|
__boundary: true;
|
|
11
11
|
title: string;
|
|
12
12
|
customSlug?: string;
|
|
13
|
+
addToAiContext?: string[];
|
|
13
14
|
};
|
|
14
15
|
export type FlatItem = Section | BoundaryMarker;
|
|
15
16
|
export declare function isBoundary(item: FlatItem): item is BoundaryMarker;
|
|
@@ -21,6 +22,7 @@ interface VirtualModuleGroup {
|
|
|
21
22
|
parentTitle: string;
|
|
22
23
|
sections: Section[];
|
|
23
24
|
contentId: string | null;
|
|
25
|
+
addToAiContext?: string[];
|
|
24
26
|
}
|
|
25
27
|
export declare function splitAtBoundaries(items: FlatItem[], parentSlug: string, parentTitle: string): VirtualModuleGroup[];
|
|
26
28
|
/**
|
package/dist/flattener/index.js
CHANGED
|
@@ -86,6 +86,36 @@ function toKebabCase(str) {
|
|
|
86
86
|
.replace(/[^a-z0-9]+/g, "-")
|
|
87
87
|
.replace(/(^-|-$)/g, "");
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse a `# Submodule:` marker's `add_to_ai_context::` field into raw wikilink
|
|
91
|
+
* strings (the same `[[...]]` form the backend resolves for every level).
|
|
92
|
+
* Returns undefined if the field is absent, [] if present with no wikilinks.
|
|
93
|
+
*/
|
|
94
|
+
function parseSubmoduleAiContext(fields) {
|
|
95
|
+
if (!("add_to_ai_context" in fields))
|
|
96
|
+
return undefined;
|
|
97
|
+
const raw = fields.add_to_ai_context ?? "";
|
|
98
|
+
return raw.match(/\[\[[^\]]+\]\]/g) ?? [];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Merge a module's add_to_ai_context with a submodule's own, deduped by exact
|
|
102
|
+
* wikilink string (first occurrence wins), module entries first. The backend
|
|
103
|
+
* treats this combined list as the section's module tier.
|
|
104
|
+
*/
|
|
105
|
+
function mergeAiContext(moduleCtx, submoduleCtx) {
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const entry of [
|
|
109
|
+
...(Array.isArray(moduleCtx) ? moduleCtx : []),
|
|
110
|
+
...submoduleCtx,
|
|
111
|
+
]) {
|
|
112
|
+
if (seen.has(entry))
|
|
113
|
+
continue;
|
|
114
|
+
seen.add(entry);
|
|
115
|
+
out.push(entry);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
89
119
|
export function validateBoundaries(items, file) {
|
|
90
120
|
const errors = [];
|
|
91
121
|
const hasBoundaries = items.some(isBoundary);
|
|
@@ -163,6 +193,7 @@ export function splitAtBoundaries(items, parentSlug, parentTitle) {
|
|
|
163
193
|
parentTitle,
|
|
164
194
|
sections: [],
|
|
165
195
|
contentId: null,
|
|
196
|
+
addToAiContext: item.addToAiContext,
|
|
166
197
|
};
|
|
167
198
|
}
|
|
168
199
|
else if (currentGroup) {
|
|
@@ -261,6 +292,7 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
261
292
|
__boundary: true,
|
|
262
293
|
title: section.title,
|
|
263
294
|
customSlug: section.fields.slug,
|
|
295
|
+
addToAiContext: parseSubmoduleAiContext(section.fields),
|
|
264
296
|
});
|
|
265
297
|
// Process children
|
|
266
298
|
if (section.children) {
|
|
@@ -271,14 +303,6 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
271
303
|
}
|
|
272
304
|
}
|
|
273
305
|
else {
|
|
274
|
-
// Check if this is an LO with submodules
|
|
275
|
-
if (section.type === "learning-outcome") {
|
|
276
|
-
const loSections = processLOWithSubmodules(section, modulePath, files, visitedPaths, tierMap, errors);
|
|
277
|
-
if (loSections) {
|
|
278
|
-
flatItems.push(...loSections);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
306
|
const sections = processSection(section);
|
|
283
307
|
flatItems.push(...sections);
|
|
284
308
|
}
|
|
@@ -297,9 +321,14 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
297
321
|
? { parentSlug: g.parentSlug, parentTitle: g.parentTitle }
|
|
298
322
|
: {}),
|
|
299
323
|
...(moduleError ? { error: moduleError } : {}),
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
324
|
+
// Module-level add_to_ai_context, with any submodule-level entries merged in
|
|
325
|
+
// (module first, deduped). When the submodule has no field, this is exactly
|
|
326
|
+
// the module's own value.
|
|
327
|
+
...(g.addToAiContext !== undefined
|
|
328
|
+
? {
|
|
329
|
+
addToAiContext: mergeAiContext(parsedModule.addToAiContext, g.addToAiContext),
|
|
330
|
+
}
|
|
331
|
+
: { addToAiContext: parsedModule.addToAiContext }),
|
|
303
332
|
}));
|
|
304
333
|
// Enrich ::card links in text segments with computed metadata (duration, attribution, displayType)
|
|
305
334
|
for (const mod of resultModules) {
|
|
@@ -317,7 +346,6 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
317
346
|
}
|
|
318
347
|
/**
|
|
319
348
|
* Build a test Section from a ParsedTestRef with inline segments.
|
|
320
|
-
* Shared by both processLOWithSubmodules() and flattenLearningOutcomeSection().
|
|
321
349
|
*/
|
|
322
350
|
function buildTestSection(testRef, loId, loName, loPath, files, visitedPaths, tierMap, errors) {
|
|
323
351
|
if (!testRef.segments.length)
|
|
@@ -346,123 +374,27 @@ function buildTestSection(testRef, loId, loName, loPath, files, visitedPaths, ti
|
|
|
346
374
|
};
|
|
347
375
|
}
|
|
348
376
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
|
|
352
|
-
function processLOWithSubmodules(section, modulePath, files, visitedPaths, tierMap, errors) {
|
|
353
|
-
// Resolve the LO file to check for submodules
|
|
354
|
-
const source = section.fields.source;
|
|
355
|
-
if (!source)
|
|
356
|
-
return null;
|
|
357
|
-
const wikilink = parseWikilink(source);
|
|
358
|
-
if (!wikilink || wikilink.error)
|
|
359
|
-
return null;
|
|
360
|
-
const loPathResolved = resolveWikilinkPath(wikilink.path, modulePath);
|
|
361
|
-
const loPath = findFileWithExtension(loPathResolved, files);
|
|
362
|
-
if (!loPath)
|
|
363
|
-
return null;
|
|
364
|
-
const loContent = files.get(loPath);
|
|
365
|
-
if (!loContent)
|
|
366
|
-
return null;
|
|
367
|
-
const loResult = parseLearningOutcome(loContent, loPath);
|
|
368
|
-
if (!loResult.learningOutcome?.submodules)
|
|
369
|
-
return null;
|
|
370
|
-
// This LO has submodules — emit boundaries and resolve lenses
|
|
371
|
-
const items = [];
|
|
372
|
-
for (const sub of loResult.learningOutcome.submodules) {
|
|
373
|
-
items.push({
|
|
374
|
-
__boundary: true,
|
|
375
|
-
title: sub.title,
|
|
376
|
-
customSlug: sub.customSlug,
|
|
377
|
-
});
|
|
378
|
-
// Resolve each lens in this submodule group
|
|
379
|
-
for (const lensRef of sub.lenses) {
|
|
380
|
-
const lensPath = findFileWithExtension(lensRef.resolvedPath, files);
|
|
381
|
-
if (!lensPath) {
|
|
382
|
-
const similarFiles = findSimilarFiles(lensRef.resolvedPath, files, "Lenses");
|
|
383
|
-
const suggestion = formatSuggestion(similarFiles, loPath) ??
|
|
384
|
-
"Check the file path in the wiki-link";
|
|
385
|
-
errors.push({
|
|
386
|
-
file: loPath,
|
|
387
|
-
message: `Referenced lens file not found: ${lensRef.resolvedPath}`,
|
|
388
|
-
suggestion,
|
|
389
|
-
severity: "error",
|
|
390
|
-
});
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
// Check tier violation
|
|
394
|
-
if (tierMap) {
|
|
395
|
-
const parentTier = tierMap.get(loPath) ?? "production";
|
|
396
|
-
const childTier = tierMap.get(lensPath) ?? "production";
|
|
397
|
-
const violation = checkTierViolation(loPath, parentTier, lensPath, childTier, "lens");
|
|
398
|
-
if (violation) {
|
|
399
|
-
errors.push(violation);
|
|
400
|
-
}
|
|
401
|
-
if (childTier === "ignored")
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (visitedPaths.has(lensPath)) {
|
|
405
|
-
errors.push({
|
|
406
|
-
file: loPath,
|
|
407
|
-
message: `Circular reference detected: ${lensPath}`,
|
|
408
|
-
severity: "error",
|
|
409
|
-
});
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
const sectionVisitedPaths = new Set(visitedPaths);
|
|
413
|
-
sectionVisitedPaths.add(lensPath);
|
|
414
|
-
const lensContent = files.get(lensPath);
|
|
415
|
-
const lensResult = parseLens(lensContent, lensPath);
|
|
416
|
-
errors.push(...lensResult.errors);
|
|
417
|
-
if (!lensResult.lens)
|
|
418
|
-
continue;
|
|
419
|
-
const lens = lensResult.lens;
|
|
420
|
-
const { segments, errors: flattenErrors } = flattenSingleLens(lens, lensPath, files, sectionVisitedPaths, tierMap);
|
|
421
|
-
errors.push(...flattenErrors);
|
|
422
|
-
const lo = loResult.learningOutcome;
|
|
423
|
-
const effectiveOptional = section.fields.optional?.toLowerCase() === "true" || lensRef.optional;
|
|
424
|
-
const effectiveHide = section.fields.hide?.toLowerCase() === "true" || lensRef.hide;
|
|
425
|
-
const hideConflict = checkHideOptionalConflict(effectiveOptional, effectiveHide, loPath, section.line);
|
|
426
|
-
if (hideConflict)
|
|
427
|
-
errors.push(hideConflict);
|
|
428
|
-
items.push({
|
|
429
|
-
type: "lens",
|
|
430
|
-
meta: { title: lens.title || section.title },
|
|
431
|
-
segments,
|
|
432
|
-
sourcePath: lensPath,
|
|
433
|
-
optional: effectiveOptional,
|
|
434
|
-
...(effectiveHide && { hide: true }),
|
|
435
|
-
learningOutcomeId: lo.id ?? null,
|
|
436
|
-
learningOutcomeName: loPath.split("/").pop()?.replace(/\.md$/i, "") ?? null,
|
|
437
|
-
contentId: lens.id ?? null,
|
|
438
|
-
tldr: lens.tldr,
|
|
439
|
-
summaryForTutor: lens.summaryForTutor,
|
|
440
|
-
minChatMessages: lens.minChatMessages,
|
|
441
|
-
addToAiContext: lens.addToAiContext,
|
|
442
|
-
addToAiContextLo: lo.addToAiContext,
|
|
443
|
-
...computeSectionStats(segments),
|
|
444
|
-
displayType: computeDisplayType(segments),
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
// Process test section if present in this submodule
|
|
448
|
-
if (sub.test) {
|
|
449
|
-
const lo = loResult.learningOutcome;
|
|
450
|
-
const testSection = buildTestSection(sub.test, lo.id ?? null, loPath.split("/").pop()?.replace(/\.md$/i, "") ?? null, loPath, files, visitedPaths, tierMap, errors);
|
|
451
|
-
if (testSection)
|
|
452
|
-
items.push(testSection);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
// Also emit LO parse errors
|
|
456
|
-
errors.push(...loResult.errors);
|
|
457
|
-
return items;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Flatten a Learning Outcome section by resolving its LO file and all referenced lenses.
|
|
461
|
-
* Each lens in the LO becomes its own section.
|
|
377
|
+
* Flatten a Learning Outcome section from a module file.
|
|
378
|
+
* In the current format an LO's lenses are suggestions only (imported
|
|
379
|
+
* explicitly by the module), so importing an LO yields just its test section.
|
|
462
380
|
*/
|
|
463
381
|
function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths, tierMap) {
|
|
464
382
|
const errors = [];
|
|
465
383
|
const sections = [];
|
|
384
|
+
// The old format cascaded these onto the LO's auto-imported lenses; the
|
|
385
|
+
// import now contributes only the test (always required), so flag them
|
|
386
|
+
// instead of silently ignoring them.
|
|
387
|
+
for (const deadField of ["optional", "hide"]) {
|
|
388
|
+
if (section.fields[deadField] !== undefined) {
|
|
389
|
+
errors.push({
|
|
390
|
+
file: modulePath,
|
|
391
|
+
line: section.line,
|
|
392
|
+
message: `'${deadField}::' on a Learning Outcome import has no effect — importing an outcome contributes only its test, which is always required`,
|
|
393
|
+
suggestion: "Remove the field here; set it on the individual lens imports instead",
|
|
394
|
+
severity: "warning",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
466
398
|
// Get the source wikilink
|
|
467
399
|
const source = section.fields.source;
|
|
468
400
|
if (!source) {
|
|
@@ -539,81 +471,9 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
539
471
|
return { sections: [], errors };
|
|
540
472
|
}
|
|
541
473
|
const lo = loResult.learningOutcome;
|
|
542
|
-
//
|
|
543
|
-
for
|
|
544
|
-
|
|
545
|
-
if (!lensPath) {
|
|
546
|
-
const similarFiles = findSimilarFiles(lensRef.resolvedPath, files, "Lenses");
|
|
547
|
-
const suggestion = formatSuggestion(similarFiles, loPath) ??
|
|
548
|
-
"Check the file path in the wiki-link";
|
|
549
|
-
errors.push({
|
|
550
|
-
file: loPath,
|
|
551
|
-
message: `Referenced lens file not found: ${lensRef.resolvedPath}`,
|
|
552
|
-
suggestion,
|
|
553
|
-
severity: "error",
|
|
554
|
-
});
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
// Check tier violation (LO -> Lens)
|
|
558
|
-
if (tierMap) {
|
|
559
|
-
const parentTier = tierMap.get(loPath) ?? "production";
|
|
560
|
-
const childTier = tierMap.get(lensPath) ?? "production";
|
|
561
|
-
const violation = checkTierViolation(loPath, parentTier, lensPath, childTier, "lens");
|
|
562
|
-
if (violation) {
|
|
563
|
-
errors.push(violation);
|
|
564
|
-
}
|
|
565
|
-
if (childTier === "ignored") {
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
// Check for circular reference
|
|
570
|
-
if (visitedPaths.has(lensPath)) {
|
|
571
|
-
errors.push({
|
|
572
|
-
file: loPath,
|
|
573
|
-
message: `Circular reference detected: ${lensPath}`,
|
|
574
|
-
severity: "error",
|
|
575
|
-
});
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
visitedPaths.add(lensPath);
|
|
579
|
-
const lensContent = files.get(lensPath);
|
|
580
|
-
// Parse the lens
|
|
581
|
-
const lensResult = parseLens(lensContent, lensPath);
|
|
582
|
-
errors.push(...lensResult.errors);
|
|
583
|
-
if (!lensResult.lens) {
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
const lens = lensResult.lens;
|
|
587
|
-
// Flatten lens segments using shared helper
|
|
588
|
-
const { segments, errors: flattenErrors } = flattenSingleLens(lens, lensPath, files, visitedPaths, tierMap);
|
|
589
|
-
errors.push(...flattenErrors);
|
|
590
|
-
// Create a section for this lens
|
|
591
|
-
const effectiveOptional = section.fields.optional?.toLowerCase() === "true" || lensRef.optional;
|
|
592
|
-
const effectiveHide = section.fields.hide?.toLowerCase() === "true" || lensRef.hide;
|
|
593
|
-
const hideConflict = checkHideOptionalConflict(effectiveOptional, effectiveHide, loPath, section.line);
|
|
594
|
-
if (hideConflict)
|
|
595
|
-
errors.push(hideConflict);
|
|
596
|
-
const resultSection = {
|
|
597
|
-
type: "lens",
|
|
598
|
-
meta: { title: lens.title || section.title },
|
|
599
|
-
segments,
|
|
600
|
-
sourcePath: lensPath,
|
|
601
|
-
optional: effectiveOptional,
|
|
602
|
-
...(effectiveHide && { hide: true }),
|
|
603
|
-
learningOutcomeId: lo.id ?? null,
|
|
604
|
-
learningOutcomeName: loPath.split("/").pop()?.replace(/\.md$/i, "") ?? null,
|
|
605
|
-
contentId: lens.id ?? null,
|
|
606
|
-
tldr: lens.tldr,
|
|
607
|
-
summaryForTutor: lens.summaryForTutor,
|
|
608
|
-
minChatMessages: lens.minChatMessages,
|
|
609
|
-
addToAiContext: lens.addToAiContext,
|
|
610
|
-
addToAiContextLo: lo.addToAiContext,
|
|
611
|
-
...computeSectionStats(segments),
|
|
612
|
-
displayType: computeDisplayType(segments),
|
|
613
|
-
};
|
|
614
|
-
sections.push(resultSection);
|
|
615
|
-
}
|
|
616
|
-
// After lens processing, add test section if present with inline segments
|
|
474
|
+
// Suggested lenses are intentionally NOT resolved here: they are
|
|
475
|
+
// suggestions for course creators, validated standalone in processContent.
|
|
476
|
+
// Emit the test section if the LO has one with inline segments
|
|
617
477
|
if (lo.test) {
|
|
618
478
|
const testSection = buildTestSection(lo.test, lo.id ?? null, loPath.split("/").pop()?.replace(/\.md$/i, "") ?? null, loPath, files, visitedPaths, tierMap, errors);
|
|
619
479
|
if (testSection)
|
|
@@ -1092,6 +952,105 @@ function convertSegment(parsedSegment, lensPath, files, visitedPaths, tierMap) {
|
|
|
1092
952
|
segment.feedback = true;
|
|
1093
953
|
return { segment, errors };
|
|
1094
954
|
}
|
|
955
|
+
case "embed": {
|
|
956
|
+
if (!parsedSegment.source) {
|
|
957
|
+
errors.push({
|
|
958
|
+
file: lensPath,
|
|
959
|
+
message: "Embed segment missing source (should have been set via inheritance)",
|
|
960
|
+
severity: "error",
|
|
961
|
+
});
|
|
962
|
+
return { segment: null, errors };
|
|
963
|
+
}
|
|
964
|
+
const wikilink = parseWikilink(parsedSegment.source);
|
|
965
|
+
if (!wikilink || wikilink.error) {
|
|
966
|
+
const suggestion = wikilink?.correctedPath
|
|
967
|
+
? `Did you mean '[[${wikilink.correctedPath}]]'?`
|
|
968
|
+
: undefined;
|
|
969
|
+
errors.push({
|
|
970
|
+
file: lensPath,
|
|
971
|
+
message: `Invalid wikilink in embed source: ${parsedSegment.source}`,
|
|
972
|
+
suggestion,
|
|
973
|
+
severity: "error",
|
|
974
|
+
});
|
|
975
|
+
return { segment: null, errors };
|
|
976
|
+
}
|
|
977
|
+
const articlePathResolved = resolveWikilinkPath(wikilink.path, lensPath);
|
|
978
|
+
const articlePath = findFileWithExtension(articlePathResolved, files);
|
|
979
|
+
if (!articlePath) {
|
|
980
|
+
const similarFiles = findSimilarFiles(articlePathResolved, files, "articles");
|
|
981
|
+
const suggestion = formatSuggestion(similarFiles, lensPath) ??
|
|
982
|
+
"Check the file path in the wiki-link";
|
|
983
|
+
errors.push({
|
|
984
|
+
file: lensPath,
|
|
985
|
+
message: `Referenced article file not found: ${articlePathResolved}`,
|
|
986
|
+
suggestion,
|
|
987
|
+
severity: "error",
|
|
988
|
+
});
|
|
989
|
+
return { segment: null, errors };
|
|
990
|
+
}
|
|
991
|
+
if (tierMap) {
|
|
992
|
+
const parentTier = tierMap.get(lensPath) ?? "production";
|
|
993
|
+
const childTier = tierMap.get(articlePath) ?? "production";
|
|
994
|
+
const violation = checkTierViolation(lensPath, parentTier, articlePath, childTier, "article");
|
|
995
|
+
if (violation) {
|
|
996
|
+
errors.push(violation);
|
|
997
|
+
}
|
|
998
|
+
if (childTier === "ignored") {
|
|
999
|
+
return { segment: null, errors };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (visitedPaths.has(articlePath)) {
|
|
1003
|
+
errors.push({
|
|
1004
|
+
file: lensPath,
|
|
1005
|
+
message: `Circular reference detected: ${articlePath}`,
|
|
1006
|
+
severity: "error",
|
|
1007
|
+
});
|
|
1008
|
+
return { segment: null, errors };
|
|
1009
|
+
}
|
|
1010
|
+
const articleContent = files.get(articlePath);
|
|
1011
|
+
const articleFrontmatter = parseFrontmatter(articleContent, articlePath);
|
|
1012
|
+
const fm = articleFrontmatter.frontmatter;
|
|
1013
|
+
if (!fm.source_url) {
|
|
1014
|
+
errors.push({
|
|
1015
|
+
file: lensPath,
|
|
1016
|
+
message: `Embed source article missing source_url: ${articlePath}`,
|
|
1017
|
+
suggestion: "Add source_url to the article frontmatter; embeds use it as the iframe URL",
|
|
1018
|
+
severity: "error",
|
|
1019
|
+
});
|
|
1020
|
+
return { segment: null, errors };
|
|
1021
|
+
}
|
|
1022
|
+
const excerptResult = extractArticleExcerpt(articleContent, parsedSegment.fromAnchor, parsedSegment.toAnchor, articlePath);
|
|
1023
|
+
if (excerptResult.error) {
|
|
1024
|
+
errors.push({ ...excerptResult.error, file: lensPath });
|
|
1025
|
+
return { segment: null, errors };
|
|
1026
|
+
}
|
|
1027
|
+
const segment = {
|
|
1028
|
+
type: "embed",
|
|
1029
|
+
url: String(fm.source_url),
|
|
1030
|
+
content: excerptResult.content,
|
|
1031
|
+
sourceUrl: String(fm.source_url),
|
|
1032
|
+
sourcePath: articlePath,
|
|
1033
|
+
};
|
|
1034
|
+
if (fm.title)
|
|
1035
|
+
segment.title = fm.title;
|
|
1036
|
+
if (fm.author) {
|
|
1037
|
+
const raw = fm.author;
|
|
1038
|
+
segment.author = Array.isArray(raw) ? raw.join(", ") : String(raw);
|
|
1039
|
+
}
|
|
1040
|
+
if (fm.published)
|
|
1041
|
+
segment.published = String(fm.published);
|
|
1042
|
+
if (parsedSegment.height)
|
|
1043
|
+
segment.height = parsedSegment.height;
|
|
1044
|
+
if (parsedSegment.width)
|
|
1045
|
+
segment.width = parsedSegment.width;
|
|
1046
|
+
if (parsedSegment.aspectRatio)
|
|
1047
|
+
segment.aspectRatio = parsedSegment.aspectRatio;
|
|
1048
|
+
if (parsedSegment.sandbox)
|
|
1049
|
+
segment.sandbox = parsedSegment.sandbox;
|
|
1050
|
+
if (parsedSegment.optional)
|
|
1051
|
+
segment.optional = true;
|
|
1052
|
+
return { segment, errors };
|
|
1053
|
+
}
|
|
1095
1054
|
default:
|
|
1096
1055
|
return { segment: null, errors };
|
|
1097
1056
|
}
|