postext 1.58.2 → 1.59.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 (195) hide show
  1. package/dist/__tests__/exercises/config.test.d.ts +2 -0
  2. package/dist/__tests__/exercises/config.test.d.ts.map +1 -0
  3. package/dist/__tests__/exercises/config.test.js +63 -0
  4. package/dist/__tests__/exercises/config.test.js.map +1 -0
  5. package/dist/__tests__/exercises/digital.test.d.ts +2 -0
  6. package/dist/__tests__/exercises/digital.test.d.ts.map +1 -0
  7. package/dist/__tests__/exercises/digital.test.js +163 -0
  8. package/dist/__tests__/exercises/digital.test.js.map +1 -0
  9. package/dist/__tests__/exercises/languages.test.d.ts +2 -0
  10. package/dist/__tests__/exercises/languages.test.d.ts.map +1 -0
  11. package/dist/__tests__/exercises/languages.test.js +109 -0
  12. package/dist/__tests__/exercises/languages.test.js.map +1 -0
  13. package/dist/__tests__/exercises/layout.test.d.ts +2 -0
  14. package/dist/__tests__/exercises/layout.test.d.ts.map +1 -0
  15. package/dist/__tests__/exercises/layout.test.js +77 -0
  16. package/dist/__tests__/exercises/layout.test.js.map +1 -0
  17. package/dist/__tests__/exercises/model.test.d.ts +2 -0
  18. package/dist/__tests__/exercises/model.test.d.ts.map +1 -0
  19. package/dist/__tests__/exercises/model.test.js +90 -0
  20. package/dist/__tests__/exercises/model.test.js.map +1 -0
  21. package/dist/__tests__/exercises/parse.test.d.ts +2 -0
  22. package/dist/__tests__/exercises/parse.test.d.ts.map +1 -0
  23. package/dist/__tests__/exercises/parse.test.js +37 -0
  24. package/dist/__tests__/exercises/parse.test.js.map +1 -0
  25. package/dist/__tests__/exercises/refs.test.d.ts +2 -0
  26. package/dist/__tests__/exercises/refs.test.d.ts.map +1 -0
  27. package/dist/__tests__/exercises/refs.test.js +46 -0
  28. package/dist/__tests__/exercises/refs.test.js.map +1 -0
  29. package/dist/__tests__/exercises/solutions.test.d.ts +2 -0
  30. package/dist/__tests__/exercises/solutions.test.d.ts.map +1 -0
  31. package/dist/__tests__/exercises/solutions.test.js +53 -0
  32. package/dist/__tests__/exercises/solutions.test.js.map +1 -0
  33. package/dist/__tests__/exports.test.js +14 -0
  34. package/dist/__tests__/exports.test.js.map +1 -1
  35. package/dist/__tests__/parse/containers.test.js +2 -2
  36. package/dist/__tests__/parse/containers.test.js.map +1 -1
  37. package/dist/__tests__/pipeline/pageShorterThanLine.test.d.ts +2 -0
  38. package/dist/__tests__/pipeline/pageShorterThanLine.test.d.ts.map +1 -0
  39. package/dist/__tests__/pipeline/pageShorterThanLine.test.js +29 -0
  40. package/dist/__tests__/pipeline/pageShorterThanLine.test.js.map +1 -0
  41. package/dist/__tests__/pipeline/spanHeadingMidPage.test.js +11 -0
  42. package/dist/__tests__/pipeline/spanHeadingMidPage.test.js.map +1 -1
  43. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  44. package/dist/canvas-backend/blockRender.js +18 -0
  45. package/dist/canvas-backend/blockRender.js.map +1 -1
  46. package/dist/canvas-backend/chip.d.ts +3 -0
  47. package/dist/canvas-backend/chip.d.ts.map +1 -1
  48. package/dist/canvas-backend/chip.js +31 -1
  49. package/dist/canvas-backend/chip.js.map +1 -1
  50. package/dist/configWarnings.d.ts +3 -1
  51. package/dist/configWarnings.d.ts.map +1 -1
  52. package/dist/configWarnings.js +5 -1
  53. package/dist/configWarnings.js.map +1 -1
  54. package/dist/defaults/chipStyles.d.ts.map +1 -1
  55. package/dist/defaults/chipStyles.js +7 -0
  56. package/dist/defaults/chipStyles.js.map +1 -1
  57. package/dist/defaults/exercises.d.ts +111 -0
  58. package/dist/defaults/exercises.d.ts.map +1 -0
  59. package/dist/defaults/exercises.js +255 -0
  60. package/dist/defaults/exercises.js.map +1 -0
  61. package/dist/defaults/index.d.ts +2 -0
  62. package/dist/defaults/index.d.ts.map +1 -1
  63. package/dist/defaults/index.js +16 -0
  64. package/dist/defaults/index.js.map +1 -1
  65. package/dist/defaults/shared.d.ts +4 -0
  66. package/dist/defaults/shared.d.ts.map +1 -1
  67. package/dist/defaults/shared.js +6 -0
  68. package/dist/defaults/shared.js.map +1 -1
  69. package/dist/design/elementKeys.d.ts +2 -0
  70. package/dist/design/elementKeys.d.ts.map +1 -1
  71. package/dist/design/elementKeys.js +1 -1
  72. package/dist/design/elementKeys.js.map +1 -1
  73. package/dist/exercises/build.d.ts +60 -0
  74. package/dist/exercises/build.d.ts.map +1 -0
  75. package/dist/exercises/build.js +177 -0
  76. package/dist/exercises/build.js.map +1 -0
  77. package/dist/exercises/configWarnings.d.ts +11 -0
  78. package/dist/exercises/configWarnings.d.ts.map +1 -0
  79. package/dist/exercises/configWarnings.js +175 -0
  80. package/dist/exercises/configWarnings.js.map +1 -0
  81. package/dist/exercises/designs.d.ts +38 -0
  82. package/dist/exercises/designs.d.ts.map +1 -0
  83. package/dist/exercises/designs.js +143 -0
  84. package/dist/exercises/designs.js.map +1 -0
  85. package/dist/exercises/digital.d.ts +69 -0
  86. package/dist/exercises/digital.d.ts.map +1 -0
  87. package/dist/exercises/digital.js +58 -0
  88. package/dist/exercises/digital.js.map +1 -0
  89. package/dist/exercises/expand.d.ts +74 -0
  90. package/dist/exercises/expand.d.ts.map +1 -0
  91. package/dist/exercises/expand.js +1089 -0
  92. package/dist/exercises/expand.js.map +1 -0
  93. package/dist/exercises/grids.d.ts +62 -0
  94. package/dist/exercises/grids.d.ts.map +1 -0
  95. package/dist/exercises/grids.js +535 -0
  96. package/dist/exercises/grids.js.map +1 -0
  97. package/dist/exercises/model.d.ts +24 -0
  98. package/dist/exercises/model.d.ts.map +1 -0
  99. package/dist/exercises/model.js +360 -0
  100. package/dist/exercises/model.js.map +1 -0
  101. package/dist/exercises/record.d.ts +94 -0
  102. package/dist/exercises/record.d.ts.map +1 -0
  103. package/dist/exercises/record.js +9 -0
  104. package/dist/exercises/record.js.map +1 -0
  105. package/dist/exercises/runtime.d.ts +65 -0
  106. package/dist/exercises/runtime.d.ts.map +1 -0
  107. package/dist/exercises/runtime.js +1261 -0
  108. package/dist/exercises/runtime.js.map +1 -0
  109. package/dist/exercises/shuffle.d.ts +13 -0
  110. package/dist/exercises/shuffle.d.ts.map +1 -0
  111. package/dist/exercises/shuffle.js +56 -0
  112. package/dist/exercises/shuffle.js.map +1 -0
  113. package/dist/exercises/styles.d.ts +51 -0
  114. package/dist/exercises/styles.d.ts.map +1 -0
  115. package/dist/exercises/styles.js +221 -0
  116. package/dist/exercises/styles.js.map +1 -0
  117. package/dist/exercises/text.d.ts +31 -0
  118. package/dist/exercises/text.d.ts.map +1 -0
  119. package/dist/exercises/text.js +82 -0
  120. package/dist/exercises/text.js.map +1 -0
  121. package/dist/exercises/types.d.ts +125 -0
  122. package/dist/exercises/types.d.ts.map +1 -0
  123. package/dist/exercises/types.js +9 -0
  124. package/dist/exercises/types.js.map +1 -0
  125. package/dist/exercises/warnings.d.ts +16 -0
  126. package/dist/exercises/warnings.d.ts.map +1 -0
  127. package/dist/exercises/warnings.js +55 -0
  128. package/dist/exercises/warnings.js.map +1 -0
  129. package/dist/html-backend.d.ts +6 -0
  130. package/dist/html-backend.d.ts.map +1 -1
  131. package/dist/html-backend.js +67 -6
  132. package/dist/html-backend.js.map +1 -1
  133. package/dist/index.d.ts +6 -0
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +4 -0
  136. package/dist/index.js.map +1 -1
  137. package/dist/measure/cache.d.ts.map +1 -1
  138. package/dist/measure/cache.js +6 -1
  139. package/dist/measure/cache.js.map +1 -1
  140. package/dist/measure/rich.d.ts.map +1 -1
  141. package/dist/measure/rich.js +11 -1
  142. package/dist/measure/rich.js.map +1 -1
  143. package/dist/migrate/markdownRuns.js +1 -1
  144. package/dist/migrate/markdownRuns.js.map +1 -1
  145. package/dist/numbering.d.ts +3 -0
  146. package/dist/numbering.d.ts.map +1 -1
  147. package/dist/numbering.js +6 -0
  148. package/dist/numbering.js.map +1 -1
  149. package/dist/parse/blockParser.d.ts.map +1 -1
  150. package/dist/parse/blockParser.js +47 -4
  151. package/dist/parse/blockParser.js.map +1 -1
  152. package/dist/parse/exerciseGaps.d.ts +50 -0
  153. package/dist/parse/exerciseGaps.d.ts.map +1 -0
  154. package/dist/parse/exerciseGaps.js +151 -0
  155. package/dist/parse/exerciseGaps.js.map +1 -0
  156. package/dist/parse/index.d.ts +1 -1
  157. package/dist/parse/index.d.ts.map +1 -1
  158. package/dist/parse/index.js.map +1 -1
  159. package/dist/parse/sourceMapping.d.ts.map +1 -1
  160. package/dist/parse/sourceMapping.js +21 -0
  161. package/dist/parse/sourceMapping.js.map +1 -1
  162. package/dist/parse/types.d.ts +49 -1
  163. package/dist/parse/types.d.ts.map +1 -1
  164. package/dist/pipeline/build.d.ts.map +1 -1
  165. package/dist/pipeline/build.js +51 -9
  166. package/dist/pipeline/build.js.map +1 -1
  167. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  168. package/dist/pipeline/calloutLayout.js +1 -0
  169. package/dist/pipeline/calloutLayout.js.map +1 -1
  170. package/dist/pipeline/chips.d.ts.map +1 -1
  171. package/dist/pipeline/chips.js +2 -0
  172. package/dist/pipeline/chips.js.map +1 -1
  173. package/dist/pipeline/config.d.ts.map +1 -1
  174. package/dist/pipeline/config.js +6 -0
  175. package/dist/pipeline/config.js.map +1 -1
  176. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  177. package/dist/pipeline/contentWarnings.js +29 -1
  178. package/dist/pipeline/contentWarnings.js.map +1 -1
  179. package/dist/pipeline/continuation.d.ts.map +1 -1
  180. package/dist/pipeline/continuation.js +21 -6
  181. package/dist/pipeline/continuation.js.map +1 -1
  182. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  183. package/dist/pipeline/measureContentBlock.js +3 -2
  184. package/dist/pipeline/measureContentBlock.js.map +1 -1
  185. package/dist/pipeline/outline.d.ts.map +1 -1
  186. package/dist/pipeline/outline.js +1 -0
  187. package/dist/pipeline/outline.js.map +1 -1
  188. package/dist/pipeline/textWrap.js +1 -1
  189. package/dist/pipeline/textWrap.js.map +1 -1
  190. package/dist/types.d.ts +303 -2
  191. package/dist/types.d.ts.map +1 -1
  192. package/dist/vdt.d.ts +67 -0
  193. package/dist/vdt.d.ts.map +1 -1
  194. package/dist/vdt.js.map +1 -1
  195. package/package.json +1 -1
@@ -0,0 +1,1261 @@
1
+ /**
2
+ * The exercise script (#1230, #1231): makes the exercises of an HTML or EPUB
3
+ * output answerable, checks the answers and keeps them.
4
+ *
5
+ * The HTML output marks where an exercise is answered — its slots
6
+ * (`.pt-ex-slot[data-pt-ex][data-pt-slot]`, the chips of blanks, options,
7
+ * words) and its boxes (`.pt-ex-box[data-pt-box]`, the outer frame, the
8
+ * writing areas and grids) — and writes the records once, in
9
+ * `<script type="application/json" class="pt-exercises">`. The script lays
10
+ * its controls over those places: an input in a blank or a grid cell, a
11
+ * toggle on an option, lines between the dots of a match, a capsule in a
12
+ * word search, a text area on a writing area, a chooser on a word to sort;
13
+ * and a bar per exercise with Check, Show solution and Try again, a score
14
+ * read out to assistive technology (`aria-live`) and right or wrong marks
15
+ * that do not rest on colour alone (✓ and ✗).
16
+ *
17
+ * The whole runtime is one function with nothing from outside it, so the
18
+ * same code runs from the package (`attachExercises`) and as a standalone
19
+ * script (`EXERCISE_SCRIPT`, its source text). It keeps to syntax every
20
+ * reading system and transpiler leaves as it is: no spread, no `for…of`,
21
+ * no `async`.
22
+ */
23
+ /* eslint-disable @typescript-eslint/no-explicit-any -- the DOM side works on untyped JSON and elements of any document */
24
+ function exerciseRuntime() {
25
+ 'use strict';
26
+ const SVG_NS = 'http://www.w3.org/2000/svg';
27
+ // Answers kept here when there is no storage (a private window, a reader
28
+ // that blocks it, `persist: false`).
29
+ const memory = {};
30
+ // --- Answers -----------------------------------------------------------------
31
+ /** An answer as compared: compatibility forms folded (full-width
32
+ * letters and digits), Arabic-Indic digits read as ASCII, curly quotes
33
+ * straight, spaces collapsed, opening and final punctuation dropped,
34
+ * lower case. */
35
+ function normalizeAnswer(text) {
36
+ let s = String(text == null ? '' : text);
37
+ if (typeof s.normalize === 'function')
38
+ s = s.normalize('NFKC');
39
+ s = s.replace(/[\u0660-\u0669]/g, (c) => String(c.charCodeAt(0) - 0x0660));
40
+ s = s.replace(/[\u06F0-\u06F9]/g, (c) => String(c.charCodeAt(0) - 0x06F0));
41
+ s = s.replace(/[\u2018\u2019\u02BC\u0060\u00B4]/g, '\'').replace(/[\u201C\u201D\u00AB\u00BB\u201E]/g, '"');
42
+ s = s.split(/\s+/).join(' ');
43
+ // Opening and final punctuation, trimmed one character at a time.
44
+ const lead = ' \u00A1\u00BF';
45
+ const tail = ' .,;:!?\u3002\uFF0E\u3001\uFF01\uFF1F\u2026\u061F\u060C';
46
+ let a = 0;
47
+ let b = s.length;
48
+ while (a < b && lead.indexOf(s.charAt(a)) >= 0)
49
+ a++;
50
+ while (b > a && tail.indexOf(s.charAt(b - 1)) >= 0)
51
+ b--;
52
+ return s.slice(a, b).toLowerCase();
53
+ }
54
+ function same(a, accept) {
55
+ if (!accept)
56
+ return false;
57
+ const n = normalizeAnswer(a);
58
+ for (let i = 0; i < accept.length; i++)
59
+ if (normalizeAnswer(accept[i]) === n)
60
+ return true;
61
+ return false;
62
+ }
63
+ /** Whether a slot is right or wrong when checked: an open question's
64
+ * area is only shown its model, and a sort exercise's columns are
65
+ * filled from the words sorted. */
66
+ function isGraded(slot, record) {
67
+ if (slot.kind === 'text')
68
+ return record.kind !== 'open' && record.kind !== 'sort' && !!slot.accept && slot.accept.length > 0;
69
+ return true;
70
+ }
71
+ /** A true/false answer as 1 (true) or 2 (false), typed or ticked. */
72
+ function truthOf(value, labels) {
73
+ if (value === 1 || value === 2)
74
+ return value;
75
+ if (typeof value !== 'string' || value.trim() === '')
76
+ return null;
77
+ if (labels && same(value, [labels[0]]))
78
+ return 1;
79
+ if (labels && same(value, [labels[1]]))
80
+ return 2;
81
+ const n = normalizeAnswer(value);
82
+ if (n === '1' || n === 't' || n === 'true' || n === 'v' || n === '\u2713' || n === '\u221A' || n === '\u25CB')
83
+ return 1;
84
+ if (n === '2' || n === 'f' || n === 'false' || n === '\u2717' || n === '\u00D7' || n === 'x')
85
+ return 2;
86
+ return 0;
87
+ }
88
+ /** `true` right, `false` wrong, `null` not answered (or not graded). */
89
+ function checkSlot(slot, value, record, trueFalse) {
90
+ if (!isGraded(slot, record))
91
+ return null;
92
+ switch (slot.kind) {
93
+ case 'choice': {
94
+ if (!Array.isArray(value) || value.length === 0)
95
+ return null;
96
+ const want = (slot.correct || []).slice().sort();
97
+ const got = value.slice().sort();
98
+ if (want.length !== got.length)
99
+ return false;
100
+ for (let i = 0; i < want.length; i++)
101
+ if (want[i] !== got[i])
102
+ return false;
103
+ return true;
104
+ }
105
+ case 'truefalse': {
106
+ const t = truthOf(value, trueFalse);
107
+ if (t === null)
108
+ return null;
109
+ return t === (slot.correct || [])[0];
110
+ }
111
+ case 'mark':
112
+ if (value !== true)
113
+ return null;
114
+ return (slot.correct || []).indexOf(1) >= 0;
115
+ case 'pair': {
116
+ if (value === null || value === undefined || value === '')
117
+ return null;
118
+ if (typeof value === 'number')
119
+ return value === (slot.correct || [])[0];
120
+ if (typeof value !== 'string')
121
+ return null;
122
+ if (same(value, slot.accept))
123
+ return true;
124
+ return normalizeAnswer(value) === String((slot.correct || [])[0]);
125
+ }
126
+ case 'word': {
127
+ const found = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
128
+ if (found.length === 0)
129
+ return null;
130
+ for (let i = 0; i < found.length; i++)
131
+ if ((slot.accept || []).indexOf(found[i]) >= 0)
132
+ return true;
133
+ return null;
134
+ }
135
+ default: {
136
+ if (typeof value !== 'string' || value.trim() === '')
137
+ return null;
138
+ if (same(value, slot.accept))
139
+ return true;
140
+ // A number written in other digits or with a leading zero.
141
+ if (slot.kind === 'rank' || slot.kind === 'cell') {
142
+ const n = parseInt(normalizeAnswer(value), 10);
143
+ const accept = slot.accept || [];
144
+ for (let i = 0; i < accept.length; i++)
145
+ if (!isNaN(n) && parseInt(accept[i], 10) === n && /^\d+$/.test(accept[i]))
146
+ return true;
147
+ }
148
+ return false;
149
+ }
150
+ }
151
+ }
152
+ /** What a slot holds when its solution is shown. */
153
+ function solutionValue(slot, record, trueFalse) {
154
+ const accept = slot.accept || [];
155
+ switch (slot.kind) {
156
+ case 'choice': return (slot.correct || []).slice();
157
+ case 'truefalse': return (slot.correct || [])[0];
158
+ case 'mark': return (slot.correct || []).indexOf(1) >= 0;
159
+ case 'pair': return accept.length > 0 ? accept[0] : (slot.correct || [])[0];
160
+ case 'word': return accept.length > 0 ? [accept[0]] : [];
161
+ case 'text': return record.kind === 'sort' ? accept.join('\n') : accept[0] || '';
162
+ default: {
163
+ void trueFalse;
164
+ return accept[0] || '';
165
+ }
166
+ }
167
+ }
168
+ // --- Storage -----------------------------------------------------------------
169
+ function load(key, persist) {
170
+ let text = null;
171
+ if (persist) {
172
+ try {
173
+ text = window.localStorage.getItem(key);
174
+ }
175
+ catch (e) {
176
+ void e;
177
+ text = null;
178
+ }
179
+ }
180
+ if (text === null && Object.prototype.hasOwnProperty.call(memory, key))
181
+ text = memory[key];
182
+ if (!text)
183
+ return null;
184
+ try {
185
+ return JSON.parse(text);
186
+ }
187
+ catch (e) {
188
+ void e;
189
+ return null;
190
+ }
191
+ }
192
+ function save(key, persist, state) {
193
+ const text = JSON.stringify(state);
194
+ memory[key] = text;
195
+ if (!persist)
196
+ return;
197
+ try {
198
+ window.localStorage.setItem(key, text);
199
+ }
200
+ catch (e) {
201
+ void e;
202
+ }
203
+ }
204
+ function forget(key, persist) {
205
+ delete memory[key];
206
+ if (!persist)
207
+ return;
208
+ try {
209
+ window.localStorage.removeItem(key);
210
+ }
211
+ catch (e) {
212
+ void e;
213
+ }
214
+ }
215
+ // --- The page ----------------------------------------------------------------
216
+ const CSS = '.pt-ex-ui{pointer-events:auto;box-sizing:border-box;z-index:1;}' +
217
+ '.pt-ex-field{position:absolute;display:block;}' +
218
+ '.pt-ex-input{position:absolute;left:0;top:0;width:100%;height:100%;margin:0;padding:0 .15em;border:0;border-radius:.15em;background:transparent;color:inherit;font:inherit;line-height:1;text-align:center;outline-offset:1px;box-sizing:border-box;}' +
219
+ '.pt-ex-input:focus{background:rgba(255,255,255,.9);outline:2px solid #1a5fb4;}' +
220
+ 'textarea.pt-ex-input{text-align:start;resize:none;overflow:hidden;background:transparent;padding:0 .2em;}' +
221
+ 'textarea.pt-ex-input:focus{background:rgba(255,255,255,.35);}' +
222
+ '.pt-ex-cell{text-transform:uppercase;}' +
223
+ '.pt-ex-toggle{cursor:pointer;pointer-events:auto;}' +
224
+ '.pt-ex-toggle:focus-visible{outline:2px solid #1a5fb4;outline-offset:2px;}' +
225
+ '.pt-ex-on.pt-ex-ring,.pt-ex-on.pt-ex-mk-circle{outline:.12em solid currentColor;outline-offset:.04em;border-radius:999px;}' +
226
+ '.pt-ex-on.pt-ex-mk-box{outline:.1em solid currentColor;}' +
227
+ '.pt-ex-on.pt-ex-mk-underline{border-bottom:.12em solid currentColor;}' +
228
+ '.pt-ex-on.pt-ex-mk-highlight{background:rgba(255,214,0,.45);}' +
229
+ '.pt-ex-on.pt-ex-mk-strike{background:linear-gradient(transparent 46%,currentColor 46%,currentColor 56%,transparent 56%);}' +
230
+ '.pt-ex-on.pt-ex-tick::after{content:"\\2713";position:absolute;left:0;right:0;top:50%;transform:translateY(-55%);text-align:center;line-height:1;font-weight:700;}' +
231
+ '.pt-ex-revealed,.pt-ex-revealed .pt-ex-input{color:var(--pt-ex-answer,#d6246e);}' +
232
+ '.pt-ex-right{outline-color:var(--pt-ex-right,#1e8e3e)!important;}' +
233
+ '.pt-ex-wrong{outline-color:var(--pt-ex-wrong,#c5221f)!important;}' +
234
+ '.pt-ex-field.pt-ex-right .pt-ex-input,textarea.pt-ex-right{box-shadow:inset 0 -.12em 0 var(--pt-ex-right,#1e8e3e);}' +
235
+ '.pt-ex-field.pt-ex-wrong .pt-ex-input,textarea.pt-ex-wrong{box-shadow:inset 0 -.12em 0 var(--pt-ex-wrong,#c5221f);}' +
236
+ '.pt-ex-mark{position:absolute;right:-.55em;top:-.6em;font:700 .62em/1 system-ui,sans-serif;padding:.12em .2em;border-radius:999px;color:#fff;pointer-events:none;z-index:2;}' +
237
+ '.pt-ex-mark.pt-ex-ok{background:var(--pt-ex-right,#1e8e3e);}' +
238
+ '.pt-ex-mark.pt-ex-no{background:var(--pt-ex-wrong,#c5221f);}' +
239
+ '.pt-ex-bar{position:absolute;right:0;top:100%;margin-top:.2em;display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:.35em;font:400 1em/1.2 system-ui,sans-serif;z-index:3;}' +
240
+ '.pt-ex-bar button{font:inherit;margin:0;padding:.18em .6em;border:1px solid rgba(0,0,0,.35);border-radius:999px;background:#fff;color:#1d1d1f;cursor:pointer;}' +
241
+ '.pt-ex-bar button:hover{background:#f1f1f3;}' +
242
+ '.pt-ex-bar button:focus-visible{outline:2px solid #1a5fb4;outline-offset:2px;}' +
243
+ '.pt-ex-score{padding:0 .3em;font-weight:600;}' +
244
+ '.pt-ex-panel{position:absolute;left:0;right:0;top:100%;margin-top:2.1em;padding:.6em .8em;background:#fff;color:#1d1d1f;border:1px solid rgba(0,0,0,.2);border-radius:.4em;box-shadow:0 .3em 1em rgba(0,0,0,.15);font:400 1em/1.35 system-ui,sans-serif;z-index:4;}' +
245
+ '.pt-ex-panel p{margin:.2em 0;}' +
246
+ '.pt-ex-panel .pt-ex-key{font-weight:600;color:var(--pt-ex-answer,#d6246e);}' +
247
+ '.pt-ex-menu{position:absolute;left:0;top:100%;margin-top:.2em;display:flex;flex-direction:column;min-width:8em;padding:.2em;background:#fff;border:1px solid rgba(0,0,0,.25);border-radius:.3em;box-shadow:0 .2em .6em rgba(0,0,0,.18);font:400 .8em/1.2 system-ui,sans-serif;z-index:5;}' +
248
+ '.pt-ex-menu button{font:inherit;text-align:start;padding:.3em .5em;border:0;background:none;color:#1d1d1f;cursor:pointer;border-radius:.2em;}' +
249
+ '.pt-ex-menu button:hover,.pt-ex-menu button:focus{background:#eef2fb;}' +
250
+ '.pt-ex-tag{position:absolute;left:50%;top:100%;transform:translate(-50%,.1em);font:600 .55em/1 system-ui,sans-serif;white-space:nowrap;padding:.1em .3em;border-radius:.3em;background:#eef2fb;color:#1d1d1f;pointer-events:none;}' +
251
+ '.pt-ex-svg{position:absolute;left:0;top:0;overflow:visible;}' +
252
+ '.pt-ex-dot{fill:transparent;stroke:transparent;cursor:pointer;pointer-events:all;}' +
253
+ '.pt-ex-dot.pt-ex-pending{stroke:#1a5fb4;stroke-width:2;}' +
254
+ '.pt-ex-dot:focus{outline:none;stroke:#1a5fb4;stroke-width:2;}' +
255
+ '.pt-ex-bar.pt-ex-flow{position:static;justify-content:flex-start;margin-top:.6em;font-size:.8em;}' +
256
+ '.pt-ex-panel.pt-ex-flow{position:static;margin-top:.5em;box-shadow:none;font-size:.9em;}' +
257
+ '.pt-ex-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}';
258
+ function addStyle(root) {
259
+ const doc = root.nodeType === 9 ? root : root.ownerDocument;
260
+ const host = root.nodeType === 11 ? root : doc.head || doc.documentElement;
261
+ if (!host || (host.querySelector && host.querySelector('style.pt-ex-style')))
262
+ return () => undefined;
263
+ const style = doc.createElement('style');
264
+ style.setAttribute('class', 'pt-ex-style');
265
+ style.textContent = CSS;
266
+ host.appendChild(style);
267
+ return () => { if (style.parentNode)
268
+ style.parentNode.removeChild(style); };
269
+ }
270
+ function attach(root, options) {
271
+ const opts = options || {};
272
+ const doc = root.nodeType === 9 ? root : root.ownerDocument;
273
+ const cleanups = [];
274
+ cleanups.push(addStyle(root));
275
+ const scopes = [];
276
+ if (opts.data) {
277
+ scopes.push({ scope: root, data: opts.data });
278
+ }
279
+ else {
280
+ const scripts = root.querySelectorAll('script.pt-exercises');
281
+ for (let i = 0; i < scripts.length; i++) {
282
+ let data = null;
283
+ try {
284
+ data = JSON.parse(scripts[i].textContent || '');
285
+ }
286
+ catch (e) {
287
+ void e;
288
+ data = null;
289
+ }
290
+ if (!data || !data.exercises)
291
+ continue;
292
+ const holder = scripts[i].parentNode;
293
+ // The output may say what the answers are kept under (an EPUB:
294
+ // the book, so an exercise split over two page files is one).
295
+ const key = scripts[i].getAttribute('data-storage-key');
296
+ scopes.push({ scope: holder && holder !== root && holder.nodeType === 1 ? holder : root, data, ...(key ? { key } : {}) });
297
+ }
298
+ }
299
+ const base = opts.storageKey || ('pt-ex:' + (typeof location !== 'undefined' ? location.pathname : ''));
300
+ for (let k = 0; k < scopes.length; k++) {
301
+ const data = scopes[k].data;
302
+ if (data.interactive === false)
303
+ continue;
304
+ // Several documents on one page (a book's chapters): each keeps its
305
+ // answers apart, as their exercise ids may meet.
306
+ const own = scopes[k].key;
307
+ const scopeKey = opts.storageKey ? base + (scopes.length > 1 ? ':' + k : '') : own || base + (scopes.length > 1 ? ':' + k : '');
308
+ for (let j = 0; j < data.exercises.length; j++)
309
+ setUp(scopes[k].scope, data, data.exercises[j], scopeKey, opts, doc, cleanups);
310
+ const scope = scopes[k].scope;
311
+ cleanups.push(() => {
312
+ const left = scope.querySelectorAll('.pt-ex-mark,.pt-ex-menu');
313
+ for (let i = 0; i < left.length; i++)
314
+ if (left[i].parentNode)
315
+ left[i].parentNode.removeChild(left[i]);
316
+ });
317
+ }
318
+ let done = false;
319
+ return () => {
320
+ if (done)
321
+ return;
322
+ done = true;
323
+ for (let i = cleanups.length - 1; i >= 0; i--) {
324
+ try {
325
+ cleanups[i]();
326
+ }
327
+ catch (e) {
328
+ void e;
329
+ }
330
+ }
331
+ };
332
+ }
333
+ /** The text of the line an element stands on (a paragraph or a list
334
+ * item in a reflowable book), for its accessible name. */
335
+ function lineText(el) {
336
+ let line = el.parentNode;
337
+ while (line && line.nodeType === 1 && !line.classList.contains('pt-line') && !/^(p|li|td|th|dd|dt)$/i.test(line.localName || ''))
338
+ line = line.parentNode;
339
+ if (line && line.nodeType !== 1)
340
+ line = null;
341
+ const text = String((line || el).textContent || '').replace(/\s+/g, ' ').trim();
342
+ return text.length > 80 ? text.slice(0, 79) + '\u2026' : text;
343
+ }
344
+ /** The text a chip shows: the runs right after its slot, inside its box. */
345
+ function chipText(el) {
346
+ const right = parseFloat(el.style.left || '0') + parseFloat(el.style.width || '0');
347
+ let text = '';
348
+ let n = el.nextSibling;
349
+ while (n) {
350
+ if (n.nodeType === 1) {
351
+ if (n.classList && n.classList.contains('pt-ex-slot'))
352
+ break;
353
+ const left = parseFloat(n.style && n.style.left ? n.style.left : 'NaN');
354
+ if (!isNaN(left) && left >= right - 0.5)
355
+ break;
356
+ text += n.textContent || '';
357
+ }
358
+ n = n.nextSibling;
359
+ }
360
+ return text.replace(/\s+/g, ' ').trim();
361
+ }
362
+ function px(v) { return (Math.round(v * 1000) / 1000) + 'px'; }
363
+ function setUp(scope, data, rec, base, opts, doc, cleanups) {
364
+ const id = String(rec.id);
365
+ const outers = scope.querySelectorAll('.pt-ex-box[data-pt-exercise="' + id + '"]');
366
+ const slotEls = {};
367
+ const found = scope.querySelectorAll('.pt-ex-slot[data-pt-ex="' + id + '"]');
368
+ for (let i = 0; i < found.length; i++)
369
+ slotEls[found[i].getAttribute('data-pt-slot')] = found[i];
370
+ const boxEls = {};
371
+ const boxes = scope.querySelectorAll('.pt-ex-box[data-pt-ex="' + id + '"]');
372
+ for (let i = 0; i < boxes.length; i++) {
373
+ const b = boxes[i].getAttribute('data-pt-box');
374
+ if (!boxEls[b])
375
+ boxEls[b] = boxes[i];
376
+ }
377
+ if (outers.length === 0 && found.length === 0)
378
+ return null;
379
+ const persist = opts.persist !== undefined ? opts.persist : data.persist !== false;
380
+ const instant = (opts.check || data.check) === 'instant';
381
+ const words = data.words || {};
382
+ const look = (data.styles && data.styles[rec.style]) || { correct: '#1e8e3e', wrong: '#c5221f', answer: '#d6246e', marker: 'none', trueFalse: words.trueFalse || ['T', 'F'] };
383
+ const tf = look.trueFalse;
384
+ const exName = (words.exercise || 'Exercise') + ' ' + rec.number;
385
+ const key = base + ':' + (rec.anchor || '#' + rec.ordinal + '.' + rec.id);
386
+ const saved = load(key, persist) || {};
387
+ const state = { v: saved.v || {}, checked: !!saved.checked, revealed: false };
388
+ const ppt = data.pxPerPt || 1;
389
+ const body = data.bodyPx || 16;
390
+ const flow = data.flow === true;
391
+ // The answer key written for a reader with no script: the bar's own
392
+ // takes its place.
393
+ const statics = scope.querySelectorAll('.pt-ex-static[data-pt-ex="' + id + '"]');
394
+ for (let i = 0; i < statics.length; i++)
395
+ setAttr(statics[i], 'hidden', 'hidden');
396
+ // The font of the exercise's text, for what is written on its areas.
397
+ const firstLine = scope.querySelector('.pt-line');
398
+ const textFont = firstLine && firstLine.style ? firstLine.style.fontFamily : '';
399
+ function el(tag, cls, parent) {
400
+ const e = doc.createElement(tag);
401
+ e.setAttribute('class', cls);
402
+ if (parent)
403
+ parent.appendChild(e);
404
+ return e;
405
+ }
406
+ function svg(tag, attrs, parent) {
407
+ const e = doc.createElementNS(SVG_NS, tag);
408
+ for (const a in attrs)
409
+ if (Object.prototype.hasOwnProperty.call(attrs, a))
410
+ e.setAttribute(a, String(attrs[a]));
411
+ if (parent)
412
+ parent.appendChild(e);
413
+ return e;
414
+ }
415
+ function colours(e) {
416
+ e.style.setProperty('--pt-ex-right', look.correct);
417
+ e.style.setProperty('--pt-ex-wrong', look.wrong);
418
+ e.style.setProperty('--pt-ex-answer', look.answer);
419
+ }
420
+ function own(e) {
421
+ cleanups.push(() => { if (e.parentNode)
422
+ e.parentNode.removeChild(e); });
423
+ return e;
424
+ }
425
+ /** Changes an attribute of a page element, put back on detach. */
426
+ function setAttr(e, name, value) {
427
+ const before = e.getAttribute(name);
428
+ if (value === null)
429
+ e.removeAttribute(name);
430
+ else
431
+ e.setAttribute(name, value);
432
+ cleanups.push(() => { if (before === null)
433
+ e.removeAttribute(name);
434
+ else
435
+ e.setAttribute(name, before); });
436
+ }
437
+ function addClass(e, cls) {
438
+ if (e.classList.contains(cls))
439
+ return;
440
+ e.classList.add(cls);
441
+ cleanups.push(() => e.classList.remove(cls));
442
+ }
443
+ function badge(holder, ok) {
444
+ const old = holder.querySelector(':scope > .pt-ex-mark');
445
+ if (old)
446
+ old.parentNode.removeChild(old);
447
+ holder.classList.remove('pt-ex-right', 'pt-ex-wrong');
448
+ if (ok === null)
449
+ return;
450
+ holder.classList.add(ok ? 'pt-ex-right' : 'pt-ex-wrong');
451
+ const m = el('span', 'pt-ex-mark ' + (ok ? 'pt-ex-ok' : 'pt-ex-no'), holder);
452
+ m.setAttribute('aria-hidden', 'true');
453
+ m.textContent = ok ? '\u2713' : '\u2717';
454
+ }
455
+ const widgets = [];
456
+ let blankN = 0;
457
+ /** An input over a slot's box: a blank, an order number, a letter of a
458
+ * match or a true/false bracket. */
459
+ function inputOn(slot, target) {
460
+ blankN++;
461
+ setAttr(target, 'role', null);
462
+ setAttr(target, 'aria-label', null);
463
+ addClass(target, 'pt-ex-ui');
464
+ // A blank may bring its input (a reflowable book's, typed into with
465
+ // no script): it is used as it is.
466
+ const given = target.querySelector('input');
467
+ const input = given || own(el('input', 'pt-ex-input pt-ex-ui', target));
468
+ if (given)
469
+ addClass(given, 'pt-ex-ui');
470
+ input.setAttribute('type', 'text');
471
+ input.setAttribute('autocomplete', 'off');
472
+ input.setAttribute('spellcheck', 'false');
473
+ input.setAttribute('aria-label', (words.blank || 'blank') + ' ' + blankN + ', ' + exName);
474
+ if (slot.kind === 'rank')
475
+ input.setAttribute('inputmode', 'numeric');
476
+ colours(target);
477
+ input.addEventListener('input', () => changed(slot, input.value));
478
+ widgets.push({
479
+ slot,
480
+ paint(v) { input.value = typeof v === 'string' ? v : v === undefined || v === null ? '' : slot.kind === 'truefalse' ? tf[v - 1] || '' : String(v); },
481
+ lock(on) { input.readOnly = on; target.classList.toggle('pt-ex-revealed', on); },
482
+ mark(ok) { badge(target, ok); if (ok === false)
483
+ input.setAttribute('aria-invalid', 'true');
484
+ else
485
+ input.removeAttribute('aria-invalid'); },
486
+ });
487
+ }
488
+ /** Toggles on the option chips of a choice (or the boxes of a
489
+ * true/false): one at a time unless several are right. */
490
+ function toggles(slot, targets, values, cls, multiple) {
491
+ const role = multiple ? 'checkbox' : 'radio';
492
+ for (let i = 0; i < targets.length; i++) {
493
+ const t = targets[i];
494
+ const value = values[i];
495
+ setAttr(t, 'role', role);
496
+ setAttr(t, 'tabindex', '0');
497
+ setAttr(t, 'aria-checked', 'false');
498
+ // A word of its own (an inline choice) names it; a bare label or
499
+ // box, the line it stands on.
500
+ const word = chipText(t);
501
+ setAttr(t, 'aria-label', (slot.kind === 'truefalse' ? tf[value - 1] + ' \u2014 ' : '') + (word.length > 2 ? word : lineText(t)));
502
+ addClass(t, 'pt-ex-toggle');
503
+ addClass(t, 'pt-ex-ui');
504
+ addClass(t, cls);
505
+ colours(t);
506
+ const flip = () => {
507
+ if (state.revealed)
508
+ return;
509
+ const cur = state.v[slot.key];
510
+ let next;
511
+ if (slot.kind === 'truefalse')
512
+ next = cur === value ? null : value;
513
+ else {
514
+ const list = Array.isArray(cur) ? cur.slice() : [];
515
+ const at = list.indexOf(value);
516
+ if (at >= 0)
517
+ list.splice(at, 1);
518
+ else if (multiple)
519
+ list.push(value);
520
+ else {
521
+ list.length = 0;
522
+ list.push(value);
523
+ }
524
+ next = list;
525
+ }
526
+ changed(slot, next);
527
+ };
528
+ const onKey = (e) => {
529
+ if (e.key === ' ' || e.key === 'Enter') {
530
+ e.preventDefault();
531
+ flip();
532
+ }
533
+ };
534
+ t.addEventListener('click', flip);
535
+ t.addEventListener('keydown', onKey);
536
+ cleanups.push(() => { t.removeEventListener('click', flip); t.removeEventListener('keydown', onKey); });
537
+ }
538
+ const on = (v, value) => (slot.kind === 'truefalse' ? v === value : Array.isArray(v) && v.indexOf(value) >= 0);
539
+ widgets.push({
540
+ slot,
541
+ paint(v) {
542
+ for (let i = 0; i < targets.length; i++) {
543
+ const sel = on(v, values[i]);
544
+ targets[i].classList.toggle('pt-ex-on', sel);
545
+ targets[i].setAttribute('aria-checked', sel ? 'true' : 'false');
546
+ }
547
+ },
548
+ lock(lock) { for (let i = 0; i < targets.length; i++)
549
+ targets[i].classList.toggle('pt-ex-revealed', lock); },
550
+ mark(ok) {
551
+ const v = state.v[slot.key];
552
+ for (let i = 0; i < targets.length; i++) {
553
+ if (ok === null || !on(v, values[i])) {
554
+ badge(targets[i], null);
555
+ continue;
556
+ }
557
+ // Each option ticked says whether it is one of the right ones
558
+ // (the score says whether the question is answered whole).
559
+ badge(targets[i], slot.kind === 'truefalse' ? ok : (slot.correct || []).indexOf(values[i]) >= 0);
560
+ }
561
+ },
562
+ });
563
+ }
564
+ /** A word to mark: a toggle in the exercise's mark. */
565
+ function markToggle(slot, t) {
566
+ setAttr(t, 'role', 'checkbox');
567
+ setAttr(t, 'tabindex', '0');
568
+ setAttr(t, 'aria-checked', 'false');
569
+ setAttr(t, 'aria-label', chipText(t) || lineText(t));
570
+ addClass(t, 'pt-ex-toggle');
571
+ addClass(t, 'pt-ex-ui');
572
+ addClass(t, 'pt-ex-mk-' + (rec.mark || 'circle'));
573
+ colours(t);
574
+ const flip = () => { if (!state.revealed)
575
+ changed(slot, state.v[slot.key] === true ? null : true); };
576
+ const onKey = (e) => { if (e.key === ' ' || e.key === 'Enter') {
577
+ e.preventDefault();
578
+ flip();
579
+ } };
580
+ t.addEventListener('click', flip);
581
+ t.addEventListener('keydown', onKey);
582
+ cleanups.push(() => { t.removeEventListener('click', flip); t.removeEventListener('keydown', onKey); });
583
+ widgets.push({
584
+ slot,
585
+ paint(v) { t.classList.toggle('pt-ex-on', v === true); t.setAttribute('aria-checked', v === true ? 'true' : 'false'); },
586
+ lock(on) { t.classList.toggle('pt-ex-revealed', on); },
587
+ mark(ok) { badge(t, ok); },
588
+ });
589
+ }
590
+ /** The categories of a sort exercise, in the order of its columns. */
591
+ const categories = [];
592
+ for (let i = 0; i < rec.slots.length; i++) {
593
+ const s = rec.slots[i];
594
+ if (s.kind === 'category' && s.accept && s.accept[0] !== undefined && categories.indexOf(s.accept[0]) < 0)
595
+ categories.push(s.accept[0]);
596
+ }
597
+ const columnWidgets = [];
598
+ function refreshColumns() {
599
+ if (columnWidgets.length === 0)
600
+ return;
601
+ const lists = categories.map(() => []);
602
+ for (let i = 0; i < widgets.length; i++) {
603
+ const w = widgets[i];
604
+ if (w.slot.kind !== 'category')
605
+ continue;
606
+ const v = state.revealed ? solutionValue(w.slot, rec) : state.v[w.slot.key];
607
+ const at = typeof v === 'string' ? categories.indexOf(v) : -1;
608
+ if (at >= 0 && w.word)
609
+ lists[at].push(w.word);
610
+ }
611
+ for (let i = 0; i < columnWidgets.length; i++)
612
+ columnWidgets[i].show((lists[i] || []).join('\n'));
613
+ }
614
+ /** A word of a sort exercise's bank: a chooser of the categories. */
615
+ function chooser(slot, t) {
616
+ const word = chipText(t);
617
+ setAttr(t, 'role', 'button');
618
+ setAttr(t, 'tabindex', '0');
619
+ setAttr(t, 'aria-haspopup', 'true');
620
+ setAttr(t, 'aria-label', word || lineText(t));
621
+ addClass(t, 'pt-ex-toggle');
622
+ addClass(t, 'pt-ex-ui');
623
+ colours(t);
624
+ const tag = own(el('span', 'pt-ex-tag', t));
625
+ tag.hidden = true;
626
+ let menu = null;
627
+ // A click anywhere else closes the menu.
628
+ const outside = (e) => { if (menu && !t.contains(e.target))
629
+ close(); };
630
+ const close = () => {
631
+ if (menu && menu.parentNode)
632
+ menu.parentNode.removeChild(menu);
633
+ menu = null;
634
+ t.style.zIndex = '';
635
+ doc.removeEventListener('click', outside, true);
636
+ };
637
+ const open = () => {
638
+ if (state.revealed)
639
+ return;
640
+ if (menu) {
641
+ close();
642
+ return;
643
+ }
644
+ // Over the controls laid after the word (the columns' areas).
645
+ t.style.zIndex = '6';
646
+ doc.addEventListener('click', outside, true);
647
+ menu = el('span', 'pt-ex-menu pt-ex-ui', t);
648
+ menu.setAttribute('role', 'menu');
649
+ const options = categories.concat(['\u2014']);
650
+ for (let i = 0; i < options.length; i++) {
651
+ const b = el('button', '', menu);
652
+ b.setAttribute('type', 'button');
653
+ b.setAttribute('role', 'menuitem');
654
+ b.textContent = options[i];
655
+ const value = i < categories.length ? categories[i] : null;
656
+ b.addEventListener('click', (e) => { e.stopPropagation(); close(); changed(slot, value); t.focus(); });
657
+ }
658
+ const first = menu.querySelector('button');
659
+ if (first)
660
+ first.focus();
661
+ };
662
+ const onKey = (e) => {
663
+ if (e.key === 'Escape') {
664
+ close();
665
+ t.focus();
666
+ return;
667
+ }
668
+ if ((e.key === ' ' || e.key === 'Enter') && e.target === t) {
669
+ e.preventDefault();
670
+ open();
671
+ }
672
+ };
673
+ const onClick = (e) => { if (e.target === t || e.target === tag)
674
+ open(); };
675
+ t.addEventListener('click', onClick);
676
+ t.addEventListener('keydown', onKey);
677
+ cleanups.push(() => { close(); t.removeEventListener('click', onClick); t.removeEventListener('keydown', onKey); });
678
+ widgets.push({
679
+ slot,
680
+ word,
681
+ paint(v) {
682
+ const at = typeof v === 'string' ? categories.indexOf(v) : -1;
683
+ tag.hidden = at < 0;
684
+ tag.textContent = at >= 0 ? categories[at] : '';
685
+ t.classList.toggle('pt-ex-on', at >= 0);
686
+ refreshColumns();
687
+ },
688
+ lock(on) { t.classList.toggle('pt-ex-revealed', on); tag.classList.toggle('pt-ex-revealed', on); },
689
+ mark(ok) { badge(t, ok); },
690
+ });
691
+ }
692
+ /** A text area on a writing area: an open answer, a sentence put in
693
+ * order; a sort exercise's column, filled from the words sorted. */
694
+ function area(slot, box) {
695
+ const sortColumn = rec.kind === 'sort';
696
+ const ta = own(el('textarea', 'pt-ex-input pt-ex-ui', box));
697
+ // A sort exercise's column is named by its category.
698
+ const column = sortColumn ? categories[columnWidgets.length] : undefined;
699
+ ta.setAttribute('aria-label', column !== undefined ? column + ', ' + exName : (words.blank || 'blank') + ' ' + (++blankN) + ', ' + exName);
700
+ ta.setAttribute('spellcheck', 'false');
701
+ const line = (slot.linePt || 0) * ppt || body * 1.35;
702
+ ta.style.fontSize = px(body);
703
+ ta.style.lineHeight = px(line);
704
+ ta.style.paddingTop = px(Math.max(0, line * 0.12));
705
+ if (textFont)
706
+ ta.style.fontFamily = textFont;
707
+ colours(ta);
708
+ if (sortColumn) {
709
+ ta.readOnly = true;
710
+ ta.setAttribute('tabindex', '-1');
711
+ ta.setAttribute('aria-live', 'polite');
712
+ }
713
+ else
714
+ ta.addEventListener('input', () => changed(slot, ta.value));
715
+ const w = {
716
+ slot,
717
+ show(text) { ta.value = text; },
718
+ paint(v) { if (!sortColumn)
719
+ ta.value = typeof v === 'string' ? v : ''; },
720
+ lock(on) { if (!sortColumn)
721
+ ta.readOnly = on; ta.classList.toggle('pt-ex-revealed', on); },
722
+ mark(ok) {
723
+ ta.classList.remove('pt-ex-right', 'pt-ex-wrong');
724
+ const old = box.querySelector(':scope > .pt-ex-mark');
725
+ if (old)
726
+ old.parentNode.removeChild(old);
727
+ if (ok === null)
728
+ return;
729
+ ta.classList.add(ok ? 'pt-ex-right' : 'pt-ex-wrong');
730
+ const m = el('span', 'pt-ex-mark ' + (ok ? 'pt-ex-ok' : 'pt-ex-no'), box);
731
+ m.setAttribute('aria-hidden', 'true');
732
+ m.textContent = ok ? '\u2713' : '\u2717';
733
+ m.style.fontSize = px(body * 0.62);
734
+ colours(m);
735
+ },
736
+ };
737
+ if (sortColumn)
738
+ columnWidgets.push(w);
739
+ widgets.push(w);
740
+ }
741
+ /** One-character inputs over the cells of a crossword or an operation. */
742
+ const cellInputs = {};
743
+ function cell(slot, box, grid) {
744
+ const r = slot.cell ? slot.cell[0] : 0;
745
+ const c = slot.cell ? slot.cell[1] : 0;
746
+ const size = grid.cellPt * ppt;
747
+ const f = own(el('span', 'pt-ex-field pt-ex-ui', box));
748
+ f.style.left = px((grid.xPt + c * grid.cellPt) * ppt);
749
+ f.style.top = px((grid.yPt + r * grid.cellPt) * ppt);
750
+ f.style.width = px(size);
751
+ f.style.height = px(size);
752
+ f.style.fontSize = px(size * 0.56);
753
+ if (textFont)
754
+ f.style.fontFamily = textFont;
755
+ colours(f);
756
+ const input = el('input', 'pt-ex-input' + (grid.kind === 'crossword' ? ' pt-ex-cell' : ''), f);
757
+ input.setAttribute('type', 'text');
758
+ input.setAttribute('autocomplete', 'off');
759
+ input.setAttribute('spellcheck', 'false');
760
+ input.setAttribute('maxlength', '2');
761
+ input.style.background = 'transparent';
762
+ if (grid.kind === 'arithmetic')
763
+ input.setAttribute('inputmode', 'numeric');
764
+ input.setAttribute('aria-label', exName + ', ' + (r + 1) + '\u00B7' + (c + 1));
765
+ cellInputs[slot.box + ':' + r + ',' + c] = input;
766
+ input.addEventListener('input', () => {
767
+ // One character: the last typed.
768
+ const chars = Array.from ? Array.from(input.value) : String(input.value).split('');
769
+ if (chars.length > 1)
770
+ input.value = chars[chars.length - 1];
771
+ changed(slot, input.value);
772
+ if (input.value && grid.kind === 'crossword') {
773
+ const next = cellInputs[slot.box + ':' + r + ',' + (c + 1)] || cellInputs[slot.box + ':' + (r + 1) + ',' + c];
774
+ if (next)
775
+ next.focus();
776
+ }
777
+ });
778
+ widgets.push({
779
+ slot,
780
+ paint(v) { input.value = typeof v === 'string' ? v : ''; },
781
+ lock(on) { input.readOnly = on; f.classList.toggle('pt-ex-revealed', on); },
782
+ mark(ok) { badge(f, ok); },
783
+ });
784
+ }
785
+ /** The dots of a match: a left dot then a right one (or the other way
786
+ * round) join them with a line. */
787
+ function matchDots(box, grid, slots) {
788
+ const w = parseFloat(box.style.width) || 0;
789
+ const h = parseFloat(box.style.height) || 0;
790
+ const s = own(svg('svg', { class: 'pt-ex-svg pt-ex-ui', width: w, height: h, viewBox: '0 0 ' + w + ' ' + h }, box));
791
+ const lines = svg('g', {}, s);
792
+ const marks = svg('g', {}, s);
793
+ const lx = grid.dotsPt[0] * ppt;
794
+ const rx = grid.dotsPt[1] * ppt;
795
+ const rows = grid.rowsPt.map((y) => y * ppt);
796
+ const radius = Math.max(body * 0.55, 6);
797
+ let pendingLeft = -1;
798
+ let pendingRight = -1;
799
+ const leftDots = [];
800
+ const rightDots = [];
801
+ const refresh = () => {
802
+ for (let i = 0; i < leftDots.length; i++)
803
+ leftDots[i].classList.toggle('pt-ex-pending', i === pendingLeft);
804
+ for (let i = 0; i < rightDots.length; i++)
805
+ rightDots[i].classList.toggle('pt-ex-pending', i === pendingRight);
806
+ };
807
+ const join = (li, ri) => {
808
+ pendingLeft = -1;
809
+ pendingRight = -1;
810
+ refresh();
811
+ // A right item takes one left item: an earlier line to it goes.
812
+ for (let i = 0; i < slots.length; i++)
813
+ if (i !== li && state.v[slots[i].key] === ri + 1)
814
+ changed(slots[i], null);
815
+ changed(slots[li], state.v[slots[li].key] === ri + 1 ? null : ri + 1);
816
+ };
817
+ const label = (side, i) => exName + ', ' + (side === 0 ? String(i + 1) : String.fromCharCode(97 + i));
818
+ for (let i = 0; i < rows.length; i++) {
819
+ for (let side = 0; side < 2; side++) {
820
+ if (side === 0 && i >= slots.length)
821
+ continue;
822
+ const dot = svg('circle', { class: 'pt-ex-dot', cx: side === 0 ? lx : rx, cy: rows[i], r: radius, tabindex: 0, role: 'button', 'aria-label': label(side, i) }, s);
823
+ (side === 0 ? leftDots : rightDots).push(dot);
824
+ const pick = () => {
825
+ if (state.revealed)
826
+ return;
827
+ if (side === 0) {
828
+ if (pendingRight >= 0)
829
+ join(i, pendingRight);
830
+ else {
831
+ pendingLeft = pendingLeft === i ? -1 : i;
832
+ refresh();
833
+ }
834
+ }
835
+ else if (pendingLeft >= 0)
836
+ join(pendingLeft, i);
837
+ else {
838
+ pendingRight = pendingRight === i ? -1 : i;
839
+ refresh();
840
+ }
841
+ };
842
+ dot.addEventListener('click', pick);
843
+ dot.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') {
844
+ e.preventDefault();
845
+ pick();
846
+ } });
847
+ }
848
+ }
849
+ for (let i = 0; i < slots.length; i++) {
850
+ const slot = slots[i];
851
+ const li = i;
852
+ let line = null;
853
+ let mark = null;
854
+ widgets.push({
855
+ slot,
856
+ paint(v) {
857
+ if (line) {
858
+ lines.removeChild(line);
859
+ line = null;
860
+ }
861
+ if (typeof v === 'number' && rows[v - 1] !== undefined) {
862
+ line = svg('line', { x1: lx, y1: rows[li], x2: rx, y2: rows[v - 1], stroke: 'currentColor', 'stroke-width': Math.max(1.5, body * 0.09), 'stroke-linecap': 'round' }, lines);
863
+ if (state.revealed)
864
+ line.setAttribute('stroke', look.answer);
865
+ }
866
+ },
867
+ lock() { },
868
+ mark(ok) {
869
+ if (mark) {
870
+ marks.removeChild(mark);
871
+ mark = null;
872
+ }
873
+ if (line)
874
+ line.setAttribute('stroke', ok === null ? (state.revealed ? look.answer : 'currentColor') : ok ? look.correct : look.wrong);
875
+ if (ok === null)
876
+ return;
877
+ mark = svg('text', { x: lx + radius * 2.2, y: rows[li] - body * 0.25, 'font-size': body * 0.8, 'font-weight': 700, fill: ok ? look.correct : look.wrong, 'text-anchor': 'middle', 'aria-hidden': 'true' }, marks);
878
+ mark.textContent = ok ? '\u2713' : '\u2717';
879
+ },
880
+ });
881
+ }
882
+ }
883
+ /** A word search: a cell then another in a line with it mark a word. */
884
+ function wordSearch(box, grid, slots) {
885
+ const w = parseFloat(box.style.width) || 0;
886
+ const h = parseFloat(box.style.height) || 0;
887
+ const cellPx = grid.cellPt * ppt;
888
+ const x0 = grid.xPt * ppt;
889
+ const y0 = grid.yPt * ppt;
890
+ const s = own(svg('svg', { class: 'pt-ex-svg pt-ex-ui', width: w, height: h, viewBox: '0 0 ' + w + ' ' + h, role: 'group', 'aria-label': exName }, box));
891
+ const paintG = svg('g', {}, s);
892
+ const hit = svg('rect', { x: x0, y: y0, width: grid.cols * cellPx, height: grid.rows * cellPx, fill: 'transparent', style: 'cursor:pointer;pointer-events:all' }, s);
893
+ let start = null;
894
+ let startMark = null;
895
+ const capsule = (sel, color) => {
896
+ const ends = sel.split('-');
897
+ const a = ends[0].split(',').map(Number);
898
+ const b = ends[1].split(',').map(Number);
899
+ return svg('line', {
900
+ x1: x0 + (a[1] + 0.5) * cellPx, y1: y0 + (a[0] + 0.5) * cellPx, x2: x0 + (b[1] + 0.5) * cellPx, y2: y0 + (b[0] + 0.5) * cellPx,
901
+ stroke: color, 'stroke-opacity': 0.75, 'stroke-width': cellPx * 0.78, 'stroke-linecap': 'round', fill: 'none', 'pointer-events': 'none',
902
+ }, paintG);
903
+ };
904
+ const onClick = (e) => {
905
+ if (state.revealed)
906
+ return;
907
+ const r = hit.getBoundingClientRect();
908
+ const sx = r.width / (grid.cols * cellPx) || 1;
909
+ const sy = r.height / (grid.rows * cellPx) || 1;
910
+ const c = Math.floor((e.clientX - r.left) / sx / cellPx);
911
+ const rr = Math.floor((e.clientY - r.top) / sy / cellPx);
912
+ if (rr < 0 || c < 0 || rr >= grid.rows || c >= grid.cols)
913
+ return;
914
+ if (!start) {
915
+ start = [rr, c];
916
+ startMark = svg('circle', { cx: x0 + (c + 0.5) * cellPx, cy: y0 + (rr + 0.5) * cellPx, r: cellPx * 0.39, fill: 'none', stroke: '#1a5fb4', 'stroke-width': 2, 'pointer-events': 'none' }, s);
917
+ return;
918
+ }
919
+ const dr = rr - start[0];
920
+ const dc = c - start[1];
921
+ const a = start;
922
+ start = null;
923
+ if (startMark) {
924
+ s.removeChild(startMark);
925
+ startMark = null;
926
+ }
927
+ if ((dr === 0 && dc === 0) || !(dr === 0 || dc === 0 || Math.abs(dr) === Math.abs(dc)))
928
+ return;
929
+ const sel = a[0] + ',' + a[1] + '-' + rr + ',' + c;
930
+ const back = rr + ',' + c + '-' + a[0] + ',' + a[1];
931
+ const list = Array.isArray(state.v._found) ? state.v._found.slice() : [];
932
+ const at = list.indexOf(sel) >= 0 ? list.indexOf(sel) : list.indexOf(back);
933
+ if (at >= 0)
934
+ list.splice(at, 1);
935
+ else
936
+ list.push(sel);
937
+ if (list.length > 0)
938
+ state.v._found = list;
939
+ else
940
+ delete state.v._found;
941
+ verdicts = null;
942
+ repaint();
943
+ persistNow();
944
+ afterChange(null);
945
+ };
946
+ hit.addEventListener('click', onClick);
947
+ let painted = [];
948
+ let verdicts = null;
949
+ const repaint = () => {
950
+ for (let i = 0; i < painted.length; i++)
951
+ if (painted[i].parentNode)
952
+ painted[i].parentNode.removeChild(painted[i]);
953
+ painted = [];
954
+ const list = state.revealed ? slots.map((sl) => (sl.accept || [])[0]).filter(Boolean) : Array.isArray(state.v._found) ? state.v._found : [];
955
+ for (let i = 0; i < list.length; i++) {
956
+ let ok = null;
957
+ if (verdicts) {
958
+ ok = false;
959
+ for (let j = 0; j < slots.length; j++) {
960
+ const acc = slots[j].accept || [];
961
+ if (acc.indexOf(list[i]) >= 0)
962
+ ok = true;
963
+ }
964
+ }
965
+ painted.push(capsule(list[i], state.revealed ? look.answer : ok === null ? '#9ec5ff' : ok ? look.correct : look.wrong));
966
+ if (ok !== null) {
967
+ const ends = list[i].split('-')[1].split(',').map(Number);
968
+ const t = svg('text', { x: x0 + (ends[1] + 1) * cellPx, y: y0 + ends[0] * cellPx + cellPx * 0.3, 'font-size': cellPx * 0.5, 'font-weight': 700, fill: ok ? look.correct : look.wrong, 'aria-hidden': 'true' }, paintG);
969
+ t.textContent = ok ? '\u2713' : '\u2717';
970
+ painted.push(t);
971
+ }
972
+ }
973
+ };
974
+ for (let i = 0; i < slots.length; i++) {
975
+ widgets.push({
976
+ slot: slots[i],
977
+ paint() { verdicts = null; repaint(); },
978
+ lock() { },
979
+ // Every word shares the capsules: they show right and wrong while
980
+ // the exercise is checked, whatever this word's own verdict.
981
+ mark(ok, checking) { void ok; verdicts = checking ? {} : null; repaint(); },
982
+ });
983
+ }
984
+ }
985
+ // The slots, each on its place.
986
+ const pairSlots = [];
987
+ const wordSlots = [];
988
+ for (let i = 0; i < rec.slots.length; i++) {
989
+ const slot = rec.slots[i];
990
+ const target = slotEls[slot.key];
991
+ if (slot.kind === 'choice') {
992
+ const targets = [];
993
+ const values = [];
994
+ for (let n = 1; n <= (slot.options || 0); n++) {
995
+ const t = slotEls[slot.key + 'o' + n];
996
+ if (t) {
997
+ targets.push(t);
998
+ values.push(n);
999
+ }
1000
+ }
1001
+ const marker = /^g\d+$/.test(slot.key) ? 'none' : look.marker;
1002
+ if (targets.length > 0)
1003
+ toggles(slot, targets, values, marker === 'none' ? 'pt-ex-ring' : 'pt-ex-tick', !!slot.multiple);
1004
+ }
1005
+ else if (slot.kind === 'truefalse') {
1006
+ const t = slotEls[slot.key + 't'];
1007
+ const f = slotEls[slot.key + 'f'];
1008
+ if (t && f)
1009
+ toggles(slot, [t, f], [1, 2], 'pt-ex-tick', false);
1010
+ else if (target)
1011
+ inputOn(slot, target);
1012
+ }
1013
+ else if (slot.kind === 'mark') {
1014
+ if (target)
1015
+ markToggle(slot, target);
1016
+ }
1017
+ else if (slot.kind === 'category') {
1018
+ if (target)
1019
+ chooser(slot, target);
1020
+ }
1021
+ else if (slot.kind === 'pair') {
1022
+ if (target)
1023
+ inputOn(slot, target);
1024
+ else
1025
+ pairSlots.push(slot);
1026
+ }
1027
+ else if (slot.kind === 'word') {
1028
+ wordSlots.push(slot);
1029
+ }
1030
+ else if (slot.kind === 'text') {
1031
+ const box = slot.box !== undefined ? boxEls[String(slot.box)] : null;
1032
+ if (box)
1033
+ area(slot, box);
1034
+ }
1035
+ else if (slot.kind === 'cell') {
1036
+ const box = slot.box !== undefined ? boxEls[String(slot.box)] : null;
1037
+ const grid = (rec.grids || []).filter((g) => g.box === slot.box)[0];
1038
+ if (box && grid)
1039
+ cell(slot, box, grid);
1040
+ }
1041
+ else if (target) {
1042
+ inputOn(slot, target);
1043
+ }
1044
+ }
1045
+ const grids = rec.grids || [];
1046
+ for (let i = 0; i < grids.length; i++) {
1047
+ const g = grids[i];
1048
+ const box = boxEls[String(g.box)];
1049
+ if (!box)
1050
+ continue;
1051
+ if (g.kind === 'match' && pairSlots.length > 0 && g.rowsPt && g.dotsPt)
1052
+ matchDots(box, g, pairSlots);
1053
+ if (g.kind === 'wordsearch' && wordSlots.length > 0)
1054
+ wordSearch(box, g, wordSlots);
1055
+ }
1056
+ // The bar: on the last part of the exercise's outer box.
1057
+ const outer = outers.length > 0 ? outers[outers.length - 1] : null;
1058
+ let scoreEl = null;
1059
+ let panel = null;
1060
+ let solutionBtn = null;
1061
+ let hintBtn = null;
1062
+ if (outer) {
1063
+ const bar = own(el('div', 'pt-ex-bar pt-ex-ui' + (flow ? ' pt-ex-flow' : ''), outer));
1064
+ bar.setAttribute('role', 'group');
1065
+ bar.setAttribute('aria-label', exName);
1066
+ if (!flow)
1067
+ bar.style.fontSize = px(Math.max(body * 0.62, 9));
1068
+ colours(bar);
1069
+ const button = (text, fn) => {
1070
+ const b = el('button', '', bar);
1071
+ b.setAttribute('type', 'button');
1072
+ b.textContent = text;
1073
+ b.addEventListener('click', fn);
1074
+ return b;
1075
+ };
1076
+ scoreEl = el('span', 'pt-ex-score', bar);
1077
+ scoreEl.setAttribute('aria-live', 'polite');
1078
+ scoreEl.setAttribute('role', 'status');
1079
+ if (rec.hints && rec.hints.length > 0)
1080
+ hintBtn = button(words.hint || 'Hint', () => showPanel(panel && panel.getAttribute('data-show') === 'hint' ? null : 'hint'));
1081
+ if (!instant)
1082
+ button(words.check || 'Check', () => { state.checked = true; persistNow(); feedback(); });
1083
+ solutionBtn = button(words.showSolution || 'Show solution', () => reveal(!state.revealed));
1084
+ button(words.reset || 'Try again', reset);
1085
+ panel = el('div', 'pt-ex-panel pt-ex-ui' + (flow ? ' pt-ex-flow' : ''), outer);
1086
+ own(panel);
1087
+ panel.hidden = true;
1088
+ if (!flow)
1089
+ panel.style.fontSize = px(Math.max(body * 0.72, 10));
1090
+ colours(panel);
1091
+ }
1092
+ void hintBtn;
1093
+ function showPanel(what) {
1094
+ if (!panel)
1095
+ return;
1096
+ while (panel.firstChild)
1097
+ panel.removeChild(panel.firstChild);
1098
+ panel.hidden = what === null;
1099
+ panel.setAttribute('data-show', what || '');
1100
+ if (what === 'hint') {
1101
+ const hints = rec.hints || [];
1102
+ for (let i = 0; i < hints.length; i++)
1103
+ el('p', '', panel).textContent = hints[i];
1104
+ }
1105
+ else if (what === 'solution') {
1106
+ if (rec.key)
1107
+ el('p', 'pt-ex-key', panel).textContent = rec.key;
1108
+ const sol = rec.solution || [];
1109
+ for (let i = 0; i < sol.length; i++)
1110
+ el('p', '', panel).textContent = sol[i];
1111
+ }
1112
+ }
1113
+ function valueOf(w) {
1114
+ if (state.revealed)
1115
+ return w.slot.kind === 'word' ? undefined : solutionValue(w.slot, rec, tf);
1116
+ return w.slot.kind === 'word' ? state.v._found : state.v[w.slot.key];
1117
+ }
1118
+ function paintAll() {
1119
+ for (let i = 0; i < widgets.length; i++) {
1120
+ widgets[i].paint(valueOf(widgets[i]));
1121
+ widgets[i].lock(state.revealed);
1122
+ }
1123
+ refreshColumns();
1124
+ }
1125
+ function verdict(slot) {
1126
+ const v = slot.kind === 'word' ? state.v._found : state.v[slot.key];
1127
+ return checkSlot(slot, v, rec, tf);
1128
+ }
1129
+ /** Right and wrong on every slot, and the score. */
1130
+ function feedback() {
1131
+ let right = 0;
1132
+ let total = 0;
1133
+ for (let i = 0; i < rec.slots.length; i++) {
1134
+ const s = rec.slots[i];
1135
+ if (!isGraded(s, rec))
1136
+ continue;
1137
+ total++;
1138
+ if (verdict(s) === true)
1139
+ right++;
1140
+ }
1141
+ for (let i = 0; i < widgets.length; i++)
1142
+ widgets[i].mark(state.revealed ? null : verdict(widgets[i].slot), !state.revealed);
1143
+ if (scoreEl) {
1144
+ const score = String(words.score || '{right} of {total} right').replace('{right}', String(right)).replace('{total}', String(total));
1145
+ scoreEl.textContent = total === 0 ? '' : (right === total ? (words.correct || '') + ' ' : '') + score;
1146
+ }
1147
+ }
1148
+ function clearFeedback() {
1149
+ for (let i = 0; i < widgets.length; i++)
1150
+ widgets[i].mark(null);
1151
+ if (scoreEl)
1152
+ scoreEl.textContent = '';
1153
+ }
1154
+ function persistNow() {
1155
+ save(key, persist, { v: state.v, checked: state.checked });
1156
+ }
1157
+ function afterChange(slot) {
1158
+ if (instant) {
1159
+ feedback();
1160
+ return;
1161
+ }
1162
+ if (state.checked) {
1163
+ state.checked = false;
1164
+ persistNow();
1165
+ }
1166
+ if (slot) {
1167
+ for (let i = 0; i < widgets.length; i++)
1168
+ if (widgets[i].slot === slot)
1169
+ widgets[i].mark(null);
1170
+ }
1171
+ if (scoreEl)
1172
+ scoreEl.textContent = '';
1173
+ }
1174
+ /** A slot answered: kept, repainted, checked when checking is instant. */
1175
+ function changed(slot, value) {
1176
+ if (state.revealed)
1177
+ return;
1178
+ if (slot.kind !== 'word') {
1179
+ if (value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0))
1180
+ delete state.v[slot.key];
1181
+ else
1182
+ state.v[slot.key] = value;
1183
+ }
1184
+ for (let i = 0; i < widgets.length; i++)
1185
+ if (widgets[i].slot === slot)
1186
+ widgets[i].paint(valueOf(widgets[i]));
1187
+ if (slot.kind === 'category')
1188
+ refreshColumns();
1189
+ persistNow();
1190
+ afterChange(slot);
1191
+ }
1192
+ function reveal(on) {
1193
+ state.revealed = on;
1194
+ if (solutionBtn)
1195
+ solutionBtn.textContent = on ? words.hideSolution || 'Hide solution' : words.showSolution || 'Show solution';
1196
+ if (solutionBtn)
1197
+ solutionBtn.setAttribute('aria-expanded', on ? 'true' : 'false');
1198
+ paintAll();
1199
+ if (on) {
1200
+ clearFeedback();
1201
+ showPanel('solution');
1202
+ }
1203
+ else {
1204
+ showPanel(null);
1205
+ if (state.checked || instant)
1206
+ feedback();
1207
+ }
1208
+ }
1209
+ function reset() {
1210
+ state.v = {};
1211
+ state.checked = false;
1212
+ forget(key, persist);
1213
+ if (state.revealed)
1214
+ reveal(false);
1215
+ paintAll();
1216
+ clearFeedback();
1217
+ showPanel(null);
1218
+ if (instant)
1219
+ feedback();
1220
+ }
1221
+ paintAll();
1222
+ if (state.checked || instant)
1223
+ feedback();
1224
+ if (solutionBtn)
1225
+ solutionBtn.setAttribute('aria-expanded', 'false');
1226
+ return { record: rec, state, reveal, reset, feedback };
1227
+ }
1228
+ return { normalizeAnswer, isGraded, checkSlot, solutionValue, attach };
1229
+ }
1230
+ /* eslint-enable @typescript-eslint/no-explicit-any */
1231
+ const runtime = exerciseRuntime();
1232
+ /** An answer as the exercise script compares it (see `attachExercises`). */
1233
+ export const normalizeAnswer = runtime.normalizeAnswer;
1234
+ /** Whether a slot counts in an exercise's score. */
1235
+ export const isGradedSlot = runtime.isGraded;
1236
+ /** Whether `value` answers `slot` rightly (`true`), wrongly (`false`), or
1237
+ * is no answer or not graded (`null`). `trueFalse` are the style's true
1238
+ * and false labels, for a typed true/false. */
1239
+ export const checkExerciseSlot = runtime.checkSlot;
1240
+ /** What a slot shows when the solution is shown. */
1241
+ export const exerciseSolutionValue = runtime.solutionValue;
1242
+ /** Makes the exercises of the HTML output under `root` answerable (see the
1243
+ * module comment). Call it once the markup is in place, again after the
1244
+ * markup changes (calling the previous call's return value first).
1245
+ * Returns a function that takes every control off and puts the page's
1246
+ * markup back as it was. */
1247
+ export function attachExercises(root, options) {
1248
+ return runtime.attach(root, options);
1249
+ }
1250
+ /** The exercise script as a standalone script, for an output with no host
1251
+ * page to call `attachExercises`: the HTML file of the command line, an
1252
+ * EPUB content document. It attaches to the document once it is parsed;
1253
+ * the answers are kept under the page's path. */
1254
+ export const EXERCISE_SCRIPT = `(function () {
1255
+ var runtime = (${exerciseRuntime.toString()})();
1256
+ function go() { runtime.attach(document, {}); }
1257
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go);
1258
+ else go();
1259
+ })();
1260
+ `;
1261
+ //# sourceMappingURL=runtime.js.map