lens-content-processor 0.79.0 → 0.81.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/authoring-markup.d.ts +22 -0
- package/dist/authoring-markup.js +24 -0
- package/dist/authoring-markup.js.map +1 -1
- package/dist/cache.d.ts +10 -0
- package/dist/cache.js +6 -5
- package/dist/cache.js.map +1 -1
- package/dist/cli-steps.d.ts +37 -0
- package/dist/cli-steps.js +80 -0
- package/dist/cli-steps.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +52 -70
- package/dist/cli.js.map +1 -1
- package/dist/content-schema.js +4 -0
- package/dist/content-schema.js.map +1 -1
- package/dist/course-graph.d.ts +2 -2
- package/dist/course-graph.js +7 -2
- package/dist/course-graph.js.map +1 -1
- package/dist/criticmarkup-forms.d.ts +8 -6
- package/dist/criticmarkup-forms.js +22 -19
- package/dist/criticmarkup-forms.js.map +1 -1
- package/dist/dedupe-errors.d.ts +16 -0
- package/dist/dedupe-errors.js +23 -9
- package/dist/dedupe-errors.js.map +1 -1
- package/dist/dependency-cache.d.ts +15 -5
- package/dist/dependency-cache.js +33 -74
- package/dist/dependency-cache.js.map +1 -1
- package/dist/dropped.d.ts +114 -0
- package/dist/dropped.js +253 -0
- package/dist/dropped.js.map +1 -0
- package/dist/engine/assemble.d.ts +44 -0
- package/dist/engine/assemble.js +132 -0
- package/dist/engine/assemble.js.map +1 -0
- package/dist/engine/build.d.ts +8 -0
- package/dist/engine/build.js +558 -0
- package/dist/engine/build.js.map +1 -0
- package/dist/engine/file-facts.d.ts +26 -0
- package/dist/engine/file-facts.js +63 -0
- package/dist/engine/file-facts.js.map +1 -0
- package/dist/engine/git-source.d.ts +35 -0
- package/dist/engine/git-source.js +242 -0
- package/dist/engine/git-source.js.map +1 -0
- package/dist/engine/overlay.d.ts +43 -0
- package/dist/engine/overlay.js +167 -0
- package/dist/engine/overlay.js.map +1 -0
- package/dist/engine/protocol.d.ts +146 -0
- package/dist/engine/protocol.js +40 -0
- package/dist/engine/protocol.js.map +1 -0
- package/dist/engine/reader.d.ts +133 -0
- package/dist/engine/reader.js +298 -0
- package/dist/engine/reader.js.map +1 -0
- package/dist/engine/sources.d.ts +39 -0
- package/dist/engine/sources.js +137 -0
- package/dist/engine/sources.js.map +1 -0
- package/dist/engine/sqlite-store.d.ts +99 -0
- package/dist/engine/sqlite-store.js +452 -0
- package/dist/engine/sqlite-store.js.map +1 -0
- package/dist/engine/store.d.ts +85 -0
- package/dist/engine/store.js +31 -0
- package/dist/engine/store.js.map +1 -0
- package/dist/engine/tables.d.ts +15 -0
- package/dist/engine/tables.js +70 -0
- package/dist/engine/tables.js.map +1 -0
- package/dist/engine/units.d.ts +58 -0
- package/dist/engine/units.js +186 -0
- package/dist/engine/units.js.map +1 -0
- package/dist/engine/version.d.ts +6 -0
- package/dist/engine/version.js +21 -0
- package/dist/engine/version.js.map +1 -0
- package/dist/engine/worker.d.ts +1 -0
- package/dist/engine/worker.js +220 -0
- package/dist/engine/worker.js.map +1 -0
- package/dist/flattener/index.d.ts +34 -17
- package/dist/flattener/index.js +252 -115
- package/dist/flattener/index.js.map +1 -1
- package/dist/flattener/resolve-text-links.d.ts +15 -14
- package/dist/flattener/resolve-text-links.js +24 -25
- package/dist/flattener/resolve-text-links.js.map +1 -1
- package/dist/glossary.d.ts +2 -2
- package/dist/glossary.js.map +1 -1
- package/dist/index.d.ts +171 -2
- package/dist/index.js +883 -639
- package/dist/index.js.map +1 -1
- package/dist/markdown-parse.d.ts +6 -0
- package/dist/markdown-parse.js +14 -0
- package/dist/markdown-parse.js.map +1 -1
- package/dist/parser/learning-outcome.js +11 -2
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/lens.d.ts +7 -1
- package/dist/parser/lens.js +43 -9
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/meeting-doc.d.ts +2 -1
- package/dist/parser/meeting-doc.js +2 -1
- package/dist/parser/meeting-doc.js.map +1 -1
- package/dist/parser/module.d.ts +3 -1
- package/dist/parser/module.js +7 -5
- package/dist/parser/module.js.map +1 -1
- package/dist/parser/question-embeds.d.ts +4 -3
- package/dist/parser/question-embeds.js.map +1 -1
- package/dist/parser/response-segments.d.ts +4 -0
- package/dist/parser/response-segments.js +1 -1
- package/dist/parser/response-segments.js.map +1 -1
- package/dist/parser/sections.d.ts +10 -0
- package/dist/parser/sections.js +8 -3
- package/dist/parser/sections.js.map +1 -1
- package/dist/parser/widget.d.ts +2 -1
- package/dist/parser/widget.js.map +1 -1
- package/dist/parser/wikilink.d.ts +13 -3
- package/dist/parser/wikilink.js +55 -34
- package/dist/parser/wikilink.js.map +1 -1
- package/dist/reference-graph.d.ts +21 -3
- package/dist/reference-graph.js +35 -12
- package/dist/reference-graph.js.map +1 -1
- package/dist/same-page-links.d.ts +11 -1
- package/dist/same-page-links.js +9 -0
- package/dist/same-page-links.js.map +1 -1
- package/dist/skill-tree.d.ts +2 -1
- package/dist/skill-tree.js.map +1 -1
- package/dist/source-location.d.ts +0 -4
- package/dist/source-location.js +0 -43
- package/dist/source-location.js.map +1 -1
- package/dist/validator/card-module-membership.d.ts +17 -1
- package/dist/validator/card-module-membership.js +60 -56
- package/dist/validator/card-module-membership.js.map +1 -1
- package/dist/validator/criticmarkup.js +1 -1
- package/dist/validator/criticmarkup.js.map +1 -1
- package/dist/validator/field-typos.d.ts +1 -1
- package/dist/validator/field-typos.js +21 -21
- package/dist/validator/field-typos.js.map +1 -1
- package/dist/validator/output-integrity.d.ts +9 -5
- package/dist/validator/output-integrity.js +43 -45
- package/dist/validator/output-integrity.js.map +1 -1
- package/dist/validator/segment-fields.js +5 -8
- package/dist/validator/segment-fields.js.map +1 -1
- package/dist/validator/tier.d.ts +2 -2
- package/dist/validator/tier.js.map +1 -1
- package/dist/validator/uuid.d.ts +1 -1
- package/package.json +2 -1
package/dist/flattener/index.js
CHANGED
|
@@ -15,7 +15,8 @@ import { fileNameToSlug } from "../utils/slug.js";
|
|
|
15
15
|
import { extractArticleExcerpt, resolveArticleSource, } from "../bundler/article.js";
|
|
16
16
|
import { extractVideoExcerpt, resolveVideoRange, } from "../bundler/video.js";
|
|
17
17
|
import { attachSourceLine, getSourceLine } from "../source-location.js";
|
|
18
|
-
import { collectBlockTargets, collectBlockTargetsFrom, resolveSamePageLinks, } from "../same-page-links.js";
|
|
18
|
+
import { collectBlockTargets, collectBlockTargetsFrom, lazyBlockTargets, resolveSamePageLinks, } from "../same-page-links.js";
|
|
19
|
+
import { attachDroppedSegments, dropCause, droppedContent, droppedSection, droppedSegment, droppedSegmentsOf, fileStem, isBuilt, placeDropped, placedAfter, } from "../dropped.js";
|
|
19
20
|
function countWords(text) {
|
|
20
21
|
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
21
22
|
}
|
|
@@ -242,6 +243,8 @@ function mergeAiContext(moduleCtx, submoduleCtx) {
|
|
|
242
243
|
}
|
|
243
244
|
return out;
|
|
244
245
|
}
|
|
246
|
+
/** The error for sections before the first `# Submodule:`, which no page holds. */
|
|
247
|
+
const OUTSIDE_SUBMODULES = "Content found outside submodule boundaries — all content must be inside a submodule";
|
|
245
248
|
export function validateBoundaries(items, file) {
|
|
246
249
|
const errors = [];
|
|
247
250
|
const hasBoundaries = items.some(isBoundary);
|
|
@@ -255,7 +258,7 @@ export function validateBoundaries(items, file) {
|
|
|
255
258
|
if (sectionsBefore.length > 0) {
|
|
256
259
|
errors.push({
|
|
257
260
|
file,
|
|
258
|
-
message:
|
|
261
|
+
message: OUTSIDE_SUBMODULES,
|
|
259
262
|
severity: "error",
|
|
260
263
|
});
|
|
261
264
|
}
|
|
@@ -333,39 +336,50 @@ export function splitAtBoundaries(items, parentSlug, parentTitle) {
|
|
|
333
336
|
return groups;
|
|
334
337
|
}
|
|
335
338
|
/**
|
|
336
|
-
* Flatten a
|
|
337
|
-
*
|
|
339
|
+
* Flatten the parsed segments of a lens or of an outcome's test into output
|
|
340
|
+
* Segments: the shared core of every lens and test path.
|
|
338
341
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
342
|
+
* Article segments are resolved up front, grouped per source file (one parsed
|
|
343
|
+
* segment can expand to several output segments when the article declares
|
|
344
|
+
* inline videos); every other segment goes through convertSegment.
|
|
345
|
+
* `collapsed: false` (test sections) skips the collapsed text at excerpt
|
|
346
|
+
* boundaries. `dropped` starts with the segments the parser refused.
|
|
341
347
|
*/
|
|
342
|
-
function
|
|
348
|
+
function flattenSegments(parsed, path, files, visitedPaths, tierMap, options) {
|
|
349
|
+
const parsedSegments = parsed.segments;
|
|
343
350
|
const errors = [];
|
|
344
351
|
const segments = [];
|
|
345
|
-
const
|
|
352
|
+
const dropped = [...droppedSegmentsOf(parsed)];
|
|
353
|
+
const blockTargets = lazyBlockTargets(() => collectBlockTargetsFrom(parsedSegments.flatMap((segment) => "content" in segment && typeof segment.content === "string"
|
|
346
354
|
? [segment.content]
|
|
347
|
-
: []));
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const articlePlan = planLensArticles(lens.segments, lensPath, files, visitedPaths, tierMap, { collapsed: true });
|
|
352
|
-
for (let i = 0; i < lens.segments.length; i++) {
|
|
353
|
-
const parsedSegment = lens.segments[i];
|
|
355
|
+
: [])));
|
|
356
|
+
const articlePlan = planLensArticles(parsedSegments, path, files, visitedPaths, tierMap, options);
|
|
357
|
+
for (let i = 0; i < parsedSegments.length; i++) {
|
|
358
|
+
const parsedSegment = parsedSegments[i];
|
|
354
359
|
const planned = articlePlan.get(i);
|
|
355
360
|
if (planned) {
|
|
356
361
|
errors.push(...planned.errors);
|
|
362
|
+
dropped.push(...planned.dropped);
|
|
357
363
|
for (const segment of planned.segments) {
|
|
358
364
|
segments.push(prependFrames(segment, parsedSegment.frames));
|
|
359
365
|
}
|
|
360
366
|
continue;
|
|
361
367
|
}
|
|
362
|
-
const segmentResult = convertSegment(parsedSegment,
|
|
368
|
+
const segmentResult = convertSegment(parsedSegment, path, files, visitedPaths, tierMap, blockTargets);
|
|
363
369
|
errors.push(...segmentResult.errors);
|
|
364
370
|
if (segmentResult.segment) {
|
|
365
371
|
segments.push(prependFrames(segmentResult.segment, parsedSegment.frames));
|
|
366
372
|
}
|
|
373
|
+
else {
|
|
374
|
+
dropped.push(...droppedSegment(parsedSegment.type, sourceOf(parsedSegment), path, segmentResult.errors));
|
|
375
|
+
}
|
|
367
376
|
}
|
|
368
|
-
return { segments, errors };
|
|
377
|
+
return { segments, errors, dropped };
|
|
378
|
+
}
|
|
379
|
+
/** The `source::` wikilink of a parsed segment, when its type has one. */
|
|
380
|
+
function sourceOf(segment) {
|
|
381
|
+
const source = segment.source;
|
|
382
|
+
return typeof source === "string" ? source : undefined;
|
|
369
383
|
}
|
|
370
384
|
// ─── Module flattening ───
|
|
371
385
|
/**
|
|
@@ -384,11 +398,11 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
384
398
|
severity: "error",
|
|
385
399
|
},
|
|
386
400
|
],
|
|
401
|
+
dropped: [],
|
|
387
402
|
};
|
|
388
403
|
}
|
|
389
404
|
visitedPaths.add(modulePath);
|
|
390
405
|
const errors = [];
|
|
391
|
-
let moduleError;
|
|
392
406
|
// Get module content
|
|
393
407
|
const moduleContent = files.get(modulePath);
|
|
394
408
|
if (!moduleContent) {
|
|
@@ -397,16 +411,27 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
397
411
|
message: `Module file not found: ${modulePath}`,
|
|
398
412
|
severity: "error",
|
|
399
413
|
});
|
|
400
|
-
return { module: null, modules: [], errors };
|
|
414
|
+
return { module: null, modules: [], errors, dropped: [] };
|
|
401
415
|
}
|
|
402
416
|
// Parse the module
|
|
403
417
|
const moduleResult = parseModule(moduleContent, modulePath);
|
|
404
418
|
errors.push(...moduleResult.errors);
|
|
405
419
|
if (!moduleResult.module) {
|
|
406
|
-
return { module: null, modules: [], errors };
|
|
420
|
+
return { module: null, modules: [], errors, dropped: [] };
|
|
407
421
|
}
|
|
408
422
|
const parsedModule = moduleResult.module;
|
|
409
423
|
const flatItems = [];
|
|
424
|
+
// A header the parser refused (an unknown type, such as "# Lense:") left out
|
|
425
|
+
// the section it would have started: a placeholder stands where the header
|
|
426
|
+
// is, and keeps its place as a lens does (src/dropped.ts).
|
|
427
|
+
const refused = [...moduleResult.refused];
|
|
428
|
+
const placeRefused = (beforeLine = Infinity) => {
|
|
429
|
+
while (refused.length > 0 && (refused[0].error.line ?? 0) < beforeLine) {
|
|
430
|
+
const { title, error } = refused.shift();
|
|
431
|
+
const drop = droppedContent("section", title, modulePath, error);
|
|
432
|
+
flatItems.push(droppedSection("lens", drop));
|
|
433
|
+
}
|
|
434
|
+
};
|
|
410
435
|
// Helper: process a section (LO or Lens). An LO import also yields the
|
|
411
436
|
// outcome itself, so lenses can be tagged with it below.
|
|
412
437
|
function processSection(section, _subsectionLevel) {
|
|
@@ -476,9 +501,10 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
476
501
|
const { sections, outcome } = processSection(section);
|
|
477
502
|
recordOutcome(outcome, true);
|
|
478
503
|
moduleLevelTestSections.push(...sections);
|
|
479
|
-
return sections.length;
|
|
504
|
+
return sections.filter(isBuilt).length;
|
|
480
505
|
}
|
|
481
506
|
for (const section of parsedModule.sections) {
|
|
507
|
+
placeRefused(section.line);
|
|
482
508
|
if (section.type === "submodule") {
|
|
483
509
|
if (seenSubmodule)
|
|
484
510
|
groupIdx++;
|
|
@@ -493,6 +519,7 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
493
519
|
// Process children
|
|
494
520
|
if (section.children) {
|
|
495
521
|
for (const child of section.children) {
|
|
522
|
+
placeRefused(child.line);
|
|
496
523
|
const { sections, outcome } = processSection(child, child.level + 1);
|
|
497
524
|
recordOutcome(outcome, false);
|
|
498
525
|
flatItems.push(...sections);
|
|
@@ -537,13 +564,31 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
537
564
|
flatItems.push(...sections);
|
|
538
565
|
}
|
|
539
566
|
}
|
|
567
|
+
placeRefused();
|
|
540
568
|
// Flat modules: append top-declared LO tests at the end of the section list.
|
|
541
569
|
if (!hasSubmodules) {
|
|
542
570
|
flatItems.push(...moduleLevelTestSections);
|
|
543
571
|
}
|
|
544
572
|
// Validate and split at boundaries
|
|
545
|
-
const boundaryErrors = validateBoundaries(flatItems, modulePath);
|
|
573
|
+
const boundaryErrors = validateBoundaries(flatItems.filter(isBuilt), modulePath);
|
|
546
574
|
errors.push(...boundaryErrors);
|
|
575
|
+
// Sections before the first `# Submodule:` are on no page. Each is recorded
|
|
576
|
+
// before the first page's sections: a built one with the error that says
|
|
577
|
+
// so, a placeholder with its own (src/dropped.ts).
|
|
578
|
+
const firstBoundary = flatItems.findIndex(isBoundary);
|
|
579
|
+
const outsideItems = flatItems.slice(0, Math.max(firstBoundary, 0));
|
|
580
|
+
const outside = boundaryErrors.find((e) => e.message === OUTSIDE_SUBMODULES);
|
|
581
|
+
const orphans = [];
|
|
582
|
+
for (const section of outsideItems) {
|
|
583
|
+
if (!isBuilt(section))
|
|
584
|
+
orphans.push(...placeDropped([section]).items);
|
|
585
|
+
else if (outside) {
|
|
586
|
+
const sourcePath = section.sourcePath ?? modulePath;
|
|
587
|
+
const title = section.meta.title || fileStem(sourcePath);
|
|
588
|
+
const drop = droppedContent("section", title, sourcePath, outside);
|
|
589
|
+
orphans.push(placedAfter(-1, [], drop));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
547
592
|
const groups = splitAtBoundaries(flatItems, parsedModule.slug, parsedModule.title);
|
|
548
593
|
// groups[i] is the i-th `# Submodule:` (the whole module when flat), the
|
|
549
594
|
// same order recordOutcome counted in.
|
|
@@ -572,7 +617,7 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
572
617
|
}
|
|
573
618
|
}
|
|
574
619
|
// Module-level LOs: append the synthesized "Test Your Understanding" entry.
|
|
575
|
-
if (hasSubmodules && moduleLevelTestSections.
|
|
620
|
+
if (hasSubmodules && moduleLevelTestSections.some(isBuilt)) {
|
|
576
621
|
const testSlug = `${parsedModule.slug}/test-your-understanding`;
|
|
577
622
|
const collision = groups.find((g) => g.slug === testSlug);
|
|
578
623
|
if (collision) {
|
|
@@ -596,16 +641,22 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
596
641
|
});
|
|
597
642
|
}
|
|
598
643
|
}
|
|
599
|
-
|
|
644
|
+
else if (hasSubmodules && moduleLevelTestSections.length > 0) {
|
|
645
|
+
// Every module-level test failed, so there is no test page: their
|
|
646
|
+
// records follow the last page.
|
|
647
|
+
groups[groups.length - 1].sections.push(...moduleLevelTestSections);
|
|
648
|
+
}
|
|
649
|
+
const placed = groups.map((g) => placeDropped(g.sections));
|
|
650
|
+
placed[0].items.unshift(...orphans);
|
|
651
|
+
const resultModules = groups.map((g, i) => ({
|
|
600
652
|
slug: g.slug,
|
|
601
653
|
title: g.title,
|
|
602
654
|
contentId: parsedModule.contentId,
|
|
603
|
-
sections:
|
|
655
|
+
sections: placed[i].sections,
|
|
604
656
|
sourcePath: modulePath,
|
|
605
657
|
...(g.parentSlug
|
|
606
658
|
? { parentSlug: g.parentSlug, parentTitle: g.parentTitle }
|
|
607
659
|
: {}),
|
|
608
|
-
...(moduleError ? { error: moduleError } : {}),
|
|
609
660
|
// Module-level add_to_ai_context, with any submodule-level entries merged in
|
|
610
661
|
// (module first, deduped). When the submodule has no field, this is exactly
|
|
611
662
|
// the module's own value.
|
|
@@ -627,7 +678,60 @@ export function flattenModule(modulePath, files, visitedPaths = new Set(), tierM
|
|
|
627
678
|
: primaryModule,
|
|
628
679
|
modules: resultModules,
|
|
629
680
|
errors,
|
|
681
|
+
dropped: placed.map((p) => p.items),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* The sections a module declares, for the cross-check in src/dropped.ts:
|
|
686
|
+
* every lens import, and every outcome import except one whose outcome
|
|
687
|
+
* declares no test segment (that builds nothing, on purpose). A section's page
|
|
688
|
+
* is the index of its `# Submodule:` (0 outside any). Paths resolve as
|
|
689
|
+
* flattenLensSection and flattenLearningOutcomeSection resolve them, so a
|
|
690
|
+
* section and its record name the same file.
|
|
691
|
+
*/
|
|
692
|
+
export function declaredSections(parsedModule, modulePath, files) {
|
|
693
|
+
const declared = [];
|
|
694
|
+
const pathOf = (source) => {
|
|
695
|
+
const link = source ? parseWikilink(source, modulePath) : null;
|
|
696
|
+
if (!link || link.error)
|
|
697
|
+
return modulePath;
|
|
698
|
+
const resolved = resolveWikilinkPath(link.path, modulePath);
|
|
699
|
+
return findFileWithExtension(resolved, files) ?? resolved;
|
|
700
|
+
};
|
|
701
|
+
const declare = (section, page) => {
|
|
702
|
+
const sourcePath = pathOf(section.fields.source);
|
|
703
|
+
const own = sourcePath === modulePath;
|
|
704
|
+
if (section.type === "lens") {
|
|
705
|
+
const title = section.title.trim() || (own ? "Lens" : fileStem(sourcePath));
|
|
706
|
+
declared.push({ sourcePath, title, line: section.line, page });
|
|
707
|
+
}
|
|
708
|
+
else if (section.type === "learning-outcome") {
|
|
709
|
+
if (!own && files.has(sourcePath)) {
|
|
710
|
+
const text = files.get(sourcePath);
|
|
711
|
+
const outcome = parseLearningOutcome(text, sourcePath).learningOutcome;
|
|
712
|
+
const test = outcome?.test;
|
|
713
|
+
const declaresSegments = !!test && test.segments.length + droppedSegmentsOf(test).length > 0;
|
|
714
|
+
if (outcome && !declaresSegments)
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const title = own ? "Test" : `Test: ${fileStem(sourcePath)}`;
|
|
718
|
+
declared.push({ sourcePath, title, line: section.line, page });
|
|
719
|
+
}
|
|
630
720
|
};
|
|
721
|
+
// A section after the k-th `# Submodule:` marker, nested or not, is on
|
|
722
|
+
// page k.
|
|
723
|
+
let page = 0;
|
|
724
|
+
let submodules = 0;
|
|
725
|
+
for (const section of parsedModule.sections) {
|
|
726
|
+
if (section.type !== "submodule")
|
|
727
|
+
declare(section, page);
|
|
728
|
+
else {
|
|
729
|
+
page = submodules++;
|
|
730
|
+
for (const child of section.children ?? [])
|
|
731
|
+
declare(child, page);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return declared;
|
|
631
735
|
}
|
|
632
736
|
/** Minutes a learning-outcome test takes to write when its outcome sets no
|
|
633
737
|
* `writing_minutes`: one figure per test, whatever its question count. */
|
|
@@ -635,34 +739,19 @@ const TEST_WRITING_MINUTES = 7;
|
|
|
635
739
|
/**
|
|
636
740
|
* Build a test Section from a ParsedTestRef with inline segments. Its time is
|
|
637
741
|
* writing time, carried as durationMinutes (a total with no tutor share).
|
|
742
|
+
* `dropped` are the segments that errors left out; when they were all of
|
|
743
|
+
* them, there is no section, and they say why.
|
|
638
744
|
*/
|
|
639
745
|
function buildTestSection(testRef, writingMinutes, outcome, loPath, files, visitedPaths, tierMap, errors) {
|
|
640
|
-
if (!testRef.segments.length)
|
|
641
|
-
return null;
|
|
642
|
-
const testSegments = [];
|
|
643
|
-
const blockTargets = collectBlockTargetsFrom(testRef.segments.flatMap((segment) => "content" in segment && typeof segment.content === "string"
|
|
644
|
-
? [segment.content]
|
|
645
|
-
: []));
|
|
646
746
|
// collapsed: false — test sections never carried collapsed_* fields
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
for (const segment of planned.segments) {
|
|
654
|
-
testSegments.push(prependFrames(segment, parsedSegment.frames));
|
|
655
|
-
}
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
const segmentResult = convertSegment(parsedSegment, loPath, files, new Set(visitedPaths), tierMap, blockTargets);
|
|
659
|
-
errors.push(...segmentResult.errors);
|
|
660
|
-
if (segmentResult.segment) {
|
|
661
|
-
testSegments.push(prependFrames(segmentResult.segment, parsedSegment.frames));
|
|
662
|
-
}
|
|
663
|
-
}
|
|
747
|
+
const flat = flattenSegments(testRef, loPath, files, visitedPaths, tierMap, {
|
|
748
|
+
collapsed: false,
|
|
749
|
+
});
|
|
750
|
+
errors.push(...flat.errors);
|
|
751
|
+
const { dropped } = flat;
|
|
752
|
+
const testSegments = flat.segments;
|
|
664
753
|
if (testSegments.length === 0)
|
|
665
|
-
return null;
|
|
754
|
+
return { section: null, dropped };
|
|
666
755
|
const hasFeedback = testSegments.some((s) => s.type === "question" && s.feedback);
|
|
667
756
|
// Grouped feedback is the legacy questions' feedback; force-feedback:: on
|
|
668
757
|
// any of them sends it on completion instead of on "Get feedback". The
|
|
@@ -673,7 +762,7 @@ function buildTestSection(testRef, writingMinutes, outcome, loPath, files, visit
|
|
|
673
762
|
: forced.includes(true)
|
|
674
763
|
? true
|
|
675
764
|
: forced[0];
|
|
676
|
-
|
|
765
|
+
const section = {
|
|
677
766
|
type: "test",
|
|
678
767
|
// Always the outcome's name: the sidebar shows it, and the title is the
|
|
679
768
|
// section's URL anchor, so two tests in one submodule must differ.
|
|
@@ -687,14 +776,15 @@ function buildTestSection(testRef, writingMinutes, outcome, loPath, files, visit
|
|
|
687
776
|
learningOutcomes: [outcome],
|
|
688
777
|
durationMinutes: writingMinutes ?? TEST_WRITING_MINUTES,
|
|
689
778
|
};
|
|
779
|
+
return { section, dropped };
|
|
690
780
|
}
|
|
691
781
|
/** Tag a lens section with every outcome that suggests its file. An inline
|
|
692
782
|
* lens's sourcePath is the module file, which no outcome suggests. */
|
|
693
|
-
function tagLensOutcomes(section, outcomes) {
|
|
783
|
+
export function tagLensOutcomes(section, outcomes) {
|
|
694
784
|
if (section.type !== "lens" || !section.sourcePath)
|
|
695
785
|
return;
|
|
696
786
|
for (const { ref, lensPaths } of outcomes) {
|
|
697
|
-
if (!lensPaths.
|
|
787
|
+
if (!lensPaths.includes(section.sourcePath))
|
|
698
788
|
continue;
|
|
699
789
|
if (section.learningOutcomes.some((o) => o.id === ref.id))
|
|
700
790
|
continue;
|
|
@@ -709,32 +799,7 @@ export function suggestedLensPaths(lo, files) {
|
|
|
709
799
|
if (resolved)
|
|
710
800
|
lensPaths.add(resolved);
|
|
711
801
|
}
|
|
712
|
-
return lensPaths;
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* A course teaches an outcome across units but tests it once, so a lens also
|
|
716
|
-
* carries every outcome that suggests it and is tested in any module of a
|
|
717
|
-
* course the lens's module is in. Runs after flattening, once the course
|
|
718
|
-
* progressions are resolved to module slugs; tags from the lens's own
|
|
719
|
-
* submodule come first, and the ones added here are marked testedElsewhere.
|
|
720
|
-
*/
|
|
721
|
-
export function tagCourseOutcomes(modules, courses, lensPathsOf) {
|
|
722
|
-
const moduleBySlug = new Map(modules.map((mod) => [mod.slug, mod]));
|
|
723
|
-
for (const course of courses) {
|
|
724
|
-
const courseModules = course.progression.flatMap((item) => {
|
|
725
|
-
const mod = item.slug ? moduleBySlug.get(item.slug) : undefined;
|
|
726
|
-
return item.type === "module" && mod ? [mod] : [];
|
|
727
|
-
});
|
|
728
|
-
const sections = courseModules.flatMap((mod) => mod.sections);
|
|
729
|
-
const outcomes = sections.flatMap((section) => section.type === "test" && section.sourcePath
|
|
730
|
-
? section.learningOutcomes.map((ref) => ({
|
|
731
|
-
ref: { ...ref, testedElsewhere: true },
|
|
732
|
-
lensPaths: lensPathsOf(section.sourcePath),
|
|
733
|
-
}))
|
|
734
|
-
: []);
|
|
735
|
-
for (const section of sections)
|
|
736
|
-
tagLensOutcomes(section, outcomes);
|
|
737
|
-
}
|
|
802
|
+
return [...lensPaths];
|
|
738
803
|
}
|
|
739
804
|
/**
|
|
740
805
|
* Flatten a Learning Outcome section from a module file.
|
|
@@ -771,6 +836,18 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
771
836
|
});
|
|
772
837
|
}
|
|
773
838
|
}
|
|
839
|
+
// An import that fails makes no test: a placeholder stands where the test
|
|
840
|
+
// would have been, with the error that stopped it (src/dropped.ts).
|
|
841
|
+
const failed = (sourcePath, cause = errors.at(-1)) => ({
|
|
842
|
+
sections: cause
|
|
843
|
+
? [
|
|
844
|
+
droppedSection("test", droppedContent("section", sourcePath === modulePath
|
|
845
|
+
? "Test"
|
|
846
|
+
: `Test: ${fileStem(sourcePath)}`, sourcePath, cause)),
|
|
847
|
+
]
|
|
848
|
+
: [],
|
|
849
|
+
errors,
|
|
850
|
+
});
|
|
774
851
|
// Get the source wikilink
|
|
775
852
|
const source = section.fields.source;
|
|
776
853
|
if (!source) {
|
|
@@ -781,7 +858,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
781
858
|
suggestion: "Add 'source:: [[../Learning Outcomes/filename.md|Display]]'",
|
|
782
859
|
severity: "error",
|
|
783
860
|
});
|
|
784
|
-
return
|
|
861
|
+
return failed(modulePath);
|
|
785
862
|
}
|
|
786
863
|
// Parse and resolve the wikilink
|
|
787
864
|
const wikilink = parseWikilink(source, modulePath);
|
|
@@ -798,7 +875,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
798
875
|
suggestion,
|
|
799
876
|
severity: "error",
|
|
800
877
|
});
|
|
801
|
-
return
|
|
878
|
+
return failed(modulePath);
|
|
802
879
|
}
|
|
803
880
|
const loPathResolved = resolveWikilinkPath(wikilink.path, modulePath);
|
|
804
881
|
const loPath = findFileWithExtension(loPathResolved, files);
|
|
@@ -814,7 +891,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
814
891
|
suggestion,
|
|
815
892
|
severity: "error",
|
|
816
893
|
});
|
|
817
|
-
return
|
|
894
|
+
return failed(loPathResolved);
|
|
818
895
|
}
|
|
819
896
|
// Check tier violation (module -> LO)
|
|
820
897
|
if (tierMap) {
|
|
@@ -825,7 +902,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
825
902
|
errors.push(violation);
|
|
826
903
|
}
|
|
827
904
|
if (childTier === "ignored") {
|
|
828
|
-
return
|
|
905
|
+
return failed(loPath, violation);
|
|
829
906
|
}
|
|
830
907
|
}
|
|
831
908
|
// Check for circular reference
|
|
@@ -836,7 +913,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
836
913
|
message: `Circular reference detected: ${loPath}`,
|
|
837
914
|
severity: "error",
|
|
838
915
|
});
|
|
839
|
-
return
|
|
916
|
+
return failed(loPath);
|
|
840
917
|
}
|
|
841
918
|
visitedPaths.add(loPath);
|
|
842
919
|
const loContent = files.get(loPath);
|
|
@@ -844,7 +921,7 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
844
921
|
const loResult = parseLearningOutcome(loContent, loPath);
|
|
845
922
|
errors.push(...loResult.errors);
|
|
846
923
|
if (!loResult.learningOutcome) {
|
|
847
|
-
return
|
|
924
|
+
return failed(loPath, dropCause(loResult.errors));
|
|
848
925
|
}
|
|
849
926
|
const lo = loResult.learningOutcome;
|
|
850
927
|
// Suggested lenses are validated standalone in processContent; here they
|
|
@@ -857,9 +934,11 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
857
934
|
const lensPaths = suggestedLensPaths(lo, files);
|
|
858
935
|
// Emit the test section if the LO has one with inline segments
|
|
859
936
|
if (lo.test) {
|
|
860
|
-
const
|
|
861
|
-
if (
|
|
862
|
-
sections.push(
|
|
937
|
+
const { section: test, dropped } = buildTestSection(lo.test, lo.writingMinutes, ref, loPath, files, visitedPaths, tierMap, errors);
|
|
938
|
+
if (test)
|
|
939
|
+
sections.push(attachDroppedSegments(test, dropped));
|
|
940
|
+
else if (dropped.length > 0)
|
|
941
|
+
sections.push(...failed(loPath, dropped[0].error).sections);
|
|
863
942
|
}
|
|
864
943
|
return { sections, errors, outcome: { ref, lensPaths } };
|
|
865
944
|
}
|
|
@@ -870,9 +949,22 @@ function flattenLearningOutcomeSection(section, modulePath, files, visitedPaths,
|
|
|
870
949
|
function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, inlineLens) {
|
|
871
950
|
const errors = [];
|
|
872
951
|
const sections = [];
|
|
952
|
+
// A lens that fails makes no section: a placeholder stands where it would
|
|
953
|
+
// have been, with the error that stopped it (src/dropped.ts).
|
|
954
|
+
const failed = (sourcePath, cause = errors.at(-1)) => ({
|
|
955
|
+
sections: cause
|
|
956
|
+
? [
|
|
957
|
+
droppedSection("lens", droppedContent("section", section.title.trim() ||
|
|
958
|
+
(sourcePath === modulePath ? "Lens" : fileStem(sourcePath)), sourcePath, cause)),
|
|
959
|
+
]
|
|
960
|
+
: [],
|
|
961
|
+
errors,
|
|
962
|
+
});
|
|
873
963
|
// Inline lens: has id:: + segments (no source:: at section level)
|
|
874
964
|
if (inlineLens) {
|
|
875
|
-
const { segments, errors: flattenErrors } =
|
|
965
|
+
const { segments, errors: flattenErrors, dropped, } = flattenSegments(inlineLens, modulePath, files, visitedPaths, tierMap, {
|
|
966
|
+
collapsed: true,
|
|
967
|
+
});
|
|
876
968
|
errors.push(...flattenErrors);
|
|
877
969
|
const resultSection = attachSourceLine({
|
|
878
970
|
type: "lens",
|
|
@@ -881,7 +973,7 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
881
973
|
...lensPlacementFields(section.fields, modulePath, section.line, errors),
|
|
882
974
|
...lensSectionFields(inlineLens, segments, modulePath, section.line, errors),
|
|
883
975
|
}, section.line);
|
|
884
|
-
sections.push(resultSection);
|
|
976
|
+
sections.push(attachDroppedSegments(resultSection, dropped));
|
|
885
977
|
return { sections, errors };
|
|
886
978
|
}
|
|
887
979
|
// Referenced lens: has source:: at section level
|
|
@@ -894,7 +986,7 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
894
986
|
suggestion: "Add 'source:: [[../Lenses/filename.md|Display]]' or 'id:: <uuid>' with #### segments",
|
|
895
987
|
severity: "error",
|
|
896
988
|
});
|
|
897
|
-
return
|
|
989
|
+
return failed(modulePath);
|
|
898
990
|
}
|
|
899
991
|
// Parse and resolve the wikilink
|
|
900
992
|
const wikilink = parseWikilink(source, modulePath);
|
|
@@ -911,7 +1003,7 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
911
1003
|
suggestion,
|
|
912
1004
|
severity: "error",
|
|
913
1005
|
});
|
|
914
|
-
return
|
|
1006
|
+
return failed(modulePath);
|
|
915
1007
|
}
|
|
916
1008
|
const lensPathResolved = resolveWikilinkPath(wikilink.path, modulePath);
|
|
917
1009
|
const lensPath = findFileWithExtension(lensPathResolved, files);
|
|
@@ -925,7 +1017,7 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
925
1017
|
suggestion,
|
|
926
1018
|
severity: "error",
|
|
927
1019
|
});
|
|
928
|
-
return
|
|
1020
|
+
return failed(lensPathResolved);
|
|
929
1021
|
}
|
|
930
1022
|
// Check tier violation
|
|
931
1023
|
if (tierMap) {
|
|
@@ -936,7 +1028,7 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
936
1028
|
errors.push(violation);
|
|
937
1029
|
}
|
|
938
1030
|
if (childTier === "ignored") {
|
|
939
|
-
return
|
|
1031
|
+
return failed(lensPath, violation);
|
|
940
1032
|
}
|
|
941
1033
|
}
|
|
942
1034
|
// Check for circular reference
|
|
@@ -946,17 +1038,19 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
946
1038
|
message: `Circular reference detected: ${lensPath}`,
|
|
947
1039
|
severity: "error",
|
|
948
1040
|
});
|
|
949
|
-
return
|
|
1041
|
+
return failed(lensPath);
|
|
950
1042
|
}
|
|
951
1043
|
visitedPaths.add(lensPath);
|
|
952
1044
|
const lensContent = files.get(lensPath);
|
|
953
1045
|
const lensResult = parseLens(lensContent, lensPath, files);
|
|
954
1046
|
errors.push(...lensResult.errors);
|
|
955
1047
|
if (!lensResult.lens) {
|
|
956
|
-
return
|
|
1048
|
+
return failed(lensPath, dropCause(lensResult.errors));
|
|
957
1049
|
}
|
|
958
1050
|
const lens = lensResult.lens;
|
|
959
|
-
const { segments, errors: flattenErrors } =
|
|
1051
|
+
const { segments, errors: flattenErrors, dropped, } = flattenSegments(lens, lensPath, files, visitedPaths, tierMap, {
|
|
1052
|
+
collapsed: true,
|
|
1053
|
+
});
|
|
960
1054
|
errors.push(...flattenErrors);
|
|
961
1055
|
const resultSection = attachSourceLine({
|
|
962
1056
|
type: "lens",
|
|
@@ -965,7 +1059,7 @@ function flattenLensSection(section, modulePath, files, visitedPaths, tierMap, i
|
|
|
965
1059
|
...lensPlacementFields(section.fields, modulePath, section.line, errors),
|
|
966
1060
|
...lensSectionFields(lens, segments, lensPath, section.line, errors),
|
|
967
1061
|
}, getSourceLine(lens));
|
|
968
|
-
sections.push(resultSection);
|
|
1062
|
+
sections.push(attachDroppedSegments(resultSection, dropped));
|
|
969
1063
|
return { sections, errors };
|
|
970
1064
|
}
|
|
971
1065
|
/**
|
|
@@ -1187,7 +1281,7 @@ function planLensArticles(parsedSegments, lensPath, files, visitedPaths, tierMap
|
|
|
1187
1281
|
const parsedSegment = parsedSegments[i];
|
|
1188
1282
|
if (parsedSegment.type !== "article")
|
|
1189
1283
|
continue;
|
|
1190
|
-
const entry = { segments: [], errors: [] };
|
|
1284
|
+
const entry = { segments: [], errors: [], dropped: [] };
|
|
1191
1285
|
plan.set(i, entry);
|
|
1192
1286
|
// Source comes from the parsed segment (set via inheritance in parseLens)
|
|
1193
1287
|
if (!parsedSegment.source) {
|
|
@@ -1253,22 +1347,32 @@ function planLensArticles(parsedSegments, lensPath, files, visitedPaths, tierMap
|
|
|
1253
1347
|
group.indices.push(i);
|
|
1254
1348
|
group.segments.push(parsedSegment);
|
|
1255
1349
|
}
|
|
1350
|
+
// A segment that joined no group was left out by the error it reported.
|
|
1351
|
+
const grouped = new Set([...groups.values()].flatMap((g) => g.indices));
|
|
1352
|
+
for (const [i, entry] of plan) {
|
|
1353
|
+
if (grouped.has(i))
|
|
1354
|
+
continue;
|
|
1355
|
+
entry.dropped.push(...droppedSegment("article", sourceOf(parsedSegments[i]), lensPath, entry.errors));
|
|
1356
|
+
}
|
|
1256
1357
|
for (const [articlePath, group] of groups) {
|
|
1257
1358
|
const articleContent = files.get(articlePath);
|
|
1258
1359
|
const articleFrontmatter = parseFrontmatter(articleContent, articlePath);
|
|
1259
1360
|
const fm = articleFrontmatter.frontmatter;
|
|
1260
|
-
const blockTargets = collectBlockTargets(articleFrontmatter.body);
|
|
1361
|
+
const blockTargets = lazyBlockTargets(() => collectBlockTargets(articleFrontmatter.body));
|
|
1362
|
+
const articleTitle = typeof fm.title === "string" ? fm.title : fileStem(articlePath);
|
|
1261
1363
|
const resolved = resolveArticleSource(articleContent, group.segments.map((ps) => ({ from: ps.fromAnchor, to: ps.toAnchor })), articlePath);
|
|
1262
1364
|
for (let k = 0; k < group.segments.length; k++) {
|
|
1263
1365
|
const parsedSegment = group.segments[k];
|
|
1264
1366
|
const entry = plan.get(group.indices[k]);
|
|
1265
1367
|
const excerpt = resolved[k];
|
|
1266
1368
|
if (excerpt.error) {
|
|
1267
|
-
|
|
1369
|
+
const error = {
|
|
1268
1370
|
...excerpt.error,
|
|
1269
1371
|
file: lensPath,
|
|
1270
1372
|
line: getSourceLine(parsedSegment),
|
|
1271
|
-
}
|
|
1373
|
+
};
|
|
1374
|
+
entry.errors.push(error);
|
|
1375
|
+
entry.dropped.push(droppedContent("segment", articleTitle, articlePath, error));
|
|
1272
1376
|
continue;
|
|
1273
1377
|
}
|
|
1274
1378
|
if (excerpt.warning) {
|
|
@@ -1330,6 +1434,8 @@ function planLensArticles(parsedSegments, lensPath, files, visitedPaths, tierMap
|
|
|
1330
1434
|
// by the lifted callouts it sits in.
|
|
1331
1435
|
const partSegments = [];
|
|
1332
1436
|
let elementFailed = false;
|
|
1437
|
+
// Each element, with the errors of its failure when it failed.
|
|
1438
|
+
const elements = [];
|
|
1333
1439
|
for (const part of excerpt.parts) {
|
|
1334
1440
|
if (part.kind === "prose") {
|
|
1335
1441
|
const segment = makeArticleSegment(part.content);
|
|
@@ -1343,9 +1449,9 @@ function planLensArticles(parsedSegments, lensPath, files, visitedPaths, tierMap
|
|
|
1343
1449
|
// The element is the RawSegment its `####` twin would be: the same
|
|
1344
1450
|
// parser and flattener conversions apply, resolved from the article.
|
|
1345
1451
|
// Its field errors were reported in the article's own parse pass.
|
|
1346
|
-
const
|
|
1347
|
-
const converted =
|
|
1348
|
-
convertSegment(
|
|
1452
|
+
const parse = parseRawSegment(part.raw, "lens", articlePath);
|
|
1453
|
+
const converted = parse.segment &&
|
|
1454
|
+
convertSegment(parse.segment, articlePath, files, visitedPaths, tierMap, new Map(), {
|
|
1349
1455
|
errorLine: findImportLine(articleContent, part.rawLine),
|
|
1350
1456
|
notFoundCode: `article.${ARTICLE_ELEMENT_KINDS[part.raw.type].code}-not-found`,
|
|
1351
1457
|
});
|
|
@@ -1353,8 +1459,13 @@ function planLensArticles(parsedSegments, lensPath, files, visitedPaths, tierMap
|
|
|
1353
1459
|
entry.errors.push(...converted.errors);
|
|
1354
1460
|
if (!converted?.segment) {
|
|
1355
1461
|
elementFailed = true;
|
|
1462
|
+
elements.push({
|
|
1463
|
+
raw: part.raw,
|
|
1464
|
+
failure: converted ? converted.errors : parse.errors,
|
|
1465
|
+
});
|
|
1356
1466
|
continue;
|
|
1357
1467
|
}
|
|
1468
|
+
elements.push({ raw: part.raw });
|
|
1358
1469
|
const segment = converted.segment;
|
|
1359
1470
|
if (segment.type === "video" ||
|
|
1360
1471
|
segment.type === "widget" ||
|
|
@@ -1369,10 +1480,23 @@ function planLensArticles(parsedSegments, lensPath, files, visitedPaths, tierMap
|
|
|
1369
1480
|
if (elementFailed) {
|
|
1370
1481
|
// A broken element must not leave a half-split article with dangling
|
|
1371
1482
|
// seam flags: fall back to the un-split excerpt (error already
|
|
1372
|
-
// recorded above).
|
|
1483
|
+
// recorded above). Every element of the excerpt is lost with the
|
|
1484
|
+
// split: a failed one by its own error, the others by the first.
|
|
1373
1485
|
const segment = makeArticleSegment(excerpt.content);
|
|
1374
1486
|
applyCollapsed([segment]);
|
|
1375
1487
|
entry.segments.push(segment);
|
|
1488
|
+
const causeOf = (failure) => failure && dropCause(failure);
|
|
1489
|
+
const firstCause = elements
|
|
1490
|
+
.map((e) => causeOf(e.failure))
|
|
1491
|
+
.find(Boolean);
|
|
1492
|
+
for (const { raw, failure } of elements) {
|
|
1493
|
+
const cause = causeOf(failure) ?? firstCause;
|
|
1494
|
+
if (!cause)
|
|
1495
|
+
continue;
|
|
1496
|
+
entry.dropped.push(...droppedSegment(raw.type, raw.fields.source, articlePath, [
|
|
1497
|
+
cause,
|
|
1498
|
+
]));
|
|
1499
|
+
}
|
|
1376
1500
|
continue;
|
|
1377
1501
|
}
|
|
1378
1502
|
applyCollapsed(partSegments);
|
|
@@ -1673,14 +1797,20 @@ origin) {
|
|
|
1673
1797
|
};
|
|
1674
1798
|
if (parsedSegment.openingMessage)
|
|
1675
1799
|
segment.openingMessage = parsedSegment.openingMessage;
|
|
1676
|
-
if (assessmentInstructions)
|
|
1800
|
+
if (assessmentInstructions) {
|
|
1677
1801
|
segment.assessmentInstructions = assessmentInstructions;
|
|
1802
|
+
segment.graded = true;
|
|
1803
|
+
}
|
|
1678
1804
|
if (parsedSegment.userCustomizable)
|
|
1679
1805
|
segment.userCustomizable = true;
|
|
1680
1806
|
if (parsedSegment.optional)
|
|
1681
1807
|
segment.optional = true;
|
|
1682
1808
|
if (parsedSegment.feedback)
|
|
1683
1809
|
segment.feedback = true;
|
|
1810
|
+
if (parsedSegment.feedbackInstructions)
|
|
1811
|
+
segment.feedbackInstructions = parsedSegment.feedbackInstructions;
|
|
1812
|
+
if (parsedSegment.forceFeedback)
|
|
1813
|
+
segment.forceFeedback = parsedSegment.forceFeedback;
|
|
1684
1814
|
return {
|
|
1685
1815
|
segment: attachSourceLine(segment, getSourceLine(parsedSegment)),
|
|
1686
1816
|
errors,
|
|
@@ -1840,7 +1970,7 @@ origin) {
|
|
|
1840
1970
|
export function flattenStandaloneArticle(articlePath, files, tierMap) {
|
|
1841
1971
|
const fileName = articlePath.slice(articlePath.lastIndexOf("/") + 1);
|
|
1842
1972
|
const plan = planLensArticles([{ type: "article", source: `[[${fileName}]]` }], articlePath, files, new Set(), tierMap, { collapsed: false });
|
|
1843
|
-
return plan.get(0)
|
|
1973
|
+
return plan.get(0);
|
|
1844
1974
|
}
|
|
1845
1975
|
/**
|
|
1846
1976
|
* Flatten a single Lens file into a FlattenedModule.
|
|
@@ -1849,7 +1979,7 @@ export function flattenLens(lensPath, files, tierMap, preParsedLens) {
|
|
|
1849
1979
|
const errors = [];
|
|
1850
1980
|
// Skip ignored lenses
|
|
1851
1981
|
if (tierMap?.get(lensPath) === "ignored") {
|
|
1852
|
-
return { module: null, modules: [], errors };
|
|
1982
|
+
return { module: null, modules: [], errors, dropped: [] };
|
|
1853
1983
|
}
|
|
1854
1984
|
const lensContent = files.get(lensPath);
|
|
1855
1985
|
if (!lensContent) {
|
|
@@ -1858,7 +1988,7 @@ export function flattenLens(lensPath, files, tierMap, preParsedLens) {
|
|
|
1858
1988
|
message: `Lens file not found: ${lensPath}`,
|
|
1859
1989
|
severity: "error",
|
|
1860
1990
|
});
|
|
1861
|
-
return { module: null, modules: [], errors };
|
|
1991
|
+
return { module: null, modules: [], errors, dropped: [] };
|
|
1862
1992
|
}
|
|
1863
1993
|
// Use pre-parsed lens if provided, otherwise parse
|
|
1864
1994
|
const lens = preParsedLens ??
|
|
@@ -1868,11 +1998,13 @@ export function flattenLens(lensPath, files, tierMap, preParsedLens) {
|
|
|
1868
1998
|
return lensResult.lens;
|
|
1869
1999
|
})();
|
|
1870
2000
|
if (!lens) {
|
|
1871
|
-
return { module: null, modules: [], errors };
|
|
2001
|
+
return { module: null, modules: [], errors, dropped: [] };
|
|
1872
2002
|
}
|
|
1873
2003
|
const visitedPaths = new Set([lensPath]);
|
|
1874
2004
|
// Flatten lens segments using shared helper
|
|
1875
|
-
const { segments, errors: flattenErrors } =
|
|
2005
|
+
const { segments, errors: flattenErrors, dropped, } = flattenSegments(lens, lensPath, files, visitedPaths, tierMap, {
|
|
2006
|
+
collapsed: true,
|
|
2007
|
+
});
|
|
1876
2008
|
errors.push(...flattenErrors);
|
|
1877
2009
|
// Derive title: frontmatter > first article/video segment > filename
|
|
1878
2010
|
const meta = {};
|
|
@@ -1896,13 +2028,13 @@ export function flattenLens(lensPath, files, tierMap, preParsedLens) {
|
|
|
1896
2028
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1897
2029
|
}
|
|
1898
2030
|
}
|
|
1899
|
-
const section = {
|
|
2031
|
+
const section = attachDroppedSegments({
|
|
1900
2032
|
type: "lens",
|
|
1901
2033
|
meta,
|
|
1902
2034
|
sourcePath: lensPath,
|
|
1903
2035
|
optional: false,
|
|
1904
2036
|
...lensSectionFields(lens, segments, lensPath, 1, errors),
|
|
1905
|
-
};
|
|
2037
|
+
}, dropped);
|
|
1906
2038
|
const flattenedModule = {
|
|
1907
2039
|
slug: "lens/" + fileNameToSlug(lensPath),
|
|
1908
2040
|
title: meta.title ?? fileNameToSlug(lensPath),
|
|
@@ -1910,6 +2042,11 @@ export function flattenLens(lensPath, files, tierMap, preParsedLens) {
|
|
|
1910
2042
|
sections: [section],
|
|
1911
2043
|
sourcePath: lensPath,
|
|
1912
2044
|
};
|
|
1913
|
-
return {
|
|
2045
|
+
return {
|
|
2046
|
+
module: flattenedModule,
|
|
2047
|
+
modules: [flattenedModule],
|
|
2048
|
+
errors,
|
|
2049
|
+
dropped: [placeDropped([section]).items],
|
|
2050
|
+
};
|
|
1914
2051
|
}
|
|
1915
2052
|
//# sourceMappingURL=index.js.map
|