lens-content-processor 0.2.2 → 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.
- package/dist/bundler/article.d.ts +7 -2
- package/dist/bundler/article.js +23 -24
- package/dist/bundler/article.js.map +1 -1
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +6 -10
- package/dist/cli.js.map +1 -1
- package/dist/content-schema.js +8 -6
- package/dist/content-schema.js.map +1 -1
- package/dist/flattener/index.d.ts +41 -2
- package/dist/flattener/index.js +699 -48
- package/dist/flattener/index.js.map +1 -1
- package/dist/flattener/test-helpers.d.ts +23 -0
- package/dist/flattener/test-helpers.js +58 -0
- package/dist/flattener/test-helpers.js.map +1 -0
- package/dist/fs/read-vault.d.ts +1 -4
- package/dist/fs/read-vault.js +4 -8
- package/dist/fs/read-vault.js.map +1 -1
- package/dist/index.d.ts +34 -6
- package/dist/index.js +170 -33
- package/dist/index.js.map +1 -1
- package/dist/parser/article.d.ts +1 -1
- package/dist/parser/article.js +14 -4
- package/dist/parser/article.js.map +1 -1
- package/dist/parser/course.d.ts +2 -2
- package/dist/parser/course.js +16 -23
- package/dist/parser/course.js.map +1 -1
- package/dist/parser/learning-outcome.d.ts +11 -2
- package/dist/parser/learning-outcome.js +135 -62
- package/dist/parser/learning-outcome.js.map +1 -1
- package/dist/parser/lens.d.ts +63 -1
- package/dist/parser/lens.js +138 -11
- package/dist/parser/lens.js.map +1 -1
- package/dist/parser/module.d.ts +1 -1
- package/dist/parser/module.js +14 -9
- package/dist/parser/module.js.map +1 -1
- package/dist/parser/sections.d.ts +2 -0
- package/dist/parser/sections.js +57 -21
- package/dist/parser/sections.js.map +1 -1
- package/dist/parser/video-transcript.js +3 -0
- package/dist/parser/video-transcript.js.map +1 -1
- package/dist/parser/wikilink.js +6 -1
- package/dist/parser/wikilink.js.map +1 -1
- package/dist/utils/slug.d.ts +5 -0
- package/dist/utils/slug.js +16 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/validator/chat-precedence.d.ts +9 -0
- package/dist/validator/chat-precedence.js +32 -0
- package/dist/validator/chat-precedence.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-typos.js +0 -9
- package/dist/validator/field-typos.js.map +1 -1
- package/dist/validator/field-values.js +14 -0
- package/dist/validator/field-values.js.map +1 -1
- package/dist/validator/tier.d.ts +18 -0
- package/dist/validator/tier.js +74 -0
- package/dist/validator/tier.js.map +1 -0
- package/dist/validator/url-reachability.js +3 -1
- 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 +5 -5
|
@@ -3,8 +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, stripAuthoringMarkup } from './lens.js';
|
|
6
7
|
export function parseLearningOutcome(content, file) {
|
|
7
8
|
const errors = [];
|
|
9
|
+
// Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
|
|
10
|
+
content = stripAuthoringMarkup(content);
|
|
8
11
|
// Step 1: Parse frontmatter and validate id field
|
|
9
12
|
const frontmatterResult = parseFrontmatter(content, file);
|
|
10
13
|
if (frontmatterResult.error) {
|
|
@@ -28,8 +31,9 @@ export function parseLearningOutcome(content, file) {
|
|
|
28
31
|
});
|
|
29
32
|
return { learningOutcome: null, errors };
|
|
30
33
|
}
|
|
31
|
-
// Step 2: Parse sections with H2 level and LO_SECTION_TYPES
|
|
32
|
-
const
|
|
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);
|
|
33
37
|
// Adjust line numbers to account for frontmatter
|
|
34
38
|
for (const error of sectionsResult.errors) {
|
|
35
39
|
if (error.line) {
|
|
@@ -40,85 +44,153 @@ export function parseLearningOutcome(content, file) {
|
|
|
40
44
|
for (const section of sectionsResult.sections) {
|
|
41
45
|
section.line += bodyStartLine - 1;
|
|
42
46
|
}
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
let testRef;
|
|
46
|
-
for (const section of sectionsResult.sections) {
|
|
47
|
-
// Detect likely typos in field names
|
|
47
|
+
// Helper: extract a lens ref from a parsed section
|
|
48
|
+
function extractLensRef(section) {
|
|
48
49
|
const typoWarnings = detectFieldTypos(section.fields, file, section.line);
|
|
49
50
|
errors.push(...typoWarnings);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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) {
|
|
63
97
|
const wikilink = parseWikilink(source);
|
|
64
98
|
if (!wikilink || wikilink.error) {
|
|
65
99
|
const suggestion = wikilink?.correctedPath
|
|
66
100
|
? `Did you mean '[[${wikilink.correctedPath}]]'?`
|
|
67
|
-
: 'Use format [[../
|
|
101
|
+
: 'Use format [[../Tests/filename.md|Display Text]]';
|
|
68
102
|
errors.push({
|
|
69
103
|
file,
|
|
70
104
|
line: section.line,
|
|
71
|
-
message:
|
|
105
|
+
message: wikilink?.error
|
|
106
|
+
? `${wikilink.error} in source:: field: ${source}`
|
|
107
|
+
: `Invalid wikilink format in source:: field: ${source}`,
|
|
72
108
|
suggestion,
|
|
73
109
|
severity: 'error',
|
|
74
110
|
});
|
|
75
|
-
|
|
111
|
+
return undefined;
|
|
76
112
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
resolvedPath = resolveWikilinkPath(wikilink.path, file);
|
|
114
|
+
}
|
|
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;
|
|
119
|
+
}
|
|
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) {
|
|
126
|
+
if (err.line) {
|
|
127
|
+
err.line += bodyStartLine - 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
errors.push(...conversionErrors);
|
|
131
|
+
if (segment) {
|
|
132
|
+
testSegments.push(segment);
|
|
87
133
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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',
|
|
94
152
|
});
|
|
95
153
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!source) {
|
|
154
|
+
submodules = [];
|
|
155
|
+
for (const section of sectionsResult.sections) {
|
|
156
|
+
if (section.type !== 'submodule')
|
|
100
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);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
101
177
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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);
|
|
116
193
|
}
|
|
117
|
-
const resolvedPath = resolveWikilinkPath(wikilink.path, file);
|
|
118
|
-
testRef = {
|
|
119
|
-
source,
|
|
120
|
-
resolvedPath,
|
|
121
|
-
};
|
|
122
194
|
}
|
|
123
195
|
}
|
|
124
196
|
// Step 4: Validate at least one lens exists
|
|
@@ -137,6 +209,7 @@ export function parseLearningOutcome(content, file) {
|
|
|
137
209
|
lenses,
|
|
138
210
|
test: testRef,
|
|
139
211
|
discussion: frontmatter.discussion,
|
|
212
|
+
submodules,
|
|
140
213
|
};
|
|
141
214
|
return { learningOutcome, errors };
|
|
142
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,
|
|
1
|
+
{"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../src/parser/learning-outcome.ts"],"names":[],"mappings":"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"}
|
package/dist/parser/lens.d.ts
CHANGED
|
@@ -24,7 +24,27 @@ export interface ParsedVideoExcerptSegment {
|
|
|
24
24
|
toTimeStr: string;
|
|
25
25
|
optional?: boolean;
|
|
26
26
|
}
|
|
27
|
-
export
|
|
27
|
+
export interface ParsedQuestionSegment {
|
|
28
|
+
type: 'question';
|
|
29
|
+
content: string;
|
|
30
|
+
assessmentInstructions?: string;
|
|
31
|
+
maxTime?: string;
|
|
32
|
+
maxChars?: number;
|
|
33
|
+
enforceVoice?: boolean;
|
|
34
|
+
optional?: boolean;
|
|
35
|
+
feedback?: boolean;
|
|
36
|
+
}
|
|
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;
|
|
28
48
|
export interface ParsedLensSection {
|
|
29
49
|
type: string;
|
|
30
50
|
title: string;
|
|
@@ -35,12 +55,53 @@ export interface ParsedLensSection {
|
|
|
35
55
|
}
|
|
36
56
|
export interface ParsedLens {
|
|
37
57
|
id: string;
|
|
58
|
+
tldr?: string;
|
|
38
59
|
sections: ParsedLensSection[];
|
|
39
60
|
}
|
|
40
61
|
export interface LensParseResult {
|
|
41
62
|
lens: ParsedLens | null;
|
|
42
63
|
errors: ContentError[];
|
|
43
64
|
}
|
|
65
|
+
interface RawSegment {
|
|
66
|
+
type: string;
|
|
67
|
+
title?: string;
|
|
68
|
+
fields: Record<string, string>;
|
|
69
|
+
line: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse H4 segments from a section body.
|
|
73
|
+
* Segments are defined by `#### <type>` headers within a section.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseSegments(sectionBody: string, bodyStartLine: number, file: string): {
|
|
76
|
+
segments: RawSegment[];
|
|
77
|
+
errors: ContentError[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Convert a raw segment to a typed ParsedLensSegment.
|
|
81
|
+
*/
|
|
82
|
+
export declare function convertSegment(raw: RawSegment, sectionType: string, file: string): {
|
|
83
|
+
segment: ParsedLensSegment | null;
|
|
84
|
+
errors: ContentError[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Strip Obsidian %% comments %% from content.
|
|
88
|
+
* Handles both inline (%% ... %% on same line) and block (multiline) comments.
|
|
89
|
+
*/
|
|
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;
|
|
44
105
|
/**
|
|
45
106
|
* Parse a lens file into structured lens data.
|
|
46
107
|
*
|
|
@@ -49,3 +110,4 @@ export interface LensParseResult {
|
|
|
49
110
|
* - H4 (`####`) for segments: Text, Chat, Article-excerpt, Video-excerpt
|
|
50
111
|
*/
|
|
51
112
|
export declare function parseLens(content: string, file: string): LensParseResult;
|
|
113
|
+
export {};
|
package/dist/parser/lens.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
+
import { ALL_KNOWN_FIELDS } from '../content-schema.js';
|
|
1
2
|
import { parseFrontmatter } from './frontmatter.js';
|
|
2
3
|
import { parseSections, LENS_SECTION_TYPES, LENS_OUTPUT_TYPE } from './sections.js';
|
|
3
4
|
import { validateSegmentFields } from '../validator/segment-fields.js';
|
|
4
5
|
import { validateFieldValues } from '../validator/field-values.js';
|
|
5
6
|
import { detectFieldTypos } from '../validator/field-typos.js';
|
|
6
7
|
import { validateFrontmatter } from '../validator/validate-frontmatter.js';
|
|
8
|
+
import { validateChatPrecedence } from '../validator/chat-precedence.js';
|
|
9
|
+
import { detectDirectivesInNonArticle } from '../validator/directives.js';
|
|
7
10
|
import { parseWikilink, hasRelativePath } from './wikilink.js';
|
|
8
11
|
import { parseTimestamp } from '../bundler/video.js';
|
|
9
12
|
// Valid segment types for lens H4 headers
|
|
10
|
-
const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt']);
|
|
13
|
+
const LENS_SEGMENT_TYPES = new Set(['text', 'chat', 'article-excerpt', 'video-excerpt', 'question', 'roleplay']);
|
|
11
14
|
// Valid segment types per section output type
|
|
12
15
|
const VALID_SEGMENTS_PER_SECTION = {
|
|
13
|
-
'page': new Set(['text', 'chat']),
|
|
14
|
-
'lens-article': new Set(['text', 'chat', 'article-excerpt']),
|
|
15
|
-
'lens-video': new Set(['text', 'chat', 'video-excerpt']),
|
|
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']),
|
|
16
19
|
};
|
|
17
20
|
// H4 segment header pattern: #### <type> or #### <type>: <title>
|
|
18
|
-
const SEGMENT_HEADER_PATTERN = /^####\s+([^:\s]+)(?::\s*(
|
|
21
|
+
const SEGMENT_HEADER_PATTERN = /^####\s+([^:\s]+)(?::\s*(.*?))?\s*$/i;
|
|
19
22
|
// Field pattern: fieldname:: value
|
|
20
|
-
const FIELD_PATTERN = /^(\w+)::\s*(.*)$/;
|
|
23
|
+
const FIELD_PATTERN = /^([\w-]+)::\s*(.*)$/;
|
|
21
24
|
/**
|
|
22
25
|
* Parse H4 segments from a section body.
|
|
23
26
|
* Segments are defined by `#### <type>` headers within a section.
|
|
24
27
|
*/
|
|
25
|
-
function parseSegments(sectionBody, bodyStartLine, file) {
|
|
28
|
+
export function parseSegments(sectionBody, bodyStartLine, file) {
|
|
26
29
|
const lines = sectionBody.split('\n');
|
|
27
30
|
const segments = [];
|
|
28
31
|
const errors = [];
|
|
@@ -61,14 +64,14 @@ function parseSegments(sectionBody, bodyStartLine, file) {
|
|
|
61
64
|
}
|
|
62
65
|
else if (currentSegment) {
|
|
63
66
|
// Check for single-colon field that should be double-colon
|
|
64
|
-
const singleColonMatch = line.match(/^(\w+):\s+(.*)$/);
|
|
65
|
-
if (singleColonMatch && !line.match(/^https?:/) && !FIELD_PATTERN.test(line)) {
|
|
67
|
+
const singleColonMatch = line.match(/^([\w-]+):\s+(.*)$/);
|
|
68
|
+
if (singleColonMatch && !line.match(/^https?:/) && !FIELD_PATTERN.test(line) && ALL_KNOWN_FIELDS.includes(singleColonMatch[1])) {
|
|
66
69
|
errors.push({
|
|
67
70
|
file,
|
|
68
71
|
line: lineNum,
|
|
69
72
|
message: `Found '${singleColonMatch[1]}:' with single colon — did you mean '${singleColonMatch[1]}::'?`,
|
|
70
73
|
suggestion: `Change '${singleColonMatch[1]}:' to '${singleColonMatch[1]}::' (double colon)`,
|
|
71
|
-
severity: '
|
|
74
|
+
severity: 'error',
|
|
72
75
|
});
|
|
73
76
|
}
|
|
74
77
|
currentFieldLines.push(line);
|
|
@@ -125,7 +128,7 @@ function parseFieldsIntoSegment(segment, lines) {
|
|
|
125
128
|
/**
|
|
126
129
|
* Convert a raw segment to a typed ParsedLensSegment.
|
|
127
130
|
*/
|
|
128
|
-
function convertSegment(raw, sectionType, file) {
|
|
131
|
+
export function convertSegment(raw, sectionType, file) {
|
|
129
132
|
const errors = [];
|
|
130
133
|
switch (raw.type) {
|
|
131
134
|
case 'text': {
|
|
@@ -159,6 +162,9 @@ function convertSegment(raw, sectionType, file) {
|
|
|
159
162
|
};
|
|
160
163
|
return { segment, errors };
|
|
161
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);
|
|
162
168
|
const segment = {
|
|
163
169
|
type: 'text',
|
|
164
170
|
content,
|
|
@@ -261,6 +267,76 @@ function convertSegment(raw, sectionType, file) {
|
|
|
261
267
|
};
|
|
262
268
|
return { segment, errors };
|
|
263
269
|
}
|
|
270
|
+
case 'question': {
|
|
271
|
+
const content = raw.fields['content'];
|
|
272
|
+
if (!content || content.trim() === '') {
|
|
273
|
+
errors.push({
|
|
274
|
+
file,
|
|
275
|
+
line: raw.line,
|
|
276
|
+
message: 'Question segment missing content:: field',
|
|
277
|
+
suggestion: "Add 'content:: Your question here'",
|
|
278
|
+
severity: 'error',
|
|
279
|
+
});
|
|
280
|
+
return { segment: null, errors };
|
|
281
|
+
}
|
|
282
|
+
const segment = {
|
|
283
|
+
type: 'question',
|
|
284
|
+
content,
|
|
285
|
+
assessmentInstructions: raw.fields['assessment-instructions'] || undefined,
|
|
286
|
+
maxTime: raw.fields['max-time'] || undefined,
|
|
287
|
+
maxChars: raw.fields['max-chars'] ? parseInt(raw.fields['max-chars'], 10) : undefined,
|
|
288
|
+
enforceVoice: raw.fields['enforce-voice']?.toLowerCase() === 'true' ? true : undefined,
|
|
289
|
+
optional: raw.fields.optional?.toLowerCase() === 'true' ? true : undefined,
|
|
290
|
+
feedback: raw.fields['feedback']?.toLowerCase() === 'true' ? true : undefined,
|
|
291
|
+
};
|
|
292
|
+
return { segment, errors };
|
|
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
|
+
}
|
|
264
340
|
default:
|
|
265
341
|
// Unknown segment type - error already reported during parseSegments
|
|
266
342
|
return { segment: null, errors };
|
|
@@ -299,6 +375,38 @@ function checkEmptySegment(raw, file) {
|
|
|
299
375
|
}
|
|
300
376
|
return null;
|
|
301
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Strip Obsidian %% comments %% from content.
|
|
380
|
+
* Handles both inline (%% ... %% on same line) and block (multiline) comments.
|
|
381
|
+
*/
|
|
382
|
+
export function stripObsidianComments(content) {
|
|
383
|
+
return content.replace(/%%.*?%%/gs, '');
|
|
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
|
+
}
|
|
302
410
|
/**
|
|
303
411
|
* Parse a lens file into structured lens data.
|
|
304
412
|
*
|
|
@@ -308,6 +416,8 @@ function checkEmptySegment(raw, file) {
|
|
|
308
416
|
*/
|
|
309
417
|
export function parseLens(content, file) {
|
|
310
418
|
const errors = [];
|
|
419
|
+
// Strip authoring markup (CriticMarkup + Obsidian comments) before parsing
|
|
420
|
+
content = stripAuthoringMarkup(content);
|
|
311
421
|
// Step 1: Parse frontmatter and validate id field
|
|
312
422
|
const frontmatterResult = parseFrontmatter(content, file);
|
|
313
423
|
if (frontmatterResult.error) {
|
|
@@ -331,6 +441,19 @@ export function parseLens(content, file) {
|
|
|
331
441
|
});
|
|
332
442
|
return { lens: null, errors };
|
|
333
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
|
+
}
|
|
334
457
|
// Step 2: Parse H3 sections (Text, Article, Video)
|
|
335
458
|
const sectionsResult = parseSections(body, 3, LENS_SECTION_TYPES, file);
|
|
336
459
|
// Adjust line numbers to account for frontmatter
|
|
@@ -423,6 +546,9 @@ export function parseLens(content, file) {
|
|
|
423
546
|
segments.push(segment);
|
|
424
547
|
}
|
|
425
548
|
}
|
|
549
|
+
// Validate Chat segments are preceded by Text segments
|
|
550
|
+
const chatErrors = validateChatPrecedence(rawSegments, file);
|
|
551
|
+
errors.push(...chatErrors);
|
|
426
552
|
// Warn if section has no segments
|
|
427
553
|
if (segments.length === 0) {
|
|
428
554
|
errors.push({
|
|
@@ -456,6 +582,7 @@ export function parseLens(content, file) {
|
|
|
456
582
|
}
|
|
457
583
|
const lens = {
|
|
458
584
|
id: frontmatter.id,
|
|
585
|
+
tldr,
|
|
459
586
|
sections: parsedSections,
|
|
460
587
|
};
|
|
461
588
|
return { lens, errors };
|