@ai-matrx/print 0.3.2 → 0.5.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/booklet.cjs +4 -5
  3. package/dist/booklet.cjs.map +1 -1
  4. package/dist/booklet.js +4 -5
  5. package/dist/booklet.js.map +1 -1
  6. package/dist/certificate.cjs +748 -0
  7. package/dist/certificate.cjs.map +1 -0
  8. package/dist/certificate.d.cts +101 -0
  9. package/dist/certificate.d.ts +101 -0
  10. package/dist/certificate.js +725 -0
  11. package/dist/certificate.js.map +1 -0
  12. package/dist/core.cjs +2 -5
  13. package/dist/core.cjs.map +1 -1
  14. package/dist/core.d.cts +1 -2
  15. package/dist/core.d.ts +1 -2
  16. package/dist/core.js +1 -4
  17. package/dist/core.js.map +1 -1
  18. package/dist/education.cjs +927 -0
  19. package/dist/education.cjs.map +1 -0
  20. package/dist/education.d.cts +79 -0
  21. package/dist/education.d.ts +79 -0
  22. package/dist/education.js +904 -0
  23. package/dist/education.js.map +1 -0
  24. package/dist/exam.cjs +647 -0
  25. package/dist/exam.cjs.map +1 -0
  26. package/dist/exam.d.cts +69 -0
  27. package/dist/exam.d.ts +69 -0
  28. package/dist/exam.js +624 -0
  29. package/dist/exam.js.map +1 -0
  30. package/dist/flashcards.cjs +25 -26
  31. package/dist/flashcards.cjs.map +1 -1
  32. package/dist/flashcards.js +24 -25
  33. package/dist/flashcards.js.map +1 -1
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +4 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/labels.cjs +14 -13
  38. package/dist/labels.cjs.map +1 -1
  39. package/dist/labels.js +14 -13
  40. package/dist/labels.js.map +1 -1
  41. package/dist/markdown.cjs +6 -5
  42. package/dist/markdown.cjs.map +1 -1
  43. package/dist/markdown.js +6 -5
  44. package/dist/markdown.js.map +1 -1
  45. package/dist/react.cjs +14 -13
  46. package/dist/react.cjs.map +1 -1
  47. package/dist/react.js +14 -13
  48. package/dist/react.js.map +1 -1
  49. package/dist/zpl.cjs +1040 -0
  50. package/dist/zpl.cjs.map +1 -0
  51. package/dist/zpl.d.cts +160 -0
  52. package/dist/zpl.d.ts +160 -0
  53. package/dist/zpl.js +1007 -0
  54. package/dist/zpl.js.map +1 -0
  55. package/package.json +43 -2
package/dist/exam.cjs ADDED
@@ -0,0 +1,647 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/exam.ts
21
+ var exam_exports = {};
22
+ __export(exam_exports, {
23
+ BUBBLE_SHEET_GEOMETRY: () => BUBBLE_SHEET_GEOMETRY,
24
+ extractPracticeTest: () => extractPracticeTest,
25
+ practiceTestPrinter: () => practiceTestPrinter
26
+ });
27
+ module.exports = __toCommonJS(exam_exports);
28
+
29
+ // src/core.ts
30
+ var import_html_escape = require("@ai-matrx/kit/html-escape");
31
+ var PRINT_STYLES = `
32
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
33
+
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
36
+ font-size: 11pt;
37
+ line-height: 1.6;
38
+ color: #1a1a1a;
39
+ background: #fff;
40
+ max-width: 860px;
41
+ margin: 0 auto;
42
+ padding: 28px 36px;
43
+ }
44
+
45
+ .print-actions {
46
+ display: flex;
47
+ gap: 10px;
48
+ margin-bottom: 24px;
49
+ padding-bottom: 18px;
50
+ border-bottom: 2px solid #e5e7eb;
51
+ }
52
+
53
+ .print-btn {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 6px;
57
+ padding: 8px 20px;
58
+ border-radius: 8px;
59
+ font-size: 13px;
60
+ font-family: inherit;
61
+ cursor: pointer;
62
+ border: none;
63
+ font-weight: 600;
64
+ }
65
+
66
+ .print-btn-primary { background: #2563eb; color: #fff; }
67
+ .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
68
+
69
+ h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }
70
+ h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }
71
+ h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }
72
+ p { margin-bottom: 0.7em; }
73
+
74
+ table {
75
+ width: 100%;
76
+ border-collapse: collapse;
77
+ margin: 1em 0;
78
+ font-size: 10pt;
79
+ }
80
+ th {
81
+ background: #1e293b;
82
+ color: #f8fafc;
83
+ font-weight: 600;
84
+ text-align: left;
85
+ padding: 7px 12px;
86
+ border: 1px solid #334155;
87
+ }
88
+ td {
89
+ padding: 6px 12px;
90
+ border: 1px solid #e2e8f0;
91
+ vertical-align: top;
92
+ }
93
+ tr:nth-child(even) td { background: #f8fafc; }
94
+
95
+ @media print {
96
+ body { max-width: 100%; padding: 0; }
97
+ .print-actions { display: none !important; }
98
+ th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
99
+ }
100
+ `;
101
+ function buildPrintDocument(bodyHtml, title = "AI Response", extraStyles = "") {
102
+ return `<!DOCTYPE html>
103
+ <html lang="en">
104
+ <head>
105
+ <meta charset="UTF-8">
106
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
107
+ <title>${(0, import_html_escape.escapeHtml)(title)}</title>
108
+ <style>
109
+ ${PRINT_STYLES}
110
+ ${extraStyles}
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div class="print-actions">
115
+ <button class="print-btn print-btn-primary" onclick="window.print()">Print / Save as PDF</button>
116
+ <button class="print-btn print-btn-secondary" onclick="window.close()">Close</button>
117
+ </div>
118
+ <div class="content">
119
+ ${bodyHtml}
120
+ </div>
121
+ </body>
122
+ </html>`;
123
+ }
124
+ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
125
+ const win = window.open("", "_blank", windowFeatures);
126
+ if (!win) {
127
+ const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
128
+ const url = URL.createObjectURL(blob);
129
+ const a = document.createElement("a");
130
+ a.href = url;
131
+ a.download = `${filename.replace(/\s+/g, "-").toLowerCase()}.html`;
132
+ document.body.appendChild(a);
133
+ a.click();
134
+ document.body.removeChild(a);
135
+ setTimeout(() => URL.revokeObjectURL(url), 0);
136
+ return "downloaded";
137
+ }
138
+ win.document.open();
139
+ win.document.write(htmlDocument);
140
+ win.document.close();
141
+ return "opened";
142
+ }
143
+
144
+ // src/exam.ts
145
+ var import_html_escape2 = require("@ai-matrx/kit/html-escape");
146
+ var BUBBLE_ROWS_PER_COLUMN = 25;
147
+ var BUBBLE_COLUMNS_PER_PAGE = 4;
148
+ var CHOICE_LETTERS = "ABCDEFGHIJ";
149
+ function letterFor(index) {
150
+ return CHOICE_LETTERS[index] ?? String(index + 1);
151
+ }
152
+ function choiceLabel(choice, index) {
153
+ const explicit = choice.label?.trim();
154
+ return explicit && explicit.length > 0 ? explicit : letterFor(index);
155
+ }
156
+ function isFreeResponse(question) {
157
+ return !question.choices || question.choices.length === 0;
158
+ }
159
+ function asString(value) {
160
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
161
+ }
162
+ function asNumber(value) {
163
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
164
+ }
165
+ function parseChoice(raw) {
166
+ if (typeof raw === "string") {
167
+ return raw.trim().length > 0 ? { text: raw } : null;
168
+ }
169
+ if (!raw || typeof raw !== "object") return null;
170
+ const obj = raw;
171
+ const text = asString(obj["text"]);
172
+ if (text === void 0) return null;
173
+ const label = asString(obj["label"]);
174
+ return label === void 0 ? { text } : { label, text };
175
+ }
176
+ function parseQuestion(raw) {
177
+ if (!raw || typeof raw !== "object") return null;
178
+ const obj = raw;
179
+ const prompt = asString(obj["prompt"]) ?? asString(obj["question"]);
180
+ if (prompt === void 0) return null;
181
+ const question = { prompt };
182
+ const rawChoices = obj["choices"] ?? obj["options"];
183
+ if (Array.isArray(rawChoices)) {
184
+ const choices = rawChoices.map((c) => parseChoice(c)).filter((c) => c !== null);
185
+ if (choices.length > 0) question.choices = choices;
186
+ }
187
+ const answer = asString(obj["answer"]);
188
+ if (answer !== void 0) question.answer = answer;
189
+ const explanation = asString(obj["explanation"]);
190
+ if (explanation !== void 0) question.explanation = explanation;
191
+ const points = asNumber(obj["points"]);
192
+ if (points !== void 0) question.points = points;
193
+ const workSpaceLines = asNumber(obj["workSpaceLines"]);
194
+ if (workSpaceLines !== void 0) {
195
+ question.workSpaceLines = Math.max(0, Math.floor(workSpaceLines));
196
+ }
197
+ return question;
198
+ }
199
+ function extractPracticeTest(data) {
200
+ const empty = { title: "Practice Test", questions: [] };
201
+ if (!data) return empty;
202
+ if (Array.isArray(data)) {
203
+ return {
204
+ title: "Practice Test",
205
+ questions: data.map(parseQuestion).filter((q) => q !== null)
206
+ };
207
+ }
208
+ if (typeof data !== "object") return empty;
209
+ const obj = data;
210
+ const rawQuestions = obj["questions"] ?? obj["items"];
211
+ const questions = Array.isArray(rawQuestions) ? rawQuestions.map(parseQuestion).filter((q) => q !== null) : [];
212
+ const test = {
213
+ title: asString(obj["title"]) ?? "Practice Test",
214
+ questions
215
+ };
216
+ const instructions = asString(obj["instructions"]);
217
+ if (instructions !== void 0) test.instructions = instructions;
218
+ return test;
219
+ }
220
+ function resolveSettings(raw) {
221
+ const num = (key) => {
222
+ const v = raw?.[key];
223
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
224
+ };
225
+ return {
226
+ from: num("fromQuestion"),
227
+ to: num("toQuestion"),
228
+ bubbleLetters: Math.min(num("bubbleLetters"), CHOICE_LETTERS.length),
229
+ showPointsColumn: raw?.["showPointsColumn"] === true
230
+ };
231
+ }
232
+ function applyRange(questions, s) {
233
+ if (questions.length === 0) return { questions: [], startIndex: 0 };
234
+ const start = s.from > 0 ? Math.min(s.from, questions.length) : 1;
235
+ const end = s.to > 0 ? Math.min(s.to, questions.length) : questions.length;
236
+ const [lo, hi] = start <= end ? [start, end] : [end, start];
237
+ return { questions: questions.slice(lo - 1, hi), startIndex: lo - 1 };
238
+ }
239
+ var EXAM_BASE_STYLES = `
240
+ .ex-title { font-size: 18pt; font-weight: 700; margin-bottom: 2px; }
241
+ .ex-subtitle { font-size: 10pt; color: #475569; margin-bottom: 14px; }
242
+ .ex-instructions {
243
+ border: 2px solid #1e293b;
244
+ border-radius: 6px;
245
+ padding: 10px 14px;
246
+ margin: 0 0 18px;
247
+ font-size: 10.5pt;
248
+ line-height: 1.5;
249
+ background: #f8fafc;
250
+ -webkit-print-color-adjust: exact;
251
+ print-color-adjust: exact;
252
+ }
253
+ .ex-instructions-heading {
254
+ font-size: 9pt;
255
+ font-weight: 700;
256
+ letter-spacing: 0.08em;
257
+ text-transform: uppercase;
258
+ color: #1e293b;
259
+ margin-bottom: 4px;
260
+ }
261
+ `;
262
+ var BOOKLET_VARIANT_STYLES = `
263
+ ${EXAM_BASE_STYLES}
264
+ .ex-q {
265
+ /* A question never splits across a page mid-choice-list \u2014 a student
266
+ reading choice (C) at the top of the next page misreads the item. */
267
+ break-inside: avoid;
268
+ page-break-inside: avoid;
269
+ margin: 0 0 16px;
270
+ }
271
+ .ex-q-head { display: flex; gap: 8px; align-items: baseline; }
272
+ .ex-q-num { font-weight: 700; min-width: 1.9em; }
273
+ .ex-q-prompt { flex: 1; font-size: 11pt; line-height: 1.5; }
274
+ .ex-q-points {
275
+ font-size: 8.5pt;
276
+ color: #475569;
277
+ white-space: nowrap;
278
+ font-weight: 600;
279
+ }
280
+ .ex-choices { list-style: none; margin: 6px 0 0 1.9em; padding: 0; }
281
+ .ex-choice { display: flex; gap: 7px; font-size: 10.5pt; line-height: 1.45; margin-bottom: 3px; }
282
+ .ex-choice-label { font-weight: 600; min-width: 1.3em; }
283
+ .ex-work { margin: 8px 0 0 1.9em; }
284
+ .ex-work-line {
285
+ border-bottom: 1px solid #94a3b8;
286
+ height: 0.32in;
287
+ -webkit-print-color-adjust: exact;
288
+ print-color-adjust: exact;
289
+ }
290
+ `;
291
+ function renderPoints(question) {
292
+ if (question.points === void 0) return "";
293
+ const unit = question.points === 1 ? "pt" : "pts";
294
+ return `<span class="ex-q-points">(${(0, import_html_escape2.escapeHtml)(String(question.points))} ${unit})</span>`;
295
+ }
296
+ function renderQuestionBooklet(test, questions, startIndex) {
297
+ const instructions = test.instructions ? `<div class="ex-instructions">
298
+ <div class="ex-instructions-heading">Instructions</div>
299
+ ${(0, import_html_escape2.escapeHtml)(test.instructions)}
300
+ </div>` : "";
301
+ const items = questions.map((q, i) => {
302
+ const number = startIndex + i + 1;
303
+ const choices = (q.choices ?? []).map(
304
+ (c, ci) => `<li class="ex-choice"><span class="ex-choice-label">${(0, import_html_escape2.escapeHtml)(
305
+ choiceLabel(c, ci)
306
+ )}.</span><span>${(0, import_html_escape2.escapeHtml)(c.text)}</span></li>`
307
+ ).join("");
308
+ const choiceList = choices.length > 0 ? `<ul class="ex-choices">${choices}</ul>` : "";
309
+ const lineCount = isFreeResponse(q) ? q.workSpaceLines ?? 0 : 0;
310
+ const work = lineCount > 0 ? `<div class="ex-work">${'<div class="ex-work-line"></div>'.repeat(lineCount)}</div>` : "";
311
+ return `<div class="ex-q" data-question="${number}">
312
+ <div class="ex-q-head">
313
+ <span class="ex-q-num">${number}.</span>
314
+ <span class="ex-q-prompt">${(0, import_html_escape2.escapeHtml)(q.prompt)}</span>
315
+ ${renderPoints(q)}
316
+ </div>
317
+ ${choiceList}
318
+ ${work}
319
+ </div>`;
320
+ }).join("\n");
321
+ return `<div class="ex-title">${(0, import_html_escape2.escapeHtml)(test.title)}</div>
322
+ <div class="ex-subtitle">Question booklet \u2014 mark your answers on the separate answer sheet</div>
323
+ ${instructions}
324
+ ${items}`;
325
+ }
326
+ var BUBBLE_STYLES = `
327
+ @page { size: 8.5in 11in; margin: 0.5in; }
328
+
329
+ .bs-header {
330
+ border: 2px solid #111;
331
+ padding: 0.12in 0.16in;
332
+ margin-bottom: 0.22in;
333
+ break-inside: avoid;
334
+ page-break-inside: avoid;
335
+ }
336
+ .bs-header-title { font-size: 13pt; font-weight: 700; margin-bottom: 0.06in; }
337
+ .bs-header-note { font-size: 8pt; color: #334155; margin-bottom: 0.1in; }
338
+ .bs-fields { display: flex; gap: 0.2in; }
339
+ .bs-field { flex: 1; font-size: 8.5pt; }
340
+ .bs-field-score { flex: 0 0 1.2in; }
341
+ .bs-field-rule {
342
+ border-bottom: 1px solid #111;
343
+ height: 0.26in;
344
+ -webkit-print-color-adjust: exact;
345
+ print-color-adjust: exact;
346
+ }
347
+ .bs-field-label {
348
+ font-size: 7.5pt;
349
+ letter-spacing: 0.06em;
350
+ text-transform: uppercase;
351
+ color: #334155;
352
+ }
353
+
354
+ .bs-columns {
355
+ display: flex;
356
+ gap: 0.3in;
357
+ break-after: page;
358
+ page-break-after: always;
359
+ }
360
+ .bs-columns:last-of-type { break-after: auto; page-break-after: auto; }
361
+
362
+ .bs-column { flex: 1; }
363
+
364
+ .bs-row {
365
+ display: flex;
366
+ align-items: center;
367
+ height: 0.28in; /* the pitch \u2014 every row identical */
368
+ break-inside: avoid;
369
+ page-break-inside: avoid;
370
+ }
371
+ .bs-row-num {
372
+ width: 0.3in;
373
+ font-size: 8.5pt;
374
+ text-align: right;
375
+ padding-right: 0.06in;
376
+ font-variant-numeric: tabular-nums;
377
+ }
378
+ .bs-bubble {
379
+ width: 0.16in;
380
+ height: 0.16in;
381
+ border: 1.25px solid #111;
382
+ border-radius: 50%;
383
+ margin-right: 0.055in;
384
+ font-size: 5.5pt;
385
+ line-height: 0.16in;
386
+ text-align: center;
387
+ color: #111;
388
+ -webkit-print-color-adjust: exact;
389
+ print-color-adjust: exact;
390
+ }
391
+ .bs-blank-line {
392
+ flex: 1;
393
+ border-bottom: 1px solid #111;
394
+ height: 0.16in;
395
+ margin-right: 0.06in;
396
+ -webkit-print-color-adjust: exact;
397
+ print-color-adjust: exact;
398
+ }
399
+ .bs-points {
400
+ width: 0.34in;
401
+ font-size: 7.5pt;
402
+ color: #334155;
403
+ text-align: right;
404
+ }
405
+ `;
406
+ function bubbleCountFor(question, s) {
407
+ if (isFreeResponse(question)) return s.bubbleLetters > 0 ? s.bubbleLetters : 0;
408
+ const own = question.choices?.length ?? 0;
409
+ const n = s.bubbleLetters > 0 ? s.bubbleLetters : own;
410
+ return Math.min(n, CHOICE_LETTERS.length);
411
+ }
412
+ function renderBubbleRow(question, number, s) {
413
+ const count = bubbleCountFor(question, s);
414
+ const cells = count > 0 ? Array.from(
415
+ { length: count },
416
+ (_, i) => `<span class="bs-bubble" data-letter="${(0, import_html_escape2.escapeHtml)(letterFor(i))}">${(0, import_html_escape2.escapeHtml)(
417
+ letterFor(i)
418
+ )}</span>`
419
+ ).join("") : (
420
+ // Free response: a short rule to write the answer on, never bubbles.
421
+ `<span class="bs-blank-line"></span>`
422
+ );
423
+ const points = s.showPointsColumn && question.points !== void 0 ? `<span class="bs-points">${(0, import_html_escape2.escapeHtml)(String(question.points))}</span>` : "";
424
+ return `<div class="bs-row" data-question="${number}" data-bubbles="${count}">
425
+ <span class="bs-row-num">${number}.</span>${cells}${points}
426
+ </div>`;
427
+ }
428
+ function renderBubbleSheet(test, questions, startIndex, s) {
429
+ const rows = questions.map(
430
+ (q, i) => renderBubbleRow(q, startIndex + i + 1, s)
431
+ );
432
+ const columns = [];
433
+ for (let i = 0; i < rows.length; i += BUBBLE_ROWS_PER_COLUMN) {
434
+ columns.push(
435
+ `<div class="bs-column">${rows.slice(i, i + BUBBLE_ROWS_PER_COLUMN).join("")}</div>`
436
+ );
437
+ }
438
+ const sets = [];
439
+ for (let i = 0; i < columns.length; i += BUBBLE_COLUMNS_PER_PAGE) {
440
+ const group = columns.slice(i, i + BUBBLE_COLUMNS_PER_PAGE);
441
+ while (group.length < BUBBLE_COLUMNS_PER_PAGE) {
442
+ group.push(`<div class="bs-column"></div>`);
443
+ }
444
+ sets.push(`<div class="bs-columns">${group.join("")}</div>`);
445
+ }
446
+ const header = `<div class="bs-header">
447
+ <div class="bs-header-title">${(0, import_html_escape2.escapeHtml)(test.title)} \u2014 Answer Sheet</div>
448
+ <div class="bs-header-note">Fill the bubble completely with a No. 2 pencil. Erase changes fully. Do not write outside the boxes.</div>
449
+ <div class="bs-fields">
450
+ <div class="bs-field"><div class="bs-field-rule"></div><div class="bs-field-label">Name</div></div>
451
+ <div class="bs-field"><div class="bs-field-rule"></div><div class="bs-field-label">Date</div></div>
452
+ <div class="bs-field bs-field-score"><div class="bs-field-rule"></div><div class="bs-field-label">Score</div></div>
453
+ </div>
454
+ </div>`;
455
+ return `${header}
456
+ ${sets.join("\n")}`;
457
+ }
458
+ var KEY_STYLES = `
459
+ ${EXAM_BASE_STYLES}
460
+ .ak-grid { column-count: 4; column-gap: 0.3in; }
461
+ .ak-row {
462
+ break-inside: avoid;
463
+ page-break-inside: avoid;
464
+ font-size: 10pt;
465
+ line-height: 1.5;
466
+ display: flex;
467
+ gap: 6px;
468
+ }
469
+ .ak-num { font-weight: 600; min-width: 2em; text-align: right; }
470
+ .ak-answer { font-weight: 700; }
471
+ .ak-points { color: #475569; font-size: 8.5pt; margin-left: auto; }
472
+ .ak-free { column-count: 1; }
473
+ .ak-free .ak-answer { font-weight: 500; }
474
+ `;
475
+ function answerText(question) {
476
+ return question.answer ?? "\u2014";
477
+ }
478
+ function renderAnswerKey(test, questions, startIndex) {
479
+ const anyPoints = questions.some((q) => q.points !== void 0);
480
+ const compact = [];
481
+ const prose = [];
482
+ questions.forEach((q, i) => {
483
+ const number = startIndex + i + 1;
484
+ const points = anyPoints && q.points !== void 0 ? `<span class="ak-points">${(0, import_html_escape2.escapeHtml)(String(q.points))}</span>` : "";
485
+ const row = `<div class="ak-row" data-question="${number}"><span class="ak-num">${number}.</span><span class="ak-answer">${(0, import_html_escape2.escapeHtml)(
486
+ answerText(q)
487
+ )}</span>${points}</div>`;
488
+ (isFreeResponse(q) ? prose : compact).push(row);
489
+ });
490
+ const compactHtml = compact.length > 0 ? `<div class="ak-grid">${compact.join("")}</div>` : "";
491
+ const proseHtml = prose.length > 0 ? `<h3>Free-response model answers</h3><div class="ak-grid ak-free">${prose.join(
492
+ ""
493
+ )}</div>` : "";
494
+ return `<div class="ex-title">${(0, import_html_escape2.escapeHtml)(test.title)}</div>
495
+ <div class="ex-subtitle">Answer key \u2014 keep separate from the question booklet</div>
496
+ ${compactHtml}
497
+ ${proseHtml}`;
498
+ }
499
+ var EXPLANATIONS_STYLES = `
500
+ ${EXAM_BASE_STYLES}
501
+ .exp-row {
502
+ break-inside: avoid;
503
+ page-break-inside: avoid;
504
+ border-bottom: 1px solid #e2e8f0;
505
+ padding: 6px 0;
506
+ }
507
+ .exp-head { display: flex; gap: 6px; font-size: 10pt; }
508
+ .exp-num { font-weight: 700; min-width: 2em; }
509
+ .exp-answer { font-weight: 700; }
510
+ .exp-prompt { color: #334155; }
511
+ .exp-body { font-size: 9.5pt; line-height: 1.45; margin: 3px 0 0 2em; color: #1f2937; }
512
+ `;
513
+ function renderExplanations(test, questions, startIndex) {
514
+ const rows = questions.map((q, i) => {
515
+ const number = startIndex + i + 1;
516
+ const explanation = q.explanation ? `<div class="exp-body">${(0, import_html_escape2.escapeHtml)(q.explanation)}</div>` : "";
517
+ return `<div class="exp-row" data-question="${number}">
518
+ <div class="exp-head">
519
+ <span class="exp-num">${number}.</span>
520
+ <span class="exp-prompt">${(0, import_html_escape2.escapeHtml)(q.prompt)}</span>
521
+ </div>
522
+ <div class="exp-body"><span class="exp-answer">Answer: ${(0, import_html_escape2.escapeHtml)(
523
+ answerText(q)
524
+ )}</span></div>
525
+ ${explanation}
526
+ </div>`;
527
+ }).join("\n");
528
+ return `<div class="ex-title">${(0, import_html_escape2.escapeHtml)(test.title)}</div>
529
+ <div class="ex-subtitle">Answers &amp; explanations \u2014 study sheet</div>
530
+ ${rows}`;
531
+ }
532
+ var EMPTY_MESSAGE = "No practice-test data available to print.";
533
+ var practiceTestPrinter = {
534
+ label: "Print practice test",
535
+ variants: [
536
+ {
537
+ id: "question-booklet",
538
+ label: "Question booklet (recommended)",
539
+ description: "Numbered questions with lettered choices and ruled work space. Contains no answers \u2014 print it once and re-use it with a fresh answer sheet each attempt"
540
+ },
541
+ {
542
+ id: "bubble-sheet",
543
+ label: "Bubble answer sheet",
544
+ description: "The scannable answer form: name/date/score header and a fixed grid of bubble rows, 25 per column. Free-response items get a write-in line"
545
+ },
546
+ {
547
+ id: "answer-key",
548
+ label: "Answer key",
549
+ description: "Compact multi-column key for grading. Keep it away from the booklet"
550
+ },
551
+ {
552
+ id: "explanations",
553
+ label: "Answers & explanations",
554
+ description: "Every question with its answer and explanation, at study-sheet density"
555
+ }
556
+ ],
557
+ settings: [
558
+ {
559
+ type: "range",
560
+ id: "questionRange",
561
+ label: "Questions to print",
562
+ description: "Leave blank to print the whole test. Question numbers stay the original ones",
563
+ fromId: "fromQuestion",
564
+ toId: "toQuestion",
565
+ defaultFrom: 0,
566
+ defaultTo: 0,
567
+ min: 1,
568
+ fromPlaceholder: "1",
569
+ toPlaceholder: "end"
570
+ },
571
+ {
572
+ type: "number",
573
+ id: "bubbleLetters",
574
+ label: "Bubbles per row (override)",
575
+ description: "0 = derive from each question's own choices (recommended). Set 4 or 5 to force a uniform grid",
576
+ defaultValue: 0,
577
+ min: 0,
578
+ max: CHOICE_LETTERS.length,
579
+ appliesTo: ["bubble-sheet"]
580
+ },
581
+ {
582
+ type: "boolean",
583
+ id: "showPointsColumn",
584
+ label: "Show points column",
585
+ description: "Prints each question's point value beside its bubble row",
586
+ defaultValue: false,
587
+ appliesTo: ["bubble-sheet"]
588
+ }
589
+ ],
590
+ print(data, variantId = "question-booklet", rawSettings) {
591
+ const test = extractPracticeTest(data);
592
+ const s = resolveSettings(rawSettings);
593
+ if (test.questions.length === 0) {
594
+ return openPrintWindow(
595
+ buildPrintDocument(`<p>${EMPTY_MESSAGE}</p>`, test.title, EXAM_BASE_STYLES),
596
+ "practice-test"
597
+ );
598
+ }
599
+ const { questions, startIndex } = applyRange(test.questions, s);
600
+ switch (variantId) {
601
+ case "bubble-sheet":
602
+ return openPrintWindow(
603
+ buildPrintDocument(
604
+ renderBubbleSheet(test, questions, startIndex, s),
605
+ `${test.title} \u2014 Answer Sheet`,
606
+ BUBBLE_STYLES
607
+ ),
608
+ `${test.title}-answer-sheet`
609
+ );
610
+ case "answer-key":
611
+ return openPrintWindow(
612
+ buildPrintDocument(
613
+ renderAnswerKey(test, questions, startIndex),
614
+ `${test.title} \u2014 Answer Key`,
615
+ KEY_STYLES
616
+ ),
617
+ `${test.title}-answer-key`
618
+ );
619
+ case "explanations":
620
+ return openPrintWindow(
621
+ buildPrintDocument(
622
+ renderExplanations(test, questions, startIndex),
623
+ `${test.title} \u2014 Explanations`,
624
+ EXPLANATIONS_STYLES
625
+ ),
626
+ `${test.title}-explanations`
627
+ );
628
+ case "question-booklet":
629
+ default:
630
+ return openPrintWindow(
631
+ buildPrintDocument(
632
+ renderQuestionBooklet(test, questions, startIndex),
633
+ test.title,
634
+ BOOKLET_VARIANT_STYLES
635
+ ),
636
+ `${test.title}-questions`
637
+ );
638
+ }
639
+ }
640
+ };
641
+ var BUBBLE_SHEET_GEOMETRY = {
642
+ rowsPerColumn: BUBBLE_ROWS_PER_COLUMN,
643
+ columnsPerPage: BUBBLE_COLUMNS_PER_PAGE,
644
+ bubbleDiameterInches: 0.16,
645
+ rowPitchInches: 0.28
646
+ };
647
+ //# sourceMappingURL=exam.cjs.map