lens-content-processor 0.43.0 → 0.45.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 (44) hide show
  1. package/dist/bundler/article.d.ts +37 -30
  2. package/dist/bundler/article.js +67 -80
  3. package/dist/bundler/article.js.map +1 -1
  4. package/dist/content-schema.js +15 -7
  5. package/dist/content-schema.js.map +1 -1
  6. package/dist/flattener/index.d.ts +11 -1
  7. package/dist/flattener/index.js +124 -85
  8. package/dist/flattener/index.js.map +1 -1
  9. package/dist/index.d.ts +10 -3
  10. package/dist/index.js +110 -35
  11. package/dist/index.js.map +1 -1
  12. package/dist/parser/article.js +4 -4
  13. package/dist/parser/article.js.map +1 -1
  14. package/dist/parser/domain.d.ts +49 -0
  15. package/dist/parser/domain.js +212 -0
  16. package/dist/parser/domain.js.map +1 -0
  17. package/dist/parser/learning-outcome.d.ts +5 -4
  18. package/dist/parser/learning-outcome.js +31 -21
  19. package/dist/parser/learning-outcome.js.map +1 -1
  20. package/dist/parser/lens.d.ts +1 -0
  21. package/dist/parser/lens.js +13 -3
  22. package/dist/parser/lens.js.map +1 -1
  23. package/dist/parser/response-segments.d.ts +2 -0
  24. package/dist/parser/response-segments.js +13 -1
  25. package/dist/parser/response-segments.js.map +1 -1
  26. package/dist/parser/widget.d.ts +8 -0
  27. package/dist/parser/widget.js +14 -1
  28. package/dist/parser/widget.js.map +1 -1
  29. package/dist/reference-graph.js +4 -2
  30. package/dist/reference-graph.js.map +1 -1
  31. package/dist/skill-tree.d.ts +70 -17
  32. package/dist/skill-tree.js +116 -78
  33. package/dist/skill-tree.js.map +1 -1
  34. package/dist/validator/article-imports.d.ts +68 -0
  35. package/dist/validator/article-imports.js +265 -0
  36. package/dist/validator/article-imports.js.map +1 -0
  37. package/dist/validator/article-structure.js +71 -27
  38. package/dist/validator/article-structure.js.map +1 -1
  39. package/dist/validator/directives.js +1 -1
  40. package/dist/validator/directives.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/validator/video-imports.d.ts +0 -35
  43. package/dist/validator/video-imports.js +0 -183
  44. package/dist/validator/video-imports.js.map +0 -1
@@ -1,92 +1,97 @@
1
1
  // src/skill-tree.ts — assembles the skill-tree structure from parsed content.
2
2
  //
3
- // The processor emits relationships (LO ↔ module ↔ course); presentation
4
- // decisions live in the frontend. Cards reference each other by LO id and
5
- // reference domains by slug.
6
- import { parseFrontmatter } from "./parser/frontmatter.js";
3
+ // The processor emits relationships (LO ↔ topic ↔ domain, LO ↔ module ↔
4
+ // course); presentation decisions live in the frontend. Cards reference each
5
+ // other by LO id, topics by slug, and domains by slug.
6
+ import { DOMAIN_METADATA_FILE, fileStem, } from "./parser/domain.js";
7
7
  import { findFileWithExtension } from "./parser/wikilink.js";
8
+ /**
9
+ * Frontmatter tag that lets a course or module show up in the skill tree's
10
+ * "Included in" links. A module inherits it from any course that has it
11
+ * (shown inside that course); a module tagged on its own shows standalone.
12
+ * Untagged modules and courses are left out of `taughtBy` entirely.
13
+ */
14
+ export const SKILL_TREE_VISIBLE_TAG = "visible-in-skilltree";
15
+ /** Kebab-case: apostrophes vanish ("You don't" → "you-dont"), every other
16
+ * non-alphanumeric run becomes one hyphen. */
8
17
  export function slugifyTitle(title) {
9
18
  return title
10
19
  .toLowerCase()
20
+ .replace(/['’]/g, "")
11
21
  .replace(/[^a-z0-9]+/g, "-")
12
22
  .replace(/^-+|-+$/g, "");
13
23
  }
14
- function fileStem(path) {
15
- const base = path.split("/").pop() ?? path;
16
- return base.replace(/\.md$/, "");
17
- }
18
- /** Parse a Domains/ placeholder file: `domain-number:` frontmatter is the
19
- * only content that matters; the title is the filename. */
20
- export function parseDomainFile(content, file) {
24
+ export function buildSkillTree(input) {
21
25
  const errors = [];
22
- const frontmatterResult = parseFrontmatter(content, file);
23
- if (frontmatterResult.error) {
24
- errors.push(frontmatterResult.error);
25
- return { domain: null, errors };
26
- }
27
- const rawNumber = frontmatterResult.frontmatter["domain-number"];
28
- if (typeof rawNumber !== "number" || !Number.isInteger(rawNumber)) {
29
- errors.push({
30
- file,
31
- line: 2,
32
- message: `Domain file must have an integer 'domain-number' field, got: ${JSON.stringify(rawNumber)}`,
33
- suggestion: "Add domain-number: <n> to the frontmatter",
34
- severity: "error",
35
- });
36
- return { domain: null, errors };
37
- }
38
- const rawStart = frontmatterResult.frontmatter["start-stage"];
39
- let startStage;
40
- if (rawStart !== undefined && rawStart !== null) {
41
- if (typeof rawStart === "string" &&
42
- ["beginner", "intermediate", "advanced"].includes(rawStart)) {
43
- startStage = rawStart;
44
- }
45
- else {
26
+ // --- Domains ---
27
+ const numberToDir = new Map();
28
+ const slugToDir = new Map();
29
+ const dirToDomain = new Map();
30
+ const folders = [...input.domainFolders].sort(([a], [b]) => a.localeCompare(b));
31
+ for (const [dir, folder] of folders) {
32
+ const slug = slugifyTitle(folder.title);
33
+ const sameNumber = numberToDir.get(folder.number);
34
+ const sameSlug = slugToDir.get(slug);
35
+ if (sameNumber || sameSlug) {
46
36
  errors.push({
47
- file,
48
- line: 2,
49
- message: `Field 'start-stage' must be beginner, intermediate or advanced, got: ${JSON.stringify(rawStart)}`,
50
- suggestion: "Use start-stage: beginner | intermediate | advanced",
37
+ file: folder.declaredBy,
38
+ line: 1,
39
+ message: sameNumber
40
+ ? `Duplicate domain number ${folder.number}: '${dir}' and '${sameNumber}'`
41
+ : `Duplicate domain '${slug}': '${dir}' and '${sameSlug}' have the same title`,
42
+ suggestion: "Give each domain folder a unique number and title",
51
43
  severity: "error",
52
44
  });
45
+ continue;
53
46
  }
47
+ numberToDir.set(folder.number, dir);
48
+ slugToDir.set(slug, dir);
49
+ dirToDomain.set(dir, {
50
+ slug,
51
+ title: folder.title,
52
+ number: folder.number,
53
+ ...(folder.metadata?.startStage
54
+ ? { startStage: folder.metadata.startStage }
55
+ : {}),
56
+ ...(folder.metadata?.description
57
+ ? { description: folder.metadata.description }
58
+ : {}),
59
+ });
54
60
  }
55
- return {
56
- domain: {
57
- title: fileStem(file),
58
- number: rawNumber,
59
- ...(startStage ? { startStage } : {}),
60
- },
61
- errors,
62
- };
63
- }
64
- export function buildSkillTree(input) {
65
- const errors = [];
66
- // --- Domains ---
67
- const numberToPath = new Map();
68
- const pathToDomain = new Map();
69
- for (const [path, parsed] of input.domainFiles) {
70
- const existing = numberToPath.get(parsed.number);
61
+ const domains = [...dirToDomain.values()].sort((a, b) => a.number - b.number);
62
+ // --- Topics ---
63
+ const pathToTopic = new Map();
64
+ const slugToTopicPath = new Map();
65
+ const topicEntries = [...input.topicFiles].sort(([a], [b]) => a.localeCompare(b));
66
+ for (const [path, parsed] of topicEntries) {
67
+ const domain = dirToDomain.get(parsed.domainDir);
68
+ if (!domain)
69
+ continue; // the folder itself was rejected above
70
+ const slug = slugifyTitle(parsed.title);
71
+ const existing = slugToTopicPath.get(slug);
71
72
  if (existing) {
72
73
  errors.push({
73
74
  file: path,
74
- line: 2,
75
- message: `Duplicate domain-number ${parsed.number} — already used by ${existing}`,
76
- suggestion: "Give each domain file a unique domain-number",
75
+ line: 1,
76
+ message: `Duplicate topic slug '${slug}': '${path}' and '${existing}' have the same title`,
77
+ suggestion: "Topic titles must be unique across all domains — rename one of the files",
77
78
  severity: "error",
78
79
  });
79
80
  continue;
80
81
  }
81
- numberToPath.set(parsed.number, path);
82
- pathToDomain.set(path, {
83
- slug: slugifyTitle(parsed.title),
82
+ slugToTopicPath.set(slug, path);
83
+ pathToTopic.set(path, {
84
+ id: parsed.id,
85
+ slug,
84
86
  title: parsed.title,
85
- number: parsed.number,
86
- ...(parsed.startStage ? { startStage: parsed.startStage } : {}),
87
+ domain: domain.slug,
88
+ ...(parsed.order !== undefined ? { order: parsed.order } : {}),
89
+ ...(parsed.description ? { description: parsed.description } : {}),
87
90
  });
88
91
  }
89
- const domains = [...pathToDomain.values()].sort((a, b) => a.number - b.number);
92
+ const domainNumber = new Map(domains.map((d) => [d.slug, d.number]));
93
+ const topics = [...pathToTopic.values()].sort((a, b) => domainNumber.get(a.domain) - domainNumber.get(b.domain) ||
94
+ compareTopicOrder(a, b));
90
95
  // --- Cards ---
91
96
  // First pass: which LO paths become cards (needed to validate requires
92
97
  // edges — an edge must point at another card, not at an excluded or
@@ -94,38 +99,56 @@ export function buildSkillTree(input) {
94
99
  const cardPaths = new Map();
95
100
  const untriaged = [];
96
101
  for (const [path, lo] of input.learningOutcomes) {
97
- if (lo.domains === undefined) {
102
+ if (lo.topics === undefined) {
98
103
  // Not yet triaged. Reported via skillTree.untriaged (a TODO list for
99
104
  // editors), not as a per-file warning — most LOs were written before
100
105
  // the skill tree existed and blanket warnings would drown /validate.
101
106
  untriaged.push(path);
102
107
  continue;
103
108
  }
104
- if (lo.domains === "none")
109
+ if (lo.topics === "none")
105
110
  continue;
106
111
  cardPaths.set(path, lo);
107
112
  }
108
113
  untriaged.sort();
109
114
  const cards = [];
110
115
  for (const [path, lo] of cardPaths) {
111
- const domainRefs = lo.domains;
116
+ const topicRefs = lo.topics;
117
+ const topicSlugs = [];
112
118
  const domainSlugs = [];
113
- for (const domainRef of domainRefs) {
114
- const domainPath = findFileWithExtension(domainRef.resolvedPath, input.files);
115
- const domain = domainPath ? pathToDomain.get(domainPath) : undefined;
116
- if (!domain) {
119
+ for (const ref of topicRefs) {
120
+ const topicPath = findFileWithExtension(ref.resolvedPath, input.files);
121
+ const topic = topicPath ? pathToTopic.get(topicPath) : undefined;
122
+ if (!topic) {
123
+ const reason = topicPath && fileStem(topicPath) === fileStem(DOMAIN_METADATA_FILE)
124
+ ? "points at a domain's metadata file, not a topic"
125
+ : topicPath
126
+ ? "does not resolve to a topic file inside a numbered domain folder"
127
+ : "does not resolve to a file";
117
128
  errors.push({
118
129
  file: path,
119
130
  line: 2,
120
- message: `Domain link does not resolve to a domain file: ${domainRef.resolvedPath}`,
121
- suggestion: "Check the file name under Domains/",
131
+ message: `'topic' link ${reason}: ${ref.resolvedPath}`,
132
+ suggestion: 'Point at a topic file: topic: "[[../Domains and Topics/<N> <Domain title>/<Topic title>]]"',
122
133
  severity: "error",
123
134
  });
124
135
  continue;
125
136
  }
126
- domainSlugs.push(domain.slug);
137
+ if (topicSlugs.includes(topic.slug)) {
138
+ errors.push({
139
+ file: path,
140
+ line: 2,
141
+ message: `Duplicate 'topic' entry: ${ref.source}`,
142
+ suggestion: "List each topic once",
143
+ severity: "error",
144
+ });
145
+ continue;
146
+ }
147
+ topicSlugs.push(topic.slug);
148
+ if (!domainSlugs.includes(topic.domain))
149
+ domainSlugs.push(topic.domain);
127
150
  }
128
- if (domainSlugs.length === 0)
151
+ if (topicSlugs.length === 0)
129
152
  continue;
130
153
  if (lo.stage === undefined)
131
154
  continue; // already reported by the parser
@@ -149,14 +172,22 @@ export function buildSkillTree(input) {
149
172
  requires.push(target.id);
150
173
  }
151
174
  const taughtBy = [];
152
- for (const modulePath of input.moduleLoRefs.get(path) ?? []) {
175
+ for (const [modulePath, submodules] of input.moduleLoRefs.get(path) ?? []) {
153
176
  const info = input.moduleInfo.get(modulePath);
154
177
  if (!info)
155
178
  continue;
179
+ // moduleCourses already holds only tagged courses.
180
+ const courses = input.moduleCourses.get(info.slug) ?? [];
181
+ if (courses.length === 0 && !info.visible)
182
+ continue;
156
183
  taughtBy.push({
157
184
  moduleSlug: info.slug,
158
185
  moduleTitle: info.title,
159
- courses: input.moduleCourses.get(info.slug) ?? [],
186
+ courses,
187
+ submodules: [...submodules].map(([slug, title]) => ({
188
+ slug: `${info.slug}/${slug}`,
189
+ title,
190
+ })),
160
191
  });
161
192
  }
162
193
  taughtBy.sort((a, b) => a.moduleSlug.localeCompare(b.moduleSlug));
@@ -164,6 +195,8 @@ export function buildSkillTree(input) {
164
195
  id: lo.id,
165
196
  title: fileStem(path),
166
197
  domains: domainSlugs,
198
+ topic: topicSlugs[0],
199
+ topics: topicSlugs,
167
200
  stage: lo.stage,
168
201
  requires,
169
202
  ...(lo.outcomeStatement ? { outcome: lo.outcomeStatement } : {}),
@@ -172,6 +205,11 @@ export function buildSkillTree(input) {
172
205
  });
173
206
  }
174
207
  cards.sort((a, b) => a.title.localeCompare(b.title));
175
- return { skillTree: { domains, cards, untriaged }, errors };
208
+ return { skillTree: { domains, topics, cards, untriaged }, errors };
209
+ }
210
+ /** `order` first (lower wins, absent last), then title. */
211
+ export function compareTopicOrder(a, b) {
212
+ return ((a.order ?? Number.POSITIVE_INFINITY) -
213
+ (b.order ?? Number.POSITIVE_INFINITY) || a.title.localeCompare(b.title));
176
214
  }
177
215
  //# sourceMappingURL=skill-tree.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill-tree.js","sourceRoot":"","sources":["../src/skill-tree.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,yEAAyE;AACzE,0EAA0E;AAC1E,6BAA6B;AAI7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAuD7D,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;IAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAQD;2DAC2D;AAC3D,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,IAAY;IAEZ,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,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,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAClC,CAAC;IACD,MAAM,SAAS,GAAG,iBAAiB,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;IACjE,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC;QAClE,MAAM,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,IAAI,EAAE,CAAC;YACP,OAAO,EAAE,gEAAgE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE;YACpG,UAAU,EAAE,2CAA2C;YACvD,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAClC,CAAC;IACD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;IAC9D,IAAI,UAA8B,CAAC;IACnC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAChD,IACE,OAAO,QAAQ,KAAK,QAAQ;YAC5B,CAAC,UAAU,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAC3D,CAAC;YACD,UAAU,GAAG,QAAQ,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI;gBACJ,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,wEAAwE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;gBAC3G,UAAU,EAAE,qDAAqD;gBACjE,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO;QACL,MAAM,EAAE;YACN,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC;YACrB,MAAM,EAAE,SAAS;YACjB,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtC;QACD,MAAM;KACP,CAAC;AACJ,CAAC;AAiBD,MAAM,UAAU,cAAc,CAAC,KAAqB;IAIlD,MAAM,MAAM,GAAmB,EAAE,CAAC;IAElC,kBAAkB;IAClB,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,MAAM,YAAY,GAAG,IAAI,GAAG,EAA2B,CAAC;IACxD,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,2BAA2B,MAAM,CAAC,MAAM,sBAAsB,QAAQ,EAAE;gBACjF,UAAU,EAAE,8CAA8C;gBAC1D,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACtC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE;YACrB,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC;YAChC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAChE,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAC7C,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAC9B,CAAC;IAEF,gBAAgB;IAChB,uEAAuE;IACvE,oEAAoE;IACpE,iBAAiB;IACjB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiC,CAAC;IAC3D,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAChD,IAAI,EAAE,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC7B,qEAAqE;YACrE,qEAAqE;YACrE,qEAAqE;YACrE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,EAAE,CAAC,OAAO,KAAK,MAAM;YAAE,SAAS;QACpC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC1B,CAAC;IACD,SAAS,CAAC,IAAI,EAAE,CAAC;IAEjB,MAAM,KAAK,GAAoB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;QACnC,MAAM,UAAU,GAAG,EAAE,CAAC,OAGrB,CAAC;QACF,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,qBAAqB,CACtC,SAAS,CAAC,YAAY,EACtB,KAAK,CAAC,KAAK,CACZ,CAAC;YACF,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACrE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,IAAI;oBACV,IAAI,EAAE,CAAC;oBACP,OAAO,EAAE,kDAAkD,SAAS,CAAC,YAAY,EAAE;oBACnF,UAAU,EAAE,oCAAoC;oBAChD,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YACD,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACvC,IAAI,EAAE,CAAC,KAAK,KAAK,SAAS;YAAE,SAAS,CAAC,iCAAiC;QAEvE,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,MAAM,GAAG,IAAI,EAAE,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YACpC,MAAM,UAAU,GAAG,qBAAqB,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YACxE,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAClE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,UAAU;oBACvB,CAAC,CAAC,8DAA8D;oBAChE,CAAC,CAAC,6CAA6C,CAAC;gBAClD,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,IAAI;oBACV,IAAI,EAAE,CAAC;oBACP,OAAO,EAAE,mBAAmB,MAAM,KAAK,GAAG,CAAC,YAAY,EAAE;oBACzD,UAAU,EACR,sEAAsE;oBACxE,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,CAAC;QAED,MAAM,QAAQ,GAAqB,EAAE,CAAC;QACtC,KAAK,MAAM,UAAU,IAAI,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC9C,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,QAAQ,CAAC,IAAI,CAAC;gBACZ,UAAU,EAAE,IAAI,CAAC,IAAI;gBACrB,WAAW,EAAE,IAAI,CAAC,KAAK;gBACvB,OAAO,EAAE,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;aAClD,CAAC,CAAC;QACL,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;QAElE,KAAK,CAAC,IAAI,CAAC;YACT,EAAE,EAAE,EAAE,CAAC,EAAE;YACT,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC;YACrB,OAAO,EAAE,WAAW;YACpB,KAAK,EAAE,EAAE,CAAC,KAAK;YACf,QAAQ;YACR,GAAG,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChD,QAAQ;SACT,CAAC,CAAC;IACL,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAErD,OAAO,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"skill-tree.js","sourceRoot":"","sources":["../src/skill-tree.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { ContentError } from "../index.js";
2
+ export interface VideoImportSyntax {
3
+ kind: "video";
4
+ /** Wikilink target, e.g. "../video_transcripts/name" */
5
+ target: string;
6
+ from?: string;
7
+ to?: string;
8
+ }
9
+ export interface WidgetEmbedSyntax {
10
+ kind: "widget";
11
+ /** Wikilink target, e.g. "../widgets/name" */
12
+ target: string;
13
+ /** `|height=480px` modifier: overrides the widget file's height here. */
14
+ height?: string;
15
+ }
16
+ export type ArticleImportSyntax = VideoImportSyntax | WidgetEmbedSyntax;
17
+ export type ArticleImportParse = {
18
+ kind: "import";
19
+ import: ArticleImportSyntax;
20
+ } | {
21
+ kind: "invalid";
22
+ kindName: "video" | "widget";
23
+ reason: string;
24
+ suggestion: string;
25
+ } | {
26
+ kind: "none";
27
+ };
28
+ /**
29
+ * Classify a single line of article body text as a ::video import.
30
+ * - "import": a valid own-line ::video import
31
+ * - "invalid": an attempted import with broken syntax
32
+ * - "none": not a video import at all
33
+ */
34
+ export declare function parseVideoImportLine(line: string): ArticleImportParse;
35
+ /** True when a wikilink target names a file directly under a widgets/ folder. */
36
+ export declare function isWidgetEmbedTarget(target: string): boolean;
37
+ /**
38
+ * Classify a single line of article body text as a widget embed.
39
+ * - "import": a valid own-line ![[../widgets/name]] embed
40
+ * - "invalid": a widget embed with broken syntax or buried in prose
41
+ * - "none": not a widget embed (other ![[...]] embeds included)
42
+ */
43
+ export declare function parseWidgetEmbedLine(line: string): ArticleImportParse;
44
+ /** Classify a line as any of the inline article imports. */
45
+ export declare function parseArticleImportLine(line: string): ArticleImportParse;
46
+ export interface ScannedArticleLine {
47
+ line: string;
48
+ /** False inside fenced code and on fence/container marker lines. */
49
+ prose: boolean;
50
+ /** Innermost open `:::hide` / `:::collapse` container, if any. */
51
+ hiddenIn?: string;
52
+ }
53
+ /**
54
+ * Walk an article body line by line, tracking fenced code and container
55
+ * directives, so every consumer of import lines skips the same regions.
56
+ */
57
+ export declare function scanArticleLines(body: string): ScannedArticleLine[];
58
+ /**
59
+ * Parse-time validation of inline imports in an article body.
60
+ * Resolution errors (missing file, bad range) are reported later by the
61
+ * flattener, which has the file map.
62
+ */
63
+ export declare function validateArticleImports(body: string, file: string, bodyStartLine: number): ContentError[];
64
+ /**
65
+ * Lens text segments do not resolve inline article imports — catch attempts
66
+ * there with a pointer to the supported mechanisms.
67
+ */
68
+ export declare function validateNoArticleImportsInLensText(content: string, file: string, line: number): ContentError[];
@@ -0,0 +1,265 @@
1
+ import { parseTimestamp } from "../bundler/video.js";
2
+ import { isValidHeight } from "../parser/widget.js";
3
+ // ::video{attr}[[path|alias]] or ::video[[path|alias]]{attr}, anchored to a
4
+ // full (trimmed) line. Alias is accepted for wikilink-tooling compatibility
5
+ // but unused — the display title comes from the transcript frontmatter.
6
+ const VIDEO_IMPORT_LINE_RE = /^::video(?:\{([^}]*)\})?\[\[([^\]|]+)(?:\|[^\]]+)?\]\](?:\{([^}]*)\})?$/;
7
+ // Anything that looks like an attempted ::video import (used to flag
8
+ // malformed/misplaced usages without false-positives on prose that merely
9
+ // mentions "::video").
10
+ const VIDEO_IMPORT_CANDIDATE_RE = /::video[[{]/;
11
+ // ![[path]] or ![[path|modifier]], anchored to a full (trimmed) line.
12
+ const WIDGET_EMBED_LINE_RE = /^!\[\[([^\]|]+)(?:\|([^\]]*))?\]\]$/;
13
+ // Every ![[...]] on a line; the target decides whether it is a widget embed.
14
+ const WIDGET_EMBED_CANDIDATE_RE = /!\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g;
15
+ // A file directly under a widgets/ folder, written relative to the article
16
+ // (`../widgets/name`) or to the vault root (`widgets/name`), no fragment.
17
+ const WIDGET_TARGET_RE = /^(?:\.\.?\/)*widgets\/[^/#|]+$/;
18
+ // Inline code is not prose: an import mentioned in backticks is neither an
19
+ // import nor a mistake.
20
+ const INLINE_CODE_RE = /`[^`]*`/g;
21
+ // CommonMark permits up to three leading spaces before a fenced code block.
22
+ const CODE_FENCE = /^ {0,3}(?:```|~~~)/;
23
+ const CONTAINER_OPEN = /^(:{3,})([\w-]+)/;
24
+ const CONTAINER_CLOSE = /^(:{3,})\s*(?:\{[^}]*\})?\s*$/;
25
+ /** Parse `from="..." to="..."` attribute text. */
26
+ function parseAttrs(attr) {
27
+ if (attr === undefined || attr.trim() === "")
28
+ return {};
29
+ const tokens = [...attr.matchAll(/([\w-]+)\s*=\s*"([^"]*)"/g)];
30
+ const consumed = tokens.map((m) => m[0]).join(" ");
31
+ if (consumed.replace(/\s+/g, " ").trim() !== attr.replace(/\s+/g, " ").trim()) {
32
+ return {
33
+ reason: `invalid attributes '{${attr}}'`,
34
+ suggestion: 'Use key="value" attributes: from and/or to',
35
+ };
36
+ }
37
+ const result = {};
38
+ for (const token of tokens) {
39
+ const key = token[1];
40
+ const value = token[2];
41
+ if (key !== "from" && key !== "to") {
42
+ return {
43
+ reason: `unrecognized attribute '${key}'`,
44
+ suggestion: "Only from and to attributes are supported",
45
+ };
46
+ }
47
+ if (result[key] !== undefined) {
48
+ return {
49
+ reason: `duplicate attribute '${key}'`,
50
+ suggestion: `Keep only one ${key} attribute`,
51
+ };
52
+ }
53
+ if (parseTimestamp(value) === null) {
54
+ return {
55
+ reason: `invalid timestamp in '${key}="${value}"'`,
56
+ suggestion: "Use M:SS (e.g. 1:30) or H:MM:SS (e.g. 1:30:00)",
57
+ };
58
+ }
59
+ result[key] = value;
60
+ }
61
+ return result;
62
+ }
63
+ /**
64
+ * Classify a single line of article body text as a ::video import.
65
+ * - "import": a valid own-line ::video import
66
+ * - "invalid": an attempted import with broken syntax
67
+ * - "none": not a video import at all
68
+ */
69
+ export function parseVideoImportLine(line) {
70
+ const trimmed = line.trim();
71
+ if (!VIDEO_IMPORT_CANDIDATE_RE.test(trimmed.replace(INLINE_CODE_RE, ""))) {
72
+ return { kind: "none" };
73
+ }
74
+ const invalid = (reason, suggestion) => ({
75
+ kind: "invalid",
76
+ kindName: "video",
77
+ reason,
78
+ suggestion,
79
+ });
80
+ const match = trimmed.match(VIDEO_IMPORT_LINE_RE);
81
+ if (!match) {
82
+ if (trimmed.startsWith("::video")) {
83
+ return invalid("malformed ::video import", 'Use ::video[[../video_transcripts/name]] with optional {from="1:30" to="4:10"}, alone on its own line');
84
+ }
85
+ // ::video appears mid-line — an import buried in prose.
86
+ return invalid("::video import must be alone on its own line", "Move the ::video[[...]] import to its own line");
87
+ }
88
+ const preAttr = match[1];
89
+ const target = match[2].trim();
90
+ const postAttr = match[3];
91
+ if (preAttr !== undefined && postAttr !== undefined) {
92
+ return invalid("attributes given both before and after the wikilink", "Use a single {from=... to=...} block after the ]]");
93
+ }
94
+ const attrs = parseAttrs(preAttr ?? postAttr);
95
+ if ("reason" in attrs)
96
+ return invalid(attrs.reason, attrs.suggestion);
97
+ if (target === "") {
98
+ return invalid("empty wikilink target", "Reference a transcript: ::video[[../video_transcripts/name]]");
99
+ }
100
+ return { kind: "import", import: { kind: "video", target, ...attrs } };
101
+ }
102
+ /** True when a wikilink target names a file directly under a widgets/ folder. */
103
+ export function isWidgetEmbedTarget(target) {
104
+ return WIDGET_TARGET_RE.test(target.trim());
105
+ }
106
+ /** True when a line mentions a widget embed anywhere outside inline code. */
107
+ function hasWidgetEmbedCandidate(line) {
108
+ return [
109
+ ...line.replace(INLINE_CODE_RE, "").matchAll(WIDGET_EMBED_CANDIDATE_RE),
110
+ ].some((m) => isWidgetEmbedTarget(m[1]));
111
+ }
112
+ /**
113
+ * Classify a single line of article body text as a widget embed.
114
+ * - "import": a valid own-line ![[../widgets/name]] embed
115
+ * - "invalid": a widget embed with broken syntax or buried in prose
116
+ * - "none": not a widget embed (other ![[...]] embeds included)
117
+ */
118
+ export function parseWidgetEmbedLine(line) {
119
+ const trimmed = line.trim();
120
+ if (!hasWidgetEmbedCandidate(trimmed))
121
+ return { kind: "none" };
122
+ const invalid = (reason, suggestion) => ({
123
+ kind: "invalid",
124
+ kindName: "widget",
125
+ reason,
126
+ suggestion,
127
+ });
128
+ const match = trimmed.match(WIDGET_EMBED_LINE_RE);
129
+ if (!match || !isWidgetEmbedTarget(match[1])) {
130
+ return invalid("widget embed must be alone on its own line", "Move the ![[../widgets/name]] embed to its own line");
131
+ }
132
+ const target = match[1].trim();
133
+ const modifier = match[2]?.trim();
134
+ if (modifier === undefined || modifier === "") {
135
+ return { kind: "import", import: { kind: "widget", target } };
136
+ }
137
+ const heightMatch = modifier.match(/^height\s*=\s*(\S+)$/);
138
+ if (!heightMatch || !isValidHeight(heightMatch[1])) {
139
+ return invalid(`invalid modifier '|${modifier}'`, 'The only modifier is height, e.g. ![[../widgets/name|height=480px]] ("auto" or a CSS length)');
140
+ }
141
+ return {
142
+ kind: "import",
143
+ import: { kind: "widget", target, height: heightMatch[1] },
144
+ };
145
+ }
146
+ /** Classify a line as any of the inline article imports. */
147
+ export function parseArticleImportLine(line) {
148
+ const video = parseVideoImportLine(line);
149
+ return video.kind === "none" ? parseWidgetEmbedLine(line) : video;
150
+ }
151
+ /**
152
+ * Walk an article body line by line, tracking fenced code and container
153
+ * directives, so every consumer of import lines skips the same regions.
154
+ */
155
+ export function scanArticleLines(body) {
156
+ const out = [];
157
+ let inCodeBlock = false;
158
+ // Names of open container directives, innermost last.
159
+ const containerStack = [];
160
+ for (const line of body.split("\n")) {
161
+ if (CODE_FENCE.test(line)) {
162
+ inCodeBlock = !inCodeBlock;
163
+ out.push({ line, prose: false });
164
+ continue;
165
+ }
166
+ if (inCodeBlock) {
167
+ out.push({ line, prose: false });
168
+ continue;
169
+ }
170
+ const openMatch = line.match(CONTAINER_OPEN);
171
+ if (openMatch) {
172
+ containerStack.push(openMatch[2]);
173
+ out.push({ line, prose: false });
174
+ continue;
175
+ }
176
+ if (CONTAINER_CLOSE.test(line)) {
177
+ containerStack.pop();
178
+ out.push({ line, prose: false });
179
+ continue;
180
+ }
181
+ const hiddenIn = containerStack.find((name) => name === "hide" || name === "collapse");
182
+ out.push(hiddenIn ? { line, prose: true, hiddenIn } : { line, prose: true });
183
+ }
184
+ return out;
185
+ }
186
+ const IMPORT_LABEL = {
187
+ video: { code: "video-import", label: "::video import" },
188
+ widget: { code: "widget-embed", label: "widget embed" },
189
+ };
190
+ /**
191
+ * Parse-time validation of inline imports in an article body.
192
+ * Resolution errors (missing file, bad range) are reported later by the
193
+ * flattener, which has the file map.
194
+ */
195
+ export function validateArticleImports(body, file, bodyStartLine) {
196
+ const errors = [];
197
+ const scanned = scanArticleLines(body);
198
+ for (let i = 0; i < scanned.length; i++) {
199
+ const { line, prose, hiddenIn } = scanned[i];
200
+ if (!prose)
201
+ continue;
202
+ const absLine = bodyStartLine + i;
203
+ const parsed = parseArticleImportLine(line);
204
+ if (parsed.kind === "none")
205
+ continue;
206
+ if (parsed.kind === "invalid") {
207
+ const { code, label } = IMPORT_LABEL[parsed.kindName];
208
+ const notOwnLine = parsed.reason.includes("own line");
209
+ errors.push({
210
+ file,
211
+ line: absLine,
212
+ code: `article.${code}-${notOwnLine ? "not-own-line" : "invalid"}`,
213
+ message: `Invalid ${label}: ${parsed.reason}`,
214
+ suggestion: parsed.suggestion,
215
+ severity: "error",
216
+ });
217
+ continue;
218
+ }
219
+ // Imports inside hide/collapse would silently vanish from rendered output
220
+ // (the collapsed string strips them), so they are rejected outright.
221
+ if (hiddenIn) {
222
+ const { code, label } = IMPORT_LABEL[parsed.import.kind];
223
+ errors.push({
224
+ file,
225
+ line: absLine,
226
+ code: `article.${code}-in-hide`,
227
+ message: `${label} inside a ':::${hiddenIn}' container`,
228
+ suggestion: `Move the import outside the hidden block — hidden text is collapsed and the ${parsed.import.kind} would never render`,
229
+ severity: "error",
230
+ });
231
+ }
232
+ }
233
+ return errors;
234
+ }
235
+ /**
236
+ * Lens text segments do not resolve inline article imports — catch attempts
237
+ * there with a pointer to the supported mechanisms.
238
+ */
239
+ export function validateNoArticleImportsInLensText(content, file, line) {
240
+ const errors = [];
241
+ const lines = content.split("\n");
242
+ for (let i = 0; i < lines.length; i++) {
243
+ const prose = lines[i].replace(INLINE_CODE_RE, "");
244
+ if (VIDEO_IMPORT_CANDIDATE_RE.test(prose)) {
245
+ errors.push({
246
+ file,
247
+ line: line + i,
248
+ message: "::video imports are only supported in article bodies",
249
+ suggestion: "Use a '#### Video' segment in the lens, or move the import into the article file",
250
+ severity: "error",
251
+ });
252
+ }
253
+ else if (hasWidgetEmbedCandidate(prose)) {
254
+ errors.push({
255
+ file,
256
+ line: line + i,
257
+ message: "Widget embeds (![[../widgets/...]]) are only supported in article bodies",
258
+ suggestion: "Use a '#### Widget' segment in the lens, or move the embed into the article file",
259
+ severity: "error",
260
+ });
261
+ }
262
+ }
263
+ return errors;
264
+ }
265
+ //# sourceMappingURL=article-imports.js.map