@pie-element/simple-cloze 0.1.4-next.5 → 0.1.4-next.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.
Files changed (39) hide show
  1. package/author.js +2 -0
  2. package/dist/author/index.d.ts +6 -0
  3. package/dist/author/index.js +12147 -21505
  4. package/dist/author/index.js.map +1 -1
  5. package/dist/browser/author/index.js +9202 -16240
  6. package/dist/browser/author/index.js.map +1 -1
  7. package/dist/browser/{client-Dt8SOe5Z.js → client-BSTc3boO.js} +650 -755
  8. package/dist/browser/client-BSTc3boO.js.map +1 -0
  9. package/dist/browser/controller/index.js +42 -32
  10. package/dist/browser/controller/index.js.map +1 -1
  11. package/dist/browser/delivery/index.js +129 -136
  12. package/dist/browser/delivery/index.js.map +1 -1
  13. package/dist/browser/dist-CYpSKy-4.js +80 -0
  14. package/dist/browser/dist-CYpSKy-4.js.map +1 -0
  15. package/dist/browser/i18n-DJMxdTMU.js +1612 -0
  16. package/dist/browser/i18n-DJMxdTMU.js.map +1 -0
  17. package/dist/browser/plain-text-iaQZhkGe.js +53 -0
  18. package/dist/browser/plain-text-iaQZhkGe.js.map +1 -0
  19. package/dist/browser/print/index.js +64 -21
  20. package/dist/browser/print/index.js.map +1 -1
  21. package/dist/controller/defaults.d.ts +1 -0
  22. package/dist/controller/index.d.ts +51 -8
  23. package/dist/controller/index.js +121 -29
  24. package/dist/controller/index.js.map +1 -1
  25. package/dist/controller/plain-text.d.ts +11 -0
  26. package/dist/delivery/index.d.ts +9 -4
  27. package/dist/delivery/index.js +2826 -267
  28. package/dist/delivery/index.js.map +1 -1
  29. package/dist/i18n.d.ts +7 -0
  30. package/dist/index.iife.js +9 -3
  31. package/dist/index.js +2826 -267
  32. package/dist/index.js.map +1 -1
  33. package/dist/print/index.d.ts +7 -0
  34. package/dist/print/index.js +2775 -28
  35. package/dist/print/index.js.map +1 -1
  36. package/module/print.js +2383 -600
  37. package/module/print.js.map +1 -1
  38. package/package.json +2 -1
  39. package/dist/browser/client-Dt8SOe5Z.js.map +0 -1
@@ -1,8 +1,51 @@
1
- export declare const getCorrectness: (question: any, session: any) => "unanswered" | "correct" | "incorrect";
2
- export declare const getPartialScore: (_question: any, session: any) => 0 | 1;
3
- export declare const outcome: (question: any, session: any, env: any) => Promise<unknown>;
4
- export declare const createDefaultModel: (model?: any) => any;
5
- export declare const normalizeSession: (s: any) => any;
6
- export declare const model: (question: any, session: any, env: any) => Promise<unknown>;
7
- export declare const createCorrectResponseSession: (question: any, env: any) => Promise<unknown>;
8
- export declare const validate: (model?: any, _config?: any) => any;
1
+ export type SimpleClozeQuestion = {
2
+ id?: string;
3
+ element?: string;
4
+ prompt?: string;
5
+ promptEnabled?: boolean;
6
+ correctAnswer?: string;
7
+ teacherInstructions?: string;
8
+ teacherInstructionsEnabled?: boolean;
9
+ /** The learner-facing strings' language, as `@pie-lib/translator` reads it (`en_US`, `es_ES`, …). */
10
+ language?: string;
11
+ [key: string]: unknown;
12
+ };
13
+ export type SimpleClozeSession = {
14
+ id?: string;
15
+ element?: string;
16
+ /** The learner's typed answer. */
17
+ value?: unknown;
18
+ [key: string]: unknown;
19
+ };
20
+ export type SimpleClozeEnv = {
21
+ mode?: string;
22
+ role?: string;
23
+ [key: string]: unknown;
24
+ };
25
+ export type SimpleClozeCorrectness = 'correct' | 'incorrect' | 'unanswered';
26
+ export type SimpleClozeOutcome = {
27
+ score: number;
28
+ empty: boolean;
29
+ traceLog: string[];
30
+ };
31
+ type FieldConfig = {
32
+ required?: boolean;
33
+ };
34
+ export type SimpleClozeValidationConfig = {
35
+ prompt?: FieldConfig;
36
+ [key: string]: unknown;
37
+ };
38
+ /**
39
+ * A response with no text is unanswered; an answer key with no text matches
40
+ * nothing, so a blank key never scores.
41
+ */
42
+ export declare const getCorrectness: (question: SimpleClozeQuestion | null | undefined, session: SimpleClozeSession | null | undefined) => SimpleClozeCorrectness;
43
+ /** Single response, all-or-nothing: 1 when it matches the answer key, else 0. */
44
+ export declare const getPartialScore: (question: SimpleClozeQuestion | null | undefined, session: SimpleClozeSession | null | undefined) => number;
45
+ export declare const outcome: (question: SimpleClozeQuestion | null | undefined, session: SimpleClozeSession | null | undefined, _env?: SimpleClozeEnv) => Promise<SimpleClozeOutcome>;
46
+ export declare const createDefaultModel: (model?: SimpleClozeQuestion) => SimpleClozeQuestion;
47
+ export declare const normalizeSession: (s: SimpleClozeSession | null | undefined) => SimpleClozeSession;
48
+ export declare const model: (question: SimpleClozeQuestion | null | undefined, session: SimpleClozeSession | null | undefined, env: SimpleClozeEnv | null | undefined) => Promise<Record<string, unknown>>;
49
+ export declare const createCorrectResponseSession: (question: SimpleClozeQuestion | null | undefined, env: SimpleClozeEnv | null | undefined) => Promise<SimpleClozeSession | null>;
50
+ export declare const validate: (model?: SimpleClozeQuestion, config?: SimpleClozeValidationConfig) => Record<string, string>;
51
+ export {};
@@ -3,7 +3,8 @@ var defaults_default = {
3
3
  model: {
4
4
  prompt: "<p>Enter your answer:</p>",
5
5
  promptEnabled: true,
6
- correctAnswer: ""
6
+ correctAnswer: "",
7
+ teacherInstructionsEnabled: true
7
8
  },
8
9
  configuration: { prompt: {
9
10
  label: "Prompt",
@@ -11,32 +12,120 @@ var defaults_default = {
11
12
  } }
12
13
  };
13
14
  //#endregion
15
+ //#region src/controller/plain-text.ts
16
+ /**
17
+ * The text an HTML fragment reads as, computed without a DOM because
18
+ * controllers also run server-side. An answer key authored in an editor is HTML
19
+ * (`<p>Salt &amp; pepper</p>`) while the learner types plain text.
20
+ */
21
+ var LATIN_1 = [
22
+ "nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr",
23
+ "deg plusmn sup2 sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest",
24
+ "Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml",
25
+ "ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig",
26
+ "agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml",
27
+ "eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml"
28
+ ].join(" ");
29
+ var GREEK = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigmaf sigma tau upsilon phi chi psi omega";
30
+ var NAMED = new Map(Object.entries({
31
+ quot: 34,
32
+ amp: 38,
33
+ apos: 39,
34
+ lt: 60,
35
+ gt: 62,
36
+ ensp: 8194,
37
+ emsp: 8195,
38
+ thinsp: 8201,
39
+ ndash: 8211,
40
+ mdash: 8212,
41
+ lsquo: 8216,
42
+ rsquo: 8217,
43
+ sbquo: 8218,
44
+ ldquo: 8220,
45
+ rdquo: 8221,
46
+ bdquo: 8222,
47
+ bull: 8226,
48
+ hellip: 8230,
49
+ prime: 8242,
50
+ Prime: 8243,
51
+ euro: 8364,
52
+ trade: 8482,
53
+ minus: 8722,
54
+ radic: 8730,
55
+ infin: 8734,
56
+ asymp: 8776,
57
+ ne: 8800,
58
+ le: 8804,
59
+ ge: 8805
60
+ }));
61
+ for (const [i, name] of LATIN_1.split(" ").entries()) NAMED.set(name, 160 + i);
62
+ for (const [i, name] of GREEK.split(" ").entries()) {
63
+ NAMED.set(name, 945 + i);
64
+ if (name !== "sigmaf") NAMED.set(name[0].toUpperCase() + name.slice(1), 913 + i);
65
+ }
66
+ var BLOCK_BOUNDARY = /<\/?(?:br|p|div|li|h[1-6]|blockquote|pre|tr|td|th)\b[^>]*>/gi;
67
+ var TAG = /<\/?[a-z][^>]*>|<!--[\s\S]*?-->/gi;
68
+ var ENTITY = /&(#[0-9]+|#x[0-9a-f]+|[a-z][a-z0-9]*);/gi;
69
+ var decodeEntity = (match, body) => {
70
+ if (body[0] === "#") {
71
+ const hex = body[1] === "x" || body[1] === "X";
72
+ const code = Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);
73
+ const valid = code > 0 && code <= 1114111 && (code < 55296 || code > 57343);
74
+ return String.fromCodePoint(valid ? code : 65533);
75
+ }
76
+ const code = NAMED.get(body);
77
+ return code === void 0 ? match : String.fromCodePoint(code);
78
+ };
79
+ /**
80
+ * Tags stripped, entities decoded (named and numeric), and each run of
81
+ * whitespace, `&nbsp;` included, collapsed to one space and trimmed. An unknown
82
+ * named entity stays as written.
83
+ */
84
+ var toPlainText = (value) => typeof value === "string" ? value.replace(BLOCK_BOUNDARY, " ").replace(TAG, "").replace(ENTITY, decodeEntity).replace(/\s+/g, " ").trim() : "";
85
+ //#endregion
14
86
  //#region src/controller/index.ts
15
- var isEmptySession = (session) => !session || typeof session === "object" && Object.keys(session).length === 0;
87
+ /**
88
+ * Both sides compare as plain text, case-insensitively: an answer key authored
89
+ * as HTML matches the text a learner types.
90
+ */
91
+ var comparable = (value) => toPlainText(value).toLowerCase();
92
+ var hasContent = (html) => typeof html === "string" && html.replace(/(<(?!img|iframe|source)([^>]+)>)/gi, "").replace(/&nbsp;/gi, " ").trim().length > 0;
93
+ /**
94
+ * A response with no text is unanswered; an answer key with no text matches
95
+ * nothing, so a blank key never scores.
96
+ */
16
97
  var getCorrectness = (question, session) => {
17
- if (!session?.response) return "unanswered";
18
- const correctAnswer = question?.correctAnswer || "";
19
- if ((session.response || "").trim().toLowerCase() === correctAnswer.trim().toLowerCase()) return "correct";
98
+ const response = comparable(session?.value);
99
+ if (!response) return "unanswered";
100
+ const key = comparable(question?.correctAnswer);
101
+ if (key && response === key) return "correct";
20
102
  return "incorrect";
21
103
  };
22
- var getPartialScore = (_question, session) => {
23
- if (isEmptySession(session)) return 0;
24
- return 1;
104
+ /** Single response, all-or-nothing: 1 when it matches the answer key, else 0. */
105
+ var getPartialScore = (question, session) => getCorrectness(question, session) === "correct" ? 1 : 0;
106
+ var getTraceLog = (question, correctness, score) => {
107
+ const traceLog = ["Response is compared with the answer key as plain text, ignoring case and extra spaces."];
108
+ if (!comparable(question?.correctAnswer)) traceLog.push("No correct answer is defined, so no response can score.");
109
+ else traceLog.push(correctness === "correct" ? "Response matches the answer key." : "Response does not match the answer key.");
110
+ traceLog.push(`Final score: ${score}.`);
111
+ return traceLog;
25
112
  };
26
- var outcome = (question, session, env) => new Promise((resolve) => {
27
- if (isEmptySession(session)) {
113
+ var outcome = (question, session, _env = {}) => new Promise((resolve) => {
114
+ const correctness = getCorrectness(question, session);
115
+ if (correctness === "unanswered") {
28
116
  resolve({
29
117
  score: 0,
30
- empty: true
118
+ empty: true,
119
+ traceLog: ["Student did not enter a response. Score is 0."]
31
120
  });
32
121
  return;
33
122
  }
34
- session = normalizeSession(session);
35
- if (env.mode !== "evaluate") resolve({
36
- score: void 0,
37
- completed: void 0
123
+ const score = correctness === "correct" ? 1 : 0;
124
+ resolve({
125
+ score,
126
+ empty: false,
127
+ traceLog: getTraceLog(question, correctness, score)
38
128
  });
39
- else resolve({ score: getCorrectness(question, session) === "correct" ? 1 : 0 });
40
129
  });
41
130
  var createDefaultModel = (model = {}) => ({
42
131
  ...defaults_default.model,
@@ -45,35 +134,38 @@ var createDefaultModel = (model = {}) => ({
45
134
  var normalizeSession = (s) => ({ ...s });
46
135
  var model = (question, session, env) => {
47
136
  return new Promise((resolve) => {
48
- session = session || {};
49
- const normalizedQuestion = createDefaultModel(question);
137
+ const safeSession = session || {};
138
+ const safeEnv = env || {};
139
+ const normalizedQuestion = createDefaultModel(question || {});
50
140
  const out = {
51
141
  prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,
52
- disabled: env.mode !== "gather",
53
- view: env.mode === "view",
54
- env
142
+ disabled: safeEnv.mode !== "gather",
143
+ mode: safeEnv.mode,
144
+ role: safeEnv.role,
145
+ language: normalizedQuestion.language
55
146
  };
56
- if (env.mode === "evaluate") {
57
- out.correctness = getCorrectness(normalizedQuestion, session);
58
- out.correctAnswer = normalizedQuestion.correctAnswer;
147
+ if (safeEnv.mode === "evaluate") {
148
+ out.correctness = getCorrectness(normalizedQuestion, safeSession);
149
+ out.correctAnswer = toPlainText(normalizedQuestion.correctAnswer);
59
150
  }
60
- if (env.role === "instructor" && (env.mode === "view" || env.mode === "evaluate")) out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled ? normalizedQuestion.teacherInstructions : null;
151
+ if (safeEnv.role === "instructor" && (safeEnv.mode === "view" || safeEnv.mode === "evaluate")) out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled ? normalizedQuestion.teacherInstructions : null;
61
152
  else out.teacherInstructions = null;
62
153
  resolve(out);
63
154
  });
64
155
  };
65
156
  var createCorrectResponseSession = (question, env) => {
66
157
  return new Promise((resolve) => {
67
- if (env.mode !== "evaluate" && env.role === "instructor") resolve({
158
+ if (env?.mode !== "evaluate" && env?.role === "instructor") resolve({
68
159
  id: "1",
69
- response: question.correctAnswer || ""
160
+ value: toPlainText(question?.correctAnswer)
70
161
  });
71
162
  else resolve(null);
72
163
  });
73
164
  };
74
- var validate = (model = {}, _config = {}) => {
165
+ var validate = (model = {}, config = {}) => {
75
166
  const errors = {};
76
- if (!model.prompt || model.prompt.trim() === "" || model.prompt === "<p></p>") errors.prompt = "Prompt is required";
167
+ if (config?.prompt?.required && !hasContent(model?.prompt)) errors.prompt = "This field is required.";
168
+ if (!comparable(model?.correctAnswer)) errors.correctAnswer = "No correct answer defined.";
77
169
  return errors;
78
170
  };
79
171
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/controller/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["export default {\n model: {\n prompt: '<p>Enter your answer:</p>',\n promptEnabled: true,\n correctAnswer: '',\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import defaults from './defaults';\n\nconst isEmptySession = (session: unknown) =>\n !session || (typeof session === 'object' && Object.keys(session).length === 0);\n\nexport const getCorrectness = (question: any, session: any) => {\n if (!session?.response) {\n return 'unanswered';\n }\n\n const correctAnswer = question?.correctAnswer || '';\n const userAnswer = session.response || '';\n\n // Simple string comparison for now\n if (userAnswer.trim().toLowerCase() === correctAnswer.trim().toLowerCase()) {\n return 'correct';\n }\n\n return 'incorrect';\n};\n\nexport const getPartialScore = (_question: any, session: any) => {\n if (isEmptySession(session)) {\n return 0;\n }\n\n return 1;\n};\n\nexport const outcome = (question: any, session: any, env: any) =>\n new Promise((resolve) => {\n if (isEmptySession(session)) {\n resolve({ score: 0, empty: true });\n return;\n }\n\n session = normalizeSession(session);\n\n if (env.mode !== 'evaluate') {\n resolve({ score: undefined, completed: undefined });\n } else {\n const correctness = getCorrectness(question, session);\n const score = correctness === 'correct' ? 1 : 0;\n resolve({ score });\n }\n });\n\nexport const createDefaultModel = (model: any = {}) => ({ ...defaults.model, ...model });\n\nexport const normalizeSession = (s: any) => ({ ...s });\n\nexport const model = (question: any, session: any, env: any) => {\n return new Promise((resolve) => {\n session = session || {};\n const normalizedQuestion = createDefaultModel(question);\n\n const out: any = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n disabled: env.mode !== 'gather',\n view: env.mode === 'view',\n env,\n };\n\n if (env.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, session);\n out.correctness = correctness;\n out.correctAnswer = normalizedQuestion.correctAnswer;\n }\n\n if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n resolve(out);\n });\n};\n\nexport const createCorrectResponseSession = (question: any, env: any) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n resolve({ id: '1', response: question.correctAnswer || '' });\n } else {\n resolve(null);\n }\n });\n};\n\nexport const validate = (model: any = {}, _config: any = {}) => {\n const errors: any = {};\n\n if (!model.prompt || model.prompt.trim() === '' || model.prompt === '<p></p>') {\n errors.prompt = 'Prompt is required';\n }\n\n return errors;\n};\n"],"mappings":";AAAA,IAAA,mBAAe;CACb,OAAO;EACL,QAAQ;EACR,eAAe;EACf,eAAe;CACjB;CACA,eAAe,EACb,QAAQ;EACN,OAAO;EACP,UAAU;CACZ,EACF;AACF;;;ACVA,IAAM,kBAAkB,YACtB,CAAC,WAAY,OAAO,YAAY,YAAY,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW;AAE9E,IAAa,kBAAkB,UAAe,YAAiB;CAC7D,IAAI,CAAC,SAAS,UACZ,OAAO;CAGT,MAAM,gBAAgB,UAAU,iBAAiB;CAIjD,KAHmB,QAAQ,YAAY,GAAA,CAGxB,KAAK,CAAC,CAAC,YAAY,MAAM,cAAc,KAAK,CAAC,CAAC,YAAY,GACvE,OAAO;CAGT,OAAO;AACT;AAEA,IAAa,mBAAmB,WAAgB,YAAiB;CAC/D,IAAI,eAAe,OAAO,GACxB,OAAO;CAGT,OAAO;AACT;AAEA,IAAa,WAAW,UAAe,SAAc,QACnD,IAAI,SAAS,YAAY;CACvB,IAAI,eAAe,OAAO,GAAG;EAC3B,QAAQ;GAAE,OAAO;GAAG,OAAO;EAAK,CAAC;EACjC;CACF;CAEA,UAAU,iBAAiB,OAAO;CAElC,IAAI,IAAI,SAAS,YACf,QAAQ;EAAE,OAAO,KAAA;EAAW,WAAW,KAAA;CAAU,CAAC;MAIlD,QAAQ,EAAE,OAFU,eAAe,UAAU,OAC/B,MAAgB,YAAY,IAAI,EAC9B,CAAC;AAErB,CAAC;AAEH,IAAa,sBAAsB,QAAa,CAAC,OAAO;CAAE,GAAG,iBAAS;CAAO,GAAG;AAAM;AAEtF,IAAa,oBAAoB,OAAY,EAAE,GAAG,EAAE;AAEpD,IAAa,SAAS,UAAe,SAAc,QAAa;CAC9D,OAAO,IAAI,SAAS,YAAY;EAC9B,UAAU,WAAW,CAAC;EACtB,MAAM,qBAAqB,mBAAmB,QAAQ;EAEtD,MAAM,MAAW;GACf,QAAQ,mBAAmB,gBAAgB,mBAAmB,SAAS;GACvE,UAAU,IAAI,SAAS;GACvB,MAAM,IAAI,SAAS;GACnB;EACF;EAEA,IAAI,IAAI,SAAS,YAAY;GAE3B,IAAI,cADgB,eAAe,oBAAoB,OACrC;GAClB,IAAI,gBAAgB,mBAAmB;EACzC;EAEA,IAAI,IAAI,SAAS,iBAAiB,IAAI,SAAS,UAAU,IAAI,SAAS,aACpE,IAAI,sBAAsB,mBAAmB,6BACzC,mBAAmB,sBACnB;OAEJ,IAAI,sBAAsB;EAG5B,QAAQ,GAAG;CACb,CAAC;AACH;AAEA,IAAa,gCAAgC,UAAe,QAAa;CACvE,OAAO,IAAI,SAAS,YAAY;EAC9B,IAAI,IAAI,SAAS,cAAc,IAAI,SAAS,cAC1C,QAAQ;GAAE,IAAI;GAAK,UAAU,SAAS,iBAAiB;EAAG,CAAC;OAE3D,QAAQ,IAAI;CAEhB,CAAC;AACH;AAEA,IAAa,YAAY,QAAa,CAAC,GAAG,UAAe,CAAC,MAAM;CAC9D,MAAM,SAAc,CAAC;CAErB,IAAI,CAAC,MAAM,UAAU,MAAM,OAAO,KAAK,MAAM,MAAM,MAAM,WAAW,WAClE,OAAO,SAAS;CAGlB,OAAO;AACT"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/controller/defaults.ts","../../src/controller/plain-text.ts","../../src/controller/index.ts"],"sourcesContent":["export default {\n model: {\n prompt: '<p>Enter your answer:</p>',\n promptEnabled: true,\n correctAnswer: '',\n teacherInstructionsEnabled: true,\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","/**\n * The text an HTML fragment reads as, computed without a DOM because\n * controllers also run server-side. An answer key authored in an editor is HTML\n * (`<p>Salt &amp; pepper</p>`) while the learner types plain text.\n */\n\n// HTML 4's Latin-1 entities name U+00A0 to U+00FF in code point order.\nconst LATIN_1 = [\n 'nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr',\n 'deg plusmn sup2 sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest',\n 'Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml',\n 'ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig',\n 'agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml',\n 'eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml',\n].join(' ');\n\n// Small Greek letters from U+03B1. Each capital sits 32 below its small letter;\n// final sigma has none, and U+03A2 is unassigned.\nconst GREEK =\n 'alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho ' +\n 'sigmaf sigma tau upsilon phi chi psi omega';\n\nconst SYMBOLS: Record<string, number> = {\n quot: 0x22,\n amp: 0x26,\n apos: 0x27,\n lt: 0x3c,\n gt: 0x3e,\n ensp: 0x2002,\n emsp: 0x2003,\n thinsp: 0x2009,\n ndash: 0x2013,\n mdash: 0x2014,\n lsquo: 0x2018,\n rsquo: 0x2019,\n sbquo: 0x201a,\n ldquo: 0x201c,\n rdquo: 0x201d,\n bdquo: 0x201e,\n bull: 0x2022,\n hellip: 0x2026,\n prime: 0x2032,\n Prime: 0x2033,\n euro: 0x20ac,\n trade: 0x2122,\n minus: 0x2212,\n radic: 0x221a,\n infin: 0x221e,\n asymp: 0x2248,\n ne: 0x2260,\n le: 0x2264,\n ge: 0x2265,\n};\n\nconst NAMED = new Map<string, number>(Object.entries(SYMBOLS));\nfor (const [i, name] of LATIN_1.split(' ').entries()) {\n NAMED.set(name, 0xa0 + i);\n}\nfor (const [i, name] of GREEK.split(' ').entries()) {\n NAMED.set(name, 0x3b1 + i);\n if (name !== 'sigmaf') NAMED.set(name[0].toUpperCase() + name.slice(1), 0x391 + i);\n}\n\n// As the HTML tokenizer reads markup: `<` opens a tag only before a letter or\n// `/`, so `3 < 5` stays text. Line breaks and block boundaries separate words;\n// inline tags do not (`<b>Par</b>is`).\nconst BLOCK_BOUNDARY = /<\\/?(?:br|p|div|li|h[1-6]|blockquote|pre|tr|td|th)\\b[^>]*>/gi;\nconst TAG = /<\\/?[a-z][^>]*>|<!--[\\s\\S]*?-->/gi;\nconst ENTITY = /&(#[0-9]+|#x[0-9a-f]+|[a-z][a-z0-9]*);/gi;\n\nconst decodeEntity = (match: string, body: string): string => {\n if (body[0] === '#') {\n const hex = body[1] === 'x' || body[1] === 'X';\n const code = Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);\n // HTML decodes NUL, a surrogate or an out-of-range number to U+FFFD.\n const valid = code > 0 && code <= 0x10ffff && (code < 0xd800 || code > 0xdfff);\n return String.fromCodePoint(valid ? code : 0xfffd);\n }\n const code = NAMED.get(body);\n return code === undefined ? match : String.fromCodePoint(code);\n};\n\n/**\n * Tags stripped, entities decoded (named and numeric), and each run of\n * whitespace, `&nbsp;` included, collapsed to one space and trimmed. An unknown\n * named entity stays as written.\n */\nexport const toPlainText = (value: unknown): string =>\n typeof value === 'string'\n ? value\n .replace(BLOCK_BOUNDARY, ' ')\n .replace(TAG, '')\n .replace(ENTITY, decodeEntity)\n .replace(/\\s+/g, ' ')\n .trim()\n : '';\n","import defaults from './defaults';\nimport { toPlainText } from './plain-text';\n\nexport type SimpleClozeQuestion = {\n id?: string;\n element?: string;\n prompt?: string;\n promptEnabled?: boolean;\n correctAnswer?: string;\n teacherInstructions?: string;\n teacherInstructionsEnabled?: boolean;\n /** The learner-facing strings' language, as `@pie-lib/translator` reads it (`en_US`, `es_ES`, …). */\n language?: string;\n [key: string]: unknown;\n};\n\nexport type SimpleClozeSession = {\n id?: string;\n element?: string;\n /** The learner's typed answer. */\n value?: unknown;\n [key: string]: unknown;\n};\n\nexport type SimpleClozeEnv = {\n mode?: string;\n role?: string;\n [key: string]: unknown;\n};\n\nexport type SimpleClozeCorrectness = 'correct' | 'incorrect' | 'unanswered';\n\nexport type SimpleClozeOutcome = {\n score: number;\n empty: boolean;\n traceLog: string[];\n};\n\ntype FieldConfig = { required?: boolean };\nexport type SimpleClozeValidationConfig = {\n prompt?: FieldConfig;\n [key: string]: unknown;\n};\n\n/**\n * Both sides compare as plain text, case-insensitively: an answer key authored\n * as HTML matches the text a learner types.\n */\nconst comparable = (value: unknown): string => toPlainText(value).toLowerCase();\n\n// Strips tags except img, iframe and source (media counts as content), as\n// multiple-choice's `getContent` does.\nconst hasContent = (html: unknown): boolean =>\n typeof html === 'string' &&\n html\n .replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '')\n .replace(/&nbsp;/gi, ' ')\n .trim().length > 0;\n\n/**\n * A response with no text is unanswered; an answer key with no text matches\n * nothing, so a blank key never scores.\n */\nexport const getCorrectness = (\n question: SimpleClozeQuestion | null | undefined,\n session: SimpleClozeSession | null | undefined\n): SimpleClozeCorrectness => {\n const response = comparable(session?.value);\n if (!response) {\n return 'unanswered';\n }\n\n const key = comparable(question?.correctAnswer);\n if (key && response === key) {\n return 'correct';\n }\n\n return 'incorrect';\n};\n\n/** Single response, all-or-nothing: 1 when it matches the answer key, else 0. */\nexport const getPartialScore = (\n question: SimpleClozeQuestion | null | undefined,\n session: SimpleClozeSession | null | undefined\n): number => (getCorrectness(question, session) === 'correct' ? 1 : 0);\n\nconst getTraceLog = (\n question: SimpleClozeQuestion | null | undefined,\n correctness: SimpleClozeCorrectness,\n score: number\n): string[] => {\n const traceLog = [\n 'Response is compared with the answer key as plain text, ignoring case and extra spaces.',\n ];\n if (!comparable(question?.correctAnswer)) {\n traceLog.push('No correct answer is defined, so no response can score.');\n } else {\n traceLog.push(\n correctness === 'correct'\n ? 'Response matches the answer key.'\n : 'Response does not match the answer key.'\n );\n }\n traceLog.push(`Final score: ${score}.`);\n return traceLog;\n};\n\nexport const outcome = (\n question: SimpleClozeQuestion | null | undefined,\n session: SimpleClozeSession | null | undefined,\n _env: SimpleClozeEnv = {}\n): Promise<SimpleClozeOutcome> =>\n new Promise((resolve) => {\n const correctness = getCorrectness(question, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not enter a response. Score is 0.'],\n });\n return;\n }\n\n const score = correctness === 'correct' ? 1 : 0;\n resolve({ score, empty: false, traceLog: getTraceLog(question, correctness, score) });\n });\n\nexport const createDefaultModel = (model: SimpleClozeQuestion = {}): SimpleClozeQuestion => ({\n ...defaults.model,\n ...model,\n});\n\nexport const normalizeSession = (s: SimpleClozeSession | null | undefined): SimpleClozeSession => ({\n ...s,\n});\n\nexport const model = (\n question: SimpleClozeQuestion | null | undefined,\n session: SimpleClozeSession | null | undefined,\n env: SimpleClozeEnv | null | undefined\n): Promise<Record<string, unknown>> => {\n return new Promise((resolve) => {\n const safeSession = session || {};\n const safeEnv = env || {};\n const normalizedQuestion = createDefaultModel(question || {});\n\n const out: Record<string, unknown> = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n disabled: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n role: safeEnv.role,\n language: normalizedQuestion.language,\n };\n\n if (safeEnv.mode === 'evaluate') {\n out.correctness = getCorrectness(normalizedQuestion, safeSession);\n // The key is shown in the text input, so it goes out as plain text.\n out.correctAnswer = toPlainText(normalizedQuestion.correctAnswer);\n }\n\n if (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n resolve(out);\n });\n};\n\nexport const createCorrectResponseSession = (\n question: SimpleClozeQuestion | null | undefined,\n env: SimpleClozeEnv | null | undefined\n): Promise<SimpleClozeSession | null> => {\n return new Promise((resolve) => {\n if (env?.mode !== 'evaluate' && env?.role === 'instructor') {\n // The player sets `id` and `element` on the entry from the item config.\n resolve({ id: '1', value: toPlainText(question?.correctAnswer) });\n } else {\n resolve(null);\n }\n });\n};\n\nexport const validate = (\n model: SimpleClozeQuestion = {},\n config: SimpleClozeValidationConfig = {}\n): Record<string, string> => {\n const errors: Record<string, string> = {};\n\n if (config?.prompt?.required && !hasContent(model?.prompt)) {\n errors.prompt = 'This field is required.';\n }\n\n if (!comparable(model?.correctAnswer)) {\n errors.correctAnswer = 'No correct answer defined.';\n }\n\n return errors;\n};\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The text an HTML fragment reads as, computed without a DOM because
3
+ * controllers also run server-side. An answer key authored in an editor is HTML
4
+ * (`<p>Salt &amp; pepper</p>`) while the learner types plain text.
5
+ */
6
+ /**
7
+ * Tags stripped, entities decoded (named and numeric), and each run of
8
+ * whitespace, `&nbsp;` included, collapsed to one space and trimmed. An unknown
9
+ * named entity stays as written.
10
+ */
11
+ export declare const toPlainText: (value: unknown) => string;
@@ -1,14 +1,19 @@
1
1
  declare const SvelteElementClass: any;
2
2
  declare class SimpleClozeElement extends SvelteElementClass {
3
- _internalSession: any;
4
3
  /**
5
- * The session object the player handed us. A player reads the learner's
6
- * response back off this object, so every update is written into it as well
7
- * as into the copy the component renders from.
4
+ * The session object the player handed us, which `session` returns. A player
5
+ * reads the learner's response back off this object, so every update is
6
+ * written into it; the component renders from a fresh copy so its `$derived`
7
+ * reads re-run.
8
8
  */
9
9
  _playerSession: any;
10
10
  _model: any;
11
11
  _isComplete: () => boolean;
12
+ /**
13
+ * Dispatched a microtask later: on load a player sets the model and then the
14
+ * session in the same task (`element.model = …; element.session = …`), and
15
+ * `complete` can only report a restored response once the session is here.
16
+ */
12
17
  _dispatchModelSet: () => void;
13
18
  _dispatchSessionChanged: () => void;
14
19
  set model(m: any);