lens-content-processor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/bundler/article.d.ts +7 -2
  2. package/dist/bundler/article.js +19 -8
  3. package/dist/bundler/article.js.map +1 -1
  4. package/dist/content-schema.js +3 -2
  5. package/dist/content-schema.js.map +1 -1
  6. package/dist/flattener/index.d.ts +20 -1
  7. package/dist/flattener/index.js +476 -72
  8. package/dist/flattener/index.js.map +1 -1
  9. package/dist/flattener/test-helpers.d.ts +23 -0
  10. package/dist/flattener/test-helpers.js +58 -0
  11. package/dist/flattener/test-helpers.js.map +1 -0
  12. package/dist/index.d.ts +18 -2
  13. package/dist/index.js +53 -23
  14. package/dist/index.js.map +1 -1
  15. package/dist/parser/article.d.ts +1 -1
  16. package/dist/parser/article.js +8 -1
  17. package/dist/parser/article.js.map +1 -1
  18. package/dist/parser/course.js +11 -8
  19. package/dist/parser/course.js.map +1 -1
  20. package/dist/parser/learning-outcome.d.ts +7 -0
  21. package/dist/parser/learning-outcome.js +129 -86
  22. package/dist/parser/learning-outcome.js.map +1 -1
  23. package/dist/parser/lens.d.ts +26 -1
  24. package/dist/parser/lens.js +99 -10
  25. package/dist/parser/lens.js.map +1 -1
  26. package/dist/parser/module.d.ts +1 -1
  27. package/dist/parser/module.js +13 -8
  28. package/dist/parser/module.js.map +1 -1
  29. package/dist/parser/sections.d.ts +2 -0
  30. package/dist/parser/sections.js +53 -19
  31. package/dist/parser/sections.js.map +1 -1
  32. package/dist/parser/video-transcript.js +3 -0
  33. package/dist/parser/video-transcript.js.map +1 -1
  34. package/dist/validator/directives.d.ts +12 -0
  35. package/dist/validator/directives.js +428 -0
  36. package/dist/validator/directives.js.map +1 -0
  37. package/dist/validator/field-values.js +14 -0
  38. package/dist/validator/field-values.js.map +1 -1
  39. package/dist/validator/url-reachability.js +3 -1
  40. package/dist/validator/url-reachability.js.map +1 -1
  41. package/dist/validator/uuid.js +9 -6
  42. package/dist/validator/uuid.js.map +1 -1
  43. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"course.js","sourceRoot":"","sources":["../../src/parser/course.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"course.js","sourceRoot":"","sources":["../../src/parser/course.ts"],"names":[],"mappings":"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"}
@@ -10,11 +10,18 @@ export interface ParsedTestRef {
10
10
  resolvedPath?: string;
11
11
  segments: ParsedLensSegment[];
12
12
  }
13
+ export interface ParsedSubmoduleGroup {
14
+ title: string;
15
+ customSlug?: string;
16
+ lenses: ParsedLensRef[];
17
+ test?: ParsedTestRef;
18
+ }
13
19
  export interface ParsedLearningOutcome {
14
20
  id: string;
15
21
  lenses: ParsedLensRef[];
16
22
  test?: ParsedTestRef;
17
23
  discussion?: string;
24
+ submodules?: ParsedSubmoduleGroup[];
18
25
  }
19
26
  export interface LearningOutcomeParseResult {
20
27
  learningOutcome: ParsedLearningOutcome | null;
@@ -3,11 +3,11 @@ import { parseSections, LO_SECTION_TYPES } from './sections.js';
3
3
  import { parseWikilink, resolveWikilinkPath, hasRelativePath } from './wikilink.js';
4
4
  import { detectFieldTypos } from '../validator/field-typos.js';
5
5
  import { validateFrontmatter } from '../validator/validate-frontmatter.js';
6
- import { parseSegments, convertSegment, stripObsidianComments } from './lens.js';
6
+ import { parseSegments, convertSegment, stripAuthoringMarkup } from './lens.js';
7
7
  export function parseLearningOutcome(content, file) {
8
8
  const errors = [];
9
- // Strip Obsidian comments before parsing
10
- content = stripObsidianComments(content);
9
+ // Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
10
+ content = stripAuthoringMarkup(content);
11
11
  // Step 1: Parse frontmatter and validate id field
12
12
  const frontmatterResult = parseFrontmatter(content, file);
13
13
  if (frontmatterResult.error) {
@@ -31,8 +31,9 @@ export function parseLearningOutcome(content, file) {
31
31
  });
32
32
  return { learningOutcome: null, errors };
33
33
  }
34
- // Step 2: Parse sections with H2 level and LO_SECTION_TYPES ('lens', 'test')
35
- const sectionsResult = parseSections(body, 2, LO_SECTION_TYPES, file);
34
+ // Step 2: Parse sections with H2 level and LO_SECTION_TYPES + 'submodule'
35
+ const loSectionTypes = new Set([...LO_SECTION_TYPES, 'submodule']);
36
+ const sectionsResult = parseSections(body, 2, loSectionTypes, file);
36
37
  // Adjust line numbers to account for frontmatter
37
38
  for (const error of sectionsResult.errors) {
38
39
  if (error.line) {
@@ -43,31 +44,61 @@ export function parseLearningOutcome(content, file) {
43
44
  for (const section of sectionsResult.sections) {
44
45
  section.line += bodyStartLine - 1;
45
46
  }
46
- // Step 3: Extract lens refs with source field and optional flag
47
- const lenses = [];
48
- let testRef;
49
- for (const section of sectionsResult.sections) {
50
- // Detect likely typos in field names
47
+ // Helper: extract a lens ref from a parsed section
48
+ function extractLensRef(section) {
51
49
  const typoWarnings = detectFieldTypos(section.fields, file, section.line);
52
50
  errors.push(...typoWarnings);
53
- if (section.type === 'lens') {
54
- const source = section.fields.source;
55
- if (!source) {
56
- errors.push({
57
- file,
58
- line: section.line,
59
- message: 'Lens section missing source:: field',
60
- suggestion: "Add 'source:: [[../Lenses/filename.md|Display]]' to the lens section",
61
- severity: 'error',
62
- });
63
- continue;
64
- }
65
- // Parse wikilink and resolve path
51
+ const source = section.fields.source;
52
+ if (!source) {
53
+ errors.push({
54
+ file,
55
+ line: section.line,
56
+ message: 'Lens section missing source:: field',
57
+ suggestion: "Add 'source:: [[../Lenses/filename.md|Display]]' to the lens section",
58
+ severity: 'error',
59
+ });
60
+ return null;
61
+ }
62
+ const wikilink = parseWikilink(source);
63
+ if (!wikilink || wikilink.error) {
64
+ const suggestion = wikilink?.correctedPath
65
+ ? `Did you mean '[[${wikilink.correctedPath}]]'?`
66
+ : 'Use format [[../Lenses/filename.md|Display Text]]';
67
+ errors.push({
68
+ file,
69
+ line: section.line,
70
+ message: wikilink?.error
71
+ ? `${wikilink.error} in source:: field: ${source}`
72
+ : `Invalid wikilink format in source:: field: ${source}`,
73
+ suggestion,
74
+ severity: 'error',
75
+ });
76
+ return null;
77
+ }
78
+ if (!hasRelativePath(wikilink.path)) {
79
+ errors.push({
80
+ file,
81
+ line: section.line,
82
+ message: `source:: path must be relative (contain /): ${wikilink.path}`,
83
+ suggestion: 'Use format [[../Lenses/filename.md|Display Text]] with relative path',
84
+ severity: 'error',
85
+ });
86
+ return null;
87
+ }
88
+ const resolvedPath = resolveWikilinkPath(wikilink.path, file);
89
+ const optional = section.fields.optional?.toLowerCase() === 'true';
90
+ return { source, resolvedPath, optional };
91
+ }
92
+ // Helper: extract test ref from a parsed section
93
+ function extractTestRef(section) {
94
+ const source = section.fields.source;
95
+ let resolvedPath;
96
+ if (source) {
66
97
  const wikilink = parseWikilink(source);
67
98
  if (!wikilink || wikilink.error) {
68
99
  const suggestion = wikilink?.correctedPath
69
100
  ? `Did you mean '[[${wikilink.correctedPath}]]'?`
70
- : 'Use format [[../Lenses/filename.md|Display Text]]';
101
+ : 'Use format [[../Tests/filename.md|Display Text]]';
71
102
  errors.push({
72
103
  file,
73
104
  line: section.line,
@@ -77,78 +108,89 @@ export function parseLearningOutcome(content, file) {
77
108
  suggestion,
78
109
  severity: 'error',
79
110
  });
80
- continue;
81
- }
82
- // Require relative path (must contain /)
83
- if (!hasRelativePath(wikilink.path)) {
84
- errors.push({
85
- file,
86
- line: section.line,
87
- message: `source:: path must be relative (contain /): ${wikilink.path}`,
88
- suggestion: 'Use format [[../Lenses/filename.md|Display Text]] with relative path',
89
- severity: 'error',
90
- });
91
- continue;
111
+ return undefined;
92
112
  }
93
- const resolvedPath = resolveWikilinkPath(wikilink.path, file);
94
- const optional = section.fields.optional?.toLowerCase() === 'true';
95
- lenses.push({
96
- source,
97
- resolvedPath,
98
- optional,
99
- });
113
+ resolvedPath = resolveWikilinkPath(wikilink.path, file);
100
114
  }
101
- else if (section.type === 'test') {
102
- const source = section.fields.source;
103
- let resolvedPath;
104
- if (source) {
105
- // Parse wikilink and resolve path
106
- const wikilink = parseWikilink(source);
107
- if (!wikilink || wikilink.error) {
108
- const suggestion = wikilink?.correctedPath
109
- ? `Did you mean '[[${wikilink.correctedPath}]]'?`
110
- : 'Use format [[../Tests/filename.md|Display Text]]';
111
- errors.push({
112
- file,
113
- line: section.line,
114
- message: wikilink?.error
115
- ? `${wikilink.error} in source:: field: ${source}`
116
- : `Invalid wikilink format in source:: field: ${source}`,
117
- suggestion,
118
- severity: 'error',
119
- });
120
- continue;
121
- }
122
- resolvedPath = resolveWikilinkPath(wikilink.path, file);
115
+ const { segments: rawSegments, errors: segmentErrors } = parseSegments(section.body, section.line + 1, file);
116
+ for (const err of segmentErrors) {
117
+ if (err.line) {
118
+ err.line += bodyStartLine - 1;
123
119
  }
124
- // Parse inline H4 segments (#### Question, #### Chat, #### Text) from body
125
- const { segments: rawSegments, errors: segmentErrors } = parseSegments(section.body, section.line + 1, file);
126
- // Adjust segment error line numbers for frontmatter offset
127
- for (const err of segmentErrors) {
120
+ }
121
+ errors.push(...segmentErrors);
122
+ const testSegments = [];
123
+ for (const rawSeg of rawSegments) {
124
+ const { segment, errors: conversionErrors } = convertSegment(rawSeg, 'page', file);
125
+ for (const err of conversionErrors) {
128
126
  if (err.line) {
129
127
  err.line += bodyStartLine - 1;
130
128
  }
131
129
  }
132
- errors.push(...segmentErrors);
133
- const testSegments = [];
134
- for (const rawSeg of rawSegments) {
135
- const { segment, errors: conversionErrors } = convertSegment(rawSeg, 'page', file);
136
- // Adjust conversion error line numbers for frontmatter offset
137
- for (const err of conversionErrors) {
138
- if (err.line) {
139
- err.line += bodyStartLine - 1;
130
+ errors.push(...conversionErrors);
131
+ if (segment) {
132
+ testSegments.push(segment);
133
+ }
134
+ }
135
+ return { source, resolvedPath, segments: testSegments };
136
+ }
137
+ // Step 3: Check for submodule sections
138
+ const hasSubmodules = sectionsResult.sections.some(s => s.type === 'submodule');
139
+ const lenses = [];
140
+ let testRef;
141
+ let submodules;
142
+ if (hasSubmodules) {
143
+ // All-or-nothing: reject top-level lens/test sections when submodules exist
144
+ const orphanedSections = sectionsResult.sections.filter(s => s.type !== 'submodule');
145
+ if (orphanedSections.length > 0) {
146
+ errors.push({
147
+ file,
148
+ line: orphanedSections[0].line,
149
+ message: 'Content found outside submodule boundaries — when using Submodule markers, all content must be inside a submodule',
150
+ suggestion: 'Move this content into a Submodule section',
151
+ severity: 'error',
152
+ });
153
+ }
154
+ submodules = [];
155
+ for (const section of sectionsResult.sections) {
156
+ if (section.type !== 'submodule')
157
+ continue;
158
+ const group = {
159
+ title: section.title,
160
+ customSlug: section.fields.slug,
161
+ lenses: [],
162
+ };
163
+ // Process children (lens/test sections at H3)
164
+ if (section.children) {
165
+ for (const child of section.children) {
166
+ if (child.type === 'lens') {
167
+ const ref = extractLensRef(child);
168
+ if (ref) {
169
+ group.lenses.push(ref);
170
+ lenses.push(ref); // Also add to flat list for validation
171
+ }
172
+ }
173
+ else if (child.type === 'test') {
174
+ group.test = extractTestRef(child);
140
175
  }
141
- }
142
- errors.push(...conversionErrors);
143
- if (segment) {
144
- testSegments.push(segment);
145
176
  }
146
177
  }
147
- testRef = {
148
- source,
149
- resolvedPath,
150
- segments: testSegments,
151
- };
178
+ submodules.push(group);
179
+ }
180
+ }
181
+ else {
182
+ // Normal path: extract lens refs from top-level sections
183
+ for (const section of sectionsResult.sections) {
184
+ const typoWarnings = detectFieldTypos(section.fields, file, section.line);
185
+ errors.push(...typoWarnings);
186
+ if (section.type === 'lens') {
187
+ const ref = extractLensRef(section);
188
+ if (ref)
189
+ lenses.push(ref);
190
+ }
191
+ else if (section.type === 'test') {
192
+ testRef = extractTestRef(section);
193
+ }
152
194
  }
153
195
  }
154
196
  // Step 4: Validate at least one lens exists
@@ -167,6 +209,7 @@ export function parseLearningOutcome(content, file) {
167
209
  lenses,
168
210
  test: testRef,
169
211
  discussion: frontmatter.discussion,
212
+ submodules,
170
213
  };
171
214
  return { learningOutcome, errors };
172
215
  }
@@ -1 +1 @@
1
- {"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,qBAAqB,EAA0B,MAAM,WAAW,CAAC;AA0BzG,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,IAAY;IAChE,MAAM,MAAM,GAAmB,EAAE,CAAC;IAElC,yCAAyC;IACzC,OAAO,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAEzC,kDAAkD;IAClD,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,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC3C,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,iBAAiB,CAAC;IAE/D,MAAM,iBAAiB,GAAG,mBAAmB,CAAC,WAAW,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;IACrF,MAAM,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC;IAElC,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QACxD,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC3C,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO,WAAW,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,IAAI,EAAE,CAAC;YACP,OAAO,EAAE,oCAAoC,OAAO,WAAW,CAAC,EAAE,EAAE;YACpE,UAAU,EAAE,yBAAyB;YACrC,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;QACH,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC3C,CAAC;IAED,6EAA6E;IAC7E,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,EAAE,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC;IAEtE,iDAAiD;IACjD,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;QAC1C,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,IAAI,aAAa,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAEtC,KAAK,MAAM,OAAO,IAAI,cAAc,CAAC,QAAQ,EAAE,CAAC;QAC9C,OAAO,CAAC,IAAI,IAAI,aAAa,GAAG,CAAC,CAAC;IACpC,CAAC;IAED,gEAAgE;IAChE,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,IAAI,OAAkC,CAAC;IAEvC,KAAK,MAAM,OAAO,IAAI,cAAc,CAAC,QAAQ,EAAE,CAAC;QAC9C,qCAAqC;QACrC,MAAM,YAAY,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1E,MAAM,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAE7B,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YACrC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI;oBACJ,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,OAAO,EAAE,qCAAqC;oBAC9C,UAAU,EAAE,sEAAsE;oBAClF,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,kCAAkC;YAClC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACvC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;gBAChC,MAAM,UAAU,GAAG,QAAQ,EAAE,aAAa;oBACxC,CAAC,CAAC,mBAAmB,QAAQ,CAAC,aAAa,MAAM;oBACjD,CAAC,CAAC,mDAAmD,CAAC;gBACxD,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI;oBACJ,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,OAAO,EAAE,QAAQ,EAAE,KAAK;wBACtB,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,uBAAuB,MAAM,EAAE;wBAClD,CAAC,CAAC,8CAA8C,MAAM,EAAE;oBAC1D,UAAU;oBACV,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,yCAAyC;YACzC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI;oBACJ,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,OAAO,EAAE,+CAA+C,QAAQ,CAAC,IAAI,EAAE;oBACvE,UAAU,EAAE,sEAAsE;oBAClF,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,MAAM,YAAY,GAAG,mBAAmB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;YAEnE,MAAM,CAAC,IAAI,CAAC;gBACV,MAAM;gBACN,YAAY;gBACZ,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACnC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YACrC,IAAI,YAAgC,CAAC;YAErC,IAAI,MAAM,EAAE,CAAC;gBACX,kCAAkC;gBAClC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;gBACvC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;oBAChC,MAAM,UAAU,GAAG,QAAQ,EAAE,aAAa;wBACxC,CAAC,CAAC,mBAAmB,QAAQ,CAAC,aAAa,MAAM;wBACjD,CAAC,CAAC,kDAAkD,CAAC;oBACvD,MAAM,CAAC,IAAI,CAAC;wBACV,IAAI;wBACJ,IAAI,EAAE,OAAO,CAAC,IAAI;wBAClB,OAAO,EAAE,QAAQ,EAAE,KAAK;4BACtB,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,uBAAuB,MAAM,EAAE;4BAClD,CAAC,CAAC,8CAA8C,MAAM,EAAE;wBAC1D,UAAU;wBACV,QAAQ,EAAE,OAAO;qBAClB,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBAED,YAAY,GAAG,mBAAmB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC1D,CAAC;YAED,2EAA2E;YAC3E,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,aAAa,CACpE,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,IAAI,GAAG,CAAC,EAChB,IAAI,CACL,CAAC;YACF,2DAA2D;YAC3D,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;gBAChC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;oBACb,GAAG,CAAC,IAAI,IAAI,aAAa,GAAG,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC;YAE9B,MAAM,YAAY,GAAwB,EAAE,CAAC;YAC7C,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;gBACjC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;gBACnF,8DAA8D;gBAC9D,KAAK,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;oBACnC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;wBACb,GAAG,CAAC,IAAI,IAAI,aAAa,GAAG,CAAC,CAAC;oBAChC,CAAC;gBACH,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;gBACjC,IAAI,OAAO,EAAE,CAAC;oBACZ,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC7B,CAAC;YACH,CAAC;YAED,OAAO,GAAG;gBACR,MAAM;gBACN,YAAY;gBACZ,QAAQ,EAAE,YAAY;aACvB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,4CAA4C;IAC5C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,0DAA0D;YACnE,UAAU,EAAE,wDAAwD;YACpE,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,2DAA2D;IAC3D,MAAM,eAAe,GAA0B;QAC7C,EAAE,EAAE,WAAW,CAAC,EAAY;QAC5B,MAAM;QACN,IAAI,EAAE,OAAO;QACb,UAAU,EAAE,WAAW,CAAC,UAAgC;KACzD,CAAC;IAEF,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;AACrC,CAAC"}
1
+ {"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.ts"],"names":[],"mappings":"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"}
@@ -34,7 +34,17 @@ export interface ParsedQuestionSegment {
34
34
  optional?: boolean;
35
35
  feedback?: boolean;
36
36
  }
37
- export type ParsedLensSegment = ParsedTextSegment | ParsedChatSegment | ParsedArticleExcerptSegment | ParsedVideoExcerptSegment | ParsedQuestionSegment;
37
+ export interface ParsedRoleplaySegment {
38
+ type: 'roleplay';
39
+ id: string;
40
+ content: string;
41
+ aiInstructions: string;
42
+ openingMessage?: string;
43
+ assessmentInstructions?: string;
44
+ optional?: boolean;
45
+ feedback?: boolean;
46
+ }
47
+ export type ParsedLensSegment = ParsedTextSegment | ParsedChatSegment | ParsedArticleExcerptSegment | ParsedVideoExcerptSegment | ParsedQuestionSegment | ParsedRoleplaySegment;
38
48
  export interface ParsedLensSection {
39
49
  type: string;
40
50
  title: string;
@@ -45,6 +55,7 @@ export interface ParsedLensSection {
45
55
  }
46
56
  export interface ParsedLens {
47
57
  id: string;
58
+ tldr?: string;
48
59
  sections: ParsedLensSection[];
49
60
  }
50
61
  export interface LensParseResult {
@@ -77,6 +88,20 @@ export declare function convertSegment(raw: RawSegment, sectionType: string, fil
77
88
  * Handles both inline (%% ... %% on same line) and block (multiline) comments.
78
89
  */
79
90
  export declare function stripObsidianComments(content: string): string;
91
+ /**
92
+ * Strip CriticMarkup from content using reject-all-changes behavior:
93
+ * - {>>comments<<} → removed
94
+ * - {++additions++} → removed
95
+ * - {--deletions--} → inner content kept (original preserved)
96
+ * - {~~old~>new~~} → old text kept
97
+ * - {==highlights==} → inner content kept, markers removed
98
+ */
99
+ export declare function stripCriticMarkup(content: string): string;
100
+ /**
101
+ * Strip all authoring markup (CriticMarkup + Obsidian comments) from content.
102
+ * Call at the top of each parser before any processing.
103
+ */
104
+ export declare function stripAuthoringMarkup(content: string): string;
80
105
  /**
81
106
  * Parse a lens file into structured lens data.
82
107
  *
@@ -6,18 +6,19 @@ import { validateFieldValues } from '../validator/field-values.js';
6
6
  import { detectFieldTypos } from '../validator/field-typos.js';
7
7
  import { validateFrontmatter } from '../validator/validate-frontmatter.js';
8
8
  import { validateChatPrecedence } from '../validator/chat-precedence.js';
9
+ import { detectDirectivesInNonArticle } from '../validator/directives.js';
9
10
  import { parseWikilink, hasRelativePath } from './wikilink.js';
10
11
  import { parseTimestamp } from '../bundler/video.js';
11
12
  // Valid segment types for lens H4 headers
12
- const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt', 'question']);
13
+ const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt', 'question', 'roleplay']);
13
14
  // Valid segment types per section output type
14
15
  const VALID_SEGMENTS_PER_SECTION = {
15
- 'page': new Set(['text', 'chat', 'question']),
16
- 'lens-article': new Set(['text', 'chat', 'article-excerpt', 'question']),
17
- 'lens-video': new Set(['text', 'chat', 'video-excerpt', 'question']),
16
+ 'page': new Set(['text', 'chat', 'question', 'roleplay']),
17
+ 'lens-article': new Set(['text', 'chat', 'article-excerpt', 'question', 'roleplay']),
18
+ 'lens-video': new Set(['text', 'chat', 'video-excerpt', 'question', 'roleplay']),
18
19
  };
19
20
  // H4 segment header pattern: #### <type> or #### <type>: <title>
20
- const SEGMENT_HEADER_PATTERN = /^####\s+([^:\s]+)(?::\s*(.*))?$/i;
21
+ const SEGMENT_HEADER_PATTERN = /^####\s+([^:\s]+)(?::\s*(.*?))?\s*$/i;
21
22
  // Field pattern: fieldname:: value
22
23
  const FIELD_PATTERN = /^([\w-]+)::\s*(.*)$/;
23
24
  /**
@@ -70,7 +71,7 @@ export function parseSegments(sectionBody, bodyStartLine, file) {
70
71
  line: lineNum,
71
72
  message: `Found '${singleColonMatch[1]}:' with single colon — did you mean '${singleColonMatch[1]}::'?`,
72
73
  suggestion: `Change '${singleColonMatch[1]}:' to '${singleColonMatch[1]}::' (double colon)`,
73
- severity: 'warning',
74
+ severity: 'error',
74
75
  });
75
76
  }
76
77
  currentFieldLines.push(line);
@@ -161,6 +162,9 @@ export function convertSegment(raw, sectionType, file) {
161
162
  };
162
163
  return { segment, errors };
163
164
  }
165
+ // Warn if directives are used in lens text segments (they only render in articles)
166
+ const directiveWarnings = detectDirectivesInNonArticle(content, file, raw.line);
167
+ errors.push(...directiveWarnings);
164
168
  const segment = {
165
169
  type: 'text',
166
170
  content,
@@ -264,7 +268,7 @@ export function convertSegment(raw, sectionType, file) {
264
268
  return { segment, errors };
265
269
  }
266
270
  case 'question': {
267
- const content = raw.fields['content'] || raw.fields['user-instruction'];
271
+ const content = raw.fields['content'];
268
272
  if (!content || content.trim() === '') {
269
273
  errors.push({
270
274
  file,
@@ -278,7 +282,7 @@ export function convertSegment(raw, sectionType, file) {
278
282
  const segment = {
279
283
  type: 'question',
280
284
  content,
281
- assessmentInstructions: raw.fields['assessment-instructions'] || raw.fields['assessment-prompt'] || undefined,
285
+ assessmentInstructions: raw.fields['assessment-instructions'] || undefined,
282
286
  maxTime: raw.fields['max-time'] || undefined,
283
287
  maxChars: raw.fields['max-chars'] ? parseInt(raw.fields['max-chars'], 10) : undefined,
284
288
  enforceVoice: raw.fields['enforce-voice']?.toLowerCase() === 'true' ? true : undefined,
@@ -287,6 +291,52 @@ export function convertSegment(raw, sectionType, file) {
287
291
  };
288
292
  return { segment, errors };
289
293
  }
294
+ case 'roleplay': {
295
+ const id = raw.fields['id'];
296
+ if (!id || id.trim() === '') {
297
+ errors.push({
298
+ file,
299
+ line: raw.line,
300
+ message: 'Roleplay segment missing id:: field',
301
+ suggestion: "Add 'id:: <uuid>' to the roleplay segment",
302
+ severity: 'error',
303
+ });
304
+ }
305
+ const content = raw.fields['content'];
306
+ if (!content || content.trim() === '') {
307
+ errors.push({
308
+ file,
309
+ line: raw.line,
310
+ message: 'Roleplay segment missing content:: field',
311
+ suggestion: "Add 'content:: Your scenario briefing here'",
312
+ severity: 'error',
313
+ });
314
+ }
315
+ const aiInstructions = raw.fields['ai-instructions'];
316
+ if (!aiInstructions || aiInstructions.trim() === '') {
317
+ errors.push({
318
+ file,
319
+ line: raw.line,
320
+ message: 'Roleplay segment missing ai-instructions:: field',
321
+ suggestion: "Add 'ai-instructions:: Character behavior description'",
322
+ severity: 'error',
323
+ });
324
+ }
325
+ if (!id || id.trim() === '' || !content || content.trim() === '' || !aiInstructions || aiInstructions.trim() === '') {
326
+ return { segment: null, errors };
327
+ }
328
+ const segment = {
329
+ type: 'roleplay',
330
+ id,
331
+ content,
332
+ aiInstructions,
333
+ openingMessage: raw.fields['opening-message'] || undefined,
334
+ assessmentInstructions: raw.fields['assessment-instructions'] || undefined,
335
+ optional: raw.fields.optional?.toLowerCase() === 'true' ? true : undefined,
336
+ feedback: raw.fields['feedback']?.toLowerCase() === 'true' ? true : undefined,
337
+ };
338
+ return { segment, errors };
339
+ }
290
340
  default:
291
341
  // Unknown segment type - error already reported during parseSegments
292
342
  return { segment: null, errors };
@@ -332,6 +382,31 @@ function checkEmptySegment(raw, file) {
332
382
  export function stripObsidianComments(content) {
333
383
  return content.replace(/%%.*?%%/gs, '');
334
384
  }
385
+ /**
386
+ * Strip CriticMarkup from content using reject-all-changes behavior:
387
+ * - {>>comments<<} → removed
388
+ * - {++additions++} → removed
389
+ * - {--deletions--} → inner content kept (original preserved)
390
+ * - {~~old~>new~~} → old text kept
391
+ * - {==highlights==} → inner content kept, markers removed
392
+ */
393
+ export function stripCriticMarkup(content) {
394
+ return content
395
+ .replace(/\{>>.*?<<\}/gs, '') // Comments → remove
396
+ .replace(/\{\+\+.*?\+\+\}/gs, '') // Additions → remove
397
+ .replace(/\{--(?:\{[^}]*\}@@)?(.*?)--\}/gs, '$1') // Deletions → keep inner (skip metadata)
398
+ .replace(/\{~~(?:\{[^}]*\}@@)?(.*?)~>.*?~~\}/gs, '$1') // Substitutions → keep old (skip metadata)
399
+ .replace(/\{==(?:\{[^}]*\}@@)?(.*?)==\}/gs, '$1'); // Highlights → keep inner (skip metadata)
400
+ }
401
+ /**
402
+ * Strip all authoring markup (CriticMarkup + Obsidian comments) from content.
403
+ * Call at the top of each parser before any processing.
404
+ */
405
+ export function stripAuthoringMarkup(content) {
406
+ const stripped = stripObsidianComments(stripCriticMarkup(content));
407
+ // Trim trailing whitespace left by inline markup removal
408
+ return stripped.split('\n').map(line => line.trimEnd()).join('\n');
409
+ }
335
410
  /**
336
411
  * Parse a lens file into structured lens data.
337
412
  *
@@ -341,8 +416,8 @@ export function stripObsidianComments(content) {
341
416
  */
342
417
  export function parseLens(content, file) {
343
418
  const errors = [];
344
- // Strip Obsidian comments before parsing
345
- content = stripObsidianComments(content);
419
+ // Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
420
+ content = stripAuthoringMarkup(content);
346
421
  // Step 1: Parse frontmatter and validate id field
347
422
  const frontmatterResult = parseFrontmatter(content, file);
348
423
  if (frontmatterResult.error) {
@@ -366,6 +441,19 @@ export function parseLens(content, file) {
366
441
  });
367
442
  return { lens: null, errors };
368
443
  }
444
+ const tldr = typeof frontmatter.tldr === 'string' ? frontmatter.tldr : undefined;
445
+ if (tldr) {
446
+ const wordCount = tldr.trim().split(/\s+/).filter(Boolean).length;
447
+ if (wordCount > 80) {
448
+ errors.push({
449
+ file,
450
+ line: 2,
451
+ message: `tldr exceeds 80 words (has ${wordCount})`,
452
+ suggestion: 'Shorten the tldr to 80 words or fewer',
453
+ severity: 'error',
454
+ });
455
+ }
456
+ }
369
457
  // Step 2: Parse H3 sections (Text, Article, Video)
370
458
  const sectionsResult = parseSections(body, 3, LENS_SECTION_TYPES, file);
371
459
  // Adjust line numbers to account for frontmatter
@@ -494,6 +582,7 @@ export function parseLens(content, file) {
494
582
  }
495
583
  const lens = {
496
584
  id: frontmatter.id,
585
+ tldr,
497
586
  sections: parsedSections,
498
587
  };
499
588
  return { lens, errors };