@memizy/oqse 1.0.0-rc.5 → 1.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +3 -1
- package/dist/checkAnswer.cjs +1 -1
- package/dist/checkAnswer.js +79 -76
- package/dist/compatibility.cjs +1 -1
- package/dist/compatibility.js +26 -24
- package/dist/fileValidation.cjs +2 -2
- package/dist/fileValidation.js +301 -122
- package/dist/headerValidation.cjs +1 -1
- package/dist/headerValidation.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +22 -20
- package/dist/index.js +21 -19
- package/dist/knownProperties.generated.cjs +1 -0
- package/dist/knownProperties.generated.js +218 -0
- package/dist/manifestValidation.cjs +1 -1
- package/dist/manifestValidation.js +42 -52
- package/dist/mustUnderstand.cjs +1 -0
- package/dist/mustUnderstand.js +11 -0
- package/dist/oqse.cjs +1 -1
- package/dist/oqse.js +59 -29
- package/dist/oqseValidation.cjs +1 -1
- package/dist/oqseValidation.js +324 -367
- package/dist/progressValidation.cjs +1 -1
- package/dist/progressValidation.js +23 -14
- package/dist/richTextFields.cjs +1 -1
- package/dist/richTextFields.js +1 -1
- package/dist/src/assets.d.ts +1 -1
- package/dist/src/checkAnswer.d.ts +1 -1
- package/dist/src/compatibility.d.ts +2 -2
- package/dist/src/features.d.ts +1 -1
- package/dist/src/fileValidation.d.ts +36 -6
- package/dist/src/header.d.ts +1 -1
- package/dist/src/headerValidation.d.ts +1 -1
- package/dist/src/index.d.ts +22 -20
- package/dist/src/knownProperties.generated.d.ts +2 -0
- package/dist/src/knownPropertiesSource.d.ts +10 -0
- package/dist/src/manifest.d.ts +10 -10
- package/dist/src/manifestValidation.d.ts +14 -29
- package/dist/src/markdownSet.d.ts +2 -2
- package/dist/src/mustUnderstand.d.ts +15 -0
- package/dist/src/oqse.d.ts +60 -33
- package/dist/src/oqseValidation.d.ts +669 -2356
- package/dist/src/plainText.d.ts +1 -1
- package/dist/src/progress.d.ts +4 -0
- package/dist/src/progressValidation.d.ts +3 -1
- package/dist/src/propertyPaths.d.ts +14 -0
- package/dist/src/richTextFields.d.ts +1 -1
- package/dist/src/richTextProcessor.d.ts +1 -1
- package/dist/src/utils.d.ts +8 -0
- package/dist/utils.cjs +1 -1
- package/dist/utils.js +7 -6
- package/oqse-header.md +7 -0
- package/oqse-manifest.md +16 -7
- package/oqse-progress.md +15 -5
- package/oqse.md +124 -67
- package/package.json +15 -3
- package/schemas/oqse-header-v1.0.json +4 -5
- package/schemas/oqse-manifest-v1.0.json +7 -14
- package/schemas/oqse-progress-v1.0.json +15 -11
- package/schemas/oqse-v1.0.json +501 -746
- package/dist/scripts/fix-types-entry.d.ts +0 -1
- package/dist/scripts/generate-schemas.d.ts +0 -1
- package/dist/src/answers.test.d.ts +0 -1
- package/dist/src/checkAnswer.test.d.ts +0 -1
- package/dist/src/compatibility.test.d.ts +0 -1
- package/dist/src/features.test.d.ts +0 -1
- package/dist/src/fileValidation.test.d.ts +0 -1
- package/dist/src/headerValidation.test.d.ts +0 -1
- package/dist/src/manifestValidation.test.d.ts +0 -1
- package/dist/src/markdownSet.test.d.ts +0 -1
- package/dist/src/oqseValidation.test.d.ts +0 -1
- package/dist/src/plainText.test.d.ts +0 -1
- package/dist/src/progressValidation.test.d.ts +0 -1
- package/dist/src/richTextProcessor.test.d.ts +0 -1
- package/dist/src/specExamples.test.d.ts +0 -5
- package/dist/src/utils.test.d.ts +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog of @memizy/oqse
|
|
2
|
+
|
|
3
|
+
The format itself has its own changelog at the end of [oqse.md](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse.md#version-10-2026-10-14) (OQSE 1.0, 2026-10-14). This file lists the releases of the npm package.
|
|
4
|
+
|
|
5
|
+
## 1.0.0-rc.7 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
Forward compatibility of OQSE 1.0 (see the "Version 1.0" changelog in `oqse.md`), checked by `forwardCompat.test.ts` with files of a "later 1.x version".
|
|
8
|
+
|
|
9
|
+
* **Must-understand properties**: `mustUnderstand` on any item lists properties it depends on; an item that needs a property this version does not know goes to `unsupportedItems` (warning `MUST_UNDERSTAND`), and `checkAnswer` refuses it instead of grading it wrong. `loadOQSEFile` / `safeValidateOQSEFile` / `validateOQSEFile` take `{ understood }` (properties the application supports itself, e.g. its own `x-` ones). `missingMustUnderstand(item, understood?)`; `KNOWN_ITEM_PROPERTIES` (generated from the schemas at build time, so `checkAnswer` needs no Zod). This is how a later 1.x may change how items are checked.
|
|
10
|
+
* Warning `REVEALING_CHOICE_ID` for choice IDs that name the answer (`correct`, `wrong_1`…): learners can see IDs.
|
|
11
|
+
* A set may hold 50,000 items (`MAX_ITEMS`, was 10,000); `MAX_JSON_DEPTH`.
|
|
12
|
+
* Spec: an item is identified by `meta.id` + `item.id`; custom names SHOULD carry the vendor (`x-<vendor>-…`). Zod: `parse` / `safeParse` of the exported schemas are covered by semver, their Zod-specific API is not.
|
|
13
|
+
* Open values: `MediaObject.type`, `subtitles[].kind`, `pedagogy.bloomLevel` / `cognitiveLoad`, `SourceMaterial.kind`, OQSEM `studyMode` / `questionDensity` accept values of later versions (`OpenValueSchema`, warning `UNKNOWN_VALUE`); `MEDIA_TYPES`, `SOURCE_KINDS`, `SUBTITLE_KINDS`, `BLOOM_LEVELS`, `COGNITIVE_LOADS` list the known ones; the type `OpenValue<T>`. Before, a new media type or source type made the whole set unreadable and a new Bloom level removed the question.
|
|
14
|
+
* Items of unknown types: `loadOQSEFile` and `safeValidateOQSEFile` return them unchanged in `unsupportedItems` (a warning, no longer an error in strict validation); `withUnsupportedItems(file, unsupportedItems)` puts them back for saving.
|
|
15
|
+
* `SourceMaterial`: `kind`, `url`, `doi`, `isbn`, `set`, `mimeType` replace `type` + `value` (`SourceMaterialType` / `SourceMaterialTypeSchema` removed; `SourceKind`, `SOURCE_KINDS`); `SourceReference.itemId`. Sets of the release candidates are converted by the repository tool `scripts/migrate-oqse` (the library itself does not carry the old shape: such a source loads as a source cited by its title); every source is validated on its own, so a bad source no longer makes the set unreadable (`INVALID_SOURCE`); `SOURCE_WITHOUT_LOCATOR` warns about a source cited by title only; an `itemId` into this very set must exist.
|
|
16
|
+
* OQSEP: `starred`, `lastAnswer.answer` (≤ 4 KB, `MAX_LAST_ANSWER_BYTES`); all OQSEP schemas keep unknown keys (they used to drop them, and the JSON Schema had `additionalProperties: false`).
|
|
17
|
+
* OQSEM: `capabilities.types` / `actions` accept lowercase names of later versions; unknown keys and asset categories are kept. OQSEH: unknown keys are kept.
|
|
18
|
+
* `checkCompatibility` does not report media of an unknown type as unsupported (they fall back to a link).
|
|
19
|
+
* Tolerant reading of real data (each case made a whole set unreadable before): BCP 47 tags with numeric regions and variants (`es-419`, `de-CH-1996`) and `cs_CZ` converted to `cs-CZ` (warning `LANGUAGE_NORMALIZED`); fractions of a second of any length (`…00.123456Z`, as databases write them) in sets and progress files; subtitles from any URI; an invalid optional `meta` field or one broken entry of `assets`, `tagDefinitions`, `translations`… is removed with an error (`INVALID_META_FIELD`) instead of refusing the set; the nesting limit is 32 levels for the whole file (`MAX_JSON_DEPTH`; it was 10 from the root, so a moderately deep `appSpecific` broke the set).
|
|
20
|
+
* OQSEP record keys are item IDs, not only UUIDs (generated items).
|
|
21
|
+
* The specification fixes for 1.x what older applications could not handle growing: size limits and ranges of scales, the HTML tags allowed without `html`, the formats of IDs, dates and language tags.
|
|
22
|
+
|
|
23
|
+
## 1.0.0-rc.6 (2026-10-14)
|
|
24
|
+
|
|
25
|
+
Release candidate of OQSE 1.0; the format is frozen (1.x only adds).
|
|
26
|
+
|
|
27
|
+
* `HotspotObject`: the shape fields (`x`, `y`, `width`, `height`, `radius`, `points`, `targetName`) are marked required in the specification, as the validators always treated them.
|
|
28
|
+
* New section "Canonical and Authoring Formats" in `oqse.md`: the JSON file is the canonical form that applications store and exchange; authoring formats (OQSE Markdown for notes, tool-specific shorthands) are only inputs converted to it, and item IDs survive edits.
|
|
29
|
+
* Aligned before 1.0: `math-input` has `correctAnswers` (a list, like `short-answer`) instead of `correctAnswer` + `alternativeAnswers`, and no `tolerance` (answers are compared as LaTeX; numbers are `numeric-input`); `match-complex` has `prompts` / `matches` (like `match-pairs`) instead of `leftItems` / `rightItems`. `checkAnswer` and the JSON Schemas follow.
|
|
30
|
+
* The sub-specifications (`oqse-manifest.md`, `oqse-progress.md`, `oqse-header.md`) point to the changelog in `oqse.md`.
|
|
31
|
+
|
|
32
|
+
## 1.0.0-rc.5
|
|
33
|
+
|
|
34
|
+
OQSE 1.0: choices with IDs, answers by ID, `skills`, Answer Formats and `checkAnswer`, typed-answer comparison, timeline rules, feature detection, version rules (0.x drafts are refused) and the JSON Schemas at `https://cdn.jsdelivr.net/npm/@memizy/oqse@1/schemas/`. See the "Version 1.0" changelog in `oqse.md`.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript SDK for the Open Quiz & Study Exchange ecosystem, published as @memizy/oqse.
|
|
4
4
|
|
|
5
|
-
🚀 **[OQSE Web Validator](https://memizy.github.io/memizy/)** - Online tool for easy testing and debugging of OQSE JSON structures in the browser (source: [`docs/`](
|
|
5
|
+
🚀 **[OQSE Web Validator](https://memizy.github.io/memizy/)** - Online tool for easy testing and debugging of OQSE JSON structures in the browser (source: [`docs/`](https://github.com/memizy/memizy/blob/main/packages/oqse/docs/), published by GitHub Pages).
|
|
6
6
|
|
|
7
7
|
This repository contains:
|
|
8
8
|
- The human-readable specifications split into four standalone documents:
|
|
@@ -36,6 +36,8 @@ const { success, data, errors, warnings } = loadOQSEFile(JSON.parse(text));
|
|
|
36
36
|
|
|
37
37
|
## Schemas
|
|
38
38
|
|
|
39
|
+
The stable way to validate are the functions (`loadOQSEFile`, `safeValidateOQSEFile`, …) and the JSON Schemas. The exported `*Schema` objects are Zod schemas: their `parse` / `safeParse` behaviour is covered by semver, their Zod-specific API (`.extend`, `.shape`, the Zod major version) is not – a 1.x release may move to a newer Zod (noted in the changelog).
|
|
40
|
+
|
|
39
41
|
The auto-generated JSON Schemas are published with this repository and are available at the following URLs:
|
|
40
42
|
|
|
41
43
|
- Core OQSE Schema: https://cdn.jsdelivr.net/npm/@memizy/oqse@1/schemas/oqse-v1.0.json
|
package/dist/checkAnswer.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require("./answers.cjs");function
|
|
1
|
+
const e=require("./answers.cjs"),t=require("./mustUnderstand.cjs");function n(n,m){if(!n||typeof n!=`object`)throw Error(`checkAnswer: missing item`);let h=t.missingMustUnderstand(n);if(h.length>0)throw Error(`checkAnswer: the item needs properties this version cannot check (${h.join(`, `)}).`);switch(n.type){case`mcq-single`:return m===n.correctId;case`mcq-multi`:{let e=a(m);return e!==null&&l(e,n.correctIds)}case`true-false`:return i(m)===n.correctAnswer;case`short-answer`:{let t=m;return typeof t==`string`&&n.correctAnswers.some(r=>e.textAnswerMatches(t,r,n))}case`numeric-input`:{let e=r(m);return e!==null&&Math.abs(e-n.correctAnswer)<=(n.tolerance??0)+1e-9}case`slider`:{let e=r(m);return e!==null&&Math.abs(e-n.correctAnswer)<=(n.tolerance??0)+1e-9}case`math-input`:{if(typeof m!=`string`)return!1;let e=u(m);return n.correctAnswers.some(t=>u(t)===e)}case`sort-items`:{let e=a(m);return e!==null&&e.length===n.correctOrder.length&&e.every((e,t)=>e===n.correctOrder[t])}case`match-pairs`:{let e=c(m);return!!e&&n.prompts.every(t=>e[t.id]===n.pairs[t.id])}case`match-complex`:{let e=o(m);if(!e)return!1;let t=new Set(n.connections.map(([e,t])=>s(e,t))),r=new Set(e.map(([e,t])=>s(e,t))),i=[...r].filter(e=>t.has(e)).length;return i===r.size&&i>=(n.minCorrect??t.size)}case`matrix`:{let e=o(m);return e?l(e.map(([e,t])=>s(e,t)),n.correctCells.map(([e,t])=>s(e,t))):!1}case`fill-in-blanks`:{let t=c(m);return t?Object.entries(n.blanks).every(([r,i])=>{let a=t[r];return typeof a==`string`&&i.some(t=>e.textAnswerMatches(a,t,n))}):!1}case`fill-in-select`:{let e=c(m);return!!e&&Object.entries(n.blanks).every(([t,n])=>e[t]===n.correctId)}case`categorize`:{let e=c(m);return!!e&&n.items.every(t=>e[t.id]===t.correctCategoryId)}case`timeline`:{let t=a(m);if(!t||t.length!==n.events.length||new Set(t).size!==t.length)return!1;let r=t.map(e=>n.events.find(t=>t.id===e));return r.every(e=>!!e)&&e.isTimelineOrderCorrect(r)}case`pin-on-image`:{let e=(Array.isArray(m)?m:[m]).map(f);if(e.length===0||e.some(e=>e===null))return!1;if(!n.multipleCorrect)return e.length===1&&n.hotspots.some(t=>p(e[0],t));let t=new Set;for(let r of e){let e=n.hotspots.findIndex(e=>p(r,e));if(e===-1)return!1;t.add(e)}return t.size>=(n.minCorrect??n.hotspots.length)}case`diagram-label`:{let t=c(m);return t?n.zones.every(r=>{let i=t[r.id];if(n.requireTyping){let t=n.labels.find(e=>e.id===r.correctLabelId);return!!t&&typeof i==`string`&&e.textAnswerMatches(i,t.text,n)}return i===r.correctLabelId}):!1}case`pin-on-model`:{let e=(Array.isArray(m)?m:[m]).filter(e=>typeof e==`string`);if(e.length===0)return!1;let t=e=>n.hotspots.findIndex(t=>e.toLowerCase().includes(t.targetName.toLowerCase()));if(!n.multipleCorrect)return e.length===1&&t(e[0])!==-1;let r=new Set;for(let n of e){let e=t(n);if(e===-1)return!1;r.add(e)}return r.size>=(n.minCorrect??n.hotspots.length)}case`chess-puzzle`:{if(!Array.isArray(m)||m.some(e=>typeof e!=`string`))return!1;let e=m.map(d);return n.correctAnswers.some(t=>t.length===e.length&&t.every((t,n)=>d(t)===e[n]))}case`flashcard`:{let e=r(m);return e===3||e===4}case`note`:case`open-ended`:throw Error(`checkAnswer: "${n.type}" items cannot be checked automatically.`);default:throw Error(`checkAnswer: unsupported item type "${n.type}".`)}}function r(e){if(typeof e==`number`)return Number.isFinite(e)?e:null;if(typeof e!=`string`)return null;let t=e.trim().replace(/\s+/g,``).replace(`,`,`.`);return/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t)?Number(t):null}function i(e){return typeof e==`boolean`?e:e===`true`||e!==`false`&&null}function a(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)?e:null}function o(e){return Array.isArray(e)&&e.every(e=>Array.isArray(e)&&e.length===2&&typeof e[0]==`string`&&typeof e[1]==`string`)?e:null}var s=(e,t)=>`${e}\u0000${t}`;function c(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:null}function l(e,t){let n=new Set(e),r=new Set(t);return n.size===e.length&&n.size===r.size&&[...n].every(e=>r.has(e))}function u(e){return e.trim().replace(/^\$\$?|\$\$?$/g,``).replace(/\s+/g,``)}function d(e){return e.trim().replace(/[+#!?]+$/g,``).replace(/^0-0(-0)?$/,e=>e.replace(/0/g,`O`)).replace(/^([a-h](?:x[a-h])?[18])([QRBN])$/,`$1=$2`)}function f(e){let t=c(e);return t&&typeof t.x==`number`&&typeof t.y==`number`?{x:t.x,y:t.y}:null}function p(e,t){switch(t.type){case`rect`:return e.x>=t.x&&e.x<=t.x+t.width&&e.y>=t.y&&e.y<=t.y+t.height;case`circle`:return(e.x-t.x)**2+(e.y-t.y)**2<=t.radius**2;case`polygon`:{let n=!1,r=t.points;for(let t=0,i=r.length-1;t<r.length;i=t++)r[t].y>e.y!=r[i].y>e.y&&e.x<(r[i].x-r[t].x)*(e.y-r[t].y)/(r[i].y-r[t].y)+r[t].x&&(n=!n);return n}}}exports.checkAnswer=n;
|
package/dist/checkAnswer.js
CHANGED
|
@@ -1,157 +1,160 @@
|
|
|
1
1
|
import { isTimelineOrderCorrect as e, textAnswerMatches as t } from "./answers.js";
|
|
2
|
+
import { missingMustUnderstand as n } from "./mustUnderstand.js";
|
|
2
3
|
//#region src/checkAnswer.ts
|
|
3
|
-
function
|
|
4
|
-
if (!
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
function r(r, h) {
|
|
5
|
+
if (!r || typeof r != "object") throw Error("checkAnswer: missing item");
|
|
6
|
+
let g = n(r);
|
|
7
|
+
if (g.length > 0) throw Error(`checkAnswer: the item needs properties this version cannot check (${g.join(", ")}).`);
|
|
8
|
+
switch (r.type) {
|
|
9
|
+
case "mcq-single": return h === r.correctId;
|
|
7
10
|
case "mcq-multi": {
|
|
8
|
-
let e =
|
|
9
|
-
return e !== null &&
|
|
11
|
+
let e = o(h);
|
|
12
|
+
return e !== null && u(e, r.correctIds);
|
|
10
13
|
}
|
|
11
|
-
case "true-false": return
|
|
14
|
+
case "true-false": return a(h) === r.correctAnswer;
|
|
12
15
|
case "short-answer": {
|
|
13
|
-
let e =
|
|
14
|
-
return typeof e == "string" &&
|
|
16
|
+
let e = h;
|
|
17
|
+
return typeof e == "string" && r.correctAnswers.some((n) => t(e, n, r));
|
|
15
18
|
}
|
|
16
19
|
case "numeric-input": {
|
|
17
|
-
let e =
|
|
18
|
-
return e !== null && Math.abs(e -
|
|
20
|
+
let e = i(h);
|
|
21
|
+
return e !== null && Math.abs(e - r.correctAnswer) <= (r.tolerance ?? 0) + 1e-9;
|
|
19
22
|
}
|
|
20
23
|
case "slider": {
|
|
21
|
-
let e =
|
|
22
|
-
return e !== null && Math.abs(e -
|
|
24
|
+
let e = i(h);
|
|
25
|
+
return e !== null && Math.abs(e - r.correctAnswer) <= (r.tolerance ?? 0) + 1e-9;
|
|
23
26
|
}
|
|
24
27
|
case "math-input": {
|
|
25
|
-
if (typeof
|
|
26
|
-
let e =
|
|
27
|
-
return
|
|
28
|
+
if (typeof h != "string") return !1;
|
|
29
|
+
let e = d(h);
|
|
30
|
+
return r.correctAnswers.some((t) => d(t) === e);
|
|
28
31
|
}
|
|
29
32
|
case "sort-items": {
|
|
30
|
-
let e =
|
|
31
|
-
return e !== null && e.length ===
|
|
33
|
+
let e = o(h);
|
|
34
|
+
return e !== null && e.length === r.correctOrder.length && e.every((e, t) => e === r.correctOrder[t]);
|
|
32
35
|
}
|
|
33
36
|
case "match-pairs": {
|
|
34
|
-
let e =
|
|
35
|
-
return !!e &&
|
|
37
|
+
let e = l(h);
|
|
38
|
+
return !!e && r.prompts.every((t) => e[t.id] === r.pairs[t.id]);
|
|
36
39
|
}
|
|
37
40
|
case "match-complex": {
|
|
38
|
-
let e =
|
|
41
|
+
let e = s(h);
|
|
39
42
|
if (!e) return !1;
|
|
40
|
-
let t = new Set(
|
|
41
|
-
return i ===
|
|
43
|
+
let t = new Set(r.connections.map(([e, t]) => c(e, t))), n = new Set(e.map(([e, t]) => c(e, t))), i = [...n].filter((e) => t.has(e)).length;
|
|
44
|
+
return i === n.size && i >= (r.minCorrect ?? t.size);
|
|
42
45
|
}
|
|
43
46
|
case "matrix": {
|
|
44
|
-
let e =
|
|
45
|
-
return e ?
|
|
47
|
+
let e = s(h);
|
|
48
|
+
return e ? u(e.map(([e, t]) => c(e, t)), r.correctCells.map(([e, t]) => c(e, t))) : !1;
|
|
46
49
|
}
|
|
47
50
|
case "fill-in-blanks": {
|
|
48
|
-
let e =
|
|
49
|
-
return e ? Object.entries(
|
|
50
|
-
let a = e[
|
|
51
|
-
return typeof a == "string" && i.some((e) => t(a, e,
|
|
51
|
+
let e = l(h);
|
|
52
|
+
return e ? Object.entries(r.blanks).every(([n, i]) => {
|
|
53
|
+
let a = e[n];
|
|
54
|
+
return typeof a == "string" && i.some((e) => t(a, e, r));
|
|
52
55
|
}) : !1;
|
|
53
56
|
}
|
|
54
57
|
case "fill-in-select": {
|
|
55
|
-
let e =
|
|
56
|
-
return !!e && Object.entries(
|
|
58
|
+
let e = l(h);
|
|
59
|
+
return !!e && Object.entries(r.blanks).every(([t, n]) => e[t] === n.correctId);
|
|
57
60
|
}
|
|
58
61
|
case "categorize": {
|
|
59
|
-
let e =
|
|
60
|
-
return !!e &&
|
|
62
|
+
let e = l(h);
|
|
63
|
+
return !!e && r.items.every((t) => e[t.id] === t.correctCategoryId);
|
|
61
64
|
}
|
|
62
65
|
case "timeline": {
|
|
63
|
-
let t =
|
|
64
|
-
if (!t || t.length !==
|
|
65
|
-
let
|
|
66
|
-
return
|
|
66
|
+
let t = o(h);
|
|
67
|
+
if (!t || t.length !== r.events.length || new Set(t).size !== t.length) return !1;
|
|
68
|
+
let n = t.map((e) => r.events.find((t) => t.id === e));
|
|
69
|
+
return n.every((e) => !!e) && e(n);
|
|
67
70
|
}
|
|
68
71
|
case "pin-on-image": {
|
|
69
|
-
let e = (Array.isArray(
|
|
72
|
+
let e = (Array.isArray(h) ? h : [h]).map(p);
|
|
70
73
|
if (e.length === 0 || e.some((e) => e === null)) return !1;
|
|
71
|
-
if (!
|
|
74
|
+
if (!r.multipleCorrect) return e.length === 1 && r.hotspots.some((t) => m(e[0], t));
|
|
72
75
|
let t = /* @__PURE__ */ new Set();
|
|
73
|
-
for (let
|
|
74
|
-
let e =
|
|
76
|
+
for (let n of e) {
|
|
77
|
+
let e = r.hotspots.findIndex((e) => m(n, e));
|
|
75
78
|
if (e === -1) return !1;
|
|
76
79
|
t.add(e);
|
|
77
80
|
}
|
|
78
|
-
return t.size >= (
|
|
81
|
+
return t.size >= (r.minCorrect ?? r.hotspots.length);
|
|
79
82
|
}
|
|
80
83
|
case "diagram-label": {
|
|
81
|
-
let e =
|
|
82
|
-
return e ?
|
|
83
|
-
let i = e[
|
|
84
|
-
if (
|
|
85
|
-
let e =
|
|
86
|
-
return !!e && typeof i == "string" && t(i, e.text,
|
|
84
|
+
let e = l(h);
|
|
85
|
+
return e ? r.zones.every((n) => {
|
|
86
|
+
let i = e[n.id];
|
|
87
|
+
if (r.requireTyping) {
|
|
88
|
+
let e = r.labels.find((e) => e.id === n.correctLabelId);
|
|
89
|
+
return !!e && typeof i == "string" && t(i, e.text, r);
|
|
87
90
|
}
|
|
88
|
-
return i ===
|
|
91
|
+
return i === n.correctLabelId;
|
|
89
92
|
}) : !1;
|
|
90
93
|
}
|
|
91
94
|
case "pin-on-model": {
|
|
92
|
-
let e = (Array.isArray(
|
|
95
|
+
let e = (Array.isArray(h) ? h : [h]).filter((e) => typeof e == "string");
|
|
93
96
|
if (e.length === 0) return !1;
|
|
94
|
-
let t = (e) =>
|
|
95
|
-
if (!
|
|
96
|
-
let
|
|
97
|
-
for (let
|
|
98
|
-
let e = t(
|
|
97
|
+
let t = (e) => r.hotspots.findIndex((t) => e.toLowerCase().includes(t.targetName.toLowerCase()));
|
|
98
|
+
if (!r.multipleCorrect) return e.length === 1 && t(e[0]) !== -1;
|
|
99
|
+
let n = /* @__PURE__ */ new Set();
|
|
100
|
+
for (let r of e) {
|
|
101
|
+
let e = t(r);
|
|
99
102
|
if (e === -1) return !1;
|
|
100
|
-
|
|
103
|
+
n.add(e);
|
|
101
104
|
}
|
|
102
|
-
return
|
|
105
|
+
return n.size >= (r.minCorrect ?? r.hotspots.length);
|
|
103
106
|
}
|
|
104
107
|
case "chess-puzzle": {
|
|
105
|
-
if (!Array.isArray(
|
|
106
|
-
let e =
|
|
107
|
-
return
|
|
108
|
+
if (!Array.isArray(h) || h.some((e) => typeof e != "string")) return !1;
|
|
109
|
+
let e = h.map(f);
|
|
110
|
+
return r.correctAnswers.some((t) => t.length === e.length && t.every((t, n) => f(t) === e[n]));
|
|
108
111
|
}
|
|
109
112
|
case "flashcard": {
|
|
110
|
-
let e =
|
|
113
|
+
let e = i(h);
|
|
111
114
|
return e === 3 || e === 4;
|
|
112
115
|
}
|
|
113
116
|
case "note":
|
|
114
|
-
case "open-ended": throw Error(`checkAnswer: "${
|
|
115
|
-
default: throw Error(`checkAnswer: unsupported item type "${
|
|
117
|
+
case "open-ended": throw Error(`checkAnswer: "${r.type}" items cannot be checked automatically.`);
|
|
118
|
+
default: throw Error(`checkAnswer: unsupported item type "${r.type}".`);
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
|
-
function
|
|
121
|
+
function i(e) {
|
|
119
122
|
if (typeof e == "number") return Number.isFinite(e) ? e : null;
|
|
120
123
|
if (typeof e != "string") return null;
|
|
121
124
|
let t = e.trim().replace(/\s+/g, "").replace(",", ".");
|
|
122
125
|
return /^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t) ? Number(t) : null;
|
|
123
126
|
}
|
|
124
|
-
function
|
|
127
|
+
function a(e) {
|
|
125
128
|
return typeof e == "boolean" ? e : e === "true" || e !== "false" && null;
|
|
126
129
|
}
|
|
127
|
-
function
|
|
130
|
+
function o(e) {
|
|
128
131
|
return Array.isArray(e) && e.every((e) => typeof e == "string") ? e : null;
|
|
129
132
|
}
|
|
130
|
-
function
|
|
133
|
+
function s(e) {
|
|
131
134
|
return Array.isArray(e) && e.every((e) => Array.isArray(e) && e.length === 2 && typeof e[0] == "string" && typeof e[1] == "string") ? e : null;
|
|
132
135
|
}
|
|
133
|
-
var
|
|
134
|
-
function
|
|
136
|
+
var c = (e, t) => `${e}\u0000${t}`;
|
|
137
|
+
function l(e) {
|
|
135
138
|
return e && typeof e == "object" && !Array.isArray(e) ? e : null;
|
|
136
139
|
}
|
|
137
|
-
function
|
|
140
|
+
function u(e, t) {
|
|
138
141
|
let n = new Set(e), r = new Set(t);
|
|
139
142
|
return n.size === e.length && n.size === r.size && [...n].every((e) => r.has(e));
|
|
140
143
|
}
|
|
141
|
-
function
|
|
144
|
+
function d(e) {
|
|
142
145
|
return e.trim().replace(/^\$\$?|\$\$?$/g, "").replace(/\s+/g, "");
|
|
143
146
|
}
|
|
144
|
-
function
|
|
147
|
+
function f(e) {
|
|
145
148
|
return e.trim().replace(/[+#!?]+$/g, "").replace(/^0-0(-0)?$/, (e) => e.replace(/0/g, "O")).replace(/^([a-h](?:x[a-h])?[18])([QRBN])$/, "$1=$2");
|
|
146
149
|
}
|
|
147
|
-
function
|
|
148
|
-
let t =
|
|
150
|
+
function p(e) {
|
|
151
|
+
let t = l(e);
|
|
149
152
|
return t && typeof t.x == "number" && typeof t.y == "number" ? {
|
|
150
153
|
x: t.x,
|
|
151
154
|
y: t.y
|
|
152
155
|
} : null;
|
|
153
156
|
}
|
|
154
|
-
function
|
|
157
|
+
function m(e, t) {
|
|
155
158
|
switch (t.type) {
|
|
156
159
|
case "rect": return e.x >= t.x && e.x <= t.x + t.width && e.y >= t.y && e.y <= t.y + t.height;
|
|
157
160
|
case "circle": return (e.x - t.x) ** 2 + (e.y - t.y) ** 2 <= t.radius ** 2;
|
|
@@ -163,4 +166,4 @@ function p(e, t) {
|
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
//#endregion
|
|
166
|
-
export {
|
|
169
|
+
export { r as checkAnswer };
|
package/dist/compatibility.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require("./
|
|
1
|
+
const e=require("./oqse.cjs"),t=require("./manifest.cjs"),n=require("./features.cjs");function r(r,i){let a=t.withManifestDefaults(i).capabilities,o=(e,t)=>!!e&&e.length>0&&(e.includes(`*`)||e.includes(t)),s=[...new Set(r.items.map(e=>e.type))].filter(e=>!o(a.types,e)),c=[],l=[],u=(t,n)=>{for(let[r,i]of Object.entries(t??{})){if(!e.MEDIA_TYPES.includes(i.type))continue;let t=a.assets?.[i.type],o={path:`${n}.${r}`,type:i.type,mimeType:i.mimeType};if(!t||t.length===0)c.push(o);else if(t.includes(`*`))continue;else i.mimeType?t.includes(i.mimeType)||c.push(o):l.push(o)}};u(r.meta.assets,`meta.assets`),r.items.forEach((e,t)=>u(e.assets,`items[${t}].assets`));let d=r.meta.requirements,f=(e,t)=>(e??[]).filter(e=>!(t??[]).includes(e)),p=f(n.effectiveFeatures(r),a.features),m=f(d?.latexPackages,a.latexPackages),h=[...f(d?.itemProperties,a.itemProperties),...f(d?.metaProperties,a.metaProperties)],g=t.isVersionCompatible(r.version,i),_=g&&s.length===0&&c.length===0;return{level:_?p.length>0||m.length>0?`simplified`:`full`:`incompatible`,compatible:_,versionCompatible:g,unsupportedTypes:s,unsupportedAssets:c,unverifiedAssets:l,missingFeatures:p,missingLatexPackages:m,missingProperties:h}}exports.checkCompatibility=r;
|
package/dist/compatibility.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { MEDIA_TYPES as e } from "./oqse.js";
|
|
2
|
+
import { isVersionCompatible as t, withManifestDefaults as n } from "./manifest.js";
|
|
3
|
+
import { effectiveFeatures as r } from "./features.js";
|
|
3
4
|
//#region src/compatibility.ts
|
|
4
|
-
function
|
|
5
|
-
let
|
|
6
|
-
for (let [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
function i(i, a) {
|
|
6
|
+
let o = n(a).capabilities, s = (e, t) => !!e && e.length > 0 && (e.includes("*") || e.includes(t)), c = [...new Set(i.items.map((e) => e.type))].filter((e) => !s(o.types, e)), l = [], u = [], d = (t, n) => {
|
|
7
|
+
for (let [r, i] of Object.entries(t ?? {})) {
|
|
8
|
+
if (!e.includes(i.type)) continue;
|
|
9
|
+
let t = o.assets?.[i.type], a = {
|
|
10
|
+
path: `${n}.${r}`,
|
|
11
|
+
type: i.type,
|
|
12
|
+
mimeType: i.mimeType
|
|
11
13
|
};
|
|
12
|
-
if (!
|
|
13
|
-
else if (
|
|
14
|
-
else
|
|
14
|
+
if (!t || t.length === 0) l.push(a);
|
|
15
|
+
else if (t.includes("*")) continue;
|
|
16
|
+
else i.mimeType ? t.includes(i.mimeType) || l.push(a) : u.push(a);
|
|
15
17
|
}
|
|
16
18
|
};
|
|
17
|
-
|
|
18
|
-
let
|
|
19
|
+
d(i.meta.assets, "meta.assets"), i.items.forEach((e, t) => d(e.assets, `items[${t}].assets`));
|
|
20
|
+
let f = i.meta.requirements, p = (e, t) => (e ?? []).filter((e) => !(t ?? []).includes(e)), m = p(r(i), o.features), h = p(f?.latexPackages, o.latexPackages), g = [...p(f?.itemProperties, o.itemProperties), ...p(f?.metaProperties, o.metaProperties)], _ = t(i.version, a), v = _ && c.length === 0 && l.length === 0;
|
|
19
21
|
return {
|
|
20
|
-
level:
|
|
21
|
-
compatible:
|
|
22
|
-
versionCompatible:
|
|
23
|
-
unsupportedTypes:
|
|
24
|
-
unsupportedAssets:
|
|
25
|
-
unverifiedAssets:
|
|
26
|
-
missingFeatures:
|
|
27
|
-
missingLatexPackages:
|
|
28
|
-
missingProperties:
|
|
22
|
+
level: v ? m.length > 0 || h.length > 0 ? "simplified" : "full" : "incompatible",
|
|
23
|
+
compatible: v,
|
|
24
|
+
versionCompatible: _,
|
|
25
|
+
unsupportedTypes: c,
|
|
26
|
+
unsupportedAssets: l,
|
|
27
|
+
unverifiedAssets: u,
|
|
28
|
+
missingFeatures: m,
|
|
29
|
+
missingLatexPackages: h,
|
|
30
|
+
missingProperties: g
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
//#endregion
|
|
32
|
-
export {
|
|
34
|
+
export { i as checkCompatibility };
|
package/dist/fileValidation.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./oqse.cjs"),t=require("./utils.cjs"),n=require("./oqseValidation.cjs"),r=require("./manifest.cjs"),i=require("./markdownUtils.cjs"),a=require("./richTextProcessor.cjs"),o=require("./
|
|
2
|
-
`)}`),this.name=`OQSEValidationError`,this.errors=e,this.warnings=t}};function u(e){return e.map(e=>e.path?`${e.path}: ${e.message}`:e.message)}var d=1,f=`1.0`;function p(e){return v(e,`load`)}function m(e){let t=v(e,`strict`),n=t.success&&t.errors.length===0;return{success:n,data:n?t.data:void 0,errors:t.errors,warnings:t.warnings}}function h(e){let t=m(e);if(!t.success)throw new l(t.errors,t.warnings);return t.data}var g=new Set(e.OFFICIAL_ITEM_TYPES),_=class{errors=[];warnings=[];add(e,n,r,i,a){let o={severity:e,code:n,message:r,path:t.formatPath(i)};a&&(o.itemId=a),(e===`error`?this.errors:this.warnings).push(o)}addZod(e,t,n,r){for(let i of e.issues)this.add(`error`,t,i.message,[...n,...i.path],r)}};function v(e,i){let o=new _,l=()=>({success:!1,errors:o.errors,warnings:o.warnings}),u=i===`strict`?`error`:`warning`;try{t.validateJsonDepth(e,10)}catch(e){return o.add(`error`,`SECURITY_LIMIT`,e.message,[]),l()}if(!w(e)||!w(e.meta)||!Array.isArray(e.items))return o.add(`error`,`INVALID_ROOT`,`An OQSE file must be an object with "version", "meta" and "items" (array).`,[]),l();let d=structuredClone(e);if(d.$schema!==void 0&&typeof d.$schema!=`string`)return o.add(`error`,`INVALID_ROOT`,`"$schema" must be a string URL.`,[`$schema`]),l();if(typeof d.version!=`string`||!/^\d+\.\d+$/.test(d.version))return o.add(`error`,`INVALID_VERSION`,`Version must be in MAJOR.MINOR format (e.g. "1.0").`,[`version`]),l();let f=Number(d.version.split(`.`)[0]);if(f===0)return o.add(`error`,`UNSUPPORTED_VERSION`,`OQSE ${d.version} is a pre-release version; this library reads OQSE 1.x (convert the set to 1.0).`,[`version`]),l();if(f!==1&&o.add(`warning`,`UNSUPPORTED_VERSION`,`Unsupported MAJOR version ${d.version}; loading in best-effort mode.`,[`version`]),d.items.length>1e4)return o.add(`error`,`TOO_MANY_ITEMS`,`Maximum 10000 items per set.`,[`items`]),l();if(i===`load`&&!C(d.meta,[`meta`,`assets`],o))return l();let p=n.OQSEMetaSchema.safeParse(d.meta);if(!p.success)return o.addZod(p.error,`INVALID_META`,[`meta`]),l();let m=p.data,h={features:r.OFFICIAL_FEATURE_KEYS,itemProperties:r.OFFICIAL_ITEM_PROPERTIES,metaProperties:r.OFFICIAL_META_PROPERTIES};for(let[e,t]of Object.entries(h))for(let n of m.requirements?.[e]??[])!n.startsWith(`x-`)&&!t.includes(n)&&o.add(`warning`,`UNKNOWN_FEATURE`,`Unknown ${e===`features`?`feature`:`property`} "${n}" in requirements.${e} (ignored).`,[`meta`,`requirements`,e]);let v=new Set(m.requirements?.features??[]),x=v.has(`html`),S=v.has(`latex`);b(m.assets,[`meta`,`assets`],o,i),m.thumbnail&&!m.assets?.[m.thumbnail]&&(o.add(u,`MISSING_THUMBNAIL_ASSET`,`Thumbnail references non-existent asset "${m.thumbnail}".`,[`meta`,`thumbnail`]),i===`load`&&delete m.thumbnail);for(let[e,t]of s.metaRichText(m)){let n=x?null:a.findRawHtml(t,{latex:S});n&&(o.add(u,`RAW_HTML_NOT_ALLOWED`,T(n),e),i===`load`&&E(m,e.slice(1)))}let D=new Set([m.id]),O=[];d.items.forEach((e,t)=>{let r=[`items`,t],a=w(e)&&typeof e.id==`string`?e.id:void 0,s=w(e)?e.type:void 0;if(typeof s==`string`&&!s.startsWith(`x-`)&&!g.has(s)){o.add(u,`UNKNOWN_ITEM_TYPE`,`Unknown item type "${s}" (custom types must use the "x-" prefix).`,[...r,`type`],a);return}if(i===`load`&&w(e)&&!C(e,[...r,`assets`],o,a))return;let c=n.getItemSchema(e).safeParse(e);if(!c.success){o.addZod(c.error,`INVALID_ITEM`,r,a);return}let l=c.data;if(D.has(l.id)){o.add(`error`,`DUPLICATE_ID`,`Duplicate ID "${l.id}" (IDs must be unique within the set, including meta.id).`,[...r,`id`],l.id);return}D.add(l.id),b(l.assets,[...r,`assets`],o,i,l.id),y(l,r,m,x,S,o,u)&&O.push({item:l,index:t})});let k=O.map(e=>e.item),A=new Set(k.map(e=>e.id)),j=new Set((m.sourceMaterials??[]).map(e=>e.id));for(let{item:e,index:t}of O){let n=[`items`,t];for(let t of[`relatedItems`,`dependencyItems`]){let r=e[t];if(!r)continue;let a=r.filter(t=>t!==e.id&&A.has(t));a.length!==r.length&&(o.add(u,`DANGLING_REFERENCE`,`${t} contains IDs that do not exist in the set or reference the item itself.`,[...n,t],e.id),i===`load`&&(e[t]=a))}if(e.sources){let t=e.sources.filter(e=>j.has(e.id));t.length!==e.sources.length&&(o.add(u,`DANGLING_REFERENCE`,`sources reference source materials that do not exist in meta.sourceMaterials.`,[...n,`sources`],e.id),i===`load`&&(e.sources=t))}}for(let e of c.detectFeatures({meta:m,items:k}))e!==`html`&&!v.has(e)&&o.add(`warning`,`UNDECLARED_FEATURE`,`The content uses "${e}", but meta.requirements.features does not declare it.`,[`meta`,`requirements`,`features`]);if(!S)for(let{item:e,index:t}of O){let n=s.itemRichText(e).find(([,e])=>c.looksLikeLatex(e));n&&o.add(`warning`,`LOOKS_LIKE_LATEX`,`The text looks like a LaTeX formula; if it is one, declare the "latex" feature (otherwise $ is an ordinary character).`,[`items`,t,...n[0]],e.id)}return{success:!0,data:{...d,meta:m,items:k},errors:o.errors,warnings:o.warnings}}function y(e,t,n,r,c,l,u){let d=!0;if(!r)for(let[n,r]of s.itemRichText(e)){let i=a.findRawHtml(r,{latex:c});i&&(l.add(`error`,`RAW_HTML_NOT_ALLOWED`,T(i),[...t,...n],e.id),d=!1)}if(`targetAsset`in e&&typeof e.targetAsset==`string`&&!e.type.startsWith(`x-`)){let r=o.resolveAsset(e.targetAsset,e,n),i=e.type===`pin-on-model`?`model`:`image`;r?r.type!==i&&(l.add(`error`,`INVALID_TARGET_ASSET`,`targetAsset "${e.targetAsset}" must be of type "${i}".`,[...t,`targetAsset`],e.id),d=!1):(l.add(`error`,`MISSING_TARGET_ASSET`,`targetAsset "${e.targetAsset}" does not exist in item.assets or meta.assets.`,[...t,`targetAsset`],e.id),d=!1)}let f=e.type===`timeline`?e.events:e.type===`categorize`?e.items:void 0;if(f){let n=f.map(e=>e.id);new Set(n).size!==n.length&&(l.add(`error`,`DUPLICATE_ID`,`Internal IDs must be unique within the item.`,[...t,e.type===`timeline`?`events`:`items`],e.id),d=!1)}if(e.type===`chess-puzzle`&&e.correctAnswers.some(e=>e.length%2==0)&&l.add(`warning`,`CHESS_SEQUENCE_LENGTH`,`Each move sequence should end with the learner's move (an odd number of moves; the application plays the replies).`,[...t,`correctAnswers`],e.id),e.type===`note`)for(let n of[`content`,`hiddenContent`]){let r=e[n];r&&i.findHeadings(i.splitLines(r)).some(e=>e.level===1)&&l.add(`warning`,`NOTE_HEADING_LEVEL`,`Headings inside a note should start at level 2 (##); the note title is level 1.`,[...t,n],e.id)}let p=e.type===`fill-in-blanks`||e.type===`fill-in-select`;for(let[n,r]of s.itemRichText(e)){for(let i of x(r)){if(/^https:\/\//i.test(i))continue;let r=/^http:\/\//i.test(i);l.add(r?`warning`:u,`INVALID_IMAGE_URL`,r?`Image "${i}" uses http:// (prefer https://).`:`Image "${i}" needs an absolute https:// URL (use <asset:key /> for other media).`,[...t,...n],e.id)}!(p&&n.length===1&&n[0]===`text`)&&/<blank:[a-zA-Z0-9_-]+\s*\/>/i.test(r)&&l.add(`warning`,`BLANK_OUTSIDE_TEXT`,`Blank tags belong only in the text of fill-in-blanks and fill-in-select (shown here as an empty line).`,[...t,...n],e.id)}for(let[r,i]of s.itemRichText(e))for(let s of a.findAssetKeys(i))o.resolveAsset(s,e,n)||l.add(`warning`,`MISSING_ASSET`,`<asset:${s} /> does not exist in item.assets or meta.assets.`,[...t,...r],e.id);for(let r of Object.keys(e.assets??{}))n.assets?.[r]&&l.add(`warning`,`ASSET_SHADOWED`,`Item asset "${r}" shadows a global asset with the same key; use unique keys.`,[...t,`assets`,r],e.id);return d}function b(e,t,n,r,i){for(let[a,o]of Object.entries(e??{}))o.type!==`image`||o.altText?.trim()||(n.add(r===`strict`?`error`:`warning`,`MISSING_ALT_TEXT`,`Image "${a}" has no altText (alternative text for screen readers).`,[...t,a,`altText`],i),r===`load`&&(o.altText=S(o.value)||a))}function x(e){return[...e.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^ {0,3}\1[`~]*[ \t]*$|(?![\s\S]))/gm,``).replace(/`[^`\n]+`/g,``).matchAll(/!\[[^\]\n]*\]\(\s*<?([^)\s>]+)/g)].map(e=>e[1])}function S(e){return(e.split(/[?#]/)[0].split(`/`).pop()??``).replace(/\.[a-z0-9]+$/i,``).replace(/[-_]+/g,` `).trim()}function C(e,t,n,r){let i=e.assets;if(!w(i))return!0;let a={};for(let[e,o]of Object.entries(i)){let i=e.toLowerCase();if(i in a)return n.add(`error`,`ASSET_KEY_COLLISION`,`Asset keys collide after lowercasing ("${e}").`,[...t,e],r),!1;i!==e&&n.add(`warning`,`ASSET_KEY_NORMALIZED`,`Asset key "${e}" was converted to "${i}".`,[...t,e],r),a[i]=o}e.assets=a;for(let t of[`thumbnail`,`targetAsset`])typeof e[t]==`string`&&(e[t]=e[t].toLowerCase());return!0}function w(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function T(e){return`Raw HTML ${e} is not allowed unless the set declares the "html" feature (use Markdown instead).`}function E(e,t){let n=e;for(let e of t.slice(0,-1))n=n[e];delete n[t[t.length-1]]}exports.OQSEValidationError=l,exports.OQSE_VERSION=f,exports.SUPPORTED_OQSE_MAJOR_VERSION=d,exports.formatOQSEIssues=u,exports.loadOQSEFile=p,exports.safeValidateOQSEFile=m,exports.validateOQSEFile=h;
|
|
1
|
+
const e=require("./oqse.cjs"),t=require("./utils.cjs"),n=require("./oqseValidation.cjs"),r=require("./manifest.cjs"),i=require("./markdownUtils.cjs"),a=require("./richTextProcessor.cjs"),o=require("./mustUnderstand.cjs"),s=require("./assets.cjs"),c=require("./richTextFields.cjs"),l=require("./features.cjs");var u=class extends Error{errors;warnings;constructor(e,t){super(`Invalid OQSE file:\n${d(e).join(`
|
|
2
|
+
`)}`),this.name=`OQSEValidationError`,this.errors=e,this.warnings=t}};function d(e){return e.map(e=>e.path?`${e.path}: ${e.message}`:e.message)}var f=1,p=`1.0`;function m(e,t={}){return b(e,`load`,t)}function h(e,t={}){let n=b(e,`strict`,t),r=n.success&&n.errors.length===0;return{success:r,data:r?n.data:void 0,unsupportedItems:n.unsupportedItems,errors:n.errors,warnings:n.warnings}}function g(e,t){let n=[...e.items];for(let{index:e,item:r}of[...t].sort((e,t)=>e.index-t.index))n.splice(Math.min(e,n.length),0,structuredClone(r));return{...e,items:n}}function _(e,t={}){let n=h(e,t);if(!n.success)throw new u(n.errors,n.warnings);return n.data}var v=new Set(e.OFFICIAL_ITEM_TYPES),y=class{errors=[];warnings=[];add(e,n,r,i,a){let o={severity:e,code:n,message:r,path:t.formatPath(i)};a&&(o.itemId=a),(e===`error`?this.errors:this.warnings).push(o)}addZod(e,t,n,r){for(let i of e.issues)this.add(`error`,t,i.message,[...n,...i.path],r)}};function b(e,i,s){let u=new y,d=[],f=()=>({success:!1,unsupportedItems:d,errors:u.errors,warnings:u.warnings}),p=i===`strict`?`error`:`warning`;try{t.validateJsonDepth(e,32)}catch(e){return u.add(`error`,`SECURITY_LIMIT`,e.message,[]),f()}if(!P(e)||!P(e.meta)||!Array.isArray(e.items))return u.add(`error`,`INVALID_ROOT`,`An OQSE file must be an object with "version", "meta" and "items" (array).`,[]),f();let m=structuredClone(e);if(m.$schema!==void 0&&typeof m.$schema!=`string`)return u.add(`error`,`INVALID_ROOT`,`"$schema" must be a string URL.`,[`$schema`]),f();if(typeof m.version!=`string`||!/^\d+\.\d+$/.test(m.version))return u.add(`error`,`INVALID_VERSION`,`Version must be in MAJOR.MINOR format (e.g. "1.0").`,[`version`]),f();let h=Number(m.version.split(`.`)[0]);if(h===0)return u.add(`error`,`UNSUPPORTED_VERSION`,`OQSE ${m.version} is a pre-release version; this library reads OQSE 1.x (convert the set to 1.0).`,[`version`]),f();if(h!==1&&u.add(`warning`,`UNSUPPORTED_VERSION`,`Unsupported MAJOR version ${m.version}; loading in best-effort mode.`,[`version`]),m.items.length>5e4)return u.add(`error`,`TOO_MANY_ITEMS`,`Maximum ${t.MAX_ITEMS} items per set.`,[`items`]),f();if(i===`load`){if(!N(m.meta,[`meta`,`assets`],u))return f();D(m.meta,`language`,[`meta`,`language`],u),m.items.forEach((e,t)=>P(e)&&D(e,`lang`,[`items`,t,`lang`],u,typeof e.id==`string`?e.id:void 0))}x(m.meta,u,i);let g=E(m.meta,u);if(!g)return f();let _={features:r.OFFICIAL_FEATURE_KEYS,itemProperties:r.OFFICIAL_ITEM_PROPERTIES,metaProperties:r.OFFICIAL_META_PROPERTIES};for(let[e,t]of Object.entries(_))for(let n of g.requirements?.[e]??[])!n.startsWith(`x-`)&&!t.includes(n)&&u.add(`warning`,`UNKNOWN_FEATURE`,`Unknown ${e===`features`?`feature`:`property`} "${n}" in requirements.${e} (ignored).`,[`meta`,`requirements`,e]);let b=new Set(g.requirements?.features??[]),w=b.has(`html`),T=b.has(`latex`);A(g.assets,[`meta`,`assets`],u,i),g.thumbnail&&!g.assets?.[g.thumbnail]&&(u.add(p,`MISSING_THUMBNAIL_ASSET`,`Thumbnail references non-existent asset "${g.thumbnail}".`,[`meta`,`thumbnail`]),i===`load`&&delete g.thumbnail);for(let[e,t]of c.metaRichText(g)){let n=w?null:a.findRawHtml(t,{latex:T});n&&(u.add(p,`RAW_HTML_NOT_ALLOWED`,F(n),e),i===`load`&&I(g,e.slice(1)))}let j=new Set([g.id]),M=[];m.items.forEach((e,t)=>{let r=[`items`,t],a=P(e)&&typeof e.id==`string`?e.id:void 0,c=P(e)?e.type:void 0;if(typeof c==`string`&&!c.startsWith(`x-`)&&!v.has(c)){let i=n.UnknownTypeItemSchema.safeParse(e);if(!i.success){u.addZod(i.error,`INVALID_ITEM`,r,a);return}if(j.has(i.data.id)){u.add(`error`,`DUPLICATE_ID`,`Duplicate ID "${i.data.id}" (IDs must be unique within the set, including meta.id).`,[...r,`id`],i.data.id);return}j.add(i.data.id),u.add(`warning`,`UNKNOWN_ITEM_TYPE`,`Unknown item type "${c}" (a newer OQSE version, or a typo; custom types use the "x-" prefix). The item is kept but not shown.`,[...r,`type`],a),d.push({index:t,item:structuredClone(e)});return}if(i===`load`&&P(e)&&!N(e,[...r,`assets`],u,a))return;let l=n.getItemSchema(e).safeParse(e);if(!l.success){u.addZod(l.error,`INVALID_ITEM`,r,a);return}let f=l.data;if(j.has(f.id)){u.add(`error`,`DUPLICATE_ID`,`Duplicate ID "${f.id}" (IDs must be unique within the set, including meta.id).`,[...r,`id`],f.id);return}j.add(f.id);for(let[e,t]of C(f))S.test(t)&&u.add(`warning`,`REVEALING_CHOICE_ID`,`Choice ID "${t}" gives the answer away (learners can see IDs); use an ID from the text or a random one.`,[...r,...e],f.id);let m=o.missingMustUnderstand(f,s.understood);if(m.length>0){u.add(`warning`,`MUST_UNDERSTAND`,`The item needs properties this application does not support (${m.join(`, `)}); it is kept but not shown.`,[...r,`mustUnderstand`],f.id),d.push({index:t,item:structuredClone(e)});return}A(f.assets,[...r,`assets`],u,i,f.id),k(f,r,g,w,T,u,p)&&M.push({item:f,index:t})});let L=M.map(e=>e.item),R=new Set(L.map(e=>e.id));for(let{item:e}of d)R.add(e.id);let z=new Set((g.sourceMaterials??[]).map(e=>e.id)),B=new Set((g.sourceMaterials??[]).filter(e=>e.set===g.id).map(e=>e.id));for(let{item:e,index:t}of M){let n=[`items`,t];for(let t of[`relatedItems`,`dependencyItems`]){let r=e[t];if(!r)continue;let a=r.filter(t=>t!==e.id&&R.has(t));a.length!==r.length&&(u.add(p,`DANGLING_REFERENCE`,`${t} contains IDs that do not exist in the set or reference the item itself.`,[...n,t],e.id),i===`load`&&(e[t]=a))}if(e.sources){let t=e.sources.filter(e=>z.has(e.id));t.length!==e.sources.length&&(u.add(p,`DANGLING_REFERENCE`,`sources reference source materials that do not exist in meta.sourceMaterials.`,[...n,`sources`],e.id),i===`load`&&(e.sources=t)),e.sources.forEach((t,r)=>{t.itemId!==void 0&&B.has(t.id)&&!R.has(t.itemId)&&(u.add(p,`DANGLING_REFERENCE`,`sources[${r}].itemId "${t.itemId}" does not exist in this set.`,[...n,`sources`,r,`itemId`],e.id),i===`load`&&delete t.itemId)})}}for(let e of l.detectFeatures({meta:g,items:L}))e!==`html`&&!b.has(e)&&u.add(`warning`,`UNDECLARED_FEATURE`,`The content uses "${e}", but meta.requirements.features does not declare it.`,[`meta`,`requirements`,`features`]);if(!T)for(let{item:e,index:t}of M){let n=c.itemRichText(e).find(([,e])=>l.looksLikeLatex(e));n&&u.add(`warning`,`LOOKS_LIKE_LATEX`,`The text looks like a LaTeX formula; if it is one, declare the "latex" feature (otherwise $ is an ordinary character).`,[`items`,t,...n[0]],e.id)}return O(g,M,u),{success:!0,data:{...m,meta:g,items:L},unsupportedItems:d,errors:u.errors,warnings:u.warnings}}function x(e,t,r){if(!Array.isArray(e.sourceMaterials))return;let i=[];e.sourceMaterials.forEach((e,a)=>{let o=[`meta`,`sourceMaterials`,a],s=n.SourceMaterialSchema.safeParse(e);if(!s.success){t.addZod(s.error,`INVALID_SOURCE`,o),r===`strict`&&i.push(e);return}let c=s.data;c.url===void 0&&c.doi===void 0&&c.isbn===void 0&&c.set===void 0&&t.add(`warning`,`SOURCE_WITHOUT_LOCATOR`,`Source "${c.id}" has none of url, doi, isbn, set: it can be cited only by its title.`,o),i.push(e)}),r===`load`&&(e.sourceMaterials=i)}var S=/(^|[^a-z])(correct|incorrect|right|wrong|answer|solution)([^a-z]|$)/i;function C(e){let t=[],n=e;for(let e of[`options`,`items`,`prompts`,`matches`,`categories`,`rows`,`columns`,`labels`,`events`,`zones`]){let r=n[e];Array.isArray(r)&&r.forEach((n,r)=>P(n)&&typeof n.id==`string`&&t.push([[e,r,`id`],n.id]))}if(e.type===`fill-in-select`&&P(n.blanks))for(let[e,r]of Object.entries(n.blanks))P(r)&&Array.isArray(r.options)&&r.options.forEach((n,r)=>P(n)&&typeof n.id==`string`&&t.push([[`blanks`,e,`options`,r,`id`],n.id]));return t}var w=new Set([`id`,`title`,`language`,`createdAt`,`updatedAt`]),T=new Set([`assets`,`tagDefinitions`,`translations`,`prerequisites`,`relatedSets`,`contributors`,`sourceMaterials`]);function E(e,t){let r=structuredClone(e);for(let e=0;e<50;e++){let e=n.OQSEMetaSchema.safeParse(r);if(e.success)return e.data;let i=[];for(let n of e.error.issues){let[e,r]=n.path;if(e===void 0||w.has(String(e)))return t.add(`error`,`INVALID_META`,n.message,[`meta`,...n.path]),null;t.add(`error`,`INVALID_META_FIELD`,`${n.message} (removed)`,[`meta`,...n.path]),i.push(T.has(String(e))&&r!==void 0?[e,r]:[e])}i.sort((e,t)=>Number(t[1]??0)-Number(e[1]??0));let a=new Set;for(let e of i){let t=e.join(`.`);if(a.has(t))continue;a.add(t);let[n,i]=e,o=r[n];i===void 0?delete r[n]:Array.isArray(o)?o.splice(Number(i),1):P(o)&&delete o[i]}}return null}function D(e,t,n,r,i){let a=e[t];typeof a==`string`&&a.includes(`_`)&&(e[t]=a.replace(/_/g,`-`),r.add(`warning`,`LANGUAGE_NORMALIZED`,`Language tag "${a}" written as "${e[t]}" (BCP 47 uses hyphens).`,n,i))}function O(t,n,r){let i=(e,t,n,i,a)=>{typeof e==`string`&&!t.includes(e)&&r.add(`warning`,`UNKNOWN_VALUE`,`Unknown value "${e}" (a newer OQSE version, or a typo): ${i}.`,n,a)},a=(t,n,r)=>{for(let[a,o]of Object.entries(t??{}))i(o.type,e.MEDIA_TYPES,[...n,a,`type`],`shown as a link`,r),o.subtitles?.forEach((t,o)=>i(t.kind,e.SUBTITLE_KINDS,[...n,a,`subtitles`,o,`kind`],`the track is not offered`,r))};a(t.assets,[`meta`,`assets`]),t.sourceMaterials?.forEach((t,n)=>i(t.kind,e.SOURCE_KINDS,[`meta`,`sourceMaterials`,n,`kind`],`treated as "other"`));for(let{item:t,index:r}of n)a(t.assets,[`items`,r,`assets`],t.id),i(t.pedagogy?.bloomLevel,e.BLOOM_LEVELS,[`items`,r,`pedagogy`,`bloomLevel`],`ignored`,t.id),i(t.pedagogy?.cognitiveLoad,e.COGNITIVE_LOADS,[`items`,r,`pedagogy`,`cognitiveLoad`],`ignored`,t.id)}function k(e,t,n,r,o,l,u){let d=!0;if(!r)for(let[n,r]of c.itemRichText(e)){let i=a.findRawHtml(r,{latex:o});i&&(l.add(`error`,`RAW_HTML_NOT_ALLOWED`,F(i),[...t,...n],e.id),d=!1)}if(`targetAsset`in e&&typeof e.targetAsset==`string`&&!e.type.startsWith(`x-`)){let r=s.resolveAsset(e.targetAsset,e,n),i=e.type===`pin-on-model`?`model`:`image`;r?r.type!==i&&(l.add(`error`,`INVALID_TARGET_ASSET`,`targetAsset "${e.targetAsset}" must be of type "${i}".`,[...t,`targetAsset`],e.id),d=!1):(l.add(`error`,`MISSING_TARGET_ASSET`,`targetAsset "${e.targetAsset}" does not exist in item.assets or meta.assets.`,[...t,`targetAsset`],e.id),d=!1)}let f=e.type===`timeline`?e.events:e.type===`categorize`?e.items:void 0;if(f){let n=f.map(e=>e.id);new Set(n).size!==n.length&&(l.add(`error`,`DUPLICATE_ID`,`Internal IDs must be unique within the item.`,[...t,e.type===`timeline`?`events`:`items`],e.id),d=!1)}if(e.type===`chess-puzzle`&&e.correctAnswers.some(e=>e.length%2==0)&&l.add(`warning`,`CHESS_SEQUENCE_LENGTH`,`Each move sequence should end with the learner's move (an odd number of moves; the application plays the replies).`,[...t,`correctAnswers`],e.id),e.type===`note`)for(let n of[`content`,`hiddenContent`]){let r=e[n];r&&i.findHeadings(i.splitLines(r)).some(e=>e.level===1)&&l.add(`warning`,`NOTE_HEADING_LEVEL`,`Headings inside a note should start at level 2 (##); the note title is level 1.`,[...t,n],e.id)}let p=e.type===`fill-in-blanks`||e.type===`fill-in-select`;for(let[n,r]of c.itemRichText(e)){for(let i of j(r)){if(/^https:\/\//i.test(i))continue;let r=/^http:\/\//i.test(i);l.add(r?`warning`:u,`INVALID_IMAGE_URL`,r?`Image "${i}" uses http:// (prefer https://).`:`Image "${i}" needs an absolute https:// URL (use <asset:key /> for other media).`,[...t,...n],e.id)}!(p&&n.length===1&&n[0]===`text`)&&/<blank:[a-zA-Z0-9_-]+\s*\/>/i.test(r)&&l.add(`warning`,`BLANK_OUTSIDE_TEXT`,`Blank tags belong only in the text of fill-in-blanks and fill-in-select (shown here as an empty line).`,[...t,...n],e.id)}for(let[r,i]of c.itemRichText(e))for(let o of a.findAssetKeys(i))s.resolveAsset(o,e,n)||l.add(`warning`,`MISSING_ASSET`,`<asset:${o} /> does not exist in item.assets or meta.assets.`,[...t,...r],e.id);for(let r of Object.keys(e.assets??{}))n.assets?.[r]&&l.add(`warning`,`ASSET_SHADOWED`,`Item asset "${r}" shadows a global asset with the same key; use unique keys.`,[...t,`assets`,r],e.id);return d}function A(e,t,n,r,i){for(let[a,o]of Object.entries(e??{}))o.type!==`image`||o.altText?.trim()||(n.add(r===`strict`?`error`:`warning`,`MISSING_ALT_TEXT`,`Image "${a}" has no altText (alternative text for screen readers).`,[...t,a,`altText`],i),r===`load`&&(o.altText=M(o.value)||a))}function j(e){return[...e.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^ {0,3}\1[`~]*[ \t]*$|(?![\s\S]))/gm,``).replace(/`[^`\n]+`/g,``).matchAll(/!\[[^\]\n]*\]\(\s*<?([^)\s>]+)/g)].map(e=>e[1])}function M(e){return(e.split(/[?#]/)[0].split(`/`).pop()??``).replace(/\.[a-z0-9]+$/i,``).replace(/[-_]+/g,` `).trim()}function N(e,t,n,r){let i=e.assets;if(!P(i))return!0;let a={};for(let[e,o]of Object.entries(i)){let i=e.toLowerCase();if(i in a)return n.add(`error`,`ASSET_KEY_COLLISION`,`Asset keys collide after lowercasing ("${e}").`,[...t,e],r),!1;i!==e&&n.add(`warning`,`ASSET_KEY_NORMALIZED`,`Asset key "${e}" was converted to "${i}".`,[...t,e],r),a[i]=o}e.assets=a;for(let t of[`thumbnail`,`targetAsset`])typeof e[t]==`string`&&(e[t]=e[t].toLowerCase());return!0}function P(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function F(e){return`Raw HTML ${e} is not allowed unless the set declares the "html" feature (use Markdown instead).`}function I(e,t){let n=e;for(let e of t.slice(0,-1))n=n[e];delete n[t[t.length-1]]}exports.OQSEValidationError=u,exports.OQSE_VERSION=p,exports.SUPPORTED_OQSE_MAJOR_VERSION=f,exports.formatOQSEIssues=d,exports.loadOQSEFile=m,exports.safeValidateOQSEFile=h,exports.validateOQSEFile=_,exports.withUnsupportedItems=g;
|