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