@pie-element/mc-populated-blank 0.2.13 → 0.3.0-next.4

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 (61) hide show
  1. package/README.md +1 -1
  2. package/configure.js +2 -0
  3. package/dist/author/index.d.ts +0 -1
  4. package/dist/author/index.js +1235 -1092
  5. package/dist/author/index.js.map +1 -1
  6. package/dist/browser/author/index.js +50 -0
  7. package/dist/browser/author/index.js.map +1 -0
  8. package/dist/browser/client-BAfWqkS6.js +2358 -0
  9. package/dist/browser/client-BAfWqkS6.js.map +1 -0
  10. package/dist/browser/controller/index.js +219 -0
  11. package/dist/browser/controller/index.js.map +1 -0
  12. package/dist/browser/delivery/index.js +1059 -0
  13. package/dist/browser/delivery/index.js.map +1 -0
  14. package/dist/browser/layoutLimits-ATKOwLHb.js +42 -0
  15. package/dist/browser/layoutLimits-ATKOwLHb.js.map +1 -0
  16. package/dist/browser/print/index.js +125 -0
  17. package/dist/browser/print/index.js.map +1 -0
  18. package/dist/controller/defaults.d.ts +0 -2
  19. package/dist/controller/index.d.ts +0 -3
  20. package/dist/controller/index.js +0 -2
  21. package/dist/controller/index.js.map +1 -1
  22. package/dist/delivery/computeAudioMode.d.ts +0 -1
  23. package/dist/delivery/computeChoiceCorrectness.d.ts +0 -1
  24. package/dist/delivery/computeDisplayState.d.ts +0 -1
  25. package/dist/delivery/computeLayoutProfile.d.ts +0 -1
  26. package/dist/delivery/computeLayoutStyle.d.ts +0 -2
  27. package/dist/delivery/index.d.ts +0 -1
  28. package/dist/delivery/index.js +1527 -1584
  29. package/dist/delivery/index.js.map +1 -1
  30. package/dist/delivery/variant-css-map.d.ts +0 -1
  31. package/dist/index.d.ts +0 -1
  32. package/dist/index.iife.d.ts +0 -3
  33. package/dist/index.iife.js +14 -15
  34. package/dist/index.js +1527 -1584
  35. package/dist/index.js.map +1 -1
  36. package/dist/print/index.d.ts +0 -1
  37. package/dist/print/index.js +1414 -1270
  38. package/dist/print/index.js.map +1 -1
  39. package/dist/runtime-support.d.ts +12 -0
  40. package/dist/runtime-support.js +14 -0
  41. package/dist/runtime-support.js.map +1 -0
  42. package/dist/shared/layoutLimits.d.ts +0 -1
  43. package/dist/shared/types.d.ts +9 -2
  44. package/module/print.js +2312 -0
  45. package/module/print.js.map +1 -0
  46. package/package.json +40 -8
  47. package/dist/author/index.d.ts.map +0 -1
  48. package/dist/controller/defaults.d.ts.map +0 -1
  49. package/dist/controller/index.d.ts.map +0 -1
  50. package/dist/delivery/computeAudioMode.d.ts.map +0 -1
  51. package/dist/delivery/computeChoiceCorrectness.d.ts.map +0 -1
  52. package/dist/delivery/computeDisplayState.d.ts.map +0 -1
  53. package/dist/delivery/computeLayoutProfile.d.ts.map +0 -1
  54. package/dist/delivery/computeLayoutStyle.d.ts.map +0 -1
  55. package/dist/delivery/index.d.ts.map +0 -1
  56. package/dist/delivery/variant-css-map.d.ts.map +0 -1
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.iife.d.ts.map +0 -1
  59. package/dist/print/index.d.ts.map +0 -1
  60. package/dist/shared/layoutLimits.d.ts.map +0 -1
  61. package/dist/shared/types.d.ts.map +0 -1
@@ -0,0 +1,219 @@
1
+ import { t as e } from "../layoutLimits-ATKOwLHb.js";
2
+ //#region src/controller/defaults.ts
3
+ var t = "{{blank}}", n = {
4
+ model: {
5
+ id: "1",
6
+ element: "mc-populated-blank",
7
+ prompt: "<p><strong>Demo prompt</strong></p>",
8
+ promptEnabled: !0,
9
+ interactionMode: "populate_blank",
10
+ layoutProfile: "",
11
+ choiceLayout: "",
12
+ layoutProfilePresets: {},
13
+ layoutLimits: { ...e },
14
+ audioButtonSkin: null,
15
+ audioButtonSkinsByLocale: {},
16
+ uiText: {
17
+ answerChoices: "Answer choices",
18
+ selectedAnswerInSentence: "blank",
19
+ showCorrectAnswer: "Show correct answer",
20
+ hideCorrectAnswer: "Hide correct answer",
21
+ clickToEnableAutoplay: "Click to enable audio autoplay",
22
+ audioResourceUnavailable: "Audio is enabled but no playable audio URL is configured.",
23
+ transcriptLabel: "Transcript",
24
+ listenLabelEn: "Listen",
25
+ listenLabelEs: "Escuchar",
26
+ listenSilentAlt: "Repeat instructions",
27
+ listenPlayingAlt: "Instructions are playing",
28
+ listenSilentAltEs: "Escuchar. Repetir las instrucciones.",
29
+ listenPlayingAltEs: "Escuchar. Estas son las instrucciones."
30
+ },
31
+ sentenceHtml: "",
32
+ template: `<p>The answer is ${t}.</p>`,
33
+ choiceMode: "text",
34
+ choices: [
35
+ {
36
+ id: "c1",
37
+ labelHtml: "<p>Option A</p>"
38
+ },
39
+ {
40
+ id: "c2",
41
+ labelHtml: "<p>Option B</p>"
42
+ },
43
+ {
44
+ id: "c3",
45
+ labelHtml: "<p>Option C</p>"
46
+ }
47
+ ],
48
+ correctChoiceId: "c2",
49
+ hasAudio: !1,
50
+ autoplayAudioEnabled: !1,
51
+ completeAudioEnabled: !1,
52
+ audioUrl: "",
53
+ audioTranscript: "",
54
+ locale: "",
55
+ teacherInstructions: "",
56
+ teacherInstructionsEnabled: !1
57
+ },
58
+ configuration: { prompt: {
59
+ label: "Prompt",
60
+ settings: !0
61
+ } }
62
+ }, r = (e) => !!e && typeof e == "object" && !Array.isArray(e) && Object.keys(e).length === 0;
63
+ function i(e) {
64
+ return e ? (e.match(/\{\{blank\}\}/g) || []).length : 0;
65
+ }
66
+ var a = (e, t) => {
67
+ if (!t?.choiceId) return "unanswered";
68
+ let n = e?.correctChoiceId || "";
69
+ return t.choiceId === n ? "correct" : "incorrect";
70
+ }, o = (e, t) => !t || r(t) || !t.choiceId ? 0 : 1, s = (e, t, n) => new Promise((i) => {
71
+ if (!t || r(t)) {
72
+ i({
73
+ score: 0,
74
+ empty: !0,
75
+ traceLog: ["Student did not select any answers. Score is 0."]
76
+ });
77
+ return;
78
+ }
79
+ t = u(t);
80
+ let o = a(e, t);
81
+ if (o === "unanswered") {
82
+ i({
83
+ score: 0,
84
+ empty: !0,
85
+ traceLog: ["Student did not select any answers. Score is 0."]
86
+ });
87
+ return;
88
+ }
89
+ let s = +(o === "correct");
90
+ i({
91
+ score: s,
92
+ empty: !1,
93
+ traceLog: [
94
+ `Mode: ${n?.mode || "unknown"}.`,
95
+ `Student selected choice: ${t.choiceId}.`,
96
+ `Correct choice: ${e?.correctChoiceId || "none"}.`,
97
+ `Final score: ${s}.`
98
+ ]
99
+ });
100
+ }), c = (t = {}) => ({
101
+ ...n.model,
102
+ ...t,
103
+ layoutLimits: {
104
+ ...e,
105
+ ...t?.layoutLimits || {}
106
+ }
107
+ }), l = (e = {}) => c(e), u = (e) => ({ ...e }), d = (e) => !!e?.shuffle, f = (e, t) => e?.lockChoiceOrder || t?.["@pie-element"]?.lockChoiceOrder ? !0 : t?.role === "instructor";
108
+ function p(e) {
109
+ let t = [...e];
110
+ for (let e = t.length - 1; e > 0; e--) {
111
+ let n = Math.floor(Math.random() * (e + 1));
112
+ [t[e], t[n]] = [t[n], t[e]];
113
+ }
114
+ return t;
115
+ }
116
+ var m = (e) => Array.isArray(e?.data?.shuffledValues) ? e.data.shuffledValues : Array.isArray(e?.shuffledValues) ? e.shuffledValues : [], h = (e, t, n) => {
117
+ let r = t.map((t) => e.find((e) => e?.[n] === t)).filter((e) => !!e);
118
+ if (r.length === e.length) return r;
119
+ let i = new Set(r.map((e) => e[n])), a = e.filter((e) => !i.has(e?.[n]));
120
+ return [...r, ...a];
121
+ }, g = async (e, t, n, r) => {
122
+ let i = Array.isArray(e?.choices) ? [...e.choices] : [];
123
+ if (!i.length || !d(e) || f(e, n || {})) return i;
124
+ let a = m(t);
125
+ if (a.length) return h(i, a, "id");
126
+ let o = p(i);
127
+ if (r && typeof r == "function" && t?.id && t?.element) {
128
+ let e = o.map((e) => e?.id).filter(Boolean);
129
+ e.length && await r(t.id, t.element, { shuffledValues: e });
130
+ }
131
+ return o;
132
+ }, _ = async (t, n, r, i) => {
133
+ let o = n || {}, s = r || {}, c = l(t), u = await g(c, o, s, i), d = {
134
+ prompt: c.promptEnabled ? c.prompt : null,
135
+ interactionMode: c.interactionMode || "populate_blank",
136
+ layoutProfile: c.layoutProfile || "",
137
+ choiceLayout: c.choiceLayout || "",
138
+ layoutProfilePresets: c.layoutProfilePresets && typeof c.layoutProfilePresets == "object" ? c.layoutProfilePresets : {},
139
+ layoutLimits: c.layoutLimits || { ...e },
140
+ audioButtonSkin: c.audioButtonSkin && typeof c.audioButtonSkin == "object" ? c.audioButtonSkin : null,
141
+ audioButtonSkinsByLocale: c.audioButtonSkinsByLocale && typeof c.audioButtonSkinsByLocale == "object" ? c.audioButtonSkinsByLocale : {},
142
+ uiText: c.uiText && typeof c.uiText == "object" ? c.uiText : {},
143
+ sentenceHtml: c.sentenceHtml || null,
144
+ template: c.template,
145
+ choiceMode: c.choiceMode,
146
+ choices: u,
147
+ hasAudio: c.hasAudio,
148
+ autoplayAudioEnabled: !!c.autoplayAudioEnabled,
149
+ completeAudioEnabled: !!c.completeAudioEnabled,
150
+ audioUrl: c.hasAudio ? c.audioUrl : null,
151
+ audioTranscript: c.hasAudio ? c.audioTranscript : null,
152
+ customType: c.customType || "",
153
+ useFeatureButtonAudio: typeof c.useFeatureButtonAudio == "boolean" ? c.useFeatureButtonAudio : void 0,
154
+ locale: c.locale || "",
155
+ disabled: s.mode !== "gather",
156
+ mode: s.mode
157
+ };
158
+ if (s.mode === "evaluate") {
159
+ let e = a(c, o);
160
+ d.correctness = e, d.responseCorrect = e === "correct", d.correctChoiceId = c.correctChoiceId;
161
+ }
162
+ return s.role === "instructor" && (s.mode === "view" || s.mode === "evaluate") ? d.teacherInstructions = c.teacherInstructionsEnabled ? c.teacherInstructions : null : d.teacherInstructions = null, d;
163
+ }, v = (e, t) => new Promise((n) => {
164
+ t.mode !== "evaluate" && t.role === "instructor" ? n({
165
+ id: "1",
166
+ element: "mc-populated-blank",
167
+ choiceId: e?.correctChoiceId || ""
168
+ }) : n(null);
169
+ }), y = (e = {}, n = {}) => {
170
+ let r = {};
171
+ if (e.promptEnabled) {
172
+ let t = e.prompt?.trim() || "";
173
+ (!t || t === "<p></p>") && (r.prompt = "Prompt is required when prompt is enabled");
174
+ }
175
+ let a = e.interactionMode || "populate_blank", o = i(e.template || "");
176
+ a === "audio_mc_only" ? o > 0 && (r.template = `Template cannot contain ${t} in audio-only mode`) : a === "populate_blank" ? o === 0 ? r.template = `Template must contain exactly one ${t} placeholder` : o > 1 && (r.template = `Template must contain only one ${t} placeholder`) : r.interactionMode = "Unknown interaction mode";
177
+ let s = Array.isArray(e.choices) ? e.choices : [];
178
+ s.length < 2 && (r.choices = "At least two choices are required");
179
+ let c = e.choiceMode || "text";
180
+ for (let e = 0; e < s.length; e++) {
181
+ let t = s[e];
182
+ if (!t?.id) {
183
+ r.choices = `Choice ${e + 1} is missing an id`;
184
+ break;
185
+ }
186
+ if (c === "text") {
187
+ let n = (t.labelHtml || "").trim();
188
+ if (!n || n === "<p></p>") {
189
+ r.choices = `Choice ${e + 1} needs label text`;
190
+ break;
191
+ }
192
+ } else {
193
+ if (!t.imageUrl?.trim()) {
194
+ r.choices = `Choice ${e + 1} needs an image URL`;
195
+ break;
196
+ }
197
+ if (!t.imageAlt?.trim()) {
198
+ r.choices = `Choice ${e + 1} needs image alt text`;
199
+ break;
200
+ }
201
+ }
202
+ }
203
+ let l = e.correctChoiceId;
204
+ (!l || !s.some((e) => e.id === l)) && (r.correctChoiceId = "Correct choice must match one of the choice ids"), e.hasAudio && (e.audioUrl?.trim() || (r.audioUrl = "A playable audio URL is required when audio is enabled"));
205
+ let u = e?.layoutLimits;
206
+ if (u && typeof u == "object") for (let e of /* @__PURE__ */ "blankStandaloneWidthRem.blankWideWidthRem.blankUnderlineWidthPx.blankUnderlineWideWidthPx.horizontalChoiceWidthPx.horizontalChoiceWidthVw.horizontalChoiceTileMinHeightRem.horizontalChoiceContentMinHeightRem.selectedImageMaxHeightRem.choiceImageMaxHeightRem.listenButtonSizePx.stimulusMinColumnPx.textMinColumnPx.legendMaxChars.choiceGroupGapRem.choiceRowGapRem.toggleButtonGapRem.horizontalChoiceRadioTopMarginRem.audioBlankTemplateMarginTopRem.audioBlankTemplateMarginBottomRem.audioInstructionsMaxWidthPx.narrowHorizontalChoiceMaxWidthPx.stimulusGridColumnGapRem.stimulusGridRowGapRem.stimulusSentenceMarginTopRem.stimulusChoicesMarginTopRem.tokenGridColumnGapRem.tokenGridRowGapRem.tokenTemplateMarginTopRem.tokenInlineTokenGapRem.tokenChoicesMarginTopRem.inlineGridColumnGapRem.inlineGridRowGapRem.inlineTemplateMarginTopRem.inlineChoicesMarginTopRem".split(".")) {
207
+ if (u[e] === void 0 || u[e] === null) continue;
208
+ let t = Number(u[e]);
209
+ if (!Number.isFinite(t) || t <= 0) {
210
+ r.layoutLimits = `${e} must be a positive number when provided`;
211
+ break;
212
+ }
213
+ }
214
+ return r;
215
+ };
216
+ //#endregion
217
+ export { i as countBlankTokens, v as createCorrectResponseSession, c as createDefaultModel, a as getCorrectness, o as getPartialScore, _ as model, l as normalize, u as normalizeSession, s as outcome, y as validate };
218
+
219
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/controller/defaults.ts","../../../src/controller/index.ts"],"sourcesContent":["import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits';\n\nexport { DEFAULT_LAYOUT_LIMITS };\n\n/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n\nexport default {\n model: {\n id: '1',\n element: 'mc-populated-blank',\n prompt: '<p><strong>Demo prompt</strong></p>',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n layoutProfilePresets: {},\n layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin: null,\n audioButtonSkinsByLocale: {},\n uiText: {\n answerChoices: 'Answer choices',\n selectedAnswerInSentence: 'blank',\n showCorrectAnswer: 'Show correct answer',\n hideCorrectAnswer: 'Hide correct answer',\n clickToEnableAutoplay: 'Click to enable audio autoplay',\n audioResourceUnavailable: 'Audio is enabled but no playable audio URL is configured.',\n transcriptLabel: 'Transcript',\n listenLabelEn: 'Listen',\n listenLabelEs: 'Escuchar',\n listenSilentAlt: 'Repeat instructions',\n listenPlayingAlt: 'Instructions are playing',\n listenSilentAltEs: 'Escuchar. Repetir las instrucciones.',\n listenPlayingAltEs: 'Escuchar. Estas son las instrucciones.',\n },\n sentenceHtml: '',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choiceMode: 'text' as const,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: false,\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';\nimport type {\n McpbChoice,\n McpbQuestion,\n McpbSession,\n McpbEnv,\n McpbCorrectness,\n} from '../shared/types';\n\nconst isEmptyObject = (value: unknown): boolean =>\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0;\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: McpbQuestion, session: McpbSession): McpbCorrectness => {\n if (!session?.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\nexport const getPartialScore = (_question: McpbQuestion, session: McpbSession) => {\n if (!session || isEmptyObject(session) || !session.choiceId) {\n return 0;\n }\n return 1;\n};\n\nexport const outcome = (question: McpbQuestion, session: McpbSession, env: McpbEnv) =>\n new Promise((resolve) => {\n if (!session || isEmptyObject(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const correctness = getCorrectness(question, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = correctness === 'correct' ? 1 : 0;\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${question?.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nexport const createDefaultModel = (model: McpbQuestion = {}) => ({\n ...defaults.model,\n ...model,\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(model?.layoutLimits || {}),\n },\n});\n\nexport const normalize = (question: McpbQuestion = {}) => createDefaultModel(question);\n\nexport const normalizeSession = (s: McpbSession): McpbSession => ({ ...s });\n\nconst shouldShuffleChoices = (question: McpbQuestion) => !!question?.shuffle;\n\nconst shouldLockChoices = (question: McpbQuestion, env: McpbEnv) => {\n if (question?.lockChoiceOrder) return true;\n if (env?.['@pie-element']?.lockChoiceOrder) return true;\n return env?.role === 'instructor';\n};\n\nfunction shuffleArray<T>(items: T[]): T[] {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n}\n\nconst getStoredShuffle = (session: McpbSession): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\nconst applyShuffledValues = (\n choices: McpbChoice[],\n shuffledValues: string[],\n choiceKey: keyof McpbChoice\n) => {\n const orderedChoices = shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter((c): c is McpbChoice => !!c);\n\n if (orderedChoices.length === choices.length) {\n return orderedChoices;\n }\n\n const orderedValues = new Set(orderedChoices.map((choice) => choice[choiceKey]));\n const leftovers = choices.filter((choice) => !orderedValues.has(choice?.[choiceKey]));\n return [...orderedChoices, ...leftovers];\n};\n\ntype UpdateSessionFn = (\n id: string,\n element: string,\n data: { shuffledValues: string[] }\n) => Promise<void>;\n\nconst getOrderedChoices = async (\n question: McpbQuestion,\n session: McpbSession,\n env: McpbEnv,\n updateSession?: UpdateSessionFn\n) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || !shouldShuffleChoices(question)) {\n return choices;\n }\n\n if (shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (\n question: McpbQuestion,\n session: McpbSession | null,\n env: McpbEnv | null,\n updateSession?: UpdateSessionFn\n) => {\n const safeSession: McpbSession = session || {};\n const safeEnv: McpbEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = await getOrderedChoices(normalizedQuestion, safeSession, safeEnv, updateSession);\n\n const out: Record<string, unknown> = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n layoutProfilePresets:\n normalizedQuestion.layoutProfilePresets &&\n typeof normalizedQuestion.layoutProfilePresets === 'object'\n ? normalizedQuestion.layoutProfilePresets\n : {},\n layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin:\n normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'\n ? normalizedQuestion.audioButtonSkin\n : null,\n audioButtonSkinsByLocale:\n normalizedQuestion.audioButtonSkinsByLocale &&\n typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'\n ? normalizedQuestion.audioButtonSkinsByLocale\n : {},\n uiText:\n normalizedQuestion.uiText && typeof normalizedQuestion.uiText === 'object'\n ? normalizedQuestion.uiText\n : {},\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n customType: normalizedQuestion.customType || '',\n useFeatureButtonAudio:\n typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'\n ? normalizedQuestion.useFeatureButtonAudio\n : undefined,\n locale: normalizedQuestion.locale || '',\n disabled: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, safeSession);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\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 return out;\n};\n\nexport const createCorrectResponseSession = (question: McpbQuestion, env: McpbEnv) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n resolve({\n id: '1',\n element: 'mc-populated-blank',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\nexport const validate = (question: McpbQuestion = {}, _config: Record<string, unknown> = {}) => {\n const errors: Record<string, string> = {};\n\n if (question.promptEnabled) {\n const p = question.prompt?.trim() || '';\n if (!p || p === '<p></p>') {\n errors.prompt = 'Prompt is required when prompt is enabled';\n }\n }\n\n const interactionMode = question.interactionMode || 'populate_blank';\n const template = question.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(question.choices) ? question.choices : [];\n if (choices.length < 2) {\n errors.choices = 'At least two choices are required';\n }\n\n const mode = question.choiceMode || 'text';\n for (let i = 0; i < choices.length; i++) {\n const c = choices[i];\n if (!c?.id) {\n errors.choices = `Choice ${i + 1} is missing an id`;\n break;\n }\n if (mode === 'text') {\n const lbl = (c.labelHtml || '').trim();\n if (!lbl || lbl === '<p></p>') {\n errors.choices = `Choice ${i + 1} needs label text`;\n break;\n }\n } else {\n if (!c.imageUrl?.trim()) {\n errors.choices = `Choice ${i + 1} needs an image URL`;\n break;\n }\n if (!c.imageAlt?.trim()) {\n errors.choices = `Choice ${i + 1} needs image alt text`;\n break;\n }\n }\n }\n\n const correct = question.correctChoiceId;\n if (!correct || !choices.some((c) => c.id === correct)) {\n errors.correctChoiceId = 'Correct choice must match one of the choice ids';\n }\n\n if (question.hasAudio) {\n const hasAudioUrl = !!question.audioUrl?.trim();\n if (!hasAudioUrl) {\n errors.audioUrl = 'A playable audio URL is required when audio is enabled';\n }\n }\n\n const limits = question?.layoutLimits;\n if (limits && typeof limits === 'object') {\n const numericLimitKeys = [\n 'blankStandaloneWidthRem',\n 'blankWideWidthRem',\n 'blankUnderlineWidthPx',\n 'blankUnderlineWideWidthPx',\n 'horizontalChoiceWidthPx',\n 'horizontalChoiceWidthVw',\n 'horizontalChoiceTileMinHeightRem',\n 'horizontalChoiceContentMinHeightRem',\n 'selectedImageMaxHeightRem',\n 'choiceImageMaxHeightRem',\n 'listenButtonSizePx',\n 'stimulusMinColumnPx',\n 'textMinColumnPx',\n 'legendMaxChars',\n 'choiceGroupGapRem',\n 'choiceRowGapRem',\n 'toggleButtonGapRem',\n 'horizontalChoiceRadioTopMarginRem',\n 'audioBlankTemplateMarginTopRem',\n 'audioBlankTemplateMarginBottomRem',\n 'audioInstructionsMaxWidthPx',\n 'narrowHorizontalChoiceMaxWidthPx',\n 'stimulusGridColumnGapRem',\n 'stimulusGridRowGapRem',\n 'stimulusSentenceMarginTopRem',\n 'stimulusChoicesMarginTopRem',\n 'tokenGridColumnGapRem',\n 'tokenGridRowGapRem',\n 'tokenTemplateMarginTopRem',\n 'tokenInlineTokenGapRem',\n 'tokenChoicesMarginTopRem',\n 'inlineGridColumnGapRem',\n 'inlineGridRowGapRem',\n 'inlineTemplateMarginTopRem',\n 'inlineChoicesMarginTopRem',\n ] as const;\n for (const key of numericLimitKeys) {\n if (limits[key] === undefined || limits[key] === null) continue;\n const value = Number(limits[key]);\n if (!Number.isFinite(value) || value <= 0) {\n errors.layoutLimits = `${key} must be a positive number when provided`;\n break;\n }\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}