tessera-learn 0.0.11 → 0.0.13

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 (75) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +2 -1
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +85 -10
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-D9DXlqNP.js → validation-B-xTvM9B.js} +342 -18
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +17 -3
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +16 -6
  23. package/src/components/Image.svelte +12 -3
  24. package/src/components/LockedBanner.svelte +2 -1
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +33 -13
  28. package/src/components/Quiz.svelte +61 -20
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +21 -18
  34. package/src/components/util.ts +3 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +4 -1
  41. package/src/plugin/export.ts +42 -14
  42. package/src/plugin/index.ts +216 -44
  43. package/src/plugin/manifest.ts +62 -22
  44. package/src/plugin/validation.ts +736 -122
  45. package/src/runtime/App.svelte +119 -48
  46. package/src/runtime/LoadingBar.svelte +12 -3
  47. package/src/runtime/Sidebar.svelte +24 -8
  48. package/src/runtime/access.ts +15 -3
  49. package/src/runtime/adapters/cmi5.ts +55 -33
  50. package/src/runtime/adapters/index.ts +22 -10
  51. package/src/runtime/adapters/retry.ts +25 -20
  52. package/src/runtime/adapters/scorm-base.ts +19 -15
  53. package/src/runtime/adapters/scorm12.ts +7 -8
  54. package/src/runtime/adapters/scorm2004.ts +11 -14
  55. package/src/runtime/adapters/web.ts +1 -1
  56. package/src/runtime/hooks.svelte.ts +152 -326
  57. package/src/runtime/interaction-format.ts +30 -12
  58. package/src/runtime/interaction.ts +44 -11
  59. package/src/runtime/navigation.svelte.ts +27 -11
  60. package/src/runtime/persistence.ts +2 -2
  61. package/src/runtime/progress.svelte.ts +13 -9
  62. package/src/runtime/quiz-engine.svelte.ts +361 -0
  63. package/src/runtime/quiz-policy.ts +9 -3
  64. package/src/runtime/types.ts +24 -2
  65. package/src/runtime/xapi/agent-rules.ts +4 -1
  66. package/src/runtime/xapi/client.ts +5 -5
  67. package/src/runtime/xapi/derive-actor.ts +2 -2
  68. package/src/runtime/xapi/publisher.ts +32 -29
  69. package/src/runtime/xapi/setup.ts +18 -15
  70. package/src/runtime/xapi/validation.ts +15 -6
  71. package/src/virtual.d.ts +4 -1
  72. package/styles/base.css +32 -11
  73. package/styles/layout.css +39 -18
  74. package/styles/theme.css +15 -3
  75. package/dist/validation-D9DXlqNP.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-B-xTvM9B.js","names":[],"sources":["../src/plugin/manifest.ts","../src/runtime/xapi/agent-rules.ts","../src/runtime/interaction-format.ts","../src/runtime/types.ts","../src/plugin/a11y/contrast.ts","../src/components/video-embed.ts","../src/plugin/validation.ts"],"sourcesContent":["import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';\nimport { resolve, basename, extname } from 'node:path';\nimport JSON5 from 'json5';\nimport type { CourseConfig, QuizConfig } from '../runtime/types.js';\n\n// ---------- Types ----------\n\nexport type { QuizConfig };\n\nexport interface ManifestPage {\n index: number;\n title: string;\n slug: string;\n importPath: string;\n quiz: QuizConfig | null;\n completesOn?: 'view';\n}\n\nexport interface ManifestLesson {\n title: string;\n slug: string;\n pages: ManifestPage[];\n}\n\nexport interface ManifestSection {\n title: string;\n slug: string;\n lessons: ManifestLesson[];\n}\n\nexport interface Manifest {\n sections: ManifestSection[];\n pages: ManifestPage[];\n totalPages: number;\n}\n\n/** Append `.svelte` if not already present. Both bare and suffixed names are accepted in author config. */\nexport function ensureSvelteSuffix(name: string): string {\n return name.endsWith('.svelte') ? name : `${name}.svelte`;\n}\n\n// ---------- File read cache ----------\n\n/**\n * Module-level cache of source file contents keyed by absolute path with\n * mtime invalidation. Both `validateProject` and `generateManifest` read the\n * same .svelte / _meta.js / course.config.js files during a single build;\n * sharing the read avoids the second disk hit (and matters most on cold-cache\n * CI runs and large courses).\n */\nconst fileContentCache = new Map<\n string,\n { mtimeMs: number; content: string }\n>();\n\nexport function readSourceFileCached(filePath: string): string {\n const stat = statSync(filePath);\n const cached = fileContentCache.get(filePath);\n if (cached && cached.mtimeMs === stat.mtimeMs) return cached.content;\n const content = readFileSync(filePath, 'utf-8');\n fileContentCache.set(filePath, { mtimeMs: stat.mtimeMs, content });\n return content;\n}\n\n// ---------- Helpers ----------\n\n/** Strip numeric prefix and hyphen: \"01-introduction\" → \"introduction\" */\nexport function stripPrefix(name: string): string {\n return name.replace(/^\\d+-/, '');\n}\n\n/** Title-case a slug: \"getting-started\" → \"Getting Started\" */\nexport function titleCase(slug: string): string {\n return slug\n .split('-')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n/** Derive slug from folder/file name */\nexport function deriveSlug(name: string, isFile = false): string {\n if (isFile) {\n return basename(name, extname(name));\n }\n return stripPrefix(name);\n}\n\n/** Matches both Svelte 5 `<script module>` and legacy `<script context=\"module\">`. */\nexport const MODULE_SCRIPT_RE =\n /<script\\s+(?:context\\s*=\\s*[\"']module[\"']|module)[^>]*>([\\s\\S]*?)<\\/script>/;\n\n/** Matches `export const pageConfig =` (RHS is read separately). */\nexport const PAGE_CONFIG_EXPORT_RE = /export\\s+const\\s+pageConfig\\s*=\\s*/;\n\n/** Matches `export default ` (RHS is read separately). */\nconst DEFAULT_EXPORT_RE = /export\\s+default\\s*/;\n\n/**\n * Locate `export default { ... }` and return the object literal substring,\n * or null if no balanced object literal follows the `export default` keyword.\n * Used by both manifest extraction and project validation.\n */\nexport function extractDefaultExportObjectLiteral(\n source: string,\n): string | null {\n const match = source.match(DEFAULT_EXPORT_RE);\n if (!match || match.index === undefined) return null;\n const startIndex = source.indexOf('{', match.index);\n if (startIndex < 0) return null;\n return extractObjectLiteral(source, startIndex);\n}\n\nexport type CourseConfigRead =\n | { ok: true; config: Partial<CourseConfig> }\n | {\n ok: false;\n reason: 'missing' | 'no-export' | 'parse-error';\n error?: unknown;\n };\n\n/**\n * Read and JSON5-parse the `export default { ... }` literal from a project's\n * course.config.js. Shared by the build plugin and the validator so the read,\n * cache, and parse rules live in one place. The discriminated `reason` lets\n * callers that care (export, validation) emit precise errors while callers\n * that just need a value can fall back on `!ok`.\n */\nexport function readCourseConfig(projectRoot: string): CourseConfigRead {\n const configPath = resolve(projectRoot, 'course.config.js');\n if (!existsSync(configPath)) return { ok: false, reason: 'missing' };\n const objectStr = extractDefaultExportObjectLiteral(\n readSourceFileCached(configPath),\n );\n if (!objectStr) return { ok: false, reason: 'no-export' };\n try {\n return { ok: true, config: JSON5.parse(objectStr) };\n } catch (error) {\n return { ok: false, reason: 'parse-error', error };\n }\n}\n\n/**\n * Read a _meta.js file and extract its default export object.\n * Uses the same JSON5 approach as pageConfig extraction — find the object literal\n * after `export default` and parse it.\n */\nexport function readMetaFile(metaPath: string): {\n title?: string;\n pages?: string[];\n} {\n if (!existsSync(metaPath)) return {};\n\n const objectStr = extractDefaultExportObjectLiteral(\n readSourceFileCached(metaPath),\n );\n if (!objectStr) return {};\n\n try {\n return JSON5.parse(objectStr);\n } catch {\n return {};\n }\n}\n\n/** Result of parsing a `.svelte` source for its `pageConfig` module-script export. */\nexport type PageConfigParseResult =\n /** No module script, or no `pageConfig =` export. Treat as \"no config\". */\n | { kind: 'none' }\n /** Found and successfully parsed. */\n | {\n kind: 'ok';\n value: { title?: string; quiz?: QuizConfig; completesOn?: 'view' };\n }\n /** Found but couldn't parse as a static object literal — non-literal RHS or JSON5 failure. */\n | { kind: 'invalid' };\n\n/** Source-level pageConfig extraction shared by manifest generation and build-time validation. */\nexport function parsePageConfigFromSource(\n content: string,\n): PageConfigParseResult {\n const moduleScriptMatch = content.match(MODULE_SCRIPT_RE);\n if (!moduleScriptMatch) return { kind: 'none' };\n\n const scriptContent = moduleScriptMatch[1];\n\n const configMatch = scriptContent.match(PAGE_CONFIG_EXPORT_RE);\n if (!configMatch || configMatch.index === undefined) return { kind: 'none' };\n\n const afterExport = scriptContent\n .slice(configMatch.index + configMatch[0].length)\n .trimStart();\n // pageConfig assigned to something other than an object literal — flag as invalid.\n if (!afterExport.startsWith('{')) return { kind: 'invalid' };\n\n const startIndex = scriptContent.indexOf(\n '{',\n configMatch.index + configMatch[0].length,\n );\n if (startIndex < 0) return { kind: 'invalid' };\n const objectStr = extractObjectLiteral(scriptContent, startIndex);\n if (!objectStr) return { kind: 'invalid' };\n\n try {\n return { kind: 'ok', value: JSON5.parse(objectStr) };\n } catch {\n return { kind: 'invalid' };\n }\n}\n\n/** Extract pageConfig from a .svelte file. Throws on parse failure. */\nexport function extractPageConfig(filePath: string): {\n title?: string;\n quiz?: QuizConfig;\n completesOn?: 'view';\n} {\n const result = parsePageConfigFromSource(readSourceFileCached(filePath));\n if (result.kind === 'ok') return result.value;\n if (result.kind === 'invalid') {\n throw new Error(\n `${filePath}: pageConfig must be a static object literal (no variables, function calls, or computed values)`,\n );\n }\n return {};\n}\n\n/**\n * Extract a balanced `{...}` or `[...]` span starting at the opening bracket,\n * skipping strings and comments. Returns the substring (inclusive) or null if\n * the open char is wrong or no matching close is found. Shared by manifest\n * extraction, _meta/pageConfig parsing, and the validator's tag-prop parser.\n */\nexport function extractObjectLiteral(\n source: string,\n startIndex: number,\n): string | null {\n const open = source[startIndex];\n if (open !== '{' && open !== '[') return null;\n\n let depth = 0;\n let inString: string | null = null;\n let escaped = false;\n\n for (let i = startIndex; i < source.length; i++) {\n const char = source[i];\n\n if (escaped) {\n escaped = false;\n continue;\n }\n\n if (char === '\\\\' && inString) {\n escaped = true;\n continue;\n }\n\n if (inString) {\n if (char === inString) {\n inString = null;\n }\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === '`') {\n inString = char;\n continue;\n }\n\n // Skip single-line comments\n if (char === '/' && i + 1 < source.length && source[i + 1] === '/') {\n const newline = source.indexOf('\\n', i);\n i = newline === -1 ? source.length : newline;\n continue;\n }\n\n // Skip multi-line comments\n if (char === '/' && i + 1 < source.length && source[i + 1] === '*') {\n const end = source.indexOf('*/', i + 2);\n i = end === -1 ? source.length : end + 1;\n continue;\n }\n\n if (char === '{' || char === '[') depth++;\n if (char === '}' || char === ']') {\n depth--;\n if (depth === 0) {\n return source.slice(startIndex, i + 1);\n }\n }\n }\n\n return null;\n}\n\n/**\n * Get sorted subdirectories of a given path.\n */\nfunction getSortedDirs(dirPath: string): string[] {\n if (!existsSync(dirPath)) return [];\n return readdirSync(dirPath)\n .filter((name) => {\n const full = resolve(dirPath, name);\n return statSync(full).isDirectory() && !name.startsWith('.');\n })\n .sort();\n}\n\n/**\n * Get .svelte files in a directory.\n */\nfunction getSvelteFiles(dirPath: string): string[] {\n if (!existsSync(dirPath)) return [];\n return readdirSync(dirPath)\n .filter((name) => name.endsWith('.svelte'))\n .sort();\n}\n\n// ---------- Main ----------\n\n/**\n * Generate a course manifest by scanning the pages/ directory.\n */\nexport function generateManifest(pagesDir: string): Manifest {\n const sections: ManifestSection[] = [];\n const flatPages: ManifestPage[] = [];\n let pageIndex = 0;\n\n const sectionDirs = getSortedDirs(pagesDir);\n\n for (const sectionName of sectionDirs) {\n const sectionPath = resolve(pagesDir, sectionName);\n const sectionMeta = readMetaFile(resolve(sectionPath, '_meta.js'));\n const sectionSlug = deriveSlug(sectionName);\n\n const section: ManifestSection = {\n title: sectionMeta.title || titleCase(sectionSlug),\n slug: sectionSlug,\n lessons: [],\n };\n\n const lessonDirs = getSortedDirs(sectionPath);\n\n for (const lessonName of lessonDirs) {\n const lessonPath = resolve(sectionPath, lessonName);\n const lessonMeta = readMetaFile(resolve(lessonPath, '_meta.js'));\n const lessonSlug = deriveSlug(lessonName);\n\n const lesson: ManifestLesson = {\n title: lessonMeta.title || titleCase(lessonSlug),\n slug: lessonSlug,\n pages: [],\n };\n\n // Determine page order\n const allSvelteFiles = getSvelteFiles(lessonPath);\n const orderedFiles = orderPageFiles(allSvelteFiles, lessonMeta.pages);\n\n for (const fileName of orderedFiles) {\n const filePath = resolve(lessonPath, fileName);\n const pageSlug = deriveSlug(fileName, true);\n\n let pageConfig: {\n title?: string;\n quiz?: QuizConfig;\n completesOn?: 'view';\n } = {};\n try {\n pageConfig = extractPageConfig(filePath);\n } catch (e) {\n // Validation errors will be handled by the validation plugin (Step 11).\n // For now, log and continue with defaults.\n console.warn(`[tessera warning] ${(e as Error).message}`);\n }\n\n const relativePath = `/pages/${sectionName}/${lessonName}/${fileName}`;\n\n const page: ManifestPage = {\n index: pageIndex,\n title: pageConfig.title || titleCase(pageSlug),\n slug: pageSlug,\n importPath: relativePath,\n quiz: pageConfig.quiz || null,\n ...(pageConfig.completesOn === 'view'\n ? { completesOn: 'view' as const }\n : {}),\n };\n\n lesson.pages.push(page);\n flatPages.push(page);\n pageIndex++;\n }\n\n section.lessons.push(lesson);\n }\n\n sections.push(section);\n }\n\n return {\n sections,\n pages: flatPages,\n totalPages: flatPages.length,\n };\n}\n\n/**\n * Order .svelte files: listed in `pages` array first (in order), then unlisted appended alphabetically.\n */\nexport function orderPageFiles(\n allFiles: string[],\n pagesArray?: string[],\n): string[] {\n if (!pagesArray || pagesArray.length === 0) {\n return allFiles;\n }\n\n const listed = pagesArray.map(ensureSvelteSuffix);\n const listedSet = new Set(listed);\n const unlisted = allFiles.filter((f) => !listedSet.has(f)).sort();\n\n // Only include listed files that actually exist\n const validListed = listed.filter((f) => allFiles.includes(f));\n\n return [...validListed, ...unlisted];\n}\n","/**\n * xAPI Identified Agent and Basic-auth credential validation rules.\n *\n * Pure logic — no Svelte/runtime imports. Imported by both `publisher.ts`\n * (runtime validation of resolved actor / auth) and `plugin/validation.ts`\n * (build-time validation of static `course.config.js` actor / auth).\n * Keeping the rules in one place prevents the two callsites from drifting.\n */\n\n/** Join a field label with a validator suffix: `.foo` chains, others get `: `. */\nexport function joinFieldError(label: string, suffix: string): string {\n return suffix.startsWith('.') ? `${label}${suffix}` : `${label}: ${suffix}`;\n}\n\n/**\n * Validate that a candidate is an Identified Agent per xAPI 1.0.3.\n * Returns null on success or a human-readable error suffix on failure.\n *\n * Suffixes are prefix-friendly: callers concatenate their own label\n * (`xapi.actor`, `xapi[0].actor`, etc.) with a single space — no \"actor\"\n * appears in the suffix to avoid doubling.\n */\nexport function validateAgent(actor: unknown): string | null {\n if (!actor || typeof actor !== 'object') {\n return 'must be an object';\n }\n const a = actor as Record<string, unknown>;\n if (Array.isArray(a.member) && a.member.length > 0) {\n return 'is a Group (has `member`); v1 supports Identified Agents only';\n }\n let count = 0;\n if (a.mbox !== undefined) count++;\n if (a.mbox_sha1sum !== undefined) count++;\n if (a.openid !== undefined) count++;\n if (a.account !== undefined) count++;\n if (count === 0) {\n return 'must have one of mbox, mbox_sha1sum, openid, or account (Identified Agent rule)';\n }\n if (count > 1) {\n return 'must have exactly one IFI (mbox / mbox_sha1sum / openid / account), not multiple';\n }\n if (a.mbox !== undefined) {\n if (typeof a.mbox !== 'string' || !a.mbox.startsWith('mailto:')) {\n return '.mbox must be a string starting with \"mailto:\"';\n }\n }\n if (a.mbox_sha1sum !== undefined) {\n if (\n typeof a.mbox_sha1sum !== 'string' ||\n !/^[0-9a-f]{40}$/i.test(a.mbox_sha1sum)\n ) {\n return '.mbox_sha1sum must be a 40-character hex string';\n }\n }\n if (a.openid !== undefined) {\n if (typeof a.openid !== 'string' || !a.openid) {\n return '.openid must be a non-empty string';\n }\n try {\n new URL(a.openid);\n } catch {\n return '.openid must be an absolute URI';\n }\n }\n if (a.account !== undefined) {\n const acc = a.account as Record<string, unknown>;\n if (!acc || typeof acc !== 'object') {\n return '.account must be an object with homePage and name';\n }\n if (typeof acc.homePage !== 'string' || !acc.homePage) {\n return '.account.homePage must be a non-empty string';\n }\n try {\n new URL(acc.homePage);\n } catch {\n return '.account.homePage must be an absolute URL';\n }\n if (typeof acc.name !== 'string' || !acc.name) {\n return '.account.name must be a non-empty string';\n }\n }\n return null;\n}\n\n/**\n * Validate a Basic-auth credential string (the value after \"Basic \").\n * v1 supports Basic only. Bearer is a hard error so OAuth users see the\n * non-goal explicitly.\n */\nexport function validateAuthCredential(auth: string): string | null {\n if (typeof auth !== 'string' || !auth) {\n return 'must be a non-empty string';\n }\n if (/^basic\\s/i.test(auth)) {\n return \"must be the Basic credential value only, not the full header. Drop the 'Basic ' prefix.\";\n }\n if (/^bearer\\s/i.test(auth)) {\n return 'Bearer/OAuth credentials are not supported in v1. Use Basic auth, or wrap your token-exchange in an auth function that returns a Basic credential.';\n }\n return null;\n}\n","/**\n * SCORM 1.2 RTE §3.4.7 vs SCORM 2004 4E RTE §4.2.7 differ in delimiter\n * encoding and identifier rules; cmi5 (xAPI) reuses 2004's delimiters but\n * not its identifier slugging.\n */\n\nimport type { Interaction } from './interaction.js';\n\nexport interface InteractionFormat {\n itemDelim: string;\n pairDelim: string;\n rangeDelim: string;\n /**\n * SCORM 1.2 has no numeric range syntax — `correct_responses.n.pattern`\n * is a single CMIDecimal. SCORM 2004 supports `min[:]max`.\n */\n supportsNumericRange: boolean;\n formatBoolean(value: boolean): string;\n identifier(value: string): string;\n}\n\nexport const SCORM12_INTERACTION_FORMAT: InteractionFormat = {\n itemDelim: ',',\n pairDelim: '.',\n rangeDelim: ':',\n supportsNumericRange: false,\n formatBoolean: (v) => (v ? 't' : 'f'),\n identifier: shortIdentifier,\n};\n\n/**\n * Bracketed delimiters are literal text, not regex. xAPI parses them the\n * same way.\n */\nexport const SCORM2004_INTERACTION_FORMAT: InteractionFormat = {\n itemDelim: '[,]',\n pairDelim: '[.]',\n rangeDelim: '[:]',\n supportsNumericRange: true,\n formatBoolean: (v) => (v ? 'true' : 'false'),\n identifier: (v) => v,\n};\n\nexport const XAPI_INTERACTION_FORMAT: InteractionFormat = {\n itemDelim: '[,]',\n pairDelim: '[.]',\n rangeDelim: '[:]',\n supportsNumericRange: true,\n formatBoolean: (v) => (v ? 'true' : 'false'),\n identifier: (v) => v,\n};\n\n/**\n * SCORM `short_identifier_type` / `CMIIdentifier`: alphanumerics +\n * underscore, max 250 chars. Strict validators (SCORM Cloud) reject raw\n * option labels with spaces or punctuation with error 405/406.\n */\nexport function shortIdentifier(value: string): string {\n const cleaned = value.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n const trimmed = cleaned.slice(0, 250);\n return trimmed || '_';\n}\n\nfunction indexLookup(\n options: string[] | undefined,\n value: string,\n): string | null {\n if (!options) return null;\n const idx = options.indexOf(value);\n return idx >= 0 ? String(idx) : null;\n}\n\nfunction encodeListItem(\n value: string,\n options: string[] | undefined,\n fmt: InteractionFormat,\n): string {\n if (fmt === SCORM12_INTERACTION_FORMAT) {\n const idx = indexLookup(options, value);\n if (idx !== null) return idx;\n }\n return fmt.identifier(value);\n}\n\nexport function formatResponse(\n i: Interaction,\n fmt: InteractionFormat = SCORM2004_INTERACTION_FORMAT,\n): string {\n switch (i.type) {\n case 'choice':\n case 'sequencing':\n return i.response\n .map((v) => encodeListItem(v, i.options, fmt))\n .join(fmt.itemDelim);\n case 'true-false':\n return fmt.formatBoolean(i.response);\n case 'fill-in':\n case 'long-fill-in':\n case 'likert':\n case 'other':\n return i.response;\n case 'matching':\n return i.response\n .map(\n ([l, r]) =>\n `${encodeListItem(l, i.optionPairs?.left, fmt)}${fmt.pairDelim}${encodeListItem(r, i.optionPairs?.right, fmt)}`,\n )\n .join(fmt.itemDelim);\n case 'numeric':\n return String(i.response);\n case 'performance':\n return i.response\n .map(\n ([s, v]) =>\n `${fmt.identifier(s)}${fmt.pairDelim}${fmt.identifier(String(v))}`,\n )\n .join(fmt.itemDelim);\n }\n}\n\n/** Returns null when no correct pattern was provided. */\nexport function formatCorrectPattern(\n i: Interaction,\n fmt: InteractionFormat = SCORM2004_INTERACTION_FORMAT,\n): string | null {\n if (i.correct === undefined) return null;\n switch (i.type) {\n case 'choice':\n case 'sequencing':\n return (i.correct as string[])\n .map((v) => encodeListItem(v, i.options, fmt))\n .join(fmt.itemDelim);\n case 'true-false':\n return fmt.formatBoolean(i.correct as boolean);\n case 'fill-in':\n case 'long-fill-in':\n return (i.correct as string[]).join(fmt.itemDelim);\n case 'matching':\n return (i.correct as Array<[string, string]>)\n .map(\n ([l, r]) =>\n `${encodeListItem(l, i.optionPairs?.left, fmt)}${fmt.pairDelim}${encodeListItem(r, i.optionPairs?.right, fmt)}`,\n )\n .join(fmt.itemDelim);\n case 'numeric': {\n const c = i.correct as { min?: number; max?: number };\n if (c.min !== undefined && c.max !== undefined && c.min === c.max) {\n return String(c.min);\n }\n if (c.min !== undefined && c.max === undefined) return String(c.min);\n if (c.min === undefined && c.max !== undefined) return String(c.max);\n // True range — drop the pattern in 1.2 (rely on `result` for pass/fail).\n if (!fmt.supportsNumericRange) return null;\n return `${c.min ?? ''}${fmt.rangeDelim}${c.max ?? ''}`;\n }\n case 'likert':\n case 'other':\n return i.correct as string;\n case 'performance':\n return (i.correct as Array<[string, string | number]>)\n .map(\n ([s, v]) =>\n `${fmt.identifier(s)}${fmt.pairDelim}${fmt.identifier(String(v))}`,\n )\n .join(fmt.itemDelim);\n }\n}\n\n/** SCORM 1.2 has no `long-fill-in` or `other` — both fall back to `fill-in`. */\nexport function scorm12Type(type: Interaction['type']): string {\n switch (type) {\n case 'long-fill-in':\n return 'fill-in';\n case 'other':\n return 'fill-in';\n default:\n return type;\n }\n}\n\nexport interface ScormInteractionSpec {\n responseField: 'student_response' | 'learner_response';\n timestampField: 'time' | 'timestamp';\n timestamp: string;\n typeValue: string;\n resultLabels: { correct: string; incorrect: string };\n format: InteractionFormat;\n}\n\nexport function buildScormInteractionFields(\n prefix: string,\n questionId: string,\n interaction: Interaction,\n correct: boolean | null,\n spec: ScormInteractionSpec,\n): Array<[string, string]> {\n const fields: Array<[string, string]> = [\n [`${prefix}.id`, spec.format.identifier(questionId)],\n [`${prefix}.type`, spec.typeValue],\n ];\n const pattern = formatCorrectPattern(interaction, spec.format);\n if (pattern !== null) {\n fields.push([`${prefix}.correct_responses.0.pattern`, pattern]);\n }\n fields.push([\n `${prefix}.${spec.responseField}`,\n formatResponse(interaction, spec.format),\n ]);\n if (correct !== null) {\n fields.push([\n `${prefix}.result`,\n correct ? spec.resultLabels.correct : spec.resultLabels.incorrect,\n ]);\n }\n fields.push([`${prefix}.${spec.timestampField}`, spec.timestamp]);\n return fields;\n}\n","import type { AccessFn } from './access.js';\nimport type { XAPIAgent } from './xapi/types.js';\n\n/**\n * Quiz enum domains as runtime tuples. The unions below derive from these, and\n * the build-time validator imports them too — so the accepted value set has a\n * single source and can't drift between the types and the validator.\n */\nexport const FEEDBACK_MODES = ['review', 'immediate', 'never'] as const;\nexport const RETRY_MODES = ['full', 'incorrect-only'] as const;\n\n/**\n * Per-page quiz configuration. Single source of truth — the build plugin\n * extracts this from `pageConfig.quiz` and embeds it in the manifest;\n * the runtime reads it from there. Keep field shapes in sync.\n */\nexport interface QuizConfig {\n graded?: boolean;\n gatesProgress?: boolean;\n maxAttempts?: number;\n feedbackMode?: (typeof FEEDBACK_MODES)[number];\n retryMode?: (typeof RETRY_MODES)[number];\n}\n\nexport interface CourseConfig {\n title: string;\n description?: string;\n author?: string;\n version?: string;\n /** BCP-47 language tag for <html lang>. Defaults to 'en'. WCAG 3.1.1. */\n language?: string;\n /** Accessibility checker configuration. */\n a11y?: A11yConfig;\n branding?: {\n logo?: string;\n primaryColor?: string;\n fontFamily?: string;\n };\n navigation: {\n mode: 'free' | 'sequential';\n canAccess?: AccessFn;\n };\n completion: ManualCompletion | QuizCompletion | PercentageCompletion;\n /** Optional under \"manual\"; required under \"quiz\". */\n scoring: {\n passingScore: number;\n };\n export: {\n standard: 'web' | 'scorm12' | 'scorm2004' | 'cmi5';\n };\n /**\n * Optional xAPI destination(s) for custom statement publishing via\n * `useXAPI()`. A single object or an array of destinations. Under cmi5\n * export, the sentinel `endpoint: 'lms'` re-uses the LMS launch's\n * credentials and shares the cmi5 adapter's queue.\n */\n xapi?: XAPIConfig | XAPIConfig[];\n}\n\n/** Accessibility checker configuration. */\nexport interface A11yConfig {\n /** Build-gate severity for promotable Tier-1 rules + Tier-1a warnings. */\n level?: 'warn' | 'error';\n /** axe ruleset tags for the Tier-2 runtime auditor. */\n standard?: 'wcag2a' | 'wcag2aa' | 'wcag21aa';\n /** Per-rule escape hatch matched literally against each diagnostic's ID. */\n ignore?: string[];\n}\n\nexport interface ManualCompletion {\n mode: 'manual';\n /**\n * Set to \"page\" to opt into a build-time check that at least one page\n * declares `completesOn: \"view\"`. Omit to skip the check; both completion\n * paths still work at runtime.\n */\n trigger?: 'page';\n /** When set, markComplete() also flips successStatus. Omit for unknown. */\n requireSuccessStatus?: 'passed' | 'failed';\n}\n\nexport interface QuizCompletion {\n mode: 'quiz';\n}\n\nexport interface PercentageCompletion {\n mode: 'percentage';\n percentageThreshold?: number;\n}\n\n/**\n * cmi5 launch-inherited destination. Only valid under `export.standard:\n * 'cmi5'`. Auth, actor, activityId, and registration are taken from the\n * launch URL, so no other fields are accepted.\n */\nexport interface XAPILMSConfig {\n endpoint: 'lms';\n}\n\n/**\n * Explicit LRS destination. The author provides every field. `actor` is\n * optional under SCORM (synthesized from `cmi.core.student_id` /\n * `cmi.learner_id`) and required under web.\n */\nexport interface XAPIExplicitConfig {\n /** Absolute http(s) URL of the LRS Statements endpoint base. */\n endpoint: string;\n /**\n * Basic-auth credential value (the part after \"Basic \"), or a function\n * that resolves one. Function form is re-invoked once on 401 to cover\n * short-lived tokens.\n */\n auth: string | (() => string | Promise<string>);\n /**\n * Identified Agent or a resolver function. Required for web export;\n * optional under SCORM where it can be synthesized from the LMS data\n * model. Optional under cmi5 where it can be inherited from the launch.\n */\n actor?: XAPIAgent | (() => XAPIAgent | Promise<XAPIAgent>);\n /** xAPI activity IRI scoped to this destination. */\n activityId: string;\n /** Optional UUID v4 — primarily a cmi5 launch concept. */\n registration?: string;\n /**\n * Override for the SCORM-derived actor's `account.homePage`. Defaults\n * to the activityId origin when activityId is http(s); required when\n * activityId uses a non-http(s) scheme.\n */\n actorAccountHomePage?: string;\n}\n\nexport type XAPIConfig = XAPILMSConfig | XAPIExplicitConfig;\n","/**\n * Pure-JS WCAG contrast helpers. App.svelte's parseColor is canvas-based and\n * browser-only; this runs at build time in the linter (rule 1.7). Only opaque\n * #hex (3/4/6/8) is parsed — other CSS color forms and translucent hex return\n * null and fall through to the Tier-2 axe audit, which uses the browser's own\n * parser.\n */\n\nfunction parseHex(\n input: string,\n): { r: number; g: number; b: number; a: number } | null {\n const v = input.trim();\n const m = /^#([0-9a-fA-F]{3,8})$/.exec(v);\n if (!m) return null;\n const h = m[1];\n let r: number,\n g: number,\n b: number,\n a = 255;\n if (h.length === 3 || h.length === 4) {\n r = parseInt(h[0] + h[0], 16);\n g = parseInt(h[1] + h[1], 16);\n b = parseInt(h[2] + h[2], 16);\n if (h.length === 4) a = parseInt(h[3] + h[3], 16);\n } else if (h.length === 6 || h.length === 8) {\n r = parseInt(h.slice(0, 2), 16);\n g = parseInt(h.slice(2, 4), 16);\n b = parseInt(h.slice(4, 6), 16);\n if (h.length === 8) a = parseInt(h.slice(6, 8), 16);\n } else {\n return null; // 5/7 hex digits are not a valid color\n }\n return { r, g, b, a };\n}\n\nfunction linearize(channel: number): number {\n const c = channel / 255;\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n}\n\n/** sRGB hex → relative luminance (0–1), or null if not a parseable opaque hex. */\nexport function relativeLuminance(hex: string): number | null {\n const rgb = parseHex(hex);\n if (!rgb) return null;\n // A translucent color's on-screen luminance depends on the backdrop; defer\n // those to the in-browser Tier-2 audit rather than guess a composite.\n if (rgb.a !== 255) return null;\n return (\n 0.2126 * linearize(rgb.r) +\n 0.7152 * linearize(rgb.g) +\n 0.0722 * linearize(rgb.b)\n );\n}\n\n/**\n * WCAG contrast ratio between two colors. Order-independent — the lighter/darker\n * ordering is handled internally, so callers may pass the colors in any order.\n * Returns null if either color isn't a parseable hex.\n */\nexport function contrastRatio(a: string, b: string): number | null {\n const la = relativeLuminance(a);\n const lb = relativeLuminance(b);\n if (la === null || lb === null) return null;\n const lighter = Math.max(la, lb);\n const darker = Math.min(la, lb);\n return (lighter + 0.05) / (darker + 0.05);\n}\n","/**\n * Shared YouTube/Vimeo embed detection. Used by Video.svelte to pick the iframe\n * vs native-<video> render path, and by the Tier-1b linter (rule 1.4) so its\n * caption/transcript guidance matches what the component actually renders.\n */\n\nconst YOUTUBE_RE =\n /(?:youtube\\.com\\/(?:watch\\?v=|embed\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/;\nconst VIMEO_RE = /vimeo\\.com\\/(?:video\\/)?(\\d+)/;\n\n/** Resolve a source URL to its embed URL, or null if it's not a known embed. */\nexport function resolveVideoEmbedUrl(src: string): string | null {\n const yt = src.match(YOUTUBE_RE);\n if (yt) return `https://www.youtube.com/embed/${yt[1]}`;\n\n const vimeo = src.match(VIMEO_RE);\n if (vimeo) return `https://player.vimeo.com/video/${vimeo[1]}`;\n\n return null;\n}\n\n/** True when the component will render an iframe embed rather than <video>. */\nexport function isVideoEmbed(src: string): boolean {\n return resolveVideoEmbedUrl(src) !== null;\n}\n","import { existsSync, readdirSync, statSync } from 'node:fs';\nimport { resolve, relative } from 'node:path';\nimport JSON5 from 'json5';\nimport {\n extractDefaultExportObjectLiteral,\n extractObjectLiteral,\n parsePageConfigFromSource,\n readSourceFileCached,\n ensureSvelteSuffix,\n readCourseConfig,\n} from './manifest.js';\nimport {\n validateAgent,\n validateAuthCredential,\n joinFieldError,\n} from '../runtime/xapi/agent-rules.js';\nimport { shortIdentifier } from '../runtime/interaction-format.js';\nimport { FEEDBACK_MODES, RETRY_MODES } from '../runtime/types.js';\nimport { contrastRatio } from './a11y/contrast.js';\nimport { isVideoEmbed } from '../components/video-embed.js';\n\n// ---------- Types ----------\n\nexport interface ValidationResult {\n errors: string[];\n warnings: string[];\n}\n\n// ---------- A11y rule IDs ----------\n\n/** Tier-1b rule IDs. `a11y.ignore` matches these literally. */\nconst A11Y_IDS = {\n imageAlt: 'tessera/image-alt',\n mediaTitle: 'tessera/media-title',\n mediaTranscript: 'tessera/media-transcript',\n mediaCaptions: 'tessera/media-captions',\n questionLabel: 'tessera/question-label',\n headingOrder: 'tessera/heading-order',\n primaryContrast: 'tessera/primary-contrast',\n lang: 'tessera/lang',\n} as const;\n\n/** Promotable by `a11y.level: 'error'`; the rest are hard contract errors. */\nconst PROMOTABLE_A11Y_IDS = new Set<string>([\n A11Y_IDS.mediaTranscript,\n A11Y_IDS.mediaCaptions,\n A11Y_IDS.questionLabel,\n A11Y_IDS.headingOrder,\n A11Y_IDS.primaryContrast,\n A11Y_IDS.lang,\n]);\n\n/** Prefix a diagnostic with its rule ID so `a11y.ignore` / `level` can match it. */\nfunction tag(id: string, message: string): string {\n return `[${id}] ${message}`;\n}\n\nfunction diagnosticId(message: string): string | null {\n const m = /^\\[([^\\]]+)\\] /.exec(message);\n return m ? m[1] : null;\n}\n\n/** True when a tagged diagnostic's rule ID is in the ignore set. */\nexport function isIgnored(\n message: string,\n ignore: ReadonlySet<string>,\n): boolean {\n const id = diagnosticId(message);\n return id !== null && ignore.has(id);\n}\n\nexport interface A11ySettings {\n level: 'warn' | 'error';\n standard: 'wcag2a' | 'wcag2aa' | 'wcag21aa';\n ignore: string[];\n}\n\nconst VALID_A11Y_LEVELS = ['warn', 'error'];\nconst VALID_A11Y_STANDARDS = ['wcag2a', 'wcag2aa', 'wcag21aa'];\n\n/** Normalize the raw `a11y` config to defaults, ignoring malformed pieces. */\nexport function normalizeA11y(raw: unknown): A11ySettings {\n const a11y =\n raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};\n const level = a11y.level === 'error' ? 'error' : 'warn';\n const standard = VALID_A11Y_STANDARDS.includes(a11y.standard as string)\n ? (a11y.standard as A11ySettings['standard'])\n : 'wcag2aa';\n const ignore = Array.isArray(a11y.ignore)\n ? a11y.ignore.filter((x): x is string => typeof x === 'string')\n : [];\n return { level, standard, ignore };\n}\n\n/**\n * Apply `a11y.ignore` (drop tagged diagnostics) and `a11y.level` (promote the\n * promotable a11y warnings to errors) to a result in place. `ignore` suppresses\n * at any severity, including hard contract errors; `level` only re-rates.\n */\nfunction applyA11ySettings(\n result: ValidationResult,\n settings: A11ySettings,\n): void {\n if (settings.ignore.length > 0) {\n const ignored = new Set(settings.ignore);\n const keep = (msg: string) => !isIgnored(msg, ignored);\n result.errors = result.errors.filter(keep);\n result.warnings = result.warnings.filter(keep);\n }\n if (settings.level === 'error') {\n const remaining: string[] = [];\n for (const msg of result.warnings) {\n const id = diagnosticId(msg);\n if (id !== null && PROMOTABLE_A11Y_IDS.has(id)) result.errors.push(msg);\n else remaining.push(msg);\n }\n result.warnings = remaining;\n }\n}\n\n/** Print validation warnings (yellow) then errors (red). Shared by the dev/build plugin and the CLI. */\nexport function reportValidationIssues({\n errors,\n warnings,\n}: ValidationResult): void {\n for (const warning of warnings) {\n console.warn(`\\x1b[33m[tessera warning]\\x1b[0m ${warning}`);\n }\n for (const error of errors) {\n console.error(`\\x1b[31m[tessera error]\\x1b[0m ${error}`);\n }\n}\n\n// Known top-level config fields\nconst KNOWN_CONFIG_FIELDS = new Set([\n 'title',\n 'description',\n 'author',\n 'version',\n 'language',\n 'branding',\n 'navigation',\n 'completion',\n 'scoring',\n 'export',\n 'chrome',\n 'xapi',\n 'a11y',\n]);\n\n// Heuristic, not a full BCP-47 grammar: a 2–3 letter primary subtag (any case)\n// plus any number of 1–8 alphanumeric subtags (script/region/variant/singleton).\nconst BCP47_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;\n\n/** Plausible BCP-47 tag? Shared by the linter and the <html lang> emitter. */\nexport function isPlausibleLanguageTag(value: unknown): value is string {\n return typeof value === 'string' && BCP47_RE.test(value);\n}\n\nconst VALID_NAV_MODES = ['free', 'sequential'];\nconst VALID_COMPLETION_MODES = ['quiz', 'percentage', 'manual'];\nconst VALID_EXPORT_STANDARDS = ['web', 'scorm12', 'scorm2004', 'cmi5'];\nconst VALID_MANUAL_TRIGGERS = ['page'];\nconst VALID_REQUIRE_SUCCESS_STATUS = ['passed', 'failed'];\n// Derived from the runtime types (single source of truth) — widened to\n// string[] so .includes() accepts an arbitrary author-supplied value.\nconst VALID_FEEDBACK_MODES: readonly string[] = FEEDBACK_MODES;\nconst VALID_RETRY_MODES: readonly string[] = RETRY_MODES;\n\n// ---------- Main ----------\n\n/**\n * Validate a Tessera project at the given root.\n * Returns errors (block build) and warnings (informational).\n */\nexport function validateProject(projectRoot: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n // 1. Check course.config.js exists\n const configPath = resolve(projectRoot, 'course.config.js');\n if (!existsSync(configPath)) {\n errors.push('course.config.js not found in project root');\n return { errors, warnings };\n }\n\n // 2. Parse and validate config\n const config = parseConfig(projectRoot, errors, warnings);\n\n // 3. Validate pages directory\n const pagesDir = resolve(projectRoot, 'pages');\n const assetsDir = resolve(projectRoot, 'assets');\n const pageResults = validatePages(\n pagesDir,\n assetsDir,\n projectRoot,\n config?.export?.standard,\n );\n errors.push(...pageResults.errors);\n warnings.push(...pageResults.warnings);\n\n // 4. Contract-bypass checks on project-root shell files\n for (const shellFile of ['layout.svelte', 'quiz.svelte']) {\n const shellPath = resolve(projectRoot, shellFile);\n if (existsSync(shellPath)) {\n validateContractBypass(\n readSourceFileCached(shellPath),\n shellFile,\n errors,\n );\n }\n }\n\n // 5. Cross-cutting validations\n if (config) {\n crossValidate(config, pageResults, errors, warnings);\n }\n\n const result: ValidationResult = { errors, warnings };\n applyA11ySettings(result, normalizeA11y(config?.a11y));\n return result;\n}\n\n// ---------- Config Validation ----------\n\ninterface ParsedConfig {\n title?: string;\n navigation?: { mode?: string };\n completion?: {\n mode?: string;\n percentageThreshold?: number;\n trigger?: string;\n requireSuccessStatus?: string;\n };\n scoring?: { passingScore?: number };\n export?: { standard?: string };\n [key: string]: unknown;\n}\n\nfunction parseConfig(\n projectRoot: string,\n errors: string[],\n warnings: string[],\n): ParsedConfig | null {\n const read = readCourseConfig(projectRoot);\n if (!read.ok) {\n // 'missing' can't occur — validateProject checks existsSync first.\n if (read.reason === 'no-export') {\n errors.push(\n 'course.config.js: could not parse — must use `export default { ... }` syntax',\n );\n } else if (read.reason === 'parse-error') {\n errors.push(\n 'course.config.js: syntax error — must export a static object literal',\n );\n }\n return null;\n }\n const config = read.config as ParsedConfig;\n\n // Check for unknown fields\n for (const key of Object.keys(config)) {\n if (!KNOWN_CONFIG_FIELDS.has(key)) {\n warnings.push(\n `course.config.js: unknown field \"${key}\" — will be ignored`,\n );\n }\n }\n\n // Validate title against the runtime merge `userConfig.title || \"Untitled\n // Course\"`: a missing or empty string falls back to the default (warn), a\n // whitespace-only string is truthy and ships verbatim (warn), and a\n // non-string is a misconfiguration — a truthy one ships as-is, a falsy one\n // falls back, but either way the author should fix it (error).\n if (config.title !== undefined && typeof config.title !== 'string') {\n errors.push(\n `course.config.js: \"title\" must be a string, got ${typeof config.title}`,\n );\n } else if (config.title === undefined || config.title === '') {\n warnings.push(\n 'course.config.js: \"title\" is missing or empty — the course will ship as \"Untitled Course\"',\n );\n } else if (config.title.trim() === '') {\n warnings.push(\n 'course.config.js: \"title\" is only whitespace — it ships verbatim and will not fall back to \"Untitled Course\"',\n );\n }\n\n // Validate branding\n if (config.branding !== undefined) {\n validateBranding(config.branding, warnings);\n }\n\n // Rule 1.8: language present and well-formed (BCP-47)\n if (config.language === undefined) {\n warnings.push(\n tag(\n A11Y_IDS.lang,\n `course.config.js: \"language\" is not set — defaulting <html lang> to \"en\". Set it to the course's language (BCP-47, e.g. \"en\", \"fr-CA\") for WCAG 3.1.1.`,\n ),\n );\n } else if (!isPlausibleLanguageTag(config.language)) {\n warnings.push(\n tag(\n A11Y_IDS.lang,\n `course.config.js: \"language\" (${JSON.stringify(config.language)}) is not a plausible BCP-47 tag — use e.g. \"en\", \"es\", or \"fr-CA\"`,\n ),\n );\n }\n\n // Validate a11y config block\n if (config.a11y !== undefined) {\n validateA11yConfig(config.a11y, errors);\n }\n\n // Validate navigation.mode\n if (config.navigation?.mode !== undefined) {\n if (!VALID_NAV_MODES.includes(config.navigation.mode)) {\n errors.push(\n `course.config.js: \"navigation.mode\" must be \"free\" or \"sequential\", got \"${config.navigation.mode}\"`,\n );\n }\n }\n\n // Validate completion.mode\n if (config.completion?.mode !== undefined) {\n if (!VALID_COMPLETION_MODES.includes(config.completion.mode)) {\n errors.push(\n `course.config.js: \"completion.mode\" must be \"quiz\", \"percentage\", or \"manual\", got \"${config.completion.mode}\"`,\n );\n }\n }\n\n if (config.completion?.trigger !== undefined) {\n if (config.completion.mode !== 'manual') {\n warnings.push(\n `course.config.js: \"completion.trigger\" is ignored unless completion.mode is \"manual\"`,\n );\n } else if (!VALID_MANUAL_TRIGGERS.includes(config.completion.trigger)) {\n errors.push(\n `course.config.js: \"completion.trigger\" must be \"page\" or omitted, got \"${config.completion.trigger}\"`,\n );\n }\n }\n\n if (config.completion?.requireSuccessStatus !== undefined) {\n if (config.completion.mode !== 'manual') {\n warnings.push(\n `course.config.js: \"completion.requireSuccessStatus\" is ignored unless completion.mode is \"manual\"`,\n );\n } else if (\n !VALID_REQUIRE_SUCCESS_STATUS.includes(\n config.completion.requireSuccessStatus,\n )\n ) {\n errors.push(\n `course.config.js: \"completion.requireSuccessStatus\" must be \"passed\" or \"failed\" (omit for \"unknown\"), got \"${config.completion.requireSuccessStatus}\"`,\n );\n }\n }\n\n // Validate export.standard\n if (config.export?.standard !== undefined) {\n if (!VALID_EXPORT_STANDARDS.includes(config.export.standard)) {\n errors.push(\n `course.config.js: \"export.standard\" must be \"web\", \"scorm12\", \"scorm2004\", or \"cmi5\", got \"${config.export.standard}\"`,\n );\n }\n }\n\n // Validate scoring.passingScore\n if (config.scoring?.passingScore !== undefined) {\n const score = config.scoring.passingScore;\n if (typeof score !== 'number' || score < 0 || score > 100) {\n errors.push(\n `course.config.js: \"scoring.passingScore\" must be 0–100, got ${score}`,\n );\n }\n }\n\n // Validate completion.percentageThreshold\n if (config.completion?.percentageThreshold !== undefined) {\n const threshold = config.completion.percentageThreshold;\n if (typeof threshold !== 'number' || threshold < 0 || threshold > 100) {\n errors.push(\n `course.config.js: \"completion.percentageThreshold\" must be 0–100, got ${threshold}`,\n );\n }\n }\n\n // Validate xapi (publisher destinations)\n if (config.xapi !== undefined) {\n validateXAPIConfig(\n config.xapi,\n config.export?.standard ?? 'web',\n errors,\n warnings,\n );\n }\n\n return config;\n}\n\n// ---------- Branding Validation ----------\n\n// Permissive approximation of the browser's accepted color set: hex 3/4/6/8,\n// any CSS functional notation (rgb/hsl/hwb/lab/lch/oklab/oklch/color), or a\n// bare keyword (named colors, transparent, currentColor). parseColor's real\n// check (App.svelte) is browser-only and the runtime degrades gracefully, so\n// an unrecognized value is advisory, never an error — lean permissive to avoid\n// rejecting values the browser would accept.\nconst HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;\nconst FUNC_COLOR_RE =\n /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\(.*\\)$/i;\nconst NAMED_COLOR_RE = /^[a-zA-Z]+$/;\n\nfunction isPlausibleColor(value: string): boolean {\n const v = value.trim();\n return (\n HEX_COLOR_RE.test(v) || FUNC_COLOR_RE.test(v) || NAMED_COLOR_RE.test(v)\n );\n}\n\n/**\n * Format checks on the branding block (advisory) plus rule 1.7's contrast check\n * on primaryColor. Runtime failures are mild: an unresolved logo ships a broken\n * <img src>, an unparseable color falls back to theme defaults.\n */\nfunction validateBranding(raw: unknown, warnings: string[]): void {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n warnings.push(\n `course.config.js: \"branding\" must be an object, got ${raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw} — will be ignored`,\n );\n return;\n }\n const branding = raw as Record<string, unknown>;\n\n const logo = branding.logo;\n if (logo !== undefined) {\n if (typeof logo !== 'string') {\n warnings.push(\n `course.config.js: \"branding.logo\" must be a string, got ${typeof logo}`,\n );\n } else if (logo.startsWith('$assets/')) {\n warnings.push(\n 'course.config.js: \"branding.logo\" starts with \"$assets/\", but branding paths are not asset-resolved — it will ship as a literal, broken src. Use a URL or a path relative to the deployed root.',\n );\n }\n }\n\n const primaryColor = branding.primaryColor;\n if (primaryColor !== undefined) {\n if (typeof primaryColor !== 'string') {\n warnings.push(\n `course.config.js: \"branding.primaryColor\" must be a string, got ${typeof primaryColor}`,\n );\n } else if (!isPlausibleColor(primaryColor)) {\n warnings.push(\n `course.config.js: \"branding.primaryColor\" \"${primaryColor}\" does not look like a valid CSS color — the theme will fall back to its default shades if the browser can't parse it`,\n );\n } else {\n // Rule 1.7: primaryColor is used both as links on the default white page\n // background and as a button fill behind white text — symmetric, so one\n // ratio covers both. Non-#hex valid colors return null and defer to Tier 2.\n const ratio = contrastRatio(primaryColor, '#ffffff');\n if (ratio !== null && ratio < 4.5) {\n warnings.push(\n tag(\n A11Y_IDS.primaryContrast,\n `course.config.js: branding.primaryColor (${primaryColor}) is ${ratio.toFixed(2)}:1 against white — it's used both for links on the page background and as a button fill behind white text, and WCAG AA needs 4.5:1 for each`,\n ),\n );\n }\n }\n }\n\n const fontFamily = branding.fontFamily;\n if (fontFamily !== undefined && typeof fontFamily !== 'string') {\n warnings.push(\n `course.config.js: \"branding.fontFamily\" must be a string, got ${typeof fontFamily}`,\n );\n }\n}\n\n// ---------- a11y Config Validation ----------\n\n/** Shape-check the `a11y` block. Malformed values can't be silenced by `ignore`. */\nfunction validateA11yConfig(raw: unknown, errors: string[]): void {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n errors.push(\n `course.config.js: \"a11y\" must be an object, got ${raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw}`,\n );\n return;\n }\n const a11y = raw as Record<string, unknown>;\n\n if (\n a11y.level !== undefined &&\n !VALID_A11Y_LEVELS.includes(a11y.level as string)\n ) {\n errors.push(\n `course.config.js: \"a11y.level\" must be \"warn\" or \"error\", got ${JSON.stringify(a11y.level)}`,\n );\n }\n if (\n a11y.standard !== undefined &&\n !VALID_A11Y_STANDARDS.includes(a11y.standard as string)\n ) {\n errors.push(\n `course.config.js: \"a11y.standard\" must be \"wcag2a\", \"wcag2aa\", or \"wcag21aa\", got ${JSON.stringify(a11y.standard)}`,\n );\n }\n if (a11y.ignore !== undefined) {\n if (\n !Array.isArray(a11y.ignore) ||\n a11y.ignore.some((x) => typeof x !== 'string')\n ) {\n errors.push(\n `course.config.js: \"a11y.ignore\" must be an array of rule-ID strings`,\n );\n }\n }\n}\n\n// ---------- xAPI Config Validation ----------\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\nfunction validateXAPIConfig(\n raw: unknown,\n standard: string,\n errors: string[],\n warnings: string[],\n): void {\n if (raw === undefined || raw === null) return;\n\n // Normalize to array form. The single-object case is shorthand for a\n // one-element array — same machinery, no special case in the runtime.\n const entries: unknown[] = Array.isArray(raw) ? raw : [raw];\n\n if (Array.isArray(raw)) {\n if (entries.length === 0) {\n errors.push(\n 'course.config.js: xapi must contain at least one destination, or be omitted',\n );\n return;\n }\n // At most one 'lms' entry — more than one is never legitimate.\n const lmsCount = entries.filter(\n (e) =>\n e &&\n typeof e === 'object' &&\n (e as { endpoint?: unknown }).endpoint === 'lms',\n ).length;\n if (lmsCount > 1) {\n errors.push(\n \"course.config.js: xapi has multiple entries with endpoint: 'lms' — only one cmi5 launch-inherited destination is allowed\",\n );\n }\n // Warn on duplicate explicit endpoints.\n const seen = new Map<string, number>();\n for (const e of entries) {\n if (e && typeof e === 'object') {\n const ep = (e as { endpoint?: unknown }).endpoint;\n if (typeof ep === 'string' && ep !== 'lms') {\n seen.set(ep, (seen.get(ep) ?? 0) + 1);\n }\n }\n }\n for (const [ep, count] of seen) {\n if (count > 1) {\n warnings.push(\n `course.config.js: xapi has ${count} entries with endpoint \"${ep}\" — usually a copy-paste mistake; ` +\n 'fan-out to the same LRS with different actors/activityIds is supported but uncommon.',\n );\n }\n }\n } else if (typeof raw !== 'object') {\n errors.push(\n 'course.config.js: xapi must be an object or an array of objects',\n );\n return;\n }\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const label = Array.isArray(raw) ? `xapi[${i}]` : 'xapi';\n if (!entry || typeof entry !== 'object') {\n errors.push(`course.config.js: ${label} must be an object`);\n continue;\n }\n validateSingleXAPIEntry(\n entry as Record<string, unknown>,\n label,\n standard,\n errors,\n warnings,\n );\n }\n}\n\nfunction validateSingleXAPIEntry(\n entry: Record<string, unknown>,\n label: string,\n standard: string,\n errors: string[],\n warnings: string[],\n): void {\n const endpoint = entry.endpoint;\n if (endpoint === undefined) {\n errors.push(`course.config.js: ${label}.endpoint is required`);\n return;\n }\n if (typeof endpoint !== 'string') {\n errors.push(`course.config.js: ${label}.endpoint must be a string`);\n return;\n }\n\n if (endpoint === 'lms') {\n // Forbid under non-cmi5 export.\n if (standard !== 'cmi5') {\n errors.push(\n `course.config.js: ${label}.endpoint: 'lms' requires export.standard: 'cmi5' (you have \"${standard}\"). ` +\n 'Either change the export standard or specify an explicit LRS endpoint.',\n );\n }\n // Forbid extra fields — everything is inherited from the cmi5 launch.\n const forbidden = [\n 'auth',\n 'actor',\n 'activityId',\n 'registration',\n 'actorAccountHomePage',\n ];\n for (const f of forbidden) {\n if (entry[f] !== undefined) {\n errors.push(\n `course.config.js: ${label}.${f} must be omitted when ${label}.endpoint is 'lms' — it is inherited from the cmi5 launch.`,\n );\n }\n }\n return;\n }\n\n // Explicit endpoint — must be an absolute http(s) URL.\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n errors.push(\n `course.config.js: ${label}.endpoint must be an absolute http(s) URL, got \"${endpoint}\"`,\n );\n return;\n }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n errors.push(\n `course.config.js: ${label}.endpoint must use http: or https:, got \"${url.protocol}\"`,\n );\n return;\n }\n if (url.protocol === 'http:' && process.env.NODE_ENV === 'production') {\n warnings.push(\n `course.config.js: ${label}.endpoint uses http:; LRS credentials will travel in cleartext. Use https in production.`,\n );\n }\n if (!endpoint.endsWith('/')) {\n warnings.push(\n `course.config.js: ${label}.endpoint should end with a slash to avoid concatenation surprises ` +\n `(e.g. 'https://lrs.example.com/xapi/' not 'https://lrs.example.com/xapi'). Runtime normalizes regardless.`,\n );\n }\n\n // auth — required for explicit endpoints.\n const auth = entry.auth;\n if (auth === undefined) {\n errors.push(`course.config.js: ${label}.auth is required`);\n } else if (typeof auth === 'string') {\n const authErr = validateAuthCredential(auth);\n if (authErr) {\n errors.push(\n `course.config.js: ${joinFieldError(`${label}.auth`, authErr)}`,\n );\n } else {\n warnings.push(\n `course.config.js: ${label}.auth is a static string and will be embedded in the bundle. ` +\n 'For production, pass a function that fetches a short-lived token from a server endpoint.',\n );\n }\n } else if (typeof auth !== 'function') {\n errors.push(\n `course.config.js: ${label}.auth must be a string or a function, got ${typeof auth}`,\n );\n }\n\n // activityId — required IRI.\n const activityId = entry.activityId;\n if (activityId === undefined || activityId === '') {\n errors.push(`course.config.js: ${label}.activityId is required`);\n } else if (typeof activityId !== 'string') {\n errors.push(`course.config.js: ${label}.activityId must be a string`);\n } else {\n try {\n // Any absolute IRI — the URL constructor accepts uncommon schemes.\n new URL(activityId);\n } catch {\n errors.push(\n `course.config.js: ${label}.activityId must be an absolute IRI, got \"${activityId}\"`,\n );\n }\n }\n\n // actor — required under web; optional otherwise.\n const actor = entry.actor;\n if (actor === undefined) {\n if (standard === 'web') {\n errors.push(\n `course.config.js: ${label}.actor is required for web export — there is no LMS to derive a learner identity from. ` +\n 'Provide either a static actor object or a function that resolves one (e.g. from your auth system).',\n );\n }\n } else if (typeof actor === 'object' && actor !== null) {\n const err = validateAgent(actor);\n if (err) {\n errors.push(`course.config.js: ${joinFieldError(`${label}.actor`, err)}`);\n }\n } else if (typeof actor !== 'function') {\n errors.push(\n `course.config.js: ${label}.actor must be an object or function, got ${typeof actor}`,\n );\n }\n\n // actorAccountHomePage — optional, only meaningful under SCORM with no\n // explicit actor.\n const aahp = entry.actorAccountHomePage;\n if (aahp !== undefined) {\n if (typeof aahp !== 'string') {\n errors.push(\n `course.config.js: ${label}.actorAccountHomePage must be a string`,\n );\n } else {\n try {\n new URL(aahp);\n } catch {\n errors.push(\n `course.config.js: ${label}.actorAccountHomePage must be an absolute URL`,\n );\n }\n }\n if (actor !== undefined) {\n warnings.push(\n `course.config.js: ${label}.actorAccountHomePage is ignored when ${label}.actor is supplied explicitly.`,\n );\n }\n if (standard === 'cmi5' || standard === 'web') {\n warnings.push(\n `course.config.js: ${label}.actorAccountHomePage is only used under scorm12/scorm2004 actor synthesis; ignored under \"${standard}\".`,\n );\n }\n }\n\n // SCORM with auto-derived actor and a non-http(s) activityId:\n // actorAccountHomePage becomes required.\n if (\n actor === undefined &&\n (standard === 'scorm12' || standard === 'scorm2004') &&\n typeof activityId === 'string'\n ) {\n let isHttp: boolean;\n try {\n const u = new URL(activityId);\n isHttp = u.protocol === 'http:' || u.protocol === 'https:';\n } catch {\n isHttp = false;\n }\n if (!isHttp && aahp === undefined) {\n errors.push(\n `course.config.js: ${label}.activityId is not an http(s) URL, so its origin can't be used as the SCORM actor's account.homePage. ` +\n `Provide ${label}.actorAccountHomePage explicitly.`,\n );\n }\n }\n\n // registration — optional UUID v4.\n const registration = entry.registration;\n if (registration !== undefined) {\n if (typeof registration !== 'string' || !UUID_RE.test(registration)) {\n errors.push(\n `course.config.js: ${label}.registration must be a UUID v4, got \"${String(registration)}\"`,\n );\n }\n if (standard !== 'cmi5') {\n warnings.push(\n `course.config.js: ${label}.registration is a cmi5 concept; the LRS will accept it under \"${standard}\" but most analytics tools won't know what to do with it.`,\n );\n }\n }\n}\n\n// ---------- Pages Validation ----------\n\ninterface PageInfo {\n fileRel: string;\n navIndex: number;\n hasGradedQuiz: boolean;\n hasQuiz: boolean;\n completesOnView: boolean;\n}\n\ninterface PagesValidationResult extends ValidationResult {\n totalPages: number;\n totalQuizzes: number;\n hasGradedQuiz: boolean;\n pages: PageInfo[];\n}\n\n/**\n * Validate a single page .svelte file. Used for both section-level (flat) and\n * lesson-level pages — the validation is identical, only the containing\n * directory differs.\n */\nfunction validatePageFile(\n filePath: string,\n projectRoot: string,\n assetsDir: string,\n navIndex: number,\n errors: string[],\n warnings: string[],\n assetExistsCache: Map<string, boolean>,\n exportStandard?: string,\n): { page: PageInfo; isQuiz: boolean; isGradedQuiz: boolean } {\n const fileRel = relative(projectRoot, filePath);\n const content = readSourceFileCached(filePath);\n\n const pageConfig = validatePageConfig(content, fileRel, errors);\n\n const isQuiz = !!pageConfig?.quiz;\n let isGradedQuiz = false;\n if (pageConfig?.quiz) {\n validateQuizConfig(pageConfig.quiz, fileRel, errors);\n if ((pageConfig.quiz as { graded?: unknown }).graded === true) {\n isGradedQuiz = true;\n }\n }\n\n const completesOnView = validateCompletesOn(pageConfig, fileRel, errors);\n\n validateAssetRefs(content, fileRel, assetsDir, warnings, assetExistsCache);\n validateQuestionComponents(\n content,\n fileRel,\n errors,\n warnings,\n exportStandard,\n );\n validateMediaComponents(content, fileRel, errors, warnings);\n validateHeadingOrder(content, fileRel, warnings);\n validateContractBypass(content, fileRel, errors);\n if (\n pageConfig?.quiz &&\n !HAS_USE_QUESTION_RE.test(content) &&\n !HAS_QUESTION_TAG_RE.test(content) &&\n !HAS_LOCAL_SVELTE_IMPORT_RE.test(content)\n ) {\n warnings.push(\n `${fileRel}: quiz page has no question components or useQuestion() calls — ` +\n `the quiz will have nothing to score`,\n );\n }\n\n return {\n page: {\n fileRel,\n navIndex,\n hasGradedQuiz: isGradedQuiz,\n hasQuiz: isQuiz,\n completesOnView,\n },\n isQuiz,\n isGradedQuiz,\n };\n}\n\nfunction validatePages(\n pagesDir: string,\n assetsDir: string,\n projectRoot: string,\n exportStandard?: string,\n): PagesValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n const pages: PageInfo[] = [];\n let totalPages = 0;\n let totalQuizzes = 0;\n let hasGradedQuiz = false;\n // One existsSync per unique asset for the whole pass.\n const assetExistsCache = new Map<string, boolean>();\n\n if (!existsSync(pagesDir)) {\n errors.push(\n 'No pages found. Create at least one section with a lesson and page in pages/',\n );\n return {\n errors,\n warnings,\n totalPages: 0,\n totalQuizzes: 0,\n hasGradedQuiz: false,\n pages,\n };\n }\n\n const topLevelEntries = readdirSync(pagesDir);\n\n // Check for stray .svelte files at pages/ root\n for (const entry of topLevelEntries) {\n const fullPath = resolve(pagesDir, entry);\n if (entry.endsWith('.svelte') && statSync(fullPath).isFile()) {\n const relPath = relative(projectRoot, fullPath);\n warnings.push(\n `${relPath}: this file is outside the section/lesson structure and will be ignored`,\n );\n }\n }\n\n // Get section directories\n const sectionDirs = topLevelEntries\n .filter((name) => {\n const full = resolve(pagesDir, name);\n return statSync(full).isDirectory() && !name.startsWith('.');\n })\n .sort();\n\n if (sectionDirs.length === 0) {\n errors.push(\n 'No pages found. Create at least one section with a lesson and page in pages/',\n );\n return {\n errors,\n warnings,\n totalPages: 0,\n totalQuizzes: 0,\n hasGradedQuiz: false,\n pages,\n };\n }\n\n for (const sectionName of sectionDirs) {\n const sectionPath = resolve(pagesDir, sectionName);\n const sectionRel = relative(projectRoot, sectionPath);\n const pagesBeforeSection = totalPages;\n\n // Validate section _meta.js\n const sectionMeta = validateMetaFile(\n resolve(sectionPath, '_meta.js'),\n sectionRel,\n errors,\n );\n\n // Flat mode: .svelte files directly at section level are pages of an\n // implicit single lesson. Validate them just like lesson-level pages.\n const sectionEntries = readdirSync(sectionPath);\n const sectionSvelteFiles = sectionEntries\n .filter((name) => {\n const full = resolve(sectionPath, name);\n return name.endsWith('.svelte') && statSync(full).isFile();\n })\n .sort();\n\n if (sectionMeta?.pages) {\n for (const pageName of sectionMeta.pages) {\n const fileName = ensureSvelteSuffix(pageName);\n if (!sectionSvelteFiles.includes(fileName)) {\n const metaRel = relative(\n projectRoot,\n resolve(sectionPath, '_meta.js'),\n );\n errors.push(\n `${metaRel}: pages array lists \"${pageName}\" but ${fileName} not found in this directory`,\n );\n }\n }\n }\n\n for (const fileName of sectionSvelteFiles) {\n const result = validatePageFile(\n resolve(sectionPath, fileName),\n projectRoot,\n assetsDir,\n totalPages,\n errors,\n warnings,\n assetExistsCache,\n exportStandard,\n );\n totalPages++;\n if (result.isQuiz) totalQuizzes++;\n if (result.isGradedQuiz) hasGradedQuiz = true;\n pages.push(result.page);\n }\n\n // Get lesson directories\n const lessonDirs = sectionEntries\n .filter((name) => {\n const full = resolve(sectionPath, name);\n return statSync(full).isDirectory() && !name.startsWith('.');\n })\n .sort();\n\n for (const lessonName of lessonDirs) {\n const lessonPath = resolve(sectionPath, lessonName);\n const lessonRel = relative(projectRoot, lessonPath);\n\n // Validate lesson _meta.js\n const meta = validateMetaFile(\n resolve(lessonPath, '_meta.js'),\n lessonRel,\n errors,\n );\n\n // Get .svelte files\n const svelteFiles = readdirSync(lessonPath)\n .filter((name) => name.endsWith('.svelte'))\n .sort();\n\n // Check pages array references\n if (meta?.pages) {\n for (const pageName of meta.pages) {\n const fileName = ensureSvelteSuffix(pageName);\n if (!svelteFiles.includes(fileName)) {\n const metaRel = relative(\n projectRoot,\n resolve(lessonPath, '_meta.js'),\n );\n errors.push(\n `${metaRel}: pages array lists \"${pageName}\" but ${fileName} not found in this directory`,\n );\n }\n }\n }\n\n // Check for unlisted .svelte files\n if (meta?.pages && meta.pages.length > 0) {\n const listedSet = new Set(meta.pages.map(ensureSvelteSuffix));\n for (const file of svelteFiles) {\n if (!listedSet.has(file)) {\n const relPath = relative(projectRoot, resolve(lessonPath, file));\n warnings.push(\n `${relPath}: not listed in _meta.js pages array — will be appended at end`,\n );\n }\n }\n }\n\n // Validate each .svelte file\n for (const fileName of svelteFiles) {\n const result = validatePageFile(\n resolve(lessonPath, fileName),\n projectRoot,\n assetsDir,\n totalPages,\n errors,\n warnings,\n assetExistsCache,\n exportStandard,\n );\n totalPages++;\n if (result.isQuiz) totalQuizzes++;\n if (result.isGradedQuiz) hasGradedQuiz = true;\n pages.push(result.page);\n }\n }\n\n // The page-count delta covers both the no-lessons and empty-lessons cases.\n if (totalPages === pagesBeforeSection) {\n warnings.push(\n `${sectionRel}: section contributed no pages and will be empty`,\n );\n }\n }\n\n if (totalPages === 0) {\n errors.push(\n 'No pages found. Create at least one section with a lesson and page in pages/',\n );\n }\n\n return { errors, warnings, totalPages, totalQuizzes, hasGradedQuiz, pages };\n}\n\n// ---------- _meta.js Validation ----------\n\nfunction validateMetaFile(\n metaPath: string,\n parentRel: string,\n errors: string[],\n): { title?: string; pages?: string[] } | null {\n if (!existsSync(metaPath)) return null;\n\n const metaRel = `${parentRel}/_meta.js`;\n const objectStr = extractDefaultExportObjectLiteral(\n readSourceFileCached(metaPath),\n );\n\n if (!objectStr) {\n errors.push(\n `${metaRel}: syntax error — must export default { title: \"...\" }`,\n );\n return null;\n }\n\n let meta: { title?: string; pages?: string[] };\n try {\n meta = JSON5.parse(objectStr);\n } catch {\n errors.push(\n `${metaRel}: syntax error — must export default { title: \"...\" }`,\n );\n return null;\n }\n\n if (!meta.title) {\n errors.push(`${metaRel}: missing required \"title\" field`);\n }\n\n return meta;\n}\n\n// ---------- pageConfig Validation ----------\n\nfunction validatePageConfig(\n content: string,\n fileRel: string,\n errors: string[],\n): { title?: string; quiz?: unknown; completesOn?: unknown } | null {\n const result = parsePageConfigFromSource(content);\n if (result.kind === 'ok') return result.value;\n if (result.kind === 'invalid') {\n errors.push(\n `${fileRel}: pageConfig must be a static object literal (no variables, function calls, or computed values)`,\n );\n }\n return null;\n}\n\nfunction validateCompletesOn(\n pageConfig: { completesOn?: unknown } | null,\n fileRel: string,\n errors: string[],\n): boolean {\n if (!pageConfig || pageConfig.completesOn === undefined) return false;\n if (pageConfig.completesOn === 'view') return true;\n errors.push(\n `${fileRel}: pageConfig.completesOn must be \"view\", got ${JSON.stringify(pageConfig.completesOn)}`,\n );\n return false;\n}\n\n// ---------- Quiz Config Validation ----------\n\nfunction validateQuizConfig(\n quiz: unknown,\n fileRel: string,\n errors: string[],\n): void {\n if (!quiz || typeof quiz !== 'object') return;\n const cfg = quiz as Record<string, unknown>;\n\n if (cfg.maxAttempts !== undefined) {\n const val = cfg.maxAttempts;\n if (\n val !== Infinity &&\n (typeof val !== 'number' || val <= 0 || !Number.isFinite(val))\n ) {\n errors.push(\n `${fileRel}: quiz.maxAttempts must be a positive number or Infinity, got ${String(val)}`,\n );\n }\n }\n\n for (const field of ['graded', 'gatesProgress']) {\n if (cfg[field] !== undefined && typeof cfg[field] !== 'boolean') {\n errors.push(\n `${fileRel}: quiz.${field} must be a boolean, got ${typeof cfg[field]}`,\n );\n }\n }\n\n if (\n cfg.feedbackMode !== undefined &&\n !VALID_FEEDBACK_MODES.includes(cfg.feedbackMode as string)\n ) {\n errors.push(\n `${fileRel}: quiz.feedbackMode must be \"review\", \"immediate\", or \"never\", got \"${String(cfg.feedbackMode)}\"`,\n );\n }\n if (\n cfg.retryMode !== undefined &&\n !VALID_RETRY_MODES.includes(cfg.retryMode as string)\n ) {\n errors.push(\n `${fileRel}: quiz.retryMode must be \"full\" or \"incorrect-only\", got \"${String(cfg.retryMode)}\"`,\n );\n }\n}\n\n// ---------- Question Component Validation ----------\n\nconst QUESTION_COMPONENT_REQUIRED: Record<string, string[]> = {\n MultipleChoice: ['question', 'options', 'correct'],\n FillInTheBlank: ['question', 'answers'],\n Matching: ['question', 'pairs'],\n Sorting: ['question', 'items', 'targets', 'correct'],\n};\n\ntype PropValue =\n | { kind: 'string'; value: string }\n | { kind: 'expr'; raw: string }\n | { kind: 'bool' };\n\n/**\n * Parse the props of an opening tag starting just after the component name.\n * Returns null if the tag can't be parsed cleanly — callers then skip it\n * rather than risk a false positive.\n */\nfunction parseTagProps(\n content: string,\n start: number,\n): { props: Map<string, PropValue>; hasSpread: boolean } | null {\n const props = new Map<string, PropValue>();\n let hasSpread = false;\n let i = start;\n while (i < content.length) {\n while (i < content.length && /\\s/.test(content[i])) i++;\n if (i >= content.length) return null;\n const c = content[i];\n if (c === '>') return { props, hasSpread };\n if (c === '/' && content[i + 1] === '>') return { props, hasSpread };\n // Spread / shorthand expression — skip the whole {...} block, but record\n // that unseen props may be supplied here so callers can suppress\n // false-positive \"missing required prop / alt / title\" diagnostics.\n if (c === '{') {\n const block = extractObjectLiteral(content, i);\n if (!block) return null;\n hasSpread = true;\n i += block.length;\n continue;\n }\n const nameMatch = /^[A-Za-z_][\\w-]*/.exec(content.slice(i));\n if (!nameMatch) return null;\n const propName = nameMatch[0];\n i += propName.length;\n while (i < content.length && /\\s/.test(content[i])) i++;\n if (content[i] !== '=') {\n props.set(propName, { kind: 'bool' });\n continue;\n }\n i++;\n while (i < content.length && /\\s/.test(content[i])) i++;\n const v = content[i];\n if (v === '\"' || v === \"'\") {\n const end = content.indexOf(v, i + 1);\n if (end === -1) return null;\n props.set(propName, { kind: 'string', value: content.slice(i + 1, end) });\n i = end + 1;\n } else if (v === '{') {\n const block = extractObjectLiteral(content, i);\n if (!block) return null;\n props.set(propName, { kind: 'expr', raw: block.slice(1, -1).trim() });\n i += block.length;\n } else {\n return null;\n }\n }\n return null;\n}\n\nfunction staticArray(prop: PropValue | undefined): unknown[] | null {\n if (prop?.kind !== 'expr' || !prop.raw.startsWith('[')) return null;\n try {\n const parsed = JSON5.parse(prop.raw);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\nfunction staticNumber(prop: PropValue | undefined): number | null {\n if (prop?.kind !== 'expr') return null;\n try {\n const parsed = JSON5.parse(prop.raw);\n return typeof parsed === 'number' ? parsed : null;\n } catch {\n return null;\n }\n}\n\nfunction validateQuestionComponents(\n content: string,\n fileRel: string,\n errors: string[],\n warnings: string[],\n exportStandard?: string,\n): void {\n const names = Object.keys(QUESTION_COMPONENT_REQUIRED).join('|');\n const tagStartRe = new RegExp(`<(${names})(?=[\\\\s/>])`, 'g');\n const seenIds = new Set<string>();\n const seenSanitized = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = tagStartRe.exec(content)) !== null) {\n const name = m[1];\n const parsed = parseTagProps(content, m.index + m[0].length);\n if (!parsed) continue;\n const { props, hasSpread } = parsed;\n\n for (const req of QUESTION_COMPONENT_REQUIRED[name]) {\n if (!hasSpread && !props.has(req)) {\n errors.push(`${fileRel}: <${name}> is missing required prop \"${req}\"`);\n }\n }\n\n // Rule 1.5: empty option/answer labels are both an a11y and a scoring bug.\n for (const labelProp of ['options', 'answers']) {\n const entries = staticArray(props.get(labelProp));\n if (entries?.some((e) => typeof e === 'string' && e.trim() === '')) {\n warnings.push(\n tag(\n A11Y_IDS.questionLabel,\n `${fileRel}: <${name}> has an empty ${labelProp === 'options' ? 'option' : 'answer'} label`,\n ),\n );\n }\n }\n\n const idProp = props.get('id');\n if (idProp?.kind === 'string') {\n if (seenIds.has(idProp.value)) {\n errors.push(\n `${fileRel}: duplicate question id \"${idProp.value}\" — each question on a page needs a unique id`,\n );\n } else if (exportStandard === 'scorm12') {\n // scorm12-only: shortIdentifier strips non-alphanumerics, so distinct\n // raw ids can collide after sanitization. Skip raw duplicates (already\n // flagged above) to avoid double-reporting the same id.\n const sane = shortIdentifier(idProp.value);\n if (sane !== idProp.value) {\n warnings.push(\n `${fileRel}: question id \"${idProp.value}\" will be rewritten to \"${sane}\" for SCORM 1.2 — use only letters and digits (underscores only between them)`,\n );\n }\n if (seenSanitized.has(sane)) {\n errors.push(\n `${fileRel}: question id \"${idProp.value}\" collides with a prior id after SCORM 1.2 sanitization (\"${sane}\")`,\n );\n }\n seenSanitized.add(sane);\n }\n seenIds.add(idProp.value);\n }\n\n const weightProp = props.get('weight');\n if (weightProp?.kind === 'string') {\n warnings.push(\n `${fileRel}: <${name}> weight=\"${weightProp.value}\" is a string and is ignored (treated as 1) — pass a number: weight={${weightProp.value}}`,\n );\n } else {\n const weight = staticNumber(weightProp);\n if (weight !== null) {\n if (!Number.isFinite(weight)) {\n errors.push(\n `${fileRel}: <${name}> weight must be finite — a non-finite weight makes the weighted score NaN, got ${weight}`,\n );\n } else if (!(weight > 0)) {\n warnings.push(\n `${fileRel}: <${name}> weight ${weight} is not positive and is ignored (treated as 1)`,\n );\n }\n }\n }\n\n if (name === 'MultipleChoice') {\n const options = staticArray(props.get('options'));\n const correct = staticNumber(props.get('correct'));\n if (options && correct !== null) {\n if (\n !Number.isInteger(correct) ||\n correct < 0 ||\n correct >= options.length\n ) {\n errors.push(\n `${fileRel}: <MultipleChoice> correct={${correct}} is out of range for ${options.length} options (valid: 0–${options.length - 1})`,\n );\n }\n }\n const optionFeedback = staticArray(props.get('optionFeedback'));\n if (options && optionFeedback && optionFeedback.length > options.length) {\n warnings.push(\n `${fileRel}: <MultipleChoice> optionFeedback has ${optionFeedback.length} entries but only ${options.length} options — the extra entries can never be shown`,\n );\n }\n } else if (name === 'Sorting') {\n const items = staticArray(props.get('items'));\n const targets = staticArray(props.get('targets'));\n const correct = staticArray(props.get('correct'));\n if (items && correct && correct.length !== items.length) {\n errors.push(\n `${fileRel}: <Sorting> correct has ${correct.length} entries but items has ${items.length} — they must be parallel arrays`,\n );\n }\n if (targets && correct) {\n for (const idx of correct) {\n if (\n typeof idx !== 'number' ||\n !Number.isInteger(idx) ||\n idx < 0 ||\n idx >= targets.length\n ) {\n errors.push(\n `${fileRel}: <Sorting> correct contains ${JSON.stringify(idx)}, out of range for ${targets.length} targets (valid: 0–${targets.length - 1})`,\n );\n break;\n }\n }\n }\n } else if (name === 'Matching') {\n const pairs = staticArray(props.get('pairs'));\n if (pairs) {\n const bad = pairs.some(\n (p) =>\n typeof p !== 'object' ||\n p === null ||\n typeof (p as { left?: unknown }).left !== 'string' ||\n typeof (p as { right?: unknown }).right !== 'string',\n );\n if (bad) {\n errors.push(\n `${fileRel}: <Matching> pairs must be an array of { left: string, right: string } objects`,\n );\n }\n }\n } else if (name === 'FillInTheBlank') {\n const answers = staticArray(props.get('answers'));\n if (answers) {\n if (answers.length === 0) {\n errors.push(`${fileRel}: <FillInTheBlank> answers must not be empty`);\n } else if (answers.some((a) => typeof a !== 'string')) {\n errors.push(\n `${fileRel}: <FillInTheBlank> answers must be an array of strings`,\n );\n }\n }\n }\n }\n}\n\n// ---------- Media Component Validation (rules 1.3 / 1.4) ----------\n\n/** Remove HTML/Svelte comments so commented-out markup isn't scanned as live. */\nconst HTML_COMMENT_RE = /<!--[\\s\\S]*?-->/g;\n\n/**\n * Sibling to validateQuestionComponents kept out of QUESTION_COMPONENT_REQUIRED\n * so media isn't treated as gradable questions. Declares `warnings` directly.\n * Non-static (kind 'expr') values are skipped, matching the rest of the linter.\n */\nfunction validateMediaComponents(\n content: string,\n fileRel: string,\n errors: string[],\n warnings: string[],\n): void {\n const scan = content.replace(HTML_COMMENT_RE, '');\n const tagStartRe = /<(Image|Video|Audio)(?=[\\s/>])/g;\n let m: RegExpExecArray | null;\n while ((m = tagStartRe.exec(scan)) !== null) {\n const name = m[1];\n const parsed = parseTagProps(scan, m.index + m[0].length);\n if (!parsed) continue;\n const { props, hasSpread } = parsed;\n\n if (name === 'Image') {\n const alt = props.get('alt');\n const decorative = props.get('decorative');\n // A string value is truthy at runtime (so decorative=\"false\" hides the\n // image), but the parser sees a string, not a boolean — flag the misuse.\n if (decorative?.kind === 'string') {\n errors.push(\n tag(\n A11Y_IDS.imageAlt,\n `${fileRel}: <Image> \"decorative\" must be a boolean — use decorative or decorative={true}, not the string ${JSON.stringify(decorative.value)}`,\n ),\n );\n continue;\n }\n const hasDecorative =\n decorative?.kind === 'bool' ||\n (decorative?.kind === 'expr' && decorative.raw.trim() === 'true');\n const altIsEmpty = alt?.kind === 'string' && alt.value.trim() === '';\n if (!hasDecorative && !hasSpread && (alt === undefined || altIsEmpty)) {\n errors.push(\n tag(\n A11Y_IDS.imageAlt,\n `${fileRel}: <Image> needs alt text, or mark it decorative={true} if purely ornamental`,\n ),\n );\n }\n if (hasDecorative && alt?.kind === 'string' && alt.value.trim() !== '') {\n warnings.push(\n tag(\n A11Y_IDS.imageAlt,\n `${fileRel}: <Image> is decorative but also has alt text — the alt will be dropped`,\n ),\n );\n }\n continue;\n }\n\n // Video / Audio\n const title = props.get('title');\n const titleIsEmpty = title?.kind === 'string' && title.value.trim() === '';\n if (!hasSpread && (title === undefined || titleIsEmpty)) {\n errors.push(\n tag(\n A11Y_IDS.mediaTitle,\n `${fileRel}: <${name}> needs a title — it's the accessible name for the player`,\n ),\n );\n }\n const src = props.get('src');\n const isEmbed = src?.kind === 'string' && isVideoEmbed(src.value);\n if (\n name === 'Video' &&\n !hasSpread &&\n isEmbed &&\n props.get('transcript') === undefined\n ) {\n warnings.push(\n tag(\n A11Y_IDS.mediaTranscript,\n `${fileRel}: <Video> embeds can't carry caption tracks — provide a transcript for WCAG 1.2`,\n ),\n );\n }\n if (\n name === 'Video' &&\n !hasSpread &&\n src?.kind === 'string' &&\n !isEmbed &&\n props.get('tracks') === undefined &&\n props.get('transcript') === undefined\n ) {\n warnings.push(\n tag(\n A11Y_IDS.mediaCaptions,\n `${fileRel}: native <Video> has no caption tracks or transcript — add tracks={[…]} or a transcript for WCAG 1.2.2`,\n ),\n );\n }\n if (\n name === 'Audio' &&\n !hasSpread &&\n props.get('transcript') === undefined\n ) {\n warnings.push(\n tag(\n A11Y_IDS.mediaTranscript,\n `${fileRel}: <Audio> has no transcript — required for WCAG 1.2.1`,\n ),\n );\n }\n }\n}\n\n// ---------- Heading Order Validation (rule 1.6) ----------\n\n/**\n * Warn on a skipped heading level (e.g. h2 → h4). Scripts, styles, and comments\n * are stripped first so string literals, CSS, and commented-out markup can't be\n * miscounted. No \"one h1 per page\" check — the layout owns the page h1 and child\n * components emit headings a static scan can't see; that belongs to the Tier-2\n * audit.\n */\nfunction validateHeadingOrder(\n content: string,\n fileRel: string,\n warnings: string[],\n): void {\n const html = content\n .replace(/<(script|style)\\b[\\s\\S]*?<\\/\\1>/gi, '')\n .replace(HTML_COMMENT_RE, '');\n const levels = [...html.matchAll(/<h([1-6])\\b/gi)].map((h) => Number(h[1]));\n let prevSeen: number | null = null;\n for (const level of levels) {\n if (prevSeen !== null && level - prevSeen > 1) {\n warnings.push(\n tag(\n A11Y_IDS.headingOrder,\n `${fileRel}: heading level jumps from h${prevSeen} to h${level} — don't skip levels (WCAG 1.3.1)`,\n ),\n );\n }\n prevSeen = level;\n }\n}\n\n// ---------- Contract Bypass Detection ----------\n\nconst QUIZ_COMPLETE_DISPATCH_RE =\n /(?:new\\s+CustomEvent\\s*\\(\\s*['\"]tessera-quiz-complete['\"]|dispatchEvent\\s*\\([\\s\\S]{0,120}tessera-quiz-complete)/;\nconst RUNTIME_INTERNAL_IMPORT_RE = /from\\s+['\"]tessera-learn\\/runtime\\//;\nconst HAS_USE_QUESTION_RE = /\\buseQuestion\\s*\\(/;\nconst HAS_QUESTION_TAG_RE = new RegExp(\n `<(${Object.keys(QUESTION_COMPONENT_REQUIRED).join('|')})(?=[\\\\s/>])`,\n);\n// Custom widget imported from a local `.svelte` file may wrap useQuestion.\n// Treat its presence as enough to suppress the \"no questions\" warning —\n// false negatives are acceptable for a heuristic that's already advisory.\nconst HAS_LOCAL_SVELTE_IMPORT_RE = /from\\s+['\"][^'\"]+\\.svelte['\"]/;\n\n/**\n * Detect ways an author file can bypass the LMS data contract. These check\n * source text for known escape hatches — they never inspect course content,\n * so they constrain how you wire things up, not what you build.\n */\nfunction validateContractBypass(\n content: string,\n fileRel: string,\n errors: string[],\n): void {\n if (QUIZ_COMPLETE_DISPATCH_RE.test(content)) {\n errors.push(\n `${fileRel}: dispatches \"tessera-quiz-complete\" directly — submit through ` +\n `useQuiz().submit() so the result reaches the LMS`,\n );\n }\n if (RUNTIME_INTERNAL_IMPORT_RE.test(content)) {\n errors.push(\n `${fileRel}: imports from tessera-learn/runtime/* — use the public hooks ` +\n `(useQuiz, useQuestion, useNavigation, …) instead`,\n );\n }\n}\n\n// ---------- Asset Reference Validation ----------\n\nconst ASSET_REF_RE = /\\$assets\\/([^\\s\"'`)]+)/g;\n\n/** Match $assets/... refs in any context (src attrs, import statements, url() etc) and dedupe. */\nfunction collectAssetRefs(content: string): string[] {\n const seen = new Set<string>();\n let match: RegExpExecArray | null;\n ASSET_REF_RE.lastIndex = 0;\n while ((match = ASSET_REF_RE.exec(content)) !== null) {\n seen.add(match[1].replace(/[?#].*$/, ''));\n }\n return [...seen];\n}\n\nfunction validateAssetRefs(\n content: string,\n fileRel: string,\n assetsDir: string,\n warnings: string[],\n existsCache: Map<string, boolean>,\n): void {\n for (const assetPath of collectAssetRefs(content)) {\n const fullAssetPath = resolve(assetsDir, assetPath);\n let exists = existsCache.get(fullAssetPath);\n if (exists === undefined) {\n exists = existsSync(fullAssetPath);\n existsCache.set(fullAssetPath, exists);\n }\n if (!exists) {\n warnings.push(\n `${fileRel}: \"$assets/${assetPath}\" not found in assets/ directory`,\n );\n }\n }\n}\n\n// ---------- Cross-Cutting Validations ----------\n\nfunction crossValidate(\n config: ParsedConfig,\n pageResults: PagesValidationResult,\n errors: string[],\n warnings: string[],\n): void {\n // completion.mode \"quiz\" but no graded quizzes\n if (config.completion?.mode === 'quiz' && !pageResults.hasGradedQuiz) {\n errors.push(\n 'completion.mode is \"quiz\" but no pages have quiz config with graded: true',\n );\n }\n\n // completion.mode \"quiz\" with an implicit pass threshold — the merge defaults\n // to 70, so this is a nudge, not an error.\n if (\n config.completion?.mode === 'quiz' &&\n config.scoring?.passingScore === undefined\n ) {\n warnings.push(\n 'completion.mode is \"quiz\" but scoring.passingScore is not set — defaulting to 70%. Set it explicitly to be sure.',\n );\n }\n\n const isManual = config.completion?.mode === 'manual';\n const completesOnPages = pageResults.pages.filter((p) => p.completesOnView);\n\n if (\n isManual &&\n config.completion?.trigger === 'page' &&\n completesOnPages.length === 0\n ) {\n errors.push(\n 'completion.mode is \"manual\" with trigger: \"page\", but no page declares pageConfig.completesOn: \"view\". ' +\n 'Either add a completesOn page or remove the trigger field to drop the static check.',\n );\n }\n\n if (isManual) {\n for (const page of pageResults.pages) {\n if (page.hasGradedQuiz) {\n warnings.push(\n `${page.fileRel}: quiz.graded is true under completion.mode: \"manual\". ` +\n 'The score will be reported to the LMS for transcripts, but it will not drive ' +\n \"completion or success status — `markComplete()` / completesOn does. If that's \" +\n 'not what you want, set graded: false or change completion.mode.',\n );\n }\n }\n }\n\n if (isManual && config.completion?.percentageThreshold !== undefined) {\n warnings.push(\n 'course.config.js: \"completion.percentageThreshold\" is ignored under completion.mode: \"manual\"',\n );\n }\n if (!isManual) {\n for (const page of completesOnPages) {\n warnings.push(\n `${page.fileRel}: pageConfig.completesOn is ignored — completion.mode is \"${config.completion?.mode ?? 'percentage'}\"`,\n );\n }\n }\n for (const page of pageResults.pages) {\n if (page.completesOnView && page.hasQuiz) {\n warnings.push(\n `${page.fileRel}: completion fires on view, before the quiz can be answered — likely a mistake`,\n );\n }\n }\n\n if (isManual) {\n const firstPage = pageResults.pages.find((p) => p.navIndex === 0);\n if (firstPage?.completesOnView) {\n warnings.push(\n `${firstPage.fileRel}: pageConfig.completesOn: \"view\" is on the first page — the course will complete immediately on launch, before the learner sees any other content.`,\n );\n }\n }\n\n // SCORM 1.2 + high page count warning\n if (config.export?.standard === 'scorm12') {\n // Estimate worst-case suspend_data size when all pages are visited, all\n // quizzes completed, all chunks revealed, and a modest amount of\n // usePersistence / standalone-question state has accumulated.\n //\n // SavedState shape (see runtime/persistence.ts) — single-letter keys:\n // b (bookmark), v (visited[]), q (quiz scores), d (duration),\n // c (chunk progress), s (standalone scores), gs (graded standalone pages),\n // u (user state from usePersistence)\n //\n // We can't statically detect calls to `useQuestion({ graded: true })` or\n // `usePersistence`, so reserve a fixed buffer per page for those.\n let visitedChars = 0;\n for (let i = 0; i < pageResults.totalPages; i++) {\n visitedChars += String(i).length + 1; // digit chars + comma\n }\n const overhead = 60; // top-level JSON overhead with all keys\n const quizBytes = pageResults.totalQuizzes * 15; // q: \"NNN\":100,\n const chunkBytes = pageResults.totalPages * 12; // c: \"NNN\":NN,\n const standaloneBytes = pageResults.totalPages * 30; // s/gs: conservative buffer per page\n const userStateBuffer = 256; // usePersistence headroom\n const estimatedSize =\n overhead +\n visitedChars +\n quizBytes +\n chunkBytes +\n standaloneBytes +\n userStateBuffer;\n\n if (estimatedSize > 3200) {\n warnings.push(\n `Course has ${pageResults.totalPages} pages with ${pageResults.totalQuizzes} quizzes — estimated SCORM 1.2 suspend_data ~${estimatedSize} bytes may exceed the 4096-byte limit when fully populated (visited + chunks + standalone scores + usePersistence). Consider using \"scorm2004\" or \"cmi5\".`,\n );\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tessera-learn",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "LMS tracking runtime for interactive learning content. One adapter layer (SCORM 1.2, SCORM 2004 4th Edition, cmi5, static Web), your choice of components.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -55,7 +55,8 @@
55
55
  "**/*.css"
56
56
  ],
57
57
  "bin": {
58
- "tessera-validate": "./dist/plugin/cli.js"
58
+ "tessera-validate": "./dist/plugin/cli.js",
59
+ "tessera-a11y": "./dist/plugin/a11y-cli.js"
59
60
  },
60
61
  "engines": {
61
62
  "node": ">=24"
@@ -66,10 +67,23 @@
66
67
  "json5": "^2.0.0",
67
68
  "svelte": "^5.55.7"
68
69
  },
70
+ "peerDependencies": {
71
+ "@axe-core/playwright": ">=4",
72
+ "playwright": ">=1.40"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@axe-core/playwright": {
76
+ "optional": true
77
+ },
78
+ "playwright": {
79
+ "optional": true
80
+ }
81
+ },
69
82
  "devDependencies": {
70
83
  "@types/node": "^25.8.0",
71
84
  "@vitest/coverage-v8": "^4.1.6",
72
85
  "jsdom": "^29.0.1",
86
+ "svelte-check": "^4.4.8",
73
87
  "tsdown": "^0.22.0",
74
88
  "typescript": "^6.0.3",
75
89
  "vite": "^8.0.13",
@@ -77,6 +91,7 @@
77
91
  },
78
92
  "scripts": {
79
93
  "build": "tsdown",
94
+ "check": "svelte-check --tsconfig ./tsconfig.json",
80
95
  "test": "vitest run",
81
96
  "test:coverage": "vitest run --coverage"
82
97
  }
@@ -11,7 +11,9 @@
11
11
  let openId = $state(null);
12
12
 
13
13
  setContext('tessera-accordion', {
14
- get openId() { return openId; },
14
+ get openId() {
15
+ return openId;
16
+ },
15
17
  toggle(id) {
16
18
  openId = openId === id ? null : id;
17
19
  },
@@ -1,7 +1,3 @@
1
- <script module>
2
- let _accordionItemCounter = 0;
3
- </script>
4
-
5
1
  <script>
6
2
  /**
7
3
  * @component AccordionItem
@@ -13,7 +9,7 @@
13
9
  import { getContext } from 'svelte';
14
10
 
15
11
  let { title, children } = $props();
16
- const id = _accordionItemCounter++;
12
+ const id = $props.id();
17
13
  const headerId = `tessera-accordion-header-${id}`;
18
14
  const panelId = `tessera-accordion-panel-${id}`;
19
15
 
@@ -4,11 +4,18 @@
4
4
  * Native audio player with optional title label.
5
5
  *
6
6
  * @prop {string} src - Audio file URL (supports $assets/ paths)
7
- * @prop {string} [title] - Label displayed above the player
7
+ * @prop {string} title - Accessible label for the player (required; rule 1.4)
8
+ * @prop {Array<{ src: string, kind?: 'captions'|'subtitles', srclang?: string, label?: string }>} [tracks] -
9
+ * Caption/subtitle tracks rendered as <track> on the native player.
10
+ * @prop {string} [transcript] - Transcript text shown in a <details> disclosure
11
+ * below the player (WCAG 1.2.1). To load it from a file, import the file with
12
+ * Vite's ?raw suffix: `import t from '$assets/x.txt?raw'` then `transcript={t}`.
8
13
  */
9
14
  import { resolveAsset } from './util.js';
15
+ import MediaTracks from './MediaTracks.svelte';
16
+ import Transcript from './Transcript.svelte';
10
17
 
11
- let { src, title = '' } = $props();
18
+ let { src, title, tracks = [], transcript = '' } = $props();
12
19
  let resolvedSrc = $derived(resolveAsset(src));
13
20
  </script>
14
21
 
@@ -16,10 +23,17 @@
16
23
  {#if title}
17
24
  <div class="tessera-audio-title">{title}</div>
18
25
  {/if}
19
- <audio controls preload="metadata" aria-label={title || 'Audio player'} class="tessera-audio-player">
26
+ <audio
27
+ controls
28
+ preload="metadata"
29
+ aria-label={title || 'Audio player'}
30
+ class="tessera-audio-player"
31
+ >
20
32
  <source src={resolvedSrc} />
33
+ <MediaTracks {tracks} />
21
34
  Your browser does not support the audio element.
22
35
  </audio>
36
+ <Transcript text={transcript} />
23
37
  </div>
24
38
 
25
39
  <style>
@@ -23,7 +23,11 @@
23
23
  };
24
24
  </script>
25
25
 
26
- <div class="tessera-callout tessera-callout-{type}" role="note" aria-label={labels[type]}>
26
+ <div
27
+ class="tessera-callout tessera-callout-{type}"
28
+ role="note"
29
+ aria-label={labels[type]}
30
+ >
27
31
  <span class="tessera-callout-icon" aria-hidden="true">{icons[type]}</span>
28
32
  <div class="tessera-callout-content">
29
33
  {@render children?.()}
@@ -14,7 +14,9 @@
14
14
  let touchEndX = 0;
15
15
 
16
16
  const ctx = {
17
- get currentSlide() { return currentSlide; },
17
+ get currentSlide() {
18
+ return currentSlide;
19
+ },
18
20
  register() {
19
21
  const index = totalSlides;
20
22
  totalSlides++;
@@ -37,8 +39,14 @@
37
39
  }
38
40
 
39
41
  function handleKeydown(e) {
40
- if (e.key === 'ArrowLeft') { prev(); e.preventDefault(); }
41
- if (e.key === 'ArrowRight') { next(); e.preventDefault(); }
42
+ if (e.key === 'ArrowLeft') {
43
+ prev();
44
+ e.preventDefault();
45
+ }
46
+ if (e.key === 'ArrowRight') {
47
+ next();
48
+ e.preventDefault();
49
+ }
42
50
  }
43
51
 
44
52
  function handleTouchStart(e) {
@@ -71,7 +79,10 @@
71
79
  tabindex="0"
72
80
  >
73
81
  <div class="tessera-carousel-viewport">
74
- <div class="tessera-carousel-track" style="transform: translateX(-{currentSlide * 100}%)">
82
+ <div
83
+ class="tessera-carousel-track"
84
+ style="transform: translateX(-{currentSlide * 100}%)"
85
+ >
75
86
  {@render children?.()}
76
87
  </div>
77
88
  </div>
@@ -86,8 +97,12 @@
86
97
 
87
98
  </button>
88
99
 
89
- <div class="tessera-carousel-dots" role="tablist" aria-label="Slide indicators">
90
- {#each dots as dot}
100
+ <div
101
+ class="tessera-carousel-dots"
102
+ role="tablist"
103
+ aria-label="Slide indicators"
104
+ >
105
+ {#each dots as dot (dot)}
91
106
  <button
92
107
  class="tessera-carousel-dot"
93
108
  class:active={dot === currentSlide}
@@ -148,8 +163,9 @@
148
163
  border: 1px solid var(--tessera-border);
149
164
  border-radius: 50%;
150
165
  cursor: pointer;
151
- transition: background-color var(--tessera-transition-fast),
152
- color var(--tessera-transition-fast);
166
+ transition:
167
+ background-color var(--tessera-transition-fast),
168
+ color var(--tessera-transition-fast);
153
169
  }
154
170
 
155
171
  .tessera-carousel-arrow:hover:not(:disabled) {
@@ -4,14 +4,15 @@
4
4
  import { requireNavContext } from '../runtime/contexts.js';
5
5
 
6
6
  let { page } = $props();
7
- const { nav, manifest, config, progress } = requireNavContext('DefaultLayout');
7
+ const { nav, manifest, config, progress } =
8
+ requireNavContext('DefaultLayout');
8
9
 
9
10
  let sidebarOpen = $state(false);
10
11
 
11
12
  let progressPercent = $derived(
12
13
  manifest.totalPages > 0
13
14
  ? Math.round((progress.visitedPages.size / manifest.totalPages) * 100)
14
- : 0
15
+ : 0,
15
16
  );
16
17
 
17
18
  function toggleSidebar() {
@@ -25,11 +26,25 @@
25
26
  function handleKeyNav(e) {
26
27
  const tag = e.target?.tagName;
27
28
  if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
28
- if (e.target?.closest('[role="radiogroup"], [role="dialog"], .tessera-accordion, .tessera-carousel, .tessera-quiz')) return;
29
+ if (
30
+ e.target?.closest(
31
+ '[role="radiogroup"], [role="dialog"], .tessera-accordion, .tessera-carousel, .tessera-quiz',
32
+ )
33
+ )
34
+ return;
29
35
 
30
- if (e.key === 'ArrowLeft') { nav.goPrev(); e.preventDefault(); }
31
- if (e.key === 'ArrowRight') { nav.goNext(); e.preventDefault(); }
32
- if (e.key === 'Escape' && sidebarOpen) { closeSidebar(); e.preventDefault(); }
36
+ if (e.key === 'ArrowLeft') {
37
+ nav.goPrev();
38
+ e.preventDefault();
39
+ }
40
+ if (e.key === 'ArrowRight') {
41
+ nav.goNext();
42
+ e.preventDefault();
43
+ }
44
+ if (e.key === 'Escape' && sidebarOpen) {
45
+ closeSidebar();
46
+ e.preventDefault();
47
+ }
33
48
  }
34
49
 
35
50
  onMount(() => {
@@ -63,7 +78,11 @@
63
78
  {/if}
64
79
 
65
80
  <div class="tessera-app" data-chrome="default">
66
- <aside class="tessera-sidebar" class:open={sidebarOpen} aria-label="Course sidebar">
81
+ <aside
82
+ class="tessera-sidebar"
83
+ class:open={sidebarOpen}
84
+ aria-label="Course sidebar"
85
+ >
67
86
  <Sidebar
68
87
  {manifest}
69
88
  {config}
@@ -100,11 +119,21 @@
100
119
  </main>
101
120
 
102
121
  <footer class="tessera-progress" aria-label="Course progress">
103
- <div class="tessera-progress-track" role="progressbar"
104
- aria-valuenow={progressPercent} aria-valuemin={0} aria-valuemax={100}
105
- aria-label="Course progress">
106
- <div class="tessera-progress-fill" style="width: {progressPercent}%"></div>
122
+ <div
123
+ class="tessera-progress-track"
124
+ role="progressbar"
125
+ aria-valuenow={progressPercent}
126
+ aria-valuemin={0}
127
+ aria-valuemax={100}
128
+ aria-label="Course progress"
129
+ >
130
+ <div
131
+ class="tessera-progress-fill"
132
+ style="width: {progressPercent}%"
133
+ ></div>
134
+ </div>
135
+ <div class="tessera-progress-label">
136
+ {progress.visitedPages.size} of {manifest.totalPages} pages
107
137
  </div>
108
- <div class="tessera-progress-label">{progress.visitedPages.size} of {manifest.totalPages} pages</div>
109
138
  </footer>
110
139
  </div>
@@ -25,7 +25,7 @@
25
25
  function checkAnswer(userAnswer) {
26
26
  if (!userAnswer || typeof userAnswer !== 'string') return false;
27
27
  const trimmed = userAnswer.trim();
28
- return answers.some(acceptable => {
28
+ return answers.some((acceptable) => {
29
29
  const a = acceptable.trim();
30
30
  if (caseSensitive) return trimmed === a;
31
31
  return trimmed.toLowerCase() === a.toLowerCase();
@@ -33,16 +33,24 @@
33
33
  }
34
34
 
35
35
  const q = useQuestion({
36
- get id() { return id ?? `fitb-${slugFromQuestion(question)}`; },
37
- get weight() { return weight; },
38
- get maxRetries() { return maxRetries; },
36
+ get id() {
37
+ return id ?? `fitb-${slugFromQuestion(question)}`;
38
+ },
39
+ get weight() {
40
+ return weight;
41
+ },
42
+ get maxRetries() {
43
+ return maxRetries;
44
+ },
39
45
  response: () => ({
40
46
  type: 'fill-in',
41
47
  response: inputValue,
42
48
  correct: Array.isArray(answers) ? answers : [answers],
43
49
  caseMatters: !!caseSensitive,
44
50
  }),
45
- reset: () => { inputValue = ''; },
51
+ reset: () => {
52
+ inputValue = '';
53
+ },
46
54
  });
47
55
 
48
56
  // `q.mode` is fixed for the lifetime of the widget; capture once.
@@ -93,7 +101,9 @@
93
101
  <button
94
102
  class="tessera-btn-primary tessera-fitb-check-btn"
95
103
  disabled={!inputValue.trim()}
96
- onclick={() => { q.submit(); }}
104
+ onclick={() => {
105
+ q.submit();
106
+ }}
97
107
  >
98
108
  Check
99
109
  </button>