lens-content-processor 0.3.0 → 0.5.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 +7 -2
- package/dist/bundler/article.js +19 -8
- package/dist/bundler/article.js.map +1 -1
- package/dist/bundler/video.d.ts +12 -6
- package/dist/bundler/video.js +57 -34
- package/dist/bundler/video.js.map +1 -1
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +36 -19
- package/dist/cli.js.map +1 -1
- package/dist/content-schema.js +64 -15
- package/dist/content-schema.js.map +1 -1
- package/dist/dedupe-errors.d.ts +14 -0
- package/dist/dedupe-errors.js +33 -0
- package/dist/dedupe-errors.js.map +1 -0
- package/dist/flattener/index.d.ts +22 -28
- package/dist/flattener/index.js +773 -613
- package/dist/flattener/index.js.map +1 -1
- package/dist/flattener/resolve-text-links.d.ts +25 -0
- package/dist/flattener/resolve-text-links.js +248 -0
- package/dist/flattener/resolve-text-links.js.map +1 -0
- package/dist/flattener/test-helpers.d.ts +23 -0
- package/dist/flattener/test-helpers.js +56 -0
- package/dist/flattener/test-helpers.js.map +1 -0
- package/dist/index.d.ts +66 -20
- package/dist/index.js +327 -152
- package/dist/index.js.map +1 -1
- package/dist/parser/article.d.ts +3 -2
- package/dist/parser/article.js +80 -12
- package/dist/parser/article.js.map +1 -1
- package/dist/parser/course.d.ts +1 -1
- package/dist/parser/course.js +88 -28
- package/dist/parser/course.js.map +1 -1
- package/dist/parser/frontmatter.d.ts +1 -1
- package/dist/parser/frontmatter.js +38 -12
- package/dist/parser/frontmatter.js.map +1 -1
- package/dist/parser/learning-outcome.d.ts +12 -2
- package/dist/parser/learning-outcome.js +177 -96
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/lens.d.ts +52 -28
- package/dist/parser/lens.js +283 -260
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/module.d.ts +7 -15
- package/dist/parser/module.js +139 -170
- package/dist/parser/module.js.map +1 -1
- package/dist/parser/sections.d.ts +14 -1
- package/dist/parser/sections.js +94 -81
- package/dist/parser/sections.js.map +1 -1
- package/dist/parser/video-transcript.d.ts +2 -1
- package/dist/parser/video-transcript.js +10 -6
- package/dist/parser/video-transcript.js.map +1 -1
- package/dist/reference-graph.d.ts +8 -0
- package/dist/reference-graph.js +105 -0
- package/dist/reference-graph.js.map +1 -0
- package/dist/validator/card-module-membership.d.ts +2 -0
- package/dist/validator/card-module-membership.js +80 -0
- package/dist/validator/card-module-membership.js.map +1 -0
- package/dist/validator/directives.d.ts +12 -0
- package/dist/validator/directives.js +428 -0
- package/dist/validator/directives.js.map +1 -0
- package/dist/validator/field-values.js +14 -0
- package/dist/validator/field-values.js.map +1 -1
- package/dist/validator/output-integrity.js +5 -5
- package/dist/validator/output-integrity.js.map +1 -1
- package/dist/validator/segment-fields.d.ts +1 -1
- package/dist/validator/segment-fields.js +2 -2
- package/dist/validator/segment-fields.js.map +1 -1
- package/dist/validator/test-segments.d.ts +12 -0
- package/dist/validator/test-segments.js +40 -0
- package/dist/validator/test-segments.js.map +1 -0
- package/dist/validator/url-reachability.d.ts +2 -1
- package/dist/validator/url-reachability.js +42 -20
- package/dist/validator/url-reachability.js.map +1 -1
- package/dist/validator/uuid.js +9 -6
- package/dist/validator/uuid.js.map +1 -1
- package/package.json +1 -1
- package/dist/validator/chat-precedence.d.ts +0 -9
- package/dist/validator/chat-precedence.js +0 -32
- package/dist/validator/chat-precedence.js.map +0 -1
package/dist/parser/sections.js
CHANGED
|
@@ -1,75 +1,101 @@
|
|
|
1
1
|
import { ALL_KNOWN_FIELDS } from '../content-schema.js';
|
|
2
2
|
import { levenshtein } from '../validator/field-typos.js';
|
|
3
3
|
// Valid section types per file type (exported for use by other parsers)
|
|
4
|
-
export const MODULE_SECTION_TYPES = new Set(['learning outcome', '
|
|
4
|
+
export const MODULE_SECTION_TYPES = new Set(['learning outcome', 'lens']);
|
|
5
5
|
export const LO_SECTION_TYPES = new Set(['lens', 'test']);
|
|
6
|
-
// Lens
|
|
7
|
-
// Output types are `lens-article`, `lens-video`, `page` (v2 format)
|
|
6
|
+
// Legacy: Lens H3 section types (no longer used — lenses are now flat H4 segments)
|
|
8
7
|
export const LENS_SECTION_TYPES = new Set(['page', 'article', 'video']);
|
|
9
8
|
// All known structural header types (sections + segments) for markdown heading detection
|
|
10
9
|
const ALL_STRUCTURAL_TYPES = new Set([
|
|
11
10
|
// Section types
|
|
12
|
-
'learning outcome', '
|
|
11
|
+
'learning outcome', 'lens', 'test', 'module', 'meeting', 'article', 'video',
|
|
13
12
|
// Segment types
|
|
14
|
-
'text', 'chat', '
|
|
13
|
+
'text', 'chat', 'question', 'roleplay',
|
|
15
14
|
]);
|
|
16
15
|
// Fields that commonly contain markdown with headings
|
|
17
|
-
const MARKDOWN_CONTENT_FIELDS = new Set(['content', 'instructions']);
|
|
16
|
+
const MARKDOWN_CONTENT_FIELDS = new Set(['content', 'instructions', 'ai-instructions']);
|
|
18
17
|
// Map input section names to output types for Lens files
|
|
19
18
|
export const LENS_OUTPUT_TYPE = {
|
|
20
19
|
'page': 'page',
|
|
21
20
|
'article': 'lens-article',
|
|
22
21
|
'video': 'lens-video',
|
|
23
22
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (adjLevel >= 1 && adjLevel <= 4) {
|
|
39
|
-
wrongLevelPatterns.push({ pattern: makeSectionPattern(adjLevel), level: adjLevel });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse sections from markdown content based on header hierarchy.
|
|
25
|
+
*
|
|
26
|
+
* @param content - The markdown body to parse
|
|
27
|
+
* @param parentLevel - Headers deeper than this level are candidates (0 = match any header)
|
|
28
|
+
* @param validTypes - Set of valid section/segment type names
|
|
29
|
+
* @param file - Source file path for error reporting
|
|
30
|
+
* @param flat - When true, all matching headers are siblings (for segments).
|
|
31
|
+
* When false, deeper headers nest inside preceding siblings (for sections).
|
|
32
|
+
*/
|
|
33
|
+
export function parseSections(content, parentLevel, validTypes, file = '', flat = false) {
|
|
34
|
+
const minLevel = parentLevel + 1;
|
|
35
|
+
// Match any header from minLevel to 6 with named capture groups
|
|
36
|
+
const HEADER_PATTERN = new RegExp(`^(?<hashes>#{${minLevel},6})\\s+(?<type>[^:]+?)(?:\\:\\s*(?<title>.*?))?\\s*$`, 'i');
|
|
42
37
|
const lines = content.split('\n');
|
|
43
38
|
const sections = [];
|
|
44
39
|
const errors = [];
|
|
45
40
|
let currentSection = null;
|
|
46
41
|
let currentBody = [];
|
|
47
42
|
let preHeaderWarned = false;
|
|
43
|
+
let currentSiblingLevel = 0; // Track sibling level for hierarchical mode
|
|
44
|
+
function finalizeSection() {
|
|
45
|
+
if (!currentSection)
|
|
46
|
+
return;
|
|
47
|
+
currentSection.body = currentBody.join('\n');
|
|
48
|
+
const { warnings } = parseFields(currentSection, file);
|
|
49
|
+
errors.push(...warnings);
|
|
50
|
+
// Submodule sections get recursive children parsing
|
|
51
|
+
if (currentSection.type === 'submodule') {
|
|
52
|
+
const childValidTypes = new Set([...validTypes]);
|
|
53
|
+
childValidTypes.delete('submodule'); // No nesting
|
|
54
|
+
const childResult = parseSections(currentSection.body, currentSection.level, childValidTypes, file);
|
|
55
|
+
currentSection.children = childResult.sections;
|
|
56
|
+
errors.push(...childResult.errors);
|
|
57
|
+
}
|
|
58
|
+
sections.push(currentSection);
|
|
59
|
+
}
|
|
48
60
|
for (let i = 0; i < lines.length; i++) {
|
|
49
61
|
const line = lines[i];
|
|
50
62
|
const lineNum = i + 1;
|
|
51
|
-
const headerMatch = line.match(
|
|
63
|
+
const headerMatch = line.match(HEADER_PATTERN);
|
|
52
64
|
if (headerMatch) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const { warnings } = parseFields(currentSection, file);
|
|
57
|
-
errors.push(...warnings);
|
|
58
|
-
sections.push(currentSection);
|
|
59
|
-
}
|
|
60
|
-
const rawType = headerMatch[1].trim();
|
|
65
|
+
const hashes = headerMatch.groups.hashes;
|
|
66
|
+
const headerLevel = hashes.length;
|
|
67
|
+
const rawType = headerMatch.groups.type.trim();
|
|
61
68
|
const normalizedType = rawType.toLowerCase();
|
|
62
|
-
const title = (headerMatch
|
|
69
|
+
const title = (headerMatch.groups.title ?? '').trim();
|
|
70
|
+
// Check if this type is valid for the current context
|
|
63
71
|
if (!validTypes.has(normalizedType)) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
// Only error for unknown types at sibling level (not deeper body content)
|
|
73
|
+
const wouldBeSibling = flat || currentSiblingLevel === 0 || headerLevel <= currentSiblingLevel;
|
|
74
|
+
if (wouldBeSibling) {
|
|
75
|
+
const capitalized = [...validTypes].map(t => t.split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' '));
|
|
76
|
+
errors.push({
|
|
77
|
+
file,
|
|
78
|
+
line: lineNum,
|
|
79
|
+
message: `Unknown section type: ${rawType}`,
|
|
80
|
+
suggestion: `Valid types: ${capitalized.join(', ')}`,
|
|
81
|
+
severity: 'error',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Treat as body content of current section
|
|
85
|
+
if (currentSection) {
|
|
86
|
+
currentBody.push(line);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
72
89
|
}
|
|
90
|
+
// Sibling boundary rule (hierarchical mode only):
|
|
91
|
+
// Deeper headers go into the preceding sibling's body
|
|
92
|
+
if (!flat && currentSiblingLevel > 0 && headerLevel > currentSiblingLevel) {
|
|
93
|
+
currentBody.push(line);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// New sibling — finalize previous section
|
|
97
|
+
finalizeSection();
|
|
98
|
+
currentSiblingLevel = headerLevel;
|
|
73
99
|
currentSection = {
|
|
74
100
|
type: normalizedType.replaceAll(' ', '-'),
|
|
75
101
|
title,
|
|
@@ -77,54 +103,33 @@ export function parseSections(content, headerLevel, validTypes, file = '') {
|
|
|
77
103
|
fields: {},
|
|
78
104
|
body: '',
|
|
79
105
|
line: lineNum,
|
|
106
|
+
level: headerLevel,
|
|
80
107
|
};
|
|
81
108
|
currentBody = [];
|
|
109
|
+
preHeaderWarned = false;
|
|
82
110
|
}
|
|
83
111
|
else {
|
|
84
112
|
if (currentSection) {
|
|
85
113
|
currentBody.push(line);
|
|
86
114
|
}
|
|
87
|
-
else
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
message: `Found '${actual} ${rawType}:' (heading level ${level}) but expected heading level ${headerLevel} (${expected} ${rawType}:)`,
|
|
100
|
-
suggestion: `Change '${actual}' to '${expected}'`,
|
|
101
|
-
severity: 'warning',
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
115
|
+
else {
|
|
116
|
+
// Before first section header — suppress warnings for field:: lines and blank lines
|
|
117
|
+
const isFieldLine = /^[\w-]+::\s/.test(line);
|
|
118
|
+
if (line.trim() && !isFieldLine && !preHeaderWarned) {
|
|
119
|
+
preHeaderWarned = true;
|
|
120
|
+
errors.push({
|
|
121
|
+
file,
|
|
122
|
+
line: lineNum,
|
|
123
|
+
message: 'Content found before first section header — this text will be ignored',
|
|
124
|
+
suggestion: 'Move this text into a section, or remove it',
|
|
125
|
+
severity: 'error',
|
|
126
|
+
});
|
|
106
127
|
}
|
|
107
128
|
}
|
|
108
|
-
if (!currentSection && line.trim() && !preHeaderWarned) {
|
|
109
|
-
// Non-blank content before any section header — will be silently lost
|
|
110
|
-
preHeaderWarned = true;
|
|
111
|
-
errors.push({
|
|
112
|
-
file,
|
|
113
|
-
line: lineNum,
|
|
114
|
-
message: 'Content found before first section header — this text will be ignored',
|
|
115
|
-
suggestion: 'Move this text into a section, or remove it',
|
|
116
|
-
severity: 'warning',
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
// Don't forget last section
|
|
122
|
-
|
|
123
|
-
currentSection.body = currentBody.join('\n');
|
|
124
|
-
const { warnings } = parseFields(currentSection, file);
|
|
125
|
-
errors.push(...warnings);
|
|
126
|
-
sections.push(currentSection);
|
|
127
|
-
}
|
|
132
|
+
finalizeSection();
|
|
128
133
|
return { sections, errors };
|
|
129
134
|
}
|
|
130
135
|
const FIELD_PATTERN = /^([\w-]+)::\s*(.*)$/;
|
|
@@ -187,7 +192,7 @@ function parseFields(section, file) {
|
|
|
187
192
|
line: lineNum,
|
|
188
193
|
message: `Duplicate field '${currentField}' (previous value will be overwritten)`,
|
|
189
194
|
suggestion: `Remove the duplicate '${currentField}::' definition`,
|
|
190
|
-
severity: '
|
|
195
|
+
severity: 'error',
|
|
191
196
|
});
|
|
192
197
|
}
|
|
193
198
|
seenFields.add(currentField);
|
|
@@ -206,7 +211,7 @@ function parseFields(section, file) {
|
|
|
206
211
|
line: lineNum,
|
|
207
212
|
message: `Found '${singleColonMatch[1]}:' with single colon — did you mean '${singleColonMatch[1]}::'?`,
|
|
208
213
|
suggestion: `Change '${singleColonMatch[1]}:' to '${singleColonMatch[1]}::' (double colon)`,
|
|
209
|
-
severity: '
|
|
214
|
+
severity: 'error',
|
|
210
215
|
});
|
|
211
216
|
}
|
|
212
217
|
else if (line.trim() && !freeTextWarned) {
|
|
@@ -217,7 +222,7 @@ function parseFields(section, file) {
|
|
|
217
222
|
line: lineNum,
|
|
218
223
|
message: `Text outside of a field:: definition will be ignored: "${preview}"`,
|
|
219
224
|
suggestion: 'Place this text inside a field (e.g., content:: your text), or remove it',
|
|
220
|
-
severity: '
|
|
225
|
+
severity: 'error',
|
|
221
226
|
});
|
|
222
227
|
}
|
|
223
228
|
}
|
|
@@ -226,6 +231,14 @@ function parseFields(section, file) {
|
|
|
226
231
|
if (currentField) {
|
|
227
232
|
section.fields[currentField] = currentValue.join('\n').trim();
|
|
228
233
|
}
|
|
234
|
+
// Unescape \# → # at start of lines in markdown content fields.
|
|
235
|
+
// Authors use \## to prevent the parser from treating ## as a structural header;
|
|
236
|
+
// the escape must be stripped before the content reaches consumers (e.g. ReactMarkdown).
|
|
237
|
+
for (const field of MARKDOWN_CONTENT_FIELDS) {
|
|
238
|
+
if (section.fields[field]) {
|
|
239
|
+
section.fields[field] = section.fields[field].replace(/^\\(#+)/gm, '$1');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
229
242
|
return { warnings };
|
|
230
243
|
}
|
|
231
244
|
//# sourceMappingURL=sections.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sections.js","sourceRoot":"","sources":["../../src/parser/sections.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"sections.js","sourceRoot":"","sources":["../../src/parser/sections.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ContentError } from
|
|
1
|
+
import type { ContentError } from "../index.js";
|
|
2
2
|
export interface ParsedVideoTranscript {
|
|
3
3
|
title: string;
|
|
4
4
|
channel: string;
|
|
5
5
|
url: string;
|
|
6
|
+
allowUnreachableUrl?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export interface VideoTranscriptParseResult {
|
|
8
9
|
transcript: ParsedVideoTranscript | null;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { parseFrontmatter } from
|
|
2
|
-
import { validateFrontmatter } from
|
|
1
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
2
|
+
import { validateFrontmatter } from "../validator/validate-frontmatter.js";
|
|
3
|
+
import { stripAuthoringMarkup } from "./lens.js";
|
|
3
4
|
export function parseVideoTranscript(content, file) {
|
|
4
5
|
const errors = [];
|
|
6
|
+
// Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
|
|
7
|
+
content = stripAuthoringMarkup(content);
|
|
5
8
|
const frontmatterResult = parseFrontmatter(content, file);
|
|
6
9
|
if (frontmatterResult.error) {
|
|
7
10
|
errors.push(frontmatterResult.error);
|
|
@@ -9,14 +12,14 @@ export function parseVideoTranscript(content, file) {
|
|
|
9
12
|
}
|
|
10
13
|
const { frontmatter } = frontmatterResult;
|
|
11
14
|
// Validate frontmatter against schema (typo detection + required fields)
|
|
12
|
-
const frontmatterErrors = validateFrontmatter(frontmatter,
|
|
15
|
+
const frontmatterErrors = validateFrontmatter(frontmatter, "video-transcript", file);
|
|
13
16
|
errors.push(...frontmatterErrors);
|
|
14
|
-
const hasRequiredError = frontmatterErrors.some(e => e.severity ===
|
|
17
|
+
const hasRequiredError = frontmatterErrors.some((e) => e.severity === "error");
|
|
15
18
|
if (hasRequiredError) {
|
|
16
19
|
return { transcript: null, errors };
|
|
17
20
|
}
|
|
18
21
|
// Check text fields for wikilinks (not url — that's a URL)
|
|
19
|
-
const textFields = [
|
|
22
|
+
const textFields = ["title", "channel"];
|
|
20
23
|
for (const field of textFields) {
|
|
21
24
|
const value = String(frontmatter[field]);
|
|
22
25
|
if (/\[\[.+?\]\]/.test(value)) {
|
|
@@ -25,7 +28,7 @@ export function parseVideoTranscript(content, file) {
|
|
|
25
28
|
line: 2,
|
|
26
29
|
message: `Field '${field}' contains a wikilink — use plain text`,
|
|
27
30
|
suggestion: `Remove [[...]] from '${field}'`,
|
|
28
|
-
severity:
|
|
31
|
+
severity: "error",
|
|
29
32
|
});
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -33,6 +36,7 @@ export function parseVideoTranscript(content, file) {
|
|
|
33
36
|
title: String(frontmatter.title),
|
|
34
37
|
channel: String(frontmatter.channel),
|
|
35
38
|
url: String(frontmatter.url),
|
|
39
|
+
allowUnreachableUrl: frontmatter.allowUnreachableUrl === true,
|
|
36
40
|
};
|
|
37
41
|
return { transcript, errors };
|
|
38
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-transcript.js","sourceRoot":"","sources":["../../src/parser/video-transcript.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"video-transcript.js","sourceRoot":"","sources":["../../src/parser/video-transcript.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAgBjD,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,IAAY;IAEZ,MAAM,MAAM,GAAmB,EAAE,CAAC;IAElC,2EAA2E;IAC3E,OAAO,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAExC,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC1D,IAAI,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACrC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,GAAG,iBAAiB,CAAC;IAE1C,yEAAyE;IACzE,MAAM,iBAAiB,GAAG,mBAAmB,CAC3C,WAAW,EACX,kBAAkB,EAClB,IAAI,CACL,CAAC;IACF,MAAM,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC;IAElC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,IAAI,CAC7C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAC9B,CAAC;IACF,IAAI,gBAAgB,EAAE,CAAC;QACrB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;IAED,2DAA2D;IAC3D,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,SAAS,CAAU,CAAC;IACjD,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QACzC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI;gBACJ,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,UAAU,KAAK,wCAAwC;gBAChE,UAAU,EAAE,wBAAwB,KAAK,GAAG;gBAC5C,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAA0B;QACxC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC;QAChC,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC;QACpC,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC;QAC5B,mBAAmB,EAAE,WAAW,CAAC,mBAAmB,KAAK,IAAI;KAC9D,CAAC;IAEF,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map each file path to the sorted list of course slugs whose reference chain
|
|
3
|
+
* transitively reaches it. Files reached by no course are simply absent (an
|
|
4
|
+
* absent or empty list means "orphaned").
|
|
5
|
+
*
|
|
6
|
+
* @param files all content files (path -> raw markdown)
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildFileToCourses(files: Map<string, string>): Record<string, string[]>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/reference-graph.ts
|
|
2
|
+
//
|
|
3
|
+
// Builds a file -> course-slugs map by walking the reference chain of each
|
|
4
|
+
// course (course -> module -> learning outcome -> lens -> article/transcript).
|
|
5
|
+
//
|
|
6
|
+
// Every reference in the content model is a wikilink: course->module sits in an
|
|
7
|
+
// H1 title, module->LO/lens and lens->article are `source::` embeds (`![[...]]`
|
|
8
|
+
// / `[[...]]`), and `::card[[...]]` links cross-reference lenses/modules. A
|
|
9
|
+
// single wikilink scan over the markup-stripped content therefore captures every
|
|
10
|
+
// edge type uniformly, with no per-parser wiring to drift out of sync.
|
|
11
|
+
//
|
|
12
|
+
// The scan is intentionally permissive (a wikilink in prose still counts as a
|
|
13
|
+
// reference). For the consumer — "which courses use this file / is it orphaned"
|
|
14
|
+
// — over-capture is the safe direction: it never marks a still-referenced file
|
|
15
|
+
// as orphaned.
|
|
16
|
+
import { resolveWikilinkPath, findFileWithExtension, } from "./parser/wikilink.js";
|
|
17
|
+
import { stripAuthoringMarkup } from "./parser/lens.js";
|
|
18
|
+
import { parseFrontmatter } from "./parser/frontmatter.js";
|
|
19
|
+
// Matches [[path]], [[path|alias]], [[path#anchor]] and (via the leading `[[`)
|
|
20
|
+
// the `![[...]]` / `::card[[...]]` forms too. Captures only the path segment.
|
|
21
|
+
const WIKILINK_RE = /\[\[\s*([^\]|#]+?)\s*(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]/g;
|
|
22
|
+
function isCourseFile(path) {
|
|
23
|
+
return path.startsWith("courses/") || path.includes("/courses/");
|
|
24
|
+
}
|
|
25
|
+
function isTranscriptMd(path) {
|
|
26
|
+
return (path.endsWith(".md") &&
|
|
27
|
+
(path.startsWith("video_transcripts/") ||
|
|
28
|
+
path.includes("/video_transcripts/")));
|
|
29
|
+
}
|
|
30
|
+
/** Resolve every wikilink in `content` to a real file path in `files`. */
|
|
31
|
+
function outgoingReferences(path, content, files) {
|
|
32
|
+
const targets = new Set();
|
|
33
|
+
const stripped = stripAuthoringMarkup(content);
|
|
34
|
+
for (const match of stripped.matchAll(WIKILINK_RE)) {
|
|
35
|
+
const rawTarget = match[1].trim();
|
|
36
|
+
if (!rawTarget)
|
|
37
|
+
continue;
|
|
38
|
+
const resolved = resolveWikilinkPath(rawTarget, path);
|
|
39
|
+
const actual = findFileWithExtension(resolved, files);
|
|
40
|
+
if (actual && actual !== path)
|
|
41
|
+
targets.add(actual);
|
|
42
|
+
}
|
|
43
|
+
// A transcript's `.timestamps.json` is paired by filename, not wikilinked, but
|
|
44
|
+
// belongs to the same course as its `.md`, so carry it along.
|
|
45
|
+
if (isTranscriptMd(path)) {
|
|
46
|
+
const ts = path.replace(/\.md$/, ".timestamps.json");
|
|
47
|
+
if (files.has(ts))
|
|
48
|
+
targets.add(ts);
|
|
49
|
+
}
|
|
50
|
+
return targets;
|
|
51
|
+
}
|
|
52
|
+
/** Read a course file's frontmatter slug, or null if absent/unparseable. */
|
|
53
|
+
function courseSlug(path, content) {
|
|
54
|
+
const { frontmatter } = parseFrontmatter(stripAuthoringMarkup(content), path);
|
|
55
|
+
const slug = frontmatter.slug;
|
|
56
|
+
return typeof slug === "string" && slug.trim() ? slug.trim() : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Map each file path to the sorted list of course slugs whose reference chain
|
|
60
|
+
* transitively reaches it. Files reached by no course are simply absent (an
|
|
61
|
+
* absent or empty list means "orphaned").
|
|
62
|
+
*
|
|
63
|
+
* @param files all content files (path -> raw markdown)
|
|
64
|
+
*/
|
|
65
|
+
export function buildFileToCourses(files) {
|
|
66
|
+
// 1. Adjacency: file -> files it references (resolved to real files only).
|
|
67
|
+
const adjacency = new Map();
|
|
68
|
+
for (const [path, content] of files.entries()) {
|
|
69
|
+
adjacency.set(path, outgoingReferences(path, content, files));
|
|
70
|
+
}
|
|
71
|
+
// 2. BFS from each course root, tagging every reached file with the slug.
|
|
72
|
+
const fileToCourses = new Map();
|
|
73
|
+
for (const [coursePath, courseContent] of files.entries()) {
|
|
74
|
+
if (!isCourseFile(coursePath))
|
|
75
|
+
continue;
|
|
76
|
+
const slug = courseSlug(coursePath, courseContent);
|
|
77
|
+
if (!slug)
|
|
78
|
+
continue; // course that failed to parse a slug — skip
|
|
79
|
+
const queue = [coursePath];
|
|
80
|
+
const seen = new Set([coursePath]);
|
|
81
|
+
// Head index instead of Array.shift() to keep the BFS dequeue O(1).
|
|
82
|
+
for (let head = 0; head < queue.length; head++) {
|
|
83
|
+
const file = queue[head];
|
|
84
|
+
let slugs = fileToCourses.get(file);
|
|
85
|
+
if (!slugs) {
|
|
86
|
+
slugs = new Set();
|
|
87
|
+
fileToCourses.set(file, slugs);
|
|
88
|
+
}
|
|
89
|
+
slugs.add(slug);
|
|
90
|
+
for (const next of adjacency.get(file) ?? []) {
|
|
91
|
+
if (!seen.has(next)) {
|
|
92
|
+
seen.add(next);
|
|
93
|
+
queue.push(next);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 3. Serialize to a stable, sorted plain object for JSON output.
|
|
99
|
+
const result = {};
|
|
100
|
+
for (const [file, slugs] of fileToCourses.entries()) {
|
|
101
|
+
result[file] = [...slugs].sort();
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=reference-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-graph.js","sourceRoot":"","sources":["../src/reference-graph.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,EAAE;AACF,2EAA2E;AAC3E,+EAA+E;AAC/E,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,4EAA4E;AAC5E,iFAAiF;AACjF,uEAAuE;AACvE,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,eAAe;AAEf,OAAO,EACL,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,+EAA+E;AAC/E,8EAA8E;AAC9E,MAAM,WAAW,GAAG,sDAAsD,CAAC;AAE3E,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IAClC,OAAO,CACL,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QACpB,CAAC,IAAI,CAAC,UAAU,CAAC,oBAAoB,CAAC;YACpC,IAAI,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAC,CACxC,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,SAAS,kBAAkB,CACzB,IAAY,EACZ,OAAe,EACf,KAA0B;IAE1B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC/C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QACnD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAClC,IAAI,CAAC,SAAS;YAAE,SAAS;QACzB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACtD,MAAM,MAAM,GAAG,qBAAqB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACtD,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC;IACD,+EAA+E;IAC/E,8DAA8D;IAC9D,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;QACrD,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,4EAA4E;AAC5E,SAAS,UAAU,CAAC,IAAY,EAAE,OAAe;IAC/C,MAAM,EAAE,WAAW,EAAE,GAAG,gBAAgB,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC;IAC9B,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAA0B;IAE1B,2EAA2E;IAC3E,MAAM,SAAS,GAAG,IAAI,GAAG,EAAuB,CAAC;IACjD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,0EAA0E;IAC1E,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuB,CAAC;IACrD,KAAK,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC;YAAE,SAAS;QACxC,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI;YAAE,SAAS,CAAC,4CAA4C;QACjE,MAAM,KAAK,GAAa,CAAC,UAAU,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,UAAU,CAAC,CAAC,CAAC;QAC3C,oEAAoE;QACpE,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;YAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACzB,IAAI,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;gBAC1B,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACjC,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAChB,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC7C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBACpB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACf,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,MAAM,MAAM,GAA6B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC;QACpD,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const CARD_HTML_RE = /<div data-lens-card='([^']+)'><\/div>/g;
|
|
2
|
+
export function validateCardModuleMembership(modules) {
|
|
3
|
+
const errors = [];
|
|
4
|
+
// A module that contains `# Submodule:` boundaries is split into multiple
|
|
5
|
+
// FlattenedModules at flatten time (slug `parent/sub-1`, `parent/sub-2`, …),
|
|
6
|
+
// each carrying its own sections. From an authoring perspective these are
|
|
7
|
+
// still one curriculum unit — a hidden Lens import in one submodule should
|
|
8
|
+
// satisfy a card link from a sibling submodule. Group siblings by their
|
|
9
|
+
// parent slug for the membership check.
|
|
10
|
+
const groupContentIds = new Map();
|
|
11
|
+
for (const mod of modules) {
|
|
12
|
+
if (mod.slug.startsWith('lens/'))
|
|
13
|
+
continue;
|
|
14
|
+
const groupKey = mod.parentSlug ?? mod.slug;
|
|
15
|
+
let s = groupContentIds.get(groupKey);
|
|
16
|
+
if (!s) {
|
|
17
|
+
s = new Set();
|
|
18
|
+
groupContentIds.set(groupKey, s);
|
|
19
|
+
}
|
|
20
|
+
for (const section of mod.sections) {
|
|
21
|
+
if (section.contentId)
|
|
22
|
+
s.add(section.contentId);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const mod of modules) {
|
|
26
|
+
// Skip standalone-lens pseudo-modules (lens/ prefix). They are single-lens
|
|
27
|
+
// entries created for lenses not imported by any real module. Checking
|
|
28
|
+
// card membership from their narrow perspective generates false positives.
|
|
29
|
+
if (mod.slug.startsWith('lens/'))
|
|
30
|
+
continue;
|
|
31
|
+
const groupKey = mod.parentSlug ?? mod.slug;
|
|
32
|
+
const groupIds = groupContentIds.get(groupKey) ?? new Set();
|
|
33
|
+
for (const section of mod.sections) {
|
|
34
|
+
for (const seg of section.segments) {
|
|
35
|
+
if (seg.type !== 'text')
|
|
36
|
+
continue;
|
|
37
|
+
if (!seg.content.includes('data-lens-card='))
|
|
38
|
+
continue;
|
|
39
|
+
const matches = seg.content.matchAll(CARD_HTML_RE);
|
|
40
|
+
for (const match of matches) {
|
|
41
|
+
let data;
|
|
42
|
+
try {
|
|
43
|
+
data = JSON.parse(match[1].replace(/'/g, "'"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (data.targetType !== 'lens')
|
|
49
|
+
continue;
|
|
50
|
+
const targetId = data.contentId;
|
|
51
|
+
if (!targetId)
|
|
52
|
+
continue; // missing contentId is a separate validation problem
|
|
53
|
+
if (groupIds.has(targetId))
|
|
54
|
+
continue; // same module-group: OK
|
|
55
|
+
if (data.allowExternal)
|
|
56
|
+
continue; // explicit external opt-in: OK
|
|
57
|
+
// Echo back the exact directive with the flag appended so the
|
|
58
|
+
// author can copy-paste it. Falls back to a `<path>` placeholder
|
|
59
|
+
// for older card data without targetPath.
|
|
60
|
+
const directive = data.targetPath
|
|
61
|
+
? `\`::card[[${data.targetPath}]]{allow-external}\``
|
|
62
|
+
: '`::card[[<path>]]{allow-external}`';
|
|
63
|
+
errors.push({
|
|
64
|
+
// Card directives live inside the section's source — typically an
|
|
65
|
+
// imported Lens, not the module file itself. Prefer the section's
|
|
66
|
+
// sourcePath so the error points at the file the author needs to
|
|
67
|
+
// edit. Falls back to mod.sourcePath for inline lenses defined in
|
|
68
|
+
// the module file directly.
|
|
69
|
+
file: section.sourcePath ?? mod.sourcePath ?? `module:${mod.slug}`,
|
|
70
|
+
message: `Card targets Lens "${data.title ?? targetId}" which is not imported by module "${groupKey}".`,
|
|
71
|
+
suggestion: `Either import the Lens into module "${groupKey}", or mark the directive as intentional by appending {allow-external}: ${directive}.`,
|
|
72
|
+
severity: 'error',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=card-module-membership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-module-membership.js","sourceRoot":"","sources":["../../src/validator/card-module-membership.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContentError } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Main validator for article body content.
|
|
4
|
+
* Checks for unclosed containers, unsupported names, empty containers,
|
|
5
|
+
* attribute typos, and {open} on closing markers.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateDirectives(body: string, file: string, bodyStartLine: number): ContentError[];
|
|
8
|
+
/**
|
|
9
|
+
* Lightweight check for directives in non-article content (e.g., lens text segments).
|
|
10
|
+
* Returns a single warning per match since directives won't render outside articles.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectDirectivesInNonArticle(content: string, file: string, line: number): ContentError[];
|