lens-content-processor 0.2.1 → 0.2.2

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 (60) hide show
  1. package/dist/bundler/article.js +16 -4
  2. package/dist/bundler/article.js.map +1 -1
  3. package/dist/bundler/video.js +4 -4
  4. package/dist/bundler/video.js.map +1 -1
  5. package/dist/cli.js +7 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/content-schema.d.ts +34 -0
  8. package/dist/content-schema.js +61 -0
  9. package/dist/content-schema.js.map +1 -0
  10. package/dist/flattener/index.js +58 -22
  11. package/dist/flattener/index.js.map +1 -1
  12. package/dist/index.d.ts +12 -1
  13. package/dist/index.js +121 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/parser/article.d.ts +16 -0
  16. package/dist/parser/article.js +67 -0
  17. package/dist/parser/article.js.map +1 -0
  18. package/dist/parser/course.js +13 -20
  19. package/dist/parser/course.js.map +1 -1
  20. package/dist/parser/frontmatter.js +2 -0
  21. package/dist/parser/frontmatter.js.map +1 -1
  22. package/dist/parser/learning-outcome.js +21 -9
  23. package/dist/parser/learning-outcome.js.map +1 -1
  24. package/dist/parser/lens.js +131 -14
  25. package/dist/parser/lens.js.map +1 -1
  26. package/dist/parser/module.d.ts +11 -5
  27. package/dist/parser/module.js +171 -75
  28. package/dist/parser/module.js.map +1 -1
  29. package/dist/parser/sections.js +113 -12
  30. package/dist/parser/sections.js.map +1 -1
  31. package/dist/parser/video-transcript.d.ts +11 -0
  32. package/dist/parser/video-transcript.js +39 -0
  33. package/dist/parser/video-transcript.js.map +1 -0
  34. package/dist/parser/wikilink.d.ts +2 -0
  35. package/dist/parser/wikilink.js +83 -24
  36. package/dist/parser/wikilink.js.map +1 -1
  37. package/dist/validator/duplicates.d.ts +13 -0
  38. package/dist/validator/duplicates.js +27 -0
  39. package/dist/validator/duplicates.js.map +1 -0
  40. package/dist/validator/field-typos.d.ts +11 -0
  41. package/dist/validator/field-typos.js +45 -28
  42. package/dist/validator/field-typos.js.map +1 -1
  43. package/dist/validator/field-values.d.ts +14 -0
  44. package/dist/validator/field-values.js +61 -7
  45. package/dist/validator/field-values.js.map +1 -1
  46. package/dist/validator/output-integrity.d.ts +7 -0
  47. package/dist/validator/output-integrity.js +57 -0
  48. package/dist/validator/output-integrity.js.map +1 -0
  49. package/dist/validator/segment-fields.js +1 -15
  50. package/dist/validator/segment-fields.js.map +1 -1
  51. package/dist/validator/timestamps.d.ts +2 -0
  52. package/dist/validator/timestamps.js +91 -0
  53. package/dist/validator/timestamps.js.map +1 -0
  54. package/dist/validator/url-reachability.d.ts +8 -0
  55. package/dist/validator/url-reachability.js +60 -0
  56. package/dist/validator/url-reachability.js.map +1 -0
  57. package/dist/validator/validate-frontmatter.d.ts +9 -0
  58. package/dist/validator/validate-frontmatter.js +53 -0
  59. package/dist/validator/validate-frontmatter.js.map +1 -0
  60. package/package.json +1 -1
@@ -1,4 +1,18 @@
1
1
  import type { ContentError } from '../index.js';
2
+ /**
3
+ * Validate slug format.
4
+ *
5
+ * Valid slugs must:
6
+ * - Contain only lowercase letters, numbers, and hyphens
7
+ * - Not start or end with a hyphen
8
+ * - Not contain spaces or special characters
9
+ *
10
+ * @param slug - The slug value to validate
11
+ * @param file - File path for error reporting
12
+ * @param line - Line number for error reporting
13
+ * @returns ContentError if invalid, null if valid
14
+ */
15
+ export declare function validateSlugFormat(slug: string, file: string, line: number): ContentError | null;
2
16
  /**
3
17
  * Validate field values, checking for appropriate types.
4
18
  * Currently validates that boolean fields contain 'true' or 'false'.
@@ -1,11 +1,65 @@
1
+ import { ALL_BOOLEAN_FIELDS } from '../content-schema.js';
1
2
  /**
2
- * Fields that should only contain boolean values ('true' or 'false').
3
+ * Valid slug pattern: lowercase letters, numbers, and hyphens only.
4
+ * Must not start or end with a hyphen.
3
5
  */
4
- const BOOLEAN_FIELDS = new Set([
5
- 'optional',
6
- 'hidePreviousContentFromUser',
7
- 'hidePreviousContentFromTutor',
8
- ]);
6
+ const VALID_SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
7
+ /**
8
+ * Validate slug format.
9
+ *
10
+ * Valid slugs must:
11
+ * - Contain only lowercase letters, numbers, and hyphens
12
+ * - Not start or end with a hyphen
13
+ * - Not contain spaces or special characters
14
+ *
15
+ * @param slug - The slug value to validate
16
+ * @param file - File path for error reporting
17
+ * @param line - Line number for error reporting
18
+ * @returns ContentError if invalid, null if valid
19
+ */
20
+ export function validateSlugFormat(slug, file, line) {
21
+ // Check for uppercase letters first (more specific message)
22
+ if (/[A-Z]/.test(slug)) {
23
+ return {
24
+ file,
25
+ line,
26
+ message: `Invalid slug format '${slug}': contains uppercase letters`,
27
+ suggestion: `Use lowercase only. Try '${slug.toLowerCase()}'`,
28
+ severity: 'error',
29
+ };
30
+ }
31
+ // Check for leading hyphen
32
+ if (slug.startsWith('-')) {
33
+ return {
34
+ file,
35
+ line,
36
+ message: `Invalid slug format '${slug}': cannot start with a hyphen`,
37
+ suggestion: 'Remove the leading hyphen',
38
+ severity: 'error',
39
+ };
40
+ }
41
+ // Check for trailing hyphen
42
+ if (slug.endsWith('-')) {
43
+ return {
44
+ file,
45
+ line,
46
+ message: `Invalid slug format '${slug}': cannot end with a hyphen`,
47
+ suggestion: 'Remove the trailing hyphen',
48
+ severity: 'error',
49
+ };
50
+ }
51
+ // Check general pattern (lowercase, numbers, hyphens only)
52
+ if (!VALID_SLUG_PATTERN.test(slug)) {
53
+ return {
54
+ file,
55
+ line,
56
+ message: `Invalid slug format '${slug}': must contain only lowercase letters, numbers, and hyphens`,
57
+ suggestion: 'Use only a-z, 0-9, and hyphens (-)',
58
+ severity: 'error',
59
+ };
60
+ }
61
+ return null;
62
+ }
9
63
  /**
10
64
  * Validate field values, checking for appropriate types.
11
65
  * Currently validates that boolean fields contain 'true' or 'false'.
@@ -19,7 +73,7 @@ export function validateFieldValues(fields, file, line) {
19
73
  const warnings = [];
20
74
  for (const [name, value] of Object.entries(fields)) {
21
75
  // Check if this is a boolean field
22
- if (BOOLEAN_FIELDS.has(name)) {
76
+ if (ALL_BOOLEAN_FIELDS.includes(name)) {
23
77
  const normalizedValue = value.toLowerCase();
24
78
  if (normalizedValue !== 'true' && normalizedValue !== 'false') {
25
79
  warnings.push({
@@ -1 +1 @@
1
- {"version":3,"file":"field-values.js","sourceRoot":"","sources":["../../src/validator/field-values.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,UAAU;IACV,6BAA6B;IAC7B,8BAA8B;CAC/B,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA8B,EAC9B,IAAY,EACZ,IAAY;IAEZ,MAAM,QAAQ,GAAmB,EAAE,CAAC;IAEpC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,mCAAmC;QACnC,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,MAAM,eAAe,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,eAAe,KAAK,MAAM,IAAI,eAAe,KAAK,OAAO,EAAE,CAAC;gBAC9D,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI;oBACJ,IAAI;oBACJ,OAAO,EAAE,UAAU,IAAI,4BAA4B,KAAK,GAAG;oBAC3D,UAAU,EAAE,4BAA4B;oBACxC,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"field-values.js","sourceRoot":"","sources":["../../src/validator/field-values.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;GAGG;AACH,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,IAAY,EACZ,IAAY;IAEZ,4DAA4D;IAC5D,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,IAAI;YACJ,IAAI;YACJ,OAAO,EAAE,wBAAwB,IAAI,+BAA+B;YACpE,UAAU,EAAE,4BAA4B,IAAI,CAAC,WAAW,EAAE,GAAG;YAC7D,QAAQ,EAAE,OAAO;SAClB,CAAC;IACJ,CAAC;IAED,2BAA2B;IAC3B,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,IAAI;YACJ,IAAI;YACJ,OAAO,EAAE,wBAAwB,IAAI,+BAA+B;YACpE,UAAU,EAAE,2BAA2B;YACvC,QAAQ,EAAE,OAAO;SAClB,CAAC;IACJ,CAAC;IAED,4BAA4B;IAC5B,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,IAAI;YACJ,IAAI;YACJ,OAAO,EAAE,wBAAwB,IAAI,6BAA6B;YAClE,UAAU,EAAE,4BAA4B;YACxC,QAAQ,EAAE,OAAO;SAClB,CAAC;IACJ,CAAC;IAED,2DAA2D;IAC3D,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,OAAO;YACL,IAAI;YACJ,IAAI;YACJ,OAAO,EAAE,wBAAwB,IAAI,8DAA8D;YACnG,UAAU,EAAE,oCAAoC;YAChD,QAAQ,EAAE,OAAO;SAClB,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA8B,EAC9B,IAAY,EACZ,IAAY;IAEZ,MAAM,QAAQ,GAAmB,EAAE,CAAC;IAEpC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,mCAAmC;QACnC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,MAAM,eAAe,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,eAAe,KAAK,MAAM,IAAI,eAAe,KAAK,OAAO,EAAE,CAAC;gBAC9D,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI;oBACJ,IAAI;oBACJ,OAAO,EAAE,UAAU,IAAI,4BAA4B,KAAK,GAAG;oBAC3D,UAAU,EAAE,4BAA4B;oBACxC,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { ContentError, FlattenedModule } from '../index.js';
2
+ /**
3
+ * Broad safety-net validation on the flattened output.
4
+ * Catches empty sections (no segments) and empty segments (no content)
5
+ * that specific validators might have missed.
6
+ */
7
+ export declare function validateOutputIntegrity(modules: FlattenedModule[], slugToPath?: Map<string, string>): ContentError[];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Broad safety-net validation on the flattened output.
3
+ * Catches empty sections (no segments) and empty segments (no content)
4
+ * that specific validators might have missed.
5
+ */
6
+ export function validateOutputIntegrity(modules, slugToPath) {
7
+ const errors = [];
8
+ for (const module of modules) {
9
+ const file = slugToPath?.get(module.slug) ?? module.slug;
10
+ for (const section of module.sections) {
11
+ const sectionLabel = section.meta.title ?? section.type;
12
+ if (section.segments.length === 0) {
13
+ errors.push({
14
+ file,
15
+ message: `Section "${sectionLabel}" has no segments`,
16
+ severity: 'error',
17
+ });
18
+ continue;
19
+ }
20
+ for (let i = 0; i < section.segments.length; i++) {
21
+ const segment = section.segments[i];
22
+ switch (segment.type) {
23
+ case 'text':
24
+ if (!segment.content?.trim()) {
25
+ errors.push({
26
+ file,
27
+ message: `Empty text segment in "${sectionLabel}" (segment ${i + 1})`,
28
+ severity: 'error',
29
+ });
30
+ }
31
+ break;
32
+ case 'article-excerpt':
33
+ if (!segment.content?.trim()) {
34
+ errors.push({
35
+ file,
36
+ message: `Empty article-excerpt segment in "${sectionLabel}" (segment ${i + 1})`,
37
+ severity: 'error',
38
+ });
39
+ }
40
+ break;
41
+ case 'video-excerpt':
42
+ if (!segment.transcript?.trim()) {
43
+ errors.push({
44
+ file,
45
+ message: `Empty video-excerpt transcript in "${sectionLabel}" (segment ${i + 1})`,
46
+ severity: 'error',
47
+ });
48
+ }
49
+ break;
50
+ // chat segments have no required content body
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return errors;
56
+ }
57
+ //# sourceMappingURL=output-integrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-integrity.js","sourceRoot":"","sources":["../../src/validator/output-integrity.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAA0B,EAC1B,UAAgC;IAEhC,MAAM,MAAM,GAAmB,EAAE,CAAC;IAElC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC;QAEzD,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACtC,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAExD,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAClC,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI;oBACJ,OAAO,EAAE,YAAY,YAAY,mBAAmB;oBACpD,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACjD,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAEpC,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;oBACrB,KAAK,MAAM;wBACT,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;4BAC7B,MAAM,CAAC,IAAI,CAAC;gCACV,IAAI;gCACJ,OAAO,EAAE,0BAA0B,YAAY,cAAc,CAAC,GAAG,CAAC,GAAG;gCACrE,QAAQ,EAAE,OAAO;6BAClB,CAAC,CAAC;wBACL,CAAC;wBACD,MAAM;oBACR,KAAK,iBAAiB;wBACpB,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;4BAC7B,MAAM,CAAC,IAAI,CAAC;gCACV,IAAI;gCACJ,OAAO,EAAE,qCAAqC,YAAY,cAAc,CAAC,GAAG,CAAC,GAAG;gCAChF,QAAQ,EAAE,OAAO;6BAClB,CAAC,CAAC;wBACL,CAAC;wBACD,MAAM;oBACR,KAAK,eAAe;wBAClB,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC;4BAChC,MAAM,CAAC,IAAI,CAAC;gCACV,IAAI;gCACJ,OAAO,EAAE,sCAAsC,YAAY,cAAc,CAAC,GAAG,CAAC,GAAG;gCACjF,QAAQ,EAAE,OAAO;6BAClB,CAAC,CAAC;wBACL,CAAC;wBACD,MAAM;oBACR,8CAA8C;gBAChD,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,18 +1,4 @@
1
- /**
2
- * Define which fields are valid for each segment type.
3
- * This allows us to warn when a field appears in the wrong segment type.
4
- */
5
- const VALID_FIELDS_BY_SEGMENT_TYPE = {
6
- text: new Set(['content', 'optional']),
7
- chat: new Set([
8
- 'instructions',
9
- 'optional',
10
- 'hidePreviousContentFromUser',
11
- 'hidePreviousContentFromTutor',
12
- ]),
13
- 'article-excerpt': new Set(['from', 'to', 'optional']),
14
- 'video-excerpt': new Set(['from', 'to', 'optional']),
15
- };
1
+ import { VALID_FIELDS_BY_SEGMENT_TYPE } from '../content-schema.js';
16
2
  /**
17
3
  * Fields that are specific to excerpt segments (not valid in text/chat).
18
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"segment-fields.js","sourceRoot":"","sources":["../../src/validator/segment-fields.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,4BAA4B,GAAgC;IAChE,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACtC,IAAI,EAAE,IAAI,GAAG,CAAC;QACZ,cAAc;QACd,UAAU;QACV,6BAA6B;QAC7B,8BAA8B;KAC/B,CAAC;IACF,iBAAiB,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IACtD,eAAe,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;CACrD,CAAC;AAEF;;GAEG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,WAAmB,EACnB,MAA8B,EAC9B,IAAY,EACZ,IAAY;IAEZ,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,MAAM,WAAW,GAAG,4BAA4B,CAAC,WAAW,CAAC,CAAC;IAE9D,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,sDAAsD;QACtD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,oEAAoE;YACpE,IAAI,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBACvC,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI;oBACJ,IAAI;oBACJ,OAAO,EAAE,UAAU,SAAS,qBAAqB,WAAW,UAAU;oBACtE,UAAU,EAAE,IAAI,SAAS,8DAA8D;oBACvF,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;YACD,2DAA2D;YAC3D,mCAAmC;QACrC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"segment-fields.js","sourceRoot":"","sources":["../../src/validator/segment-fields.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAEpE;;GAEG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,WAAmB,EACnB,MAA8B,EAC9B,IAAY,EACZ,IAAY;IAEZ,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,MAAM,WAAW,GAAG,4BAA4B,CAAC,WAAW,CAAC,CAAC;IAE9D,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,sDAAsD;QACtD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,oEAAoE;YACpE,IAAI,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBACvC,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI;oBACJ,IAAI;oBACJ,OAAO,EAAE,UAAU,SAAS,qBAAqB,WAAW,UAAU;oBACtE,UAAU,EAAE,IAAI,SAAS,8DAA8D;oBACvF,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;YACD,2DAA2D;YAC3D,mCAAmC;QACrC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ContentError } from '../index.js';
2
+ export declare function validateTimestamps(content: string, file: string): ContentError[];
@@ -0,0 +1,91 @@
1
+ import { parseTimestamp } from '../bundler/video.js';
2
+ export function validateTimestamps(content, file) {
3
+ const errors = [];
4
+ let parsed;
5
+ try {
6
+ parsed = JSON.parse(content);
7
+ }
8
+ catch (e) {
9
+ errors.push({
10
+ file,
11
+ line: 1,
12
+ message: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`,
13
+ severity: 'error',
14
+ });
15
+ return errors;
16
+ }
17
+ if (!Array.isArray(parsed)) {
18
+ errors.push({
19
+ file,
20
+ line: 1,
21
+ message: 'Timestamps file must contain a JSON array',
22
+ severity: 'error',
23
+ });
24
+ return errors;
25
+ }
26
+ if (parsed.length === 0) {
27
+ errors.push({
28
+ file,
29
+ line: 1,
30
+ message: 'Timestamps array is empty',
31
+ severity: 'warning',
32
+ });
33
+ return errors;
34
+ }
35
+ let prevSeconds = null;
36
+ for (let i = 0; i < parsed.length; i++) {
37
+ const entry = parsed[i];
38
+ if (typeof entry.text !== 'string' && entry.text !== undefined) {
39
+ errors.push({
40
+ file,
41
+ message: `Entry ${i}: 'text' must be a string`,
42
+ severity: 'error',
43
+ });
44
+ }
45
+ else if (entry.text === undefined) {
46
+ errors.push({
47
+ file,
48
+ message: `Entry ${i}: missing required field 'text'`,
49
+ severity: 'error',
50
+ });
51
+ }
52
+ if (entry.start === undefined) {
53
+ errors.push({
54
+ file,
55
+ message: `Entry ${i}: missing required field 'start'`,
56
+ severity: 'error',
57
+ });
58
+ }
59
+ else if (typeof entry.start === 'string') {
60
+ const seconds = parseTimestamp(entry.start);
61
+ if (seconds === null) {
62
+ errors.push({
63
+ file,
64
+ message: `Entry ${i}: invalid timestamp format '${entry.start}'`,
65
+ suggestion: 'Use format M:SS.ms (e.g., 0:01.32)',
66
+ severity: 'error',
67
+ });
68
+ }
69
+ else {
70
+ if (prevSeconds !== null && seconds < prevSeconds) {
71
+ errors.push({
72
+ file,
73
+ message: `Entry ${i}: non-monotonic timestamp '${entry.start}' (previous was later)`,
74
+ severity: 'warning',
75
+ });
76
+ }
77
+ prevSeconds = seconds;
78
+ }
79
+ }
80
+ else {
81
+ errors.push({
82
+ file,
83
+ message: `Entry ${i}: 'start' must be a string, got ${typeof entry.start}`,
84
+ suggestion: "Use string format like '0:01.32' instead of a number",
85
+ severity: 'error',
86
+ });
87
+ }
88
+ }
89
+ return errors;
90
+ }
91
+ //# sourceMappingURL=timestamps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamps.js","sourceRoot":"","sources":["../../src/validator/timestamps.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ContentError } from '../index.js';
2
+ export interface UrlToValidate {
3
+ url: string;
4
+ file: string;
5
+ line: number;
6
+ label: string;
7
+ }
8
+ export declare function validateUrls(urls: UrlToValidate[]): Promise<ContentError[]>;
@@ -0,0 +1,60 @@
1
+ const CONCURRENCY = 10;
2
+ const TIMEOUT_MS = 15_000;
3
+ // Status codes that mean "server is alive" even though response.ok is false
4
+ const REACHABLE_STATUSES = new Set([429]);
5
+ async function fetchWithTimeout(url, method, signal) {
6
+ return fetch(url, { method, signal });
7
+ }
8
+ async function checkUrl(entry) {
9
+ try {
10
+ const controller = new AbortController();
11
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
12
+ let response = await fetchWithTimeout(entry.url, 'HEAD', controller.signal);
13
+ // Some servers reject HEAD — retry with GET
14
+ if (response.status === 405) {
15
+ response = await fetchWithTimeout(entry.url, 'GET', controller.signal);
16
+ }
17
+ clearTimeout(timeout);
18
+ if (!response.ok && !REACHABLE_STATUSES.has(response.status)) {
19
+ return {
20
+ file: entry.file,
21
+ line: entry.line,
22
+ message: `${entry.label} returned HTTP ${response.status}: ${entry.url}`,
23
+ severity: 'warning',
24
+ };
25
+ }
26
+ return null;
27
+ }
28
+ catch (error) {
29
+ if (error instanceof DOMException && error.name === 'AbortError') {
30
+ return {
31
+ file: entry.file,
32
+ line: entry.line,
33
+ message: `${entry.label} timed out: ${entry.url}`,
34
+ severity: 'warning',
35
+ };
36
+ }
37
+ return {
38
+ file: entry.file,
39
+ line: entry.line,
40
+ message: `${entry.label} unreachable: ${entry.url}`,
41
+ severity: 'warning',
42
+ };
43
+ }
44
+ }
45
+ export async function validateUrls(urls) {
46
+ if (urls.length === 0)
47
+ return [];
48
+ const warnings = [];
49
+ // Process in batches to avoid overwhelming servers/network
50
+ for (let i = 0; i < urls.length; i += CONCURRENCY) {
51
+ const batch = urls.slice(i, i + CONCURRENCY);
52
+ const results = await Promise.all(batch.map(checkUrl));
53
+ for (const result of results) {
54
+ if (result)
55
+ warnings.push(result);
56
+ }
57
+ }
58
+ return warnings;
59
+ }
60
+ //# sourceMappingURL=url-reachability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-reachability.js","sourceRoot":"","sources":["../../src/validator/url-reachability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { ContentError } from '../index.js';
2
+ /**
3
+ * Generic frontmatter validation against the schema for a content type.
4
+ * Checks required fields (present + non-empty) and detects typos.
5
+ *
6
+ * Note: This does NOT handle type-specific validation like slug format
7
+ * or id-must-be-string. Parsers still handle those themselves.
8
+ */
9
+ export declare function validateFrontmatter(frontmatter: Record<string, unknown>, contentType: string, file: string): ContentError[];
@@ -0,0 +1,53 @@
1
+ import { CONTENT_SCHEMAS } from '../content-schema.js';
2
+ import { detectFrontmatterTypos, levenshtein } from './field-typos.js';
3
+ /**
4
+ * Generic frontmatter validation against the schema for a content type.
5
+ * Checks required fields (present + non-empty) and detects typos.
6
+ *
7
+ * Note: This does NOT handle type-specific validation like slug format
8
+ * or id-must-be-string. Parsers still handle those themselves.
9
+ */
10
+ export function validateFrontmatter(frontmatter, contentType, file) {
11
+ const schema = CONTENT_SCHEMAS[contentType];
12
+ if (!schema)
13
+ return [];
14
+ const errors = [];
15
+ // Detect typos in field names
16
+ errors.push(...detectFrontmatterTypos(frontmatter, schema.allFields, file));
17
+ // Check required fields
18
+ for (const field of schema.requiredFields) {
19
+ const value = frontmatter[field];
20
+ if (value === undefined || value === null) {
21
+ // Check if a similar field exists (likely typo or wrong name)
22
+ let suggestion = `Add '${field}' to frontmatter`;
23
+ const presentFields = Object.keys(frontmatter);
24
+ for (const present of presentFields) {
25
+ if (schema.requiredFields.includes(present))
26
+ continue; // skip other required fields
27
+ const dist = levenshtein(field.toLowerCase(), present.toLowerCase());
28
+ if (dist <= 3 || field.toLowerCase().includes(present.toLowerCase()) || present.toLowerCase().includes(field.toLowerCase())) {
29
+ suggestion = `Did you mean '${field}' instead of '${present}'?`;
30
+ break;
31
+ }
32
+ }
33
+ errors.push({
34
+ file,
35
+ line: 2,
36
+ message: `Missing required field: ${field}`,
37
+ suggestion,
38
+ severity: 'error',
39
+ });
40
+ }
41
+ else if (typeof value === 'string' && value.trim() === '') {
42
+ errors.push({
43
+ file,
44
+ line: 2,
45
+ message: `Field '${field}' must not be empty`,
46
+ suggestion: `Provide a value for '${field}'`,
47
+ severity: 'error',
48
+ });
49
+ }
50
+ }
51
+ return errors;
52
+ }
53
+ //# sourceMappingURL=validate-frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-frontmatter.js","sourceRoot":"","sources":["../../src/validator/validate-frontmatter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,WAAoC,EACpC,WAAmB,EACnB,IAAY;IAEZ,MAAM,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,MAAM,MAAM,GAAmB,EAAE,CAAC;IAElC,8BAA8B;IAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC;IAE5E,wBAAwB;IACxB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1C,8DAA8D;YAC9D,IAAI,UAAU,GAAG,QAAQ,KAAK,kBAAkB,CAAC;YACjD,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAC/C,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,SAAS,CAAC,6BAA6B;gBACpF,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;gBACrE,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;oBAC5H,UAAU,GAAG,iBAAiB,KAAK,iBAAiB,OAAO,IAAI,CAAC;oBAChE,MAAM;gBACR,CAAC;YACH,CAAC;YACD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI;gBACJ,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,2BAA2B,KAAK,EAAE;gBAC3C,UAAU;gBACV,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI;gBACJ,IAAI,EAAE,CAAC;gBACP,OAAO,EAAE,UAAU,KAAK,qBAAqB;gBAC7C,UAAU,EAAE,wBAAwB,KAAK,GAAG;gBAC5C,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lens-content-processor",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Parser and validator for Lens educational content (Markdown modules, lenses, learning outcomes)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",