@intellectif/lk-core 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/dist/ai-check.cjs +2 -2
- package/dist/ai-check.d.cts +1 -1
- package/dist/ai-check.d.ts +1 -1
- package/dist/ai-check.js +2 -2
- package/dist/{ai-Cu17iVHa.d.cts → ai-mC-5XhBc.d.cts} +212 -5
- package/dist/ai-mC-5XhBc.d.cts.map +1 -0
- package/dist/{ai-Cu17iVHa.d.ts → ai-mC-5XhBc.d.ts} +212 -5
- package/dist/ai-mC-5XhBc.d.ts.map +1 -0
- package/dist/{ai-writing-gO4GRscd.cjs → ai-writing-Bm2TUWQ-.cjs} +24 -5
- package/dist/ai-writing-Bm2TUWQ-.cjs.map +1 -0
- package/dist/{ai-writing-BgeeCOl7.js → ai-writing-D3oREPsp.js} +24 -5
- package/dist/ai-writing-D3oREPsp.js.map +1 -0
- package/dist/{index-BTZWUwOe.d.ts → index-2UQ5v6NZ.d.ts} +2 -2
- package/dist/{index-BTZWUwOe.d.ts.map → index-2UQ5v6NZ.d.ts.map} +1 -1
- package/dist/{index-B1zEN1Ur.d.ts → index-BudO4ES2.d.ts} +2 -2
- package/dist/{index-B1zEN1Ur.d.ts.map → index-BudO4ES2.d.ts.map} +1 -1
- package/dist/{index-BfUyl6ml.d.ts → index-CFKxHXAY.d.ts} +33 -4
- package/dist/index-CFKxHXAY.d.ts.map +1 -0
- package/dist/{index-DjZ1vIVg.d.cts → index-CagMlF4b.d.cts} +33 -4
- package/dist/index-CagMlF4b.d.cts.map +1 -0
- package/dist/{index-CpamCGyZ.d.cts → index-CqmNcMFM.d.cts} +2 -2
- package/dist/{index-CpamCGyZ.d.cts.map → index-CqmNcMFM.d.cts.map} +1 -1
- package/dist/{index-DYnacygI.d.cts → index-MgPVCiSJ.d.cts} +2 -2
- package/dist/{index-DYnacygI.d.cts.map → index-MgPVCiSJ.d.cts.map} +1 -1
- package/dist/index.cjs +17 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -13
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +90 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -8
- package/dist/index.js.map +1 -1
- package/dist/{item-group-CnlBLM9Z.js → item-group-BIl1ZJHg.js} +55 -22
- package/dist/item-group-BIl1ZJHg.js.map +1 -0
- package/dist/{item-group-CRZNJ08j.cjs → item-group-DOk562oI.cjs} +55 -22
- package/dist/item-group-DOk562oI.cjs.map +1 -0
- package/dist/{registry-BClnM5V3.js → registry-CroAI5nc.js} +898 -8
- package/dist/registry-CroAI5nc.js.map +1 -0
- package/dist/{registry-DgLcT4rZ.cjs → registry-Dw19lNo2.cjs} +933 -7
- package/dist/registry-Dw19lNo2.cjs.map +1 -0
- package/dist/{schemas-BA8Y8z-S.cjs → schemas-CEv2otNw.cjs} +3 -3
- package/dist/{schemas-BA8Y8z-S.cjs.map → schemas-CEv2otNw.cjs.map} +1 -1
- package/dist/{schemas-VZnN68qG.js → schemas-COw7muXH.js} +3 -3
- package/dist/{schemas-VZnN68qG.js.map → schemas-COw7muXH.js.map} +1 -1
- package/dist/schemas.cjs +3 -3
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +3 -3
- package/dist/{scoring-BxDiwN7I.js → scoring-BxvOhHE0.js} +2 -2
- package/dist/{scoring-BxDiwN7I.js.map → scoring-BxvOhHE0.js.map} +1 -1
- package/dist/{scoring-Cfu_2qQ1.cjs → scoring-EpNBJ8Fs.cjs} +2 -2
- package/dist/{scoring-Cfu_2qQ1.cjs.map → scoring-EpNBJ8Fs.cjs.map} +1 -1
- package/dist/scoring.cjs +2 -2
- package/dist/scoring.d.cts +2 -2
- package/dist/scoring.d.ts +2 -2
- package/dist/scoring.js +2 -2
- package/dist/{xapi-BhROEuZQ.cjs → xapi-C-cQgWRX.cjs} +2 -2
- package/dist/{xapi-BhROEuZQ.cjs.map → xapi-C-cQgWRX.cjs.map} +1 -1
- package/dist/{xapi-BitXmAYB.js → xapi-DHHm7Os9.js} +2 -2
- package/dist/{xapi-BitXmAYB.js.map → xapi-DHHm7Os9.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -1
- package/package.json +1 -1
- package/vectors/scoring.json +1767 -1
- package/vectors/validation.json +1458 -1
- package/dist/ai-Cu17iVHa.d.cts.map +0 -1
- package/dist/ai-Cu17iVHa.d.ts.map +0 -1
- package/dist/ai-writing-BgeeCOl7.js.map +0 -1
- package/dist/ai-writing-gO4GRscd.cjs.map +0 -1
- package/dist/index-BfUyl6ml.d.ts.map +0 -1
- package/dist/index-DjZ1vIVg.d.cts.map +0 -1
- package/dist/item-group-CRZNJ08j.cjs.map +0 -1
- package/dist/item-group-CnlBLM9Z.js.map +0 -1
- package/dist/registry-BClnM5V3.js.map +0 -1
- package/dist/registry-DgLcT4rZ.cjs.map +0 -1
|
@@ -464,7 +464,9 @@ const BUILT_IN_ACTIVITY_TYPES = [
|
|
|
464
464
|
"gap-select",
|
|
465
465
|
"dictation",
|
|
466
466
|
"read-aloud",
|
|
467
|
-
"speaking-response"
|
|
467
|
+
"speaking-response",
|
|
468
|
+
"matching",
|
|
469
|
+
"mark-the-words"
|
|
468
470
|
];
|
|
469
471
|
/** Whether `type` names an activity type the SDK ships. */
|
|
470
472
|
function isBuiltInActivityType(type) {
|
|
@@ -1075,7 +1077,7 @@ function containsUnspacedScript(text) {
|
|
|
1075
1077
|
}
|
|
1076
1078
|
/** What is drawn onto the character before it: a match never ends just before one. */
|
|
1077
1079
|
const EXTENDING_RE = new RegExp(`^[\\p{M}${JOINERS}${TAGS}]$`, "u");
|
|
1078
|
-
const WORD = 1;
|
|
1080
|
+
const WORD$1 = 1;
|
|
1079
1081
|
const UNSPACED = 2;
|
|
1080
1082
|
const EXTENDING = 4;
|
|
1081
1083
|
const KNOWN = 8;
|
|
@@ -1089,7 +1091,7 @@ const KNOWN = 8;
|
|
|
1089
1091
|
*/
|
|
1090
1092
|
const FLAGS = [];
|
|
1091
1093
|
function flagsOfCharacter(character) {
|
|
1092
|
-
return KNOWN | (WORD_RE.test(character) ? WORD : 0) | (UNSPACED_CHARACTER_RE.test(character) ? UNSPACED : 0) | (EXTENDING_RE.test(character) ? EXTENDING : 0);
|
|
1094
|
+
return KNOWN | (WORD_RE.test(character) ? WORD$1 : 0) | (UNSPACED_CHARACTER_RE.test(character) ? UNSPACED : 0) | (EXTENDING_RE.test(character) ? EXTENDING : 0);
|
|
1093
1095
|
}
|
|
1094
1096
|
/** The flags of the code point that starts at UTF-16 index `index` of `text`. */
|
|
1095
1097
|
function flagsAt(text, index) {
|
|
@@ -1319,12 +1321,12 @@ function isAsciiWordUnit(unit) {
|
|
|
1319
1321
|
}
|
|
1320
1322
|
function isWordAt(text, index) {
|
|
1321
1323
|
const unit = text.charCodeAt(index);
|
|
1322
|
-
return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & WORD) !== 0;
|
|
1324
|
+
return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & WORD$1) !== 0;
|
|
1323
1325
|
}
|
|
1324
1326
|
/** A word character of a script that separates its words with spaces. */
|
|
1325
1327
|
function isSpacedWordAt(text, index) {
|
|
1326
1328
|
const unit = text.charCodeAt(index);
|
|
1327
|
-
return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & 3) === WORD;
|
|
1329
|
+
return unit < 128 ? isAsciiWordUnit(unit) : (flagsAt(text, index) & 3) === WORD$1;
|
|
1328
1330
|
}
|
|
1329
1331
|
function isExtendingAt(text, index) {
|
|
1330
1332
|
return text.charCodeAt(index) >= 128 && (flagsAt(text, index) & EXTENDING) !== 0;
|
|
@@ -2349,6 +2351,29 @@ const DRAFT_ISSUE_SEVERITY = {
|
|
|
2349
2351
|
gs_choice_text_required: "incomplete",
|
|
2350
2352
|
gs_correct_choice_required: "incomplete",
|
|
2351
2353
|
gs_correct_choice_unknown: "invalid",
|
|
2354
|
+
mt_prompts_required: "incomplete",
|
|
2355
|
+
mt_prompt_id_required: "invalid",
|
|
2356
|
+
mt_prompt_id_duplicate: "invalid",
|
|
2357
|
+
mt_prompt_text_required: "incomplete",
|
|
2358
|
+
mt_options_too_few: "incomplete",
|
|
2359
|
+
mt_option_id_required: "invalid",
|
|
2360
|
+
mt_option_id_duplicate: "invalid",
|
|
2361
|
+
mt_option_text_required: "incomplete",
|
|
2362
|
+
mt_option_label_required: "incomplete",
|
|
2363
|
+
mt_option_label_duplicate: "invalid",
|
|
2364
|
+
mt_option_use_invalid: "invalid",
|
|
2365
|
+
mt_correct_option_required: "incomplete",
|
|
2366
|
+
mt_correct_option_unknown: "invalid",
|
|
2367
|
+
mt_option_reused: "invalid",
|
|
2368
|
+
mw_text_required: "incomplete",
|
|
2369
|
+
mw_text_too_long: "invalid",
|
|
2370
|
+
mw_targets_required: "incomplete",
|
|
2371
|
+
mw_target_text_required: "incomplete",
|
|
2372
|
+
mw_target_no_words: "invalid",
|
|
2373
|
+
mw_target_not_found: "invalid",
|
|
2374
|
+
mw_target_ambiguous: "incomplete",
|
|
2375
|
+
mw_target_occurrence_invalid: "invalid",
|
|
2376
|
+
mw_target_overlap: "invalid",
|
|
2352
2377
|
wr_prompt_required: "incomplete",
|
|
2353
2378
|
wr_min_words_required: "incomplete",
|
|
2354
2379
|
wr_min_words_invalid: "invalid",
|
|
@@ -3108,6 +3133,13 @@ const ITEM_FINDING_SEVERITY = {
|
|
|
3108
3133
|
gs_choices_duplicate: "warning",
|
|
3109
3134
|
gs_bank_no_distractor: "warning",
|
|
3110
3135
|
gs_answer_in_passage: "warning",
|
|
3136
|
+
mt_title_reveals_answer: "warning",
|
|
3137
|
+
mt_options_duplicate: "warning",
|
|
3138
|
+
mt_answer_in_prompt: "warning",
|
|
3139
|
+
mt_no_distractor: "warning",
|
|
3140
|
+
mw_title_reveals_answer: "warning",
|
|
3141
|
+
mw_targets_dense: "warning",
|
|
3142
|
+
mw_target_elsewhere: "advice",
|
|
3111
3143
|
wr_criterion_name_duplicate: "warning",
|
|
3112
3144
|
wr_levels_unreadable: "warning",
|
|
3113
3145
|
ra_text_long_for_time: "warning",
|
|
@@ -3378,6 +3410,40 @@ function critiqueGapSelect(draft) {
|
|
|
3378
3410
|
});
|
|
3379
3411
|
return findings;
|
|
3380
3412
|
}
|
|
3413
|
+
/** Whether `text` writes out `answer` as whole words — an answer of one short word never counts. */
|
|
3414
|
+
function writesOut$1(text, answer) {
|
|
3415
|
+
const needle = fold(answer);
|
|
3416
|
+
return needle.length > 0 && !isShort(needle) && containsWords(fold(text), needle);
|
|
3417
|
+
}
|
|
3418
|
+
function critiqueMatching(draft) {
|
|
3419
|
+
const findings = [];
|
|
3420
|
+
const prompts = listOf(draft.prompts);
|
|
3421
|
+
const options = listOf(draft.options);
|
|
3422
|
+
const optionText = (option) => isRecord(option) ? written(option.text) : void 0;
|
|
3423
|
+
const answerOf = (prompt) => isRecord(prompt) ? optionText(options.find((option) => isRecord(option) && option.id === prompt.correctOptionId)) : void 0;
|
|
3424
|
+
const title = written(draft.title);
|
|
3425
|
+
if (title !== void 0 && prompts.some((prompt) => {
|
|
3426
|
+
const answer = answerOf(prompt);
|
|
3427
|
+
return answer !== void 0 && writesOut$1(title, answer);
|
|
3428
|
+
})) findings.push(finding("mt_title_reveals_answer", ["title"], "The title, which the learner sees, contains the answer to a prompt."));
|
|
3429
|
+
for (const { first, second } of duplicates(options, optionText)) findings.push(finding("mt_options_duplicate", [
|
|
3430
|
+
"options",
|
|
3431
|
+
second,
|
|
3432
|
+
"text"
|
|
3433
|
+
], `Options ${ordinal(first)} and ${ordinal(second)} read the same, so a learner cannot tell them apart.`));
|
|
3434
|
+
prompts.forEach((prompt, index) => {
|
|
3435
|
+
const text = isRecord(prompt) ? written(prompt.text) : void 0;
|
|
3436
|
+
const answer = answerOf(prompt);
|
|
3437
|
+
if (text !== void 0 && answer !== void 0 && writesOut$1(text, answer)) findings.push(finding("mt_answer_in_prompt", [
|
|
3438
|
+
"prompts",
|
|
3439
|
+
index,
|
|
3440
|
+
"text"
|
|
3441
|
+
], `Prompt ${ordinal(index)} contains "${answer}", the option that answers it, word for word.`));
|
|
3442
|
+
});
|
|
3443
|
+
const promptCount = prompts.filter(isRecord).length;
|
|
3444
|
+
if (draft.optionUse === "once" && promptCount >= 2 && options.length <= promptCount) findings.push(finding("mt_no_distractor", ["options"], `Each option is used once, and there are ${options.length} options for ${promptCount} prompts, so the last prompt is answered by elimination. Add an option that answers no prompt.`));
|
|
3445
|
+
return findings;
|
|
3446
|
+
}
|
|
3381
3447
|
/** Why a criterion's levels cannot be graded on, said to the author. */
|
|
3382
3448
|
const LEVELS_PROBLEM = {
|
|
3383
3449
|
"not-a-list": (criterion) => `Criterion ${criterion}'s levels are not a list`,
|
|
@@ -3767,6 +3833,514 @@ function pairsOneToOne(placeholders, gaps) {
|
|
|
3767
3833
|
if (new Set(ids).size !== ids.length || placeholders.size !== ids.length) return false;
|
|
3768
3834
|
return ids.every((id) => typeof id === "string" && placeholders.get(id) === 1);
|
|
3769
3835
|
}
|
|
3836
|
+
// src/mark-the-words.ts
|
|
3837
|
+
/**
|
|
3838
|
+
* The words of a mark-the-words text, and where an author's targets fall
|
|
3839
|
+
* among them. A leaf module: the schema, the scorer, the draft checks and the
|
|
3840
|
+
* component all read it, and it imports nothing.
|
|
3841
|
+
*
|
|
3842
|
+
* A learner marks words, never characters, and a response records a word by
|
|
3843
|
+
* its position — so how a text is cut into words is part of every stored
|
|
3844
|
+
* answer. The cut is the SDK's own (`lk-words@1`), not `Intl.Segmenter`,
|
|
3845
|
+
* whose word boundaries change with the ICU a runtime ships: the same text
|
|
3846
|
+
* must be the same words in every browser and on every server, now and when
|
|
3847
|
+
* the answer is graded again later.
|
|
3848
|
+
*/
|
|
3849
|
+
/** The name of the word cut. A change to it is a major: stored answers name words by position. */
|
|
3850
|
+
const MARK_THE_WORDS_TOKENIZER = "lk-words@1";
|
|
3851
|
+
/** The most words a mark-the-words text may have. */
|
|
3852
|
+
const MARK_THE_WORDS_MAX_WORDS = 1500;
|
|
3853
|
+
/**
|
|
3854
|
+
* A word: a run of letters, marks and digits, which may hold an apostrophe or
|
|
3855
|
+
* a hyphen between two such runs — "don't", "well-known", "1960s". Punctuation
|
|
3856
|
+
* and spaces are never part of one and can never be marked.
|
|
3857
|
+
*/
|
|
3858
|
+
const WORD = /[\p{L}\p{M}\p{N}]+(?:['’ʼ\-‐][\p{L}\p{M}\p{N}]+)*/gu;
|
|
3859
|
+
/** Every markable word of `text`, in order, cut by `lk-words@1`. */
|
|
3860
|
+
function markableWords(text) {
|
|
3861
|
+
const words = [];
|
|
3862
|
+
for (const match of text.matchAll(WORD)) words.push({
|
|
3863
|
+
index: words.length,
|
|
3864
|
+
text: match[0],
|
|
3865
|
+
start: match.index,
|
|
3866
|
+
end: match.index + match[0].length
|
|
3867
|
+
});
|
|
3868
|
+
return words;
|
|
3869
|
+
}
|
|
3870
|
+
/** A word as compared with a target's: composed, and with one apostrophe and one hyphen. */
|
|
3871
|
+
function wordKey(word) {
|
|
3872
|
+
return word.normalize("NFC").replace(/[’ʼ]/g, "'").replace(/‐/g, "-");
|
|
3873
|
+
}
|
|
3874
|
+
/** Every place `needle` appears in `words`, as the position of its first word. */
|
|
3875
|
+
function placesOf(words, needle) {
|
|
3876
|
+
const places = [];
|
|
3877
|
+
for (let at = 0; at + needle.length <= words.length; at += 1) if (needle.every((word, offset) => words[at + offset] === word)) places.push(at);
|
|
3878
|
+
return places;
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* Where a target falls among `words`. Its text must be whole words of the text,
|
|
3882
|
+
* in order — case, accents and punctuation as written, apart from the kind of
|
|
3883
|
+
* apostrophe — and must say which appearance it means when there are several.
|
|
3884
|
+
*/
|
|
3885
|
+
function resolveMarkTarget(words, target) {
|
|
3886
|
+
const needle = markableWords(typeof target.text === "string" ? target.text : "").map((word) => wordKey(word.text));
|
|
3887
|
+
if (needle.length === 0) return {
|
|
3888
|
+
ok: false,
|
|
3889
|
+
problem: "no-words",
|
|
3890
|
+
found: 0
|
|
3891
|
+
};
|
|
3892
|
+
const places = placesOf(words.map((word) => wordKey(word.text)), needle);
|
|
3893
|
+
if (places.length === 0) return {
|
|
3894
|
+
ok: false,
|
|
3895
|
+
problem: "not-found",
|
|
3896
|
+
found: 0
|
|
3897
|
+
};
|
|
3898
|
+
const occurrence = target.occurrence;
|
|
3899
|
+
let at;
|
|
3900
|
+
if (occurrence === void 0) {
|
|
3901
|
+
if (places.length > 1) return {
|
|
3902
|
+
ok: false,
|
|
3903
|
+
problem: "ambiguous",
|
|
3904
|
+
found: places.length
|
|
3905
|
+
};
|
|
3906
|
+
at = places[0];
|
|
3907
|
+
} else {
|
|
3908
|
+
at = typeof occurrence === "number" && Number.isInteger(occurrence) && occurrence >= 1 ? places[occurrence - 1] : void 0;
|
|
3909
|
+
if (at === void 0) return {
|
|
3910
|
+
ok: false,
|
|
3911
|
+
problem: "no-such-occurrence",
|
|
3912
|
+
found: places.length
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
return {
|
|
3916
|
+
ok: true,
|
|
3917
|
+
indices: needle.map((_, offset) => at + offset)
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* The word positions of each target of an item, in target order; a target
|
|
3922
|
+
* that falls nowhere has none. Read defensively: the scorer is reachable with
|
|
3923
|
+
* content an older build stored.
|
|
3924
|
+
*/
|
|
3925
|
+
function markTargetIndices(data) {
|
|
3926
|
+
const words = markableWords(typeof data.text === "string" ? data.text : "");
|
|
3927
|
+
return (Array.isArray(data.targets) ? data.targets : []).map((target) => {
|
|
3928
|
+
if (typeof target !== "object" || target === null) return [];
|
|
3929
|
+
const found = resolveMarkTarget(words, target);
|
|
3930
|
+
return found.ok ? found.indices : [];
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* A text and its targets from markup that stars each target, as an H5P "Mark
|
|
3935
|
+
* the Words" text does: `I *went* home and *had gone* to bed.` A `\*` is a
|
|
3936
|
+
* literal star. Each target names its appearance only where its words appear
|
|
3937
|
+
* more than once in the text.
|
|
3938
|
+
*
|
|
3939
|
+
* @throws Error when a star is left open.
|
|
3940
|
+
*/
|
|
3941
|
+
function markTheWordsFromMarkup(markup) {
|
|
3942
|
+
let text = "";
|
|
3943
|
+
const spans = [];
|
|
3944
|
+
let open = null;
|
|
3945
|
+
for (let at = 0; at < markup.length; at += 1) {
|
|
3946
|
+
const char = markup[at];
|
|
3947
|
+
if (char === "\\" && markup[at + 1] === "*") {
|
|
3948
|
+
text += "*";
|
|
3949
|
+
at += 1;
|
|
3950
|
+
} else if (char === "*") {
|
|
3951
|
+
if (open === null) open = text.length;
|
|
3952
|
+
else {
|
|
3953
|
+
spans.push({
|
|
3954
|
+
start: open,
|
|
3955
|
+
end: text.length
|
|
3956
|
+
});
|
|
3957
|
+
open = null;
|
|
3958
|
+
}
|
|
3959
|
+
} else text += char;
|
|
3960
|
+
}
|
|
3961
|
+
if (open !== null) throw new Error("markTheWordsFromMarkup: a * is opened and never closed.");
|
|
3962
|
+
const words = markableWords(text);
|
|
3963
|
+
const keys = words.map((word) => wordKey(word.text));
|
|
3964
|
+
const targets = [];
|
|
3965
|
+
for (const span of spans) {
|
|
3966
|
+
const inside = words.filter((word) => word.start >= span.start && word.end <= span.end);
|
|
3967
|
+
const first = inside[0];
|
|
3968
|
+
if (first === void 0) continue;
|
|
3969
|
+
const quote = text.slice(first.start, inside.at(-1).end);
|
|
3970
|
+
const places = placesOf(keys, inside.map((word) => wordKey(word.text)));
|
|
3971
|
+
targets.push(places.length > 1 ? {
|
|
3972
|
+
text: quote,
|
|
3973
|
+
occurrence: places.indexOf(first.index) + 1
|
|
3974
|
+
} : { text: quote });
|
|
3975
|
+
}
|
|
3976
|
+
return {
|
|
3977
|
+
text,
|
|
3978
|
+
targets
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3981
|
+
// src/authoring/mark-the-words.ts
|
|
3982
|
+
/**
|
|
3983
|
+
* Draft support for `mark-the-words`.
|
|
3984
|
+
*
|
|
3985
|
+
* A new draft has an empty text and no targets, and `scoringStrategy` starts
|
|
3986
|
+
* at `all-or-nothing`, as on every built-in. An editor usually lets the author
|
|
3987
|
+
* click the words to mark; `markTheWordsFromMarkup` turns starred text into
|
|
3988
|
+
* the same `text` and `targets`.
|
|
3989
|
+
*/
|
|
3990
|
+
const markTheWordsAuthoring = {
|
|
3991
|
+
createDraft: ({ newId }) => ({
|
|
3992
|
+
schemaVersion: "1.0",
|
|
3993
|
+
type: "mark-the-words",
|
|
3994
|
+
id: newId(),
|
|
3995
|
+
title: "",
|
|
3996
|
+
text: "",
|
|
3997
|
+
targets: [],
|
|
3998
|
+
scoringStrategy: "all-or-nothing"
|
|
3999
|
+
}),
|
|
4000
|
+
checkDraft: checkMarkTheWordsDraft,
|
|
4001
|
+
critique: critiqueMarkTheWords
|
|
4002
|
+
};
|
|
4003
|
+
/** The words of a draft's text, or `null` when there is no usable text to resolve targets in. */
|
|
4004
|
+
function wordsOf(draft) {
|
|
4005
|
+
if (typeof draft.text !== "string") return null;
|
|
4006
|
+
const words = markableWords(draft.text);
|
|
4007
|
+
return words.length === 0 || words.length > 1500 ? null : words;
|
|
4008
|
+
}
|
|
4009
|
+
function checkMarkTheWordsDraft(draft) {
|
|
4010
|
+
const issues = checkIdentity(draft, "mark-the-words");
|
|
4011
|
+
issues.push(...checkScoringStrategy(draft));
|
|
4012
|
+
const text = draft.text;
|
|
4013
|
+
if (isUnwritten(text)) issues.push(issue("mw_text_required", ["text"], "Write the text the learner marks words in."));
|
|
4014
|
+
else if (typeof text === "string") {
|
|
4015
|
+
const count = markableWords(text).length;
|
|
4016
|
+
if (count === 0) issues.push(issue("mw_text_required", ["text"], "The text has no word to mark yet. Write it out."));
|
|
4017
|
+
else if (count > 1500) issues.push(issue("mw_text_too_long", ["text"], `The text has ${count} words; at most ${MARK_THE_WORDS_MAX_WORDS} can be marked in. Split it.`));
|
|
4018
|
+
}
|
|
4019
|
+
const targets = isUnset(draft.targets) ? [] : draft.targets;
|
|
4020
|
+
if (Array.isArray(targets)) {
|
|
4021
|
+
if (targets.length === 0) issues.push(issue("mw_targets_required", ["targets"], "Choose at least one word to mark."));
|
|
4022
|
+
issues.push(...checkTargets(targets, wordsOf(draft)));
|
|
4023
|
+
}
|
|
4024
|
+
issues.push(...checkSharedOptional(draft));
|
|
4025
|
+
return issues;
|
|
4026
|
+
}
|
|
4027
|
+
/**
|
|
4028
|
+
* Every target, each at its own path, where the schema reports it. Against
|
|
4029
|
+
* the text only when the text is usable: an unwritten one is already
|
|
4030
|
+
* reported, and every target would only restate that it is not found.
|
|
4031
|
+
*/
|
|
4032
|
+
function checkTargets(targets, words) {
|
|
4033
|
+
const issues = [];
|
|
4034
|
+
const owner = /* @__PURE__ */ new Map();
|
|
4035
|
+
for (const [index, target] of targets.entries()) {
|
|
4036
|
+
if (!isRecord(target)) continue;
|
|
4037
|
+
const ordinal = index + 1;
|
|
4038
|
+
const occurrence = target.occurrence;
|
|
4039
|
+
const occurrenceWellFormed = isUnset(occurrence) || typeof occurrence === "number" && Number.isInteger(occurrence) && occurrence >= 1;
|
|
4040
|
+
if (!occurrenceWellFormed) issues.push(issue("mw_target_occurrence_invalid", [
|
|
4041
|
+
"targets",
|
|
4042
|
+
index,
|
|
4043
|
+
"occurrence"
|
|
4044
|
+
], `Target ${ordinal}'s appearance must be a whole number from 1, or left out.`));
|
|
4045
|
+
if (isUnwritten(target.text)) {
|
|
4046
|
+
issues.push(issue("mw_target_text_required", [
|
|
4047
|
+
"targets",
|
|
4048
|
+
index,
|
|
4049
|
+
"text"
|
|
4050
|
+
], `Write the word or words target ${ordinal} marks.`));
|
|
4051
|
+
continue;
|
|
4052
|
+
}
|
|
4053
|
+
if (typeof target.text !== "string" || words === null || !occurrenceWellFormed) continue;
|
|
4054
|
+
const found = resolveMarkTarget(words, {
|
|
4055
|
+
text: target.text,
|
|
4056
|
+
...isUnset(occurrence) ? {} : { occurrence }
|
|
4057
|
+
});
|
|
4058
|
+
if (!found.ok) {
|
|
4059
|
+
issues.push(targetProblem(found.problem, found.found, index, target.text));
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
if (found.indices.some((word) => owner.has(word))) {
|
|
4063
|
+
issues.push(issue("mw_target_overlap", ["targets", index], `Target ${ordinal} shares a word with target ${owner.get(found.indices.find((word) => owner.has(word))) + 1}. A word belongs to one target at most.`));
|
|
4064
|
+
continue;
|
|
4065
|
+
}
|
|
4066
|
+
for (const word of found.indices) owner.set(word, index);
|
|
4067
|
+
}
|
|
4068
|
+
return issues;
|
|
4069
|
+
}
|
|
4070
|
+
function targetProblem(problem, count, index, text) {
|
|
4071
|
+
const ordinal = index + 1;
|
|
4072
|
+
switch (problem) {
|
|
4073
|
+
case "no-words": return issue("mw_target_no_words", [
|
|
4074
|
+
"targets",
|
|
4075
|
+
index,
|
|
4076
|
+
"text"
|
|
4077
|
+
], `Target ${ordinal} has no word in it: punctuation alone cannot be marked.`);
|
|
4078
|
+
case "not-found": return issue("mw_target_not_found", [
|
|
4079
|
+
"targets",
|
|
4080
|
+
index,
|
|
4081
|
+
"text"
|
|
4082
|
+
], `"${text}" is not in the text as whole words. Copy it from the text, with the same capitals and accents.`);
|
|
4083
|
+
case "ambiguous": return issue("mw_target_ambiguous", [
|
|
4084
|
+
"targets",
|
|
4085
|
+
index,
|
|
4086
|
+
"text"
|
|
4087
|
+
], `"${text}" appears ${count} times in the text. Say which one target ${ordinal} is, with its appearance (1 to ${count}).`);
|
|
4088
|
+
case "no-such-occurrence": return issue("mw_target_occurrence_invalid", [
|
|
4089
|
+
"targets",
|
|
4090
|
+
index,
|
|
4091
|
+
"occurrence"
|
|
4092
|
+
], `"${text}" appears ${count === 1 ? "once" : `${count} times`} in the text, so it has no such appearance.`);
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
/** Whether `text` writes out `answer` as whole words — a short answer never counts. */
|
|
4096
|
+
function writesOut(text, answer) {
|
|
4097
|
+
const needle = fold(answer);
|
|
4098
|
+
return needle.length > 0 && !isShort(needle) && containsWords(fold(text), needle);
|
|
4099
|
+
}
|
|
4100
|
+
function critiqueMarkTheWords(draft) {
|
|
4101
|
+
const findings = [];
|
|
4102
|
+
const words = wordsOf(draft);
|
|
4103
|
+
const targets = Array.isArray(draft.targets) ? draft.targets : [];
|
|
4104
|
+
if (words === null) return findings;
|
|
4105
|
+
const keys = words.map((word) => wordKey(word.text));
|
|
4106
|
+
const resolved = targets.map((target) => {
|
|
4107
|
+
if (!isRecord(target) || typeof target.text !== "string") return null;
|
|
4108
|
+
const found = resolveMarkTarget(words, {
|
|
4109
|
+
text: target.text,
|
|
4110
|
+
...typeof target.occurrence === "number" ? { occurrence: target.occurrence } : {}
|
|
4111
|
+
});
|
|
4112
|
+
return found.ok ? found.indices : null;
|
|
4113
|
+
});
|
|
4114
|
+
const marked = new Set(resolved.flatMap((indices) => indices ?? []));
|
|
4115
|
+
const title = typeof draft.title === "string" ? draft.title : "";
|
|
4116
|
+
if (resolved.some((indices) => indices !== null && writesOut(title, indices.map((word) => words[word].text).join(" ")))) findings.push(finding("mw_title_reveals_answer", ["title"], "The title, which the learner sees, contains a word to mark."));
|
|
4117
|
+
if (marked.size > 0 && marked.size * 2 > words.length) {
|
|
4118
|
+
const everything = Math.round((2 * marked.size - words.length) / marked.size * 100);
|
|
4119
|
+
findings.push(finding("mw_targets_dense", ["targets"], `${marked.size} of the text's ${words.length} words are to be marked, so a learner who marks every word scores ${everything}%. Mark fewer, or use a longer text.`));
|
|
4120
|
+
}
|
|
4121
|
+
resolved.forEach((indices, index) => {
|
|
4122
|
+
if (indices === null) return;
|
|
4123
|
+
const needle = indices.map((word) => keys[word]);
|
|
4124
|
+
for (let at = 0; at + needle.length <= keys.length; at += 1) if (needle.every((key, offset) => keys[at + offset] === key) && !indices.includes(at) && !marked.has(at)) {
|
|
4125
|
+
findings.push(finding("mw_target_elsewhere", ["targets", index], `"${needle.length === 1 ? words[indices[0]].text : indices.map((word) => words[word].text).join(" ")}" also appears where it is not to be marked. A learner who marks it there loses a mark: check that is meant.`));
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
4128
|
+
});
|
|
4129
|
+
return findings;
|
|
4130
|
+
}
|
|
4131
|
+
// src/schemas/matching.ts
|
|
4132
|
+
/** Zod schema for one option of the shared list. Loose: a consumer sidecar survives validation. */
|
|
4133
|
+
const MatchingOptionSchema = zod_v4.looseObject({
|
|
4134
|
+
id: zod_v4.string().min(1),
|
|
4135
|
+
label: zod_v4.string().min(1).optional(),
|
|
4136
|
+
text: zod_v4.string().min(1)
|
|
4137
|
+
});
|
|
4138
|
+
/** Zod schema for one prompt. Whether its `correctOptionId` exists is checked on the activity. */
|
|
4139
|
+
const MatchingPromptSchema = zod_v4.looseObject({
|
|
4140
|
+
id: zod_v4.string().min(1),
|
|
4141
|
+
text: zod_v4.string().min(1),
|
|
4142
|
+
correctOptionId: zod_v4.string().min(1),
|
|
4143
|
+
feedback: zod_v4.string().optional()
|
|
4144
|
+
});
|
|
4145
|
+
/** Labels compared as a learner reads them: `"A"`, `" a "` and `"a"` are one key. */
|
|
4146
|
+
function labelKey(label) {
|
|
4147
|
+
return label.trim().toLowerCase();
|
|
4148
|
+
}
|
|
4149
|
+
/** Whether every element of `values` is distinct. */
|
|
4150
|
+
const distinct = (values) => new Set(values).size === values.length;
|
|
4151
|
+
/**
|
|
4152
|
+
* Zod schema validating the full Matching data contract.
|
|
4153
|
+
*
|
|
4154
|
+
* Five semantic guards, each an authoring error that would otherwise reach a
|
|
4155
|
+
* learner:
|
|
4156
|
+
*
|
|
4157
|
+
* 1. **Prompt ids are unique** — the scorer looks a match up by prompt id, so a
|
|
4158
|
+
* duplicate makes one prompt unscoreable.
|
|
4159
|
+
* 2. **Option ids are unique** — an answer key naming an id two options share
|
|
4160
|
+
* means two right answers nobody wrote.
|
|
4161
|
+
* 3. **Option labels are unique**, ignoring case and surrounding space — two
|
|
4162
|
+
* options both labelled "C" cannot be told apart by the key the learner
|
|
4163
|
+
* answers with.
|
|
4164
|
+
* 4. **Every `correctOptionId` names an option.** An answer key pointing at an
|
|
4165
|
+
* option nobody can pick is a prompt that cannot be passed.
|
|
4166
|
+
* 5. **Under `optionUse: 'once'`, no option answers two prompts.** Otherwise
|
|
4167
|
+
* the item says each option is used once while its own key uses one twice.
|
|
4168
|
+
*/
|
|
4169
|
+
const MatchingDataSchema = zod_v4.looseObject({
|
|
4170
|
+
schemaVersion: zod_v4.literal("1.0"),
|
|
4171
|
+
type: zod_v4.literal("matching"),
|
|
4172
|
+
id: zod_v4.string().min(1),
|
|
4173
|
+
title: zod_v4.string().min(1),
|
|
4174
|
+
instructions: zod_v4.string().optional(),
|
|
4175
|
+
optionsTitle: zod_v4.string().optional(),
|
|
4176
|
+
prompts: zod_v4.array(MatchingPromptSchema).min(1),
|
|
4177
|
+
options: zod_v4.array(MatchingOptionSchema).min(2),
|
|
4178
|
+
optionUse: zod_v4.enum(["once", "many"]).optional(),
|
|
4179
|
+
scoringStrategy: zod_v4.enum(["all-or-nothing", "partial"]),
|
|
4180
|
+
shuffleOptions: zod_v4.boolean().optional(),
|
|
4181
|
+
media: MediaSchema.optional(),
|
|
4182
|
+
feedback: FeedbackSchema.optional(),
|
|
4183
|
+
passThreshold: zod_v4.number().min(0).max(1).optional(),
|
|
4184
|
+
locale: zod_v4.string().optional(),
|
|
4185
|
+
learningObjectives: zod_v4.array(zod_v4.string()).optional(),
|
|
4186
|
+
difficultyLevel: zod_v4.literal([
|
|
4187
|
+
1,
|
|
4188
|
+
2,
|
|
4189
|
+
3,
|
|
4190
|
+
4,
|
|
4191
|
+
5
|
|
4192
|
+
]).optional(),
|
|
4193
|
+
ai: AiPermissionsSchema.optional()
|
|
4194
|
+
}).refine((data) => distinct(data.prompts.map((prompt) => prompt.id)), {
|
|
4195
|
+
error: "Prompt ids must be unique.",
|
|
4196
|
+
path: ["prompts"]
|
|
4197
|
+
}).refine((data) => distinct(data.options.map((option) => option.id)), {
|
|
4198
|
+
error: "Option ids must be unique.",
|
|
4199
|
+
path: ["options"]
|
|
4200
|
+
}).refine((data) => distinct(data.options.map((option) => option.label).filter((label) => label !== void 0).map(labelKey)), {
|
|
4201
|
+
error: "Option labels must be unique.",
|
|
4202
|
+
path: ["options"]
|
|
4203
|
+
}).refine((data) => {
|
|
4204
|
+
const ids = new Set(data.options.map((option) => option.id));
|
|
4205
|
+
return data.prompts.every((prompt) => ids.has(prompt.correctOptionId));
|
|
4206
|
+
}, {
|
|
4207
|
+
error: "Each prompt needs a `correctOptionId` that is one of the options.",
|
|
4208
|
+
path: ["prompts"]
|
|
4209
|
+
}).refine((data) => data.optionUse !== "once" || distinct(data.prompts.map((prompt) => prompt.correctOptionId)), {
|
|
4210
|
+
error: "With `optionUse: \"once\"`, no option may be the answer to more than one prompt.",
|
|
4211
|
+
path: ["prompts"]
|
|
4212
|
+
});
|
|
4213
|
+
// src/authoring/matching.ts
|
|
4214
|
+
/** Mirrors `MatchingDataSchema`: `options.min(2)`. */
|
|
4215
|
+
const MIN_OPTIONS$1 = 2;
|
|
4216
|
+
const OPTION_USES = ["once", "many"];
|
|
4217
|
+
/**
|
|
4218
|
+
* Draft support for `matching`.
|
|
4219
|
+
*
|
|
4220
|
+
* A new draft has no prompts and no options, and `scoringStrategy` starts at
|
|
4221
|
+
* `all-or-nothing`, as on every built-in: partial credit is the author's to
|
|
4222
|
+
* give — though an exam's matching task is marked prompt by prompt, so one
|
|
4223
|
+
* copied from a paper wants `partial`.
|
|
4224
|
+
*
|
|
4225
|
+
* As in gap-select, no answer is pre-selected: a prompt's `correctOptionId`
|
|
4226
|
+
* starts unwritten, and the draft stays `incomplete` until someone says which
|
|
4227
|
+
* option answers each prompt.
|
|
4228
|
+
*/
|
|
4229
|
+
const matchingAuthoring = {
|
|
4230
|
+
createDraft: ({ newId }) => ({
|
|
4231
|
+
schemaVersion: "1.0",
|
|
4232
|
+
type: "matching",
|
|
4233
|
+
id: newId(),
|
|
4234
|
+
title: "",
|
|
4235
|
+
prompts: [],
|
|
4236
|
+
options: [],
|
|
4237
|
+
scoringStrategy: "all-or-nothing"
|
|
4238
|
+
}),
|
|
4239
|
+
checkDraft: checkMatchingDraft,
|
|
4240
|
+
critique: critiqueMatching
|
|
4241
|
+
};
|
|
4242
|
+
function checkMatchingDraft(draft) {
|
|
4243
|
+
const issues = checkIdentity(draft, "matching");
|
|
4244
|
+
issues.push(...checkScoringStrategy(draft));
|
|
4245
|
+
if (!isUnset(draft.optionUse) && !OPTION_USES.includes(draft.optionUse)) issues.push(issue("mt_option_use_invalid", ["optionUse"], "Say whether each option is used \"once\" or \"many\" times, or leave it out for \"many\"."));
|
|
4246
|
+
const options = isUnset(draft.options) ? [] : draft.options;
|
|
4247
|
+
const optionIds = /* @__PURE__ */ new Set();
|
|
4248
|
+
if (Array.isArray(options)) issues.push(...checkOptions(options, optionIds));
|
|
4249
|
+
const prompts = isUnset(draft.prompts) ? [] : draft.prompts;
|
|
4250
|
+
if (Array.isArray(prompts)) issues.push(...checkPrompts(prompts, optionIds, draft.optionUse === "once"));
|
|
4251
|
+
issues.push(...checkSharedOptional(draft));
|
|
4252
|
+
return issues;
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* The shared list. Its uniqueness rules are reported at `options`, where the
|
|
4256
|
+
* schema reports them; what is missing from one option, at that option.
|
|
4257
|
+
*/
|
|
4258
|
+
function checkOptions(options, optionIds) {
|
|
4259
|
+
const issues = [];
|
|
4260
|
+
const atOptions = [];
|
|
4261
|
+
if (options.length < MIN_OPTIONS$1) issues.push(issue("mt_options_too_few", ["options"], `Add at least ${MIN_OPTIONS$1} options to the list the prompts are matched with.`));
|
|
4262
|
+
const repeatedIds = /* @__PURE__ */ new Set();
|
|
4263
|
+
const labels = /* @__PURE__ */ new Map();
|
|
4264
|
+
const repeatedLabels = /* @__PURE__ */ new Set();
|
|
4265
|
+
for (const [index, option] of options.entries()) {
|
|
4266
|
+
if (!isRecord(option)) continue;
|
|
4267
|
+
const ordinal = index + 1;
|
|
4268
|
+
if (isMissingId(option.id)) issues.push(issue("mt_option_id_required", [
|
|
4269
|
+
"options",
|
|
4270
|
+
index,
|
|
4271
|
+
"id"
|
|
4272
|
+
], `Option ${ordinal} has no id.`));
|
|
4273
|
+
else if (typeof option.id === "string") {
|
|
4274
|
+
if (optionIds.has(option.id)) repeatedIds.add(option.id);
|
|
4275
|
+
optionIds.add(option.id);
|
|
4276
|
+
}
|
|
4277
|
+
if (isUnwritten(option.text)) issues.push(issue("mt_option_text_required", [
|
|
4278
|
+
"options",
|
|
4279
|
+
index,
|
|
4280
|
+
"text"
|
|
4281
|
+
], `Write the text of option ${ordinal}.`));
|
|
4282
|
+
if (typeof option.label === "string" && option.label.trim() === "") issues.push(issue("mt_option_label_required", [
|
|
4283
|
+
"options",
|
|
4284
|
+
index,
|
|
4285
|
+
"label"
|
|
4286
|
+
], `Write the label of option ${ordinal}, such as "A" or "iv", or remove it.`));
|
|
4287
|
+
else if (typeof option.label === "string") {
|
|
4288
|
+
const key = labelKey(option.label);
|
|
4289
|
+
if (labels.has(key)) repeatedLabels.add(labels.get(key));
|
|
4290
|
+
else labels.set(key, option.label.trim());
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
for (const id of repeatedIds) atOptions.push(issue("mt_option_id_duplicate", ["options"], `More than one option has the id "${id}".`));
|
|
4294
|
+
for (const label of repeatedLabels) atOptions.push(issue("mt_option_label_duplicate", ["options"], `More than one option is labelled "${label}". Learners answer with the label, so each needs its own.`));
|
|
4295
|
+
return [...issues, ...atOptions];
|
|
4296
|
+
}
|
|
4297
|
+
/**
|
|
4298
|
+
* Every prompt. Its uniqueness rule and its answer key against the list are
|
|
4299
|
+
* reported at `prompts`, where the schema reports them; a missing id, text or
|
|
4300
|
+
* key, at the prompt's own field.
|
|
4301
|
+
*/
|
|
4302
|
+
function checkPrompts(prompts, optionIds, once) {
|
|
4303
|
+
const issues = [];
|
|
4304
|
+
const atPrompts = [];
|
|
4305
|
+
if (prompts.length === 0) issues.push(issue("mt_prompts_required", ["prompts"], "Add at least one prompt to match with an option."));
|
|
4306
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4307
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
4308
|
+
const answering = /* @__PURE__ */ new Map();
|
|
4309
|
+
for (const [index, prompt] of prompts.entries()) {
|
|
4310
|
+
if (!isRecord(prompt)) continue;
|
|
4311
|
+
const ordinal = index + 1;
|
|
4312
|
+
const name = typeof prompt.id === "string" && prompt.id !== "" ? `"${prompt.id}"` : ordinal;
|
|
4313
|
+
if (isMissingId(prompt.id)) issues.push(issue("mt_prompt_id_required", [
|
|
4314
|
+
"prompts",
|
|
4315
|
+
index,
|
|
4316
|
+
"id"
|
|
4317
|
+
], `Prompt ${ordinal} has no id.`));
|
|
4318
|
+
else if (typeof prompt.id === "string") {
|
|
4319
|
+
if (seen.has(prompt.id)) repeated.add(prompt.id);
|
|
4320
|
+
seen.add(prompt.id);
|
|
4321
|
+
}
|
|
4322
|
+
if (isUnwritten(prompt.text)) issues.push(issue("mt_prompt_text_required", [
|
|
4323
|
+
"prompts",
|
|
4324
|
+
index,
|
|
4325
|
+
"text"
|
|
4326
|
+
], `Write the text of prompt ${ordinal}.`));
|
|
4327
|
+
const key = prompt.correctOptionId;
|
|
4328
|
+
if (isUnwritten(key)) issues.push(issue("mt_correct_option_required", [
|
|
4329
|
+
"prompts",
|
|
4330
|
+
index,
|
|
4331
|
+
"correctOptionId"
|
|
4332
|
+
], `Choose the option that answers prompt ${name}.`));
|
|
4333
|
+
else if (typeof key === "string") {
|
|
4334
|
+
if (!optionIds.has(key)) atPrompts.push(issue("mt_correct_option_unknown", ["prompts"], `Prompt ${name} is answered by "${key}", which is not one of the options.`));
|
|
4335
|
+
answering.set(key, [...answering.get(key) ?? [], String(name)]);
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
for (const id of repeated) atPrompts.push(issue("mt_prompt_id_duplicate", ["prompts"], `More than one prompt has the id "${id}".`));
|
|
4339
|
+
if (once) {
|
|
4340
|
+
for (const [optionId, names] of answering) if (names.length > 1) atPrompts.push(issue("mt_option_reused", ["prompts"], `Option "${optionId}" answers prompts ${names.join(" and ")}, but each option is used once. Choose another answer, or let options be used more than once.`));
|
|
4341
|
+
}
|
|
4342
|
+
return [...issues, ...atPrompts];
|
|
4343
|
+
}
|
|
3770
4344
|
// src/schemas/multiple-choice.ts
|
|
3771
4345
|
/**
|
|
3772
4346
|
* Zod schema for a single Multiple Choice option. Loose: unknown keys are
|
|
@@ -4770,6 +5344,97 @@ const GapSelectDataSchema = zod_v4.looseObject({
|
|
|
4770
5344
|
error: "Each gap needs unique choice ids and a `correctChoiceId` that is one of the choices it offers.",
|
|
4771
5345
|
path: ["gaps"]
|
|
4772
5346
|
});
|
|
5347
|
+
// src/schemas/mark-the-words.ts
|
|
5348
|
+
/** Zod schema for one target. Loose: a consumer sidecar survives validation. */
|
|
5349
|
+
const MarkTheWordsTargetSchema = zod_v4.looseObject({
|
|
5350
|
+
text: zod_v4.string().min(1),
|
|
5351
|
+
occurrence: zod_v4.number().int().min(1).optional(),
|
|
5352
|
+
feedback: zod_v4.string().optional()
|
|
5353
|
+
});
|
|
5354
|
+
/** What the schema says when a target falls on no words of the text. */
|
|
5355
|
+
const MARK_TARGET_MESSAGES = {
|
|
5356
|
+
"no-words": "A target must contain a word: punctuation alone cannot be marked.",
|
|
5357
|
+
"not-found": "A target must be whole words of the text, in order, written as the text writes them.",
|
|
5358
|
+
ambiguous: "These words appear more than once in the text: say which appearance with `occurrence`.",
|
|
5359
|
+
"no-such-occurrence": "The text does not have that many appearances of these words."
|
|
5360
|
+
};
|
|
5361
|
+
/**
|
|
5362
|
+
* Zod schema validating the full Mark the Words data contract.
|
|
5363
|
+
*
|
|
5364
|
+
* Beyond the shape, three guards, each reported at the field it is about:
|
|
5365
|
+
*
|
|
5366
|
+
* 1. **The text has a word to mark**, and at most 1500 of them.
|
|
5367
|
+
* 2. **Every target falls on whole words of the text** — once, or at the
|
|
5368
|
+
* appearance its `occurrence` names. A target that could be either of two
|
|
5369
|
+
* places is refused: which one is marked right would be a guess.
|
|
5370
|
+
* 3. **No two targets share a word.** A word marked once cannot count twice.
|
|
5371
|
+
*/
|
|
5372
|
+
const MarkTheWordsDataSchema = zod_v4.looseObject({
|
|
5373
|
+
schemaVersion: zod_v4.literal("1.0"),
|
|
5374
|
+
type: zod_v4.literal("mark-the-words"),
|
|
5375
|
+
id: zod_v4.string().min(1),
|
|
5376
|
+
title: zod_v4.string().min(1),
|
|
5377
|
+
instructions: zod_v4.string().optional(),
|
|
5378
|
+
text: zod_v4.string().min(1),
|
|
5379
|
+
targets: zod_v4.array(MarkTheWordsTargetSchema).min(1),
|
|
5380
|
+
scoringStrategy: zod_v4.enum(["all-or-nothing", "partial"]),
|
|
5381
|
+
media: MediaSchema.optional(),
|
|
5382
|
+
feedback: FeedbackSchema.optional(),
|
|
5383
|
+
passThreshold: zod_v4.number().min(0).max(1).optional(),
|
|
5384
|
+
locale: zod_v4.string().optional(),
|
|
5385
|
+
learningObjectives: zod_v4.array(zod_v4.string()).optional(),
|
|
5386
|
+
difficultyLevel: zod_v4.literal([
|
|
5387
|
+
1,
|
|
5388
|
+
2,
|
|
5389
|
+
3,
|
|
5390
|
+
4,
|
|
5391
|
+
5
|
|
5392
|
+
]).optional(),
|
|
5393
|
+
ai: AiPermissionsSchema.optional()
|
|
5394
|
+
}).superRefine((data, ctx) => {
|
|
5395
|
+
const words = markableWords(data.text);
|
|
5396
|
+
if (words.length === 0) {
|
|
5397
|
+
ctx.addIssue({
|
|
5398
|
+
code: "custom",
|
|
5399
|
+
path: ["text"],
|
|
5400
|
+
message: "The text has no word to mark."
|
|
5401
|
+
});
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
if (words.length > 1500) {
|
|
5405
|
+
ctx.addIssue({
|
|
5406
|
+
code: "custom",
|
|
5407
|
+
path: ["text"],
|
|
5408
|
+
message: `The text may have at most ${MARK_THE_WORDS_MAX_WORDS} words; it has ${words.length}.`
|
|
5409
|
+
});
|
|
5410
|
+
return;
|
|
5411
|
+
}
|
|
5412
|
+
const owner = /* @__PURE__ */ new Map();
|
|
5413
|
+
data.targets.forEach((target, index) => {
|
|
5414
|
+
const found = resolveMarkTarget(words, target);
|
|
5415
|
+
if (!found.ok) {
|
|
5416
|
+
ctx.addIssue({
|
|
5417
|
+
code: "custom",
|
|
5418
|
+
path: [
|
|
5419
|
+
"targets",
|
|
5420
|
+
index,
|
|
5421
|
+
found.problem === "no-such-occurrence" ? "occurrence" : "text"
|
|
5422
|
+
],
|
|
5423
|
+
message: MARK_TARGET_MESSAGES[found.problem]
|
|
5424
|
+
});
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5427
|
+
if (found.indices.some((word) => owner.has(word))) {
|
|
5428
|
+
ctx.addIssue({
|
|
5429
|
+
code: "custom",
|
|
5430
|
+
path: ["targets", index],
|
|
5431
|
+
message: "Two targets share a word: each word belongs to one target at most."
|
|
5432
|
+
});
|
|
5433
|
+
return;
|
|
5434
|
+
}
|
|
5435
|
+
for (const word of found.indices) owner.set(word, index);
|
|
5436
|
+
});
|
|
5437
|
+
});
|
|
4773
5438
|
// src/schemas/written-response.ts
|
|
4774
5439
|
/** Zod schema for a single rubric criterion. Loose: unknown keys preserved. */
|
|
4775
5440
|
const WrittenResponseRubricCriterionSchema = zod_v4.looseObject({
|
|
@@ -4968,6 +5633,48 @@ const RedactedGapSelectDataSchema = zod_v4.strictObject({
|
|
|
4968
5633
|
presentation: zod_v4.literal("dropdown").optional(),
|
|
4969
5634
|
shuffleChoices: zod_v4.boolean().optional()
|
|
4970
5635
|
});
|
|
5636
|
+
/** A redacted matching option: unchanged, because the learner picks from the list. */
|
|
5637
|
+
const RedactedMatchingOptionSchema = zod_v4.strictObject({
|
|
5638
|
+
id: zod_v4.string().min(1),
|
|
5639
|
+
label: zod_v4.string().min(1).optional(),
|
|
5640
|
+
text: zod_v4.string().min(1)
|
|
5641
|
+
});
|
|
5642
|
+
/** A redacted prompt: what the learner reads; `correctOptionId` and feedback are gone. */
|
|
5643
|
+
const RedactedMatchingPromptSchema = zod_v4.strictObject({
|
|
5644
|
+
id: zod_v4.string().min(1),
|
|
5645
|
+
text: zod_v4.string().min(1)
|
|
5646
|
+
});
|
|
5647
|
+
/**
|
|
5648
|
+
* Redacted Matching data. As in Gap Select, the list the learner picks from
|
|
5649
|
+
* survives in full and only the key is withheld: each prompt's
|
|
5650
|
+
* `correctOptionId`. `optionUse` survives because it changes what the learner
|
|
5651
|
+
* may do — an exam says "You may use any letter more than once" in its
|
|
5652
|
+
* instructions, and the component moves an option used once. `scoringStrategy`
|
|
5653
|
+
* stays answer-key, as on every type.
|
|
5654
|
+
*/
|
|
5655
|
+
const RedactedMatchingDataSchema = zod_v4.strictObject({
|
|
5656
|
+
...redactedBase,
|
|
5657
|
+
type: zod_v4.literal("matching"),
|
|
5658
|
+
instructions: zod_v4.string().optional(),
|
|
5659
|
+
optionsTitle: zod_v4.string().optional(),
|
|
5660
|
+
prompts: zod_v4.array(RedactedMatchingPromptSchema).min(1),
|
|
5661
|
+
options: zod_v4.array(RedactedMatchingOptionSchema).min(2),
|
|
5662
|
+
optionUse: zod_v4.enum(["once", "many"]).optional(),
|
|
5663
|
+
shuffleOptions: zod_v4.boolean().optional()
|
|
5664
|
+
});
|
|
5665
|
+
/**
|
|
5666
|
+
* Redacted Mark the Words data: the text, and nothing about which words are
|
|
5667
|
+
* right — not the targets, not even how many there are, since a count tells a
|
|
5668
|
+
* learner when to stop. `scoringStrategy` stays answer-key, as on every type:
|
|
5669
|
+
* whether a wrong mark costs anything is the difference between marking
|
|
5670
|
+
* carefully and marking everything.
|
|
5671
|
+
*/
|
|
5672
|
+
const RedactedMarkTheWordsDataSchema = zod_v4.strictObject({
|
|
5673
|
+
...redactedBase,
|
|
5674
|
+
type: zod_v4.literal("mark-the-words"),
|
|
5675
|
+
instructions: zod_v4.string().optional(),
|
|
5676
|
+
text: zod_v4.string().min(1)
|
|
5677
|
+
});
|
|
4971
5678
|
/**
|
|
4972
5679
|
* Redacted Written Response data: the prompt, word bounds and rubric are
|
|
4973
5680
|
* learner-visible (a rubric tells the learner what they are graded on);
|
|
@@ -5207,6 +5914,104 @@ function scoreGapSelect(data, response) {
|
|
|
5207
5914
|
details
|
|
5208
5915
|
};
|
|
5209
5916
|
}
|
|
5917
|
+
// src/scoring/activity-scorers/mark-the-words.ts
|
|
5918
|
+
/**
|
|
5919
|
+
* Scores a Mark the Words response.
|
|
5920
|
+
*
|
|
5921
|
+
* Each target is found when every one of its words is marked. Every marked
|
|
5922
|
+
* word that belongs to no target is a wrong mark, and costs one target's
|
|
5923
|
+
* worth: `partial` is (targets found − wrong marks) / targets, never below 0,
|
|
5924
|
+
* so marking every word is not a strategy. `all-or-nothing` is 1 only with
|
|
5925
|
+
* every target found and no wrong mark.
|
|
5926
|
+
*
|
|
5927
|
+
* A position that names no word, repeats one, or is not a whole number (a
|
|
5928
|
+
* tampered payload, or a text edited after the attempt) is ignored: it marks
|
|
5929
|
+
* nothing, right or wrong.
|
|
5930
|
+
*
|
|
5931
|
+
* Details: one per target (`t1`, `t2`… in target order) — `correct`, or
|
|
5932
|
+
* `incorrect` when only some of its words are marked, or `incorrect-omission`
|
|
5933
|
+
* when none are — with the target's word positions as `correctResponse`; then
|
|
5934
|
+
* one per wrong mark (`w` and the word's position), `incorrect`, with no
|
|
5935
|
+
* correct response. A review reads both to mark the text.
|
|
5936
|
+
*/
|
|
5937
|
+
function scoreMarkTheWords(data, response) {
|
|
5938
|
+
const count = markableWords(data.text).length;
|
|
5939
|
+
const raw = Array.isArray(response.marked) ? response.marked : [];
|
|
5940
|
+
const marked = new Set(raw.filter((word) => typeof word === "number" && Number.isInteger(word) && word >= 0).filter((word) => word < count));
|
|
5941
|
+
const targets = markTargetIndices(data);
|
|
5942
|
+
const inTargets = new Set(targets.flat());
|
|
5943
|
+
const details = [];
|
|
5944
|
+
let found = 0;
|
|
5945
|
+
targets.forEach((words, index) => {
|
|
5946
|
+
const hit = words.filter((word) => marked.has(word));
|
|
5947
|
+
const correct = words.length > 0 && hit.length === words.length;
|
|
5948
|
+
if (correct) found += 1;
|
|
5949
|
+
details.push({
|
|
5950
|
+
itemId: `t${index + 1}`,
|
|
5951
|
+
outcome: correct ? "correct" : hit.length > 0 ? "incorrect" : "incorrect-omission",
|
|
5952
|
+
learnerResponse: hit.map(String),
|
|
5953
|
+
correctResponse: words.map(String),
|
|
5954
|
+
weight: 1
|
|
5955
|
+
});
|
|
5956
|
+
});
|
|
5957
|
+
const wrong = [...marked].filter((word) => !inTargets.has(word)).sort((a, b) => a - b);
|
|
5958
|
+
for (const word of wrong) details.push({
|
|
5959
|
+
itemId: `w${word}`,
|
|
5960
|
+
outcome: "incorrect",
|
|
5961
|
+
learnerResponse: [String(word)],
|
|
5962
|
+
correctResponse: [],
|
|
5963
|
+
weight: 1
|
|
5964
|
+
});
|
|
5965
|
+
const total = targets.length;
|
|
5966
|
+
return {
|
|
5967
|
+
score: total === 0 ? 0 : data.scoringStrategy === "all-or-nothing" ? found === total && wrong.length === 0 ? 1 : 0 : Math.max(0, found - wrong.length) / total,
|
|
5968
|
+
maxScore: 1,
|
|
5969
|
+
feedback: null,
|
|
5970
|
+
details
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
// src/scoring/activity-scorers/matching.ts
|
|
5974
|
+
/**
|
|
5975
|
+
* Scores a Matching response.
|
|
5976
|
+
*
|
|
5977
|
+
* Each prompt is an identity comparison between the option the learner matched
|
|
5978
|
+
* it with and `correctOptionId`, as a gap select's gap is. A prompt with no
|
|
5979
|
+
* match, or matched with an id that is not an option (a tampered payload, or
|
|
5980
|
+
* content edited after the attempt), is scored incorrect and reported as
|
|
5981
|
+
* `incorrect-omission`, so an unanswered prompt stays distinguishable from a
|
|
5982
|
+
* wrong one downstream.
|
|
5983
|
+
*
|
|
5984
|
+
* Under `optionUse: 'once'` a response that uses one option twice is scored
|
|
5985
|
+
* prompt by prompt like any other: the SDK's component never produces one, and
|
|
5986
|
+
* a second use earns nothing the key does not give it, since the key uses each
|
|
5987
|
+
* option at most once.
|
|
5988
|
+
*/
|
|
5989
|
+
function scoreMatching(data, response) {
|
|
5990
|
+
const optionIds = new Set(data.options.map((option) => option.id));
|
|
5991
|
+
const details = [];
|
|
5992
|
+
const perPromptCorrect = [];
|
|
5993
|
+
for (const prompt of data.prompts) {
|
|
5994
|
+
const raw = response.matches[prompt.id];
|
|
5995
|
+
const matched = typeof raw === "string" ? raw : "";
|
|
5996
|
+
const answered = matched !== "" && optionIds.has(matched);
|
|
5997
|
+
const correct = answered && matched === prompt.correctOptionId;
|
|
5998
|
+
perPromptCorrect.push(correct);
|
|
5999
|
+
details.push({
|
|
6000
|
+
itemId: prompt.id,
|
|
6001
|
+
outcome: correct ? "correct" : answered ? "incorrect" : "incorrect-omission",
|
|
6002
|
+
learnerResponse: [matched],
|
|
6003
|
+
correctResponse: [prompt.correctOptionId],
|
|
6004
|
+
weight: 1
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
6007
|
+
const correctPrompts = perPromptCorrect.filter(Boolean).length;
|
|
6008
|
+
return {
|
|
6009
|
+
score: data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perPromptCorrect) : partialBlankStrategy(correctPrompts, data.prompts.length),
|
|
6010
|
+
maxScore: 1,
|
|
6011
|
+
feedback: null,
|
|
6012
|
+
details
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
5210
6015
|
// src/scoring/activity-scorers/multiple-choice.ts
|
|
5211
6016
|
/**
|
|
5212
6017
|
* Scores a Multiple Choice response. Pure; trusts its typed inputs (validation
|
|
@@ -5469,6 +6274,45 @@ const GAP_SELECT_FIELD_POLICY = {
|
|
|
5469
6274
|
feedback: "answer-key"
|
|
5470
6275
|
}
|
|
5471
6276
|
};
|
|
6277
|
+
/**
|
|
6278
|
+
* Matching: as in Gap Select, the list the learner picks from is public, key
|
|
6279
|
+
* by key, and each prompt's `correctOptionId` is the one field withheld.
|
|
6280
|
+
* `optionUse` is public because it changes what the learner may do: the
|
|
6281
|
+
* component moves an option used once, and the paper says so.
|
|
6282
|
+
*/
|
|
6283
|
+
const MATCHING_FIELD_POLICY = {
|
|
6284
|
+
...SHARED_PUBLIC_FIELDS,
|
|
6285
|
+
instructions: "public",
|
|
6286
|
+
optionsTitle: "public",
|
|
6287
|
+
optionUse: "public",
|
|
6288
|
+
shuffleOptions: "public",
|
|
6289
|
+
scoringStrategy: "answer-key",
|
|
6290
|
+
feedback: FEEDBACK_FIELD_POLICY,
|
|
6291
|
+
options: {
|
|
6292
|
+
id: "public",
|
|
6293
|
+
label: "public",
|
|
6294
|
+
text: "public"
|
|
6295
|
+
},
|
|
6296
|
+
prompts: {
|
|
6297
|
+
id: "public",
|
|
6298
|
+
text: "public",
|
|
6299
|
+
correctOptionId: "answer-key",
|
|
6300
|
+
feedback: "answer-key"
|
|
6301
|
+
}
|
|
6302
|
+
};
|
|
6303
|
+
/**
|
|
6304
|
+
* Mark the Words: the text is what the learner marks in, so it is public; the
|
|
6305
|
+
* targets are the key, as one leaf — their number included, since a count
|
|
6306
|
+
* tells a learner when to stop marking.
|
|
6307
|
+
*/
|
|
6308
|
+
const MARK_THE_WORDS_FIELD_POLICY = {
|
|
6309
|
+
...SHARED_PUBLIC_FIELDS,
|
|
6310
|
+
instructions: "public",
|
|
6311
|
+
text: "public",
|
|
6312
|
+
targets: "answer-key",
|
|
6313
|
+
scoringStrategy: "answer-key",
|
|
6314
|
+
feedback: FEEDBACK_FIELD_POLICY
|
|
6315
|
+
};
|
|
5472
6316
|
const FILL_IN_THE_BLANKS_FIELD_POLICY = {
|
|
5473
6317
|
...SHARED_PUBLIC_FIELDS,
|
|
5474
6318
|
instructions: "public",
|
|
@@ -5649,6 +6493,50 @@ const gapSelectType = defineActivityType({
|
|
|
5649
6493
|
authoring: gapSelectAuthoring,
|
|
5650
6494
|
mediaSlots: () => MAIN_MEDIA
|
|
5651
6495
|
});
|
|
6496
|
+
/** Built-in Matching descriptor. */
|
|
6497
|
+
const matchingType = defineActivityType({
|
|
6498
|
+
type: "matching",
|
|
6499
|
+
schema: MatchingDataSchema,
|
|
6500
|
+
scoring: {
|
|
6501
|
+
kind: "sync",
|
|
6502
|
+
score: scoreMatching
|
|
6503
|
+
},
|
|
6504
|
+
isAnswered: (response) => Object.values(response?.matches ?? {}).some((optionId) => optionId !== ""),
|
|
6505
|
+
fieldPolicy: MATCHING_FIELD_POLICY,
|
|
6506
|
+
redactedSchema: RedactedMatchingDataSchema,
|
|
6507
|
+
interop: {
|
|
6508
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
6509
|
+
xapiInteractionType: "matching",
|
|
6510
|
+
correctResponsesPattern: (data) => [data.prompts.map((prompt) => `${prompt.id}[.]${prompt.correctOptionId}`).join("[,]")]
|
|
6511
|
+
},
|
|
6512
|
+
interactions: ["option-matched", "submitted"],
|
|
6513
|
+
authoring: matchingAuthoring,
|
|
6514
|
+
mediaSlots: () => MAIN_MEDIA
|
|
6515
|
+
});
|
|
6516
|
+
/** Built-in Mark the Words descriptor. */
|
|
6517
|
+
const markTheWordsType = defineActivityType({
|
|
6518
|
+
type: "mark-the-words",
|
|
6519
|
+
schema: MarkTheWordsDataSchema,
|
|
6520
|
+
scoring: {
|
|
6521
|
+
kind: "sync",
|
|
6522
|
+
score: scoreMarkTheWords
|
|
6523
|
+
},
|
|
6524
|
+
isAnswered: (response) => Array.isArray(response?.marked) && response.marked.length > 0,
|
|
6525
|
+
fieldPolicy: MARK_THE_WORDS_FIELD_POLICY,
|
|
6526
|
+
redactedSchema: RedactedMarkTheWordsDataSchema,
|
|
6527
|
+
interop: {
|
|
6528
|
+
xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
|
6529
|
+
xapiInteractionType: "choice",
|
|
6530
|
+
correctResponsesPattern: (data) => [markTargetIndices(data).flat().sort((a, b) => a - b).join("[,]")]
|
|
6531
|
+
},
|
|
6532
|
+
interactions: [
|
|
6533
|
+
"word-marked",
|
|
6534
|
+
"word-unmarked",
|
|
6535
|
+
"submitted"
|
|
6536
|
+
],
|
|
6537
|
+
authoring: markTheWordsAuthoring,
|
|
6538
|
+
mediaSlots: () => MAIN_MEDIA
|
|
6539
|
+
});
|
|
5652
6540
|
/**
|
|
5653
6541
|
* Built-in Dictation descriptor. Graded synchronously: the score is the
|
|
5654
6542
|
* character-level similarity of the typed text to the transcript, a pure
|
|
@@ -5825,7 +6713,9 @@ const BUILT_IN_DESCRIPTORS = {
|
|
|
5825
6713
|
],
|
|
5826
6714
|
authoring: speakingResponseAuthoring,
|
|
5827
6715
|
mediaSlots: () => MAIN_MEDIA
|
|
5828
|
-
})
|
|
6716
|
+
}),
|
|
6717
|
+
matching: matchingType,
|
|
6718
|
+
"mark-the-words": markTheWordsType
|
|
5829
6719
|
};
|
|
5830
6720
|
// src/schemas/read-schema.ts
|
|
5831
6721
|
/**
|
|
@@ -6079,6 +6969,18 @@ Object.defineProperty(exports, "LEVEL_PROBABILITY_TOLERANCE", {
|
|
|
6079
6969
|
return LEVEL_PROBABILITY_TOLERANCE;
|
|
6080
6970
|
}
|
|
6081
6971
|
});
|
|
6972
|
+
Object.defineProperty(exports, "MARK_THE_WORDS_MAX_WORDS", {
|
|
6973
|
+
enumerable: true,
|
|
6974
|
+
get: function() {
|
|
6975
|
+
return MARK_THE_WORDS_MAX_WORDS;
|
|
6976
|
+
}
|
|
6977
|
+
});
|
|
6978
|
+
Object.defineProperty(exports, "MARK_THE_WORDS_TOKENIZER", {
|
|
6979
|
+
enumerable: true,
|
|
6980
|
+
get: function() {
|
|
6981
|
+
return MARK_THE_WORDS_TOKENIZER;
|
|
6982
|
+
}
|
|
6983
|
+
});
|
|
6082
6984
|
Object.defineProperty(exports, "MEDIA_FIELD_POLICY", {
|
|
6083
6985
|
enumerable: true,
|
|
6084
6986
|
get: function() {
|
|
@@ -6391,6 +7293,24 @@ Object.defineProperty(exports, "levenshteinDistance", {
|
|
|
6391
7293
|
return levenshteinDistance;
|
|
6392
7294
|
}
|
|
6393
7295
|
});
|
|
7296
|
+
Object.defineProperty(exports, "markTargetIndices", {
|
|
7297
|
+
enumerable: true,
|
|
7298
|
+
get: function() {
|
|
7299
|
+
return markTargetIndices;
|
|
7300
|
+
}
|
|
7301
|
+
});
|
|
7302
|
+
Object.defineProperty(exports, "markTheWordsFromMarkup", {
|
|
7303
|
+
enumerable: true,
|
|
7304
|
+
get: function() {
|
|
7305
|
+
return markTheWordsFromMarkup;
|
|
7306
|
+
}
|
|
7307
|
+
});
|
|
7308
|
+
Object.defineProperty(exports, "markableWords", {
|
|
7309
|
+
enumerable: true,
|
|
7310
|
+
get: function() {
|
|
7311
|
+
return markableWords;
|
|
7312
|
+
}
|
|
7313
|
+
});
|
|
6394
7314
|
Object.defineProperty(exports, "matchText", {
|
|
6395
7315
|
enumerable: true,
|
|
6396
7316
|
get: function() {
|
|
@@ -6487,6 +7407,12 @@ Object.defineProperty(exports, "registeredActivityTypes", {
|
|
|
6487
7407
|
return registeredActivityTypes;
|
|
6488
7408
|
}
|
|
6489
7409
|
});
|
|
7410
|
+
Object.defineProperty(exports, "resolveMarkTarget", {
|
|
7411
|
+
enumerable: true,
|
|
7412
|
+
get: function() {
|
|
7413
|
+
return resolveMarkTarget;
|
|
7414
|
+
}
|
|
7415
|
+
});
|
|
6490
7416
|
Object.defineProperty(exports, "revealsAnswer", {
|
|
6491
7417
|
enumerable: true,
|
|
6492
7418
|
get: function() {
|
|
@@ -6512,4 +7438,4 @@ Object.defineProperty(exports, "writtenResponseType", {
|
|
|
6512
7438
|
}
|
|
6513
7439
|
});
|
|
6514
7440
|
|
|
6515
|
-
//# sourceMappingURL=registry-
|
|
7441
|
+
//# sourceMappingURL=registry-Dw19lNo2.cjs.map
|