@khorsheed/dsh-library 0.0.0-stage → 0.1.1
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 +19 -0
- package/README.en.md +49 -0
- package/README.i18n.yaml +6 -0
- package/README.md +48 -2
- package/cordis.patch.yml +17 -0
- package/lib/client.js +66334 -0
- package/lib/index.js +3318 -0
- package/lib/profiles-CFrPQdfO.js +1692 -0
- package/lib/tool.js +1597 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3811 -0
- package/lib/typert.remote-client.d.ts +137 -0
- package/lib/typert.remote-client.js +3077 -0
- package/lib/types/cards.d.ts +41 -0
- package/lib/types/cards.js +111 -0
- package/lib/types/client/LibraryPane.d.ts +26 -0
- package/lib/types/client/LibraryPane.js +129 -0
- package/lib/types/client/bind/BindChip.d.ts +33 -0
- package/lib/types/client/bind/BindChip.js +102 -0
- package/lib/types/client/contract.d.ts +167 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/covers.d.ts +36 -0
- package/lib/types/client/covers.js +66 -0
- package/lib/types/client/definition.d.ts +27 -0
- package/lib/types/client/definition.js +32 -0
- package/lib/types/client/icons-local.d.ts +41 -0
- package/lib/types/client/icons-local.js +25 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.js +231 -0
- package/lib/types/client/judge.d.ts +70 -0
- package/lib/types/client/judge.js +262 -0
- package/lib/types/client/locales.d.ts +553 -0
- package/lib/types/client/locales.js +1076 -0
- package/lib/types/client/look.d.ts +63 -0
- package/lib/types/client/look.js +64 -0
- package/lib/types/client/math.d.ts +24 -0
- package/lib/types/client/math.js +47 -0
- package/lib/types/client/mistakes.d.ts +21 -0
- package/lib/types/client/mistakes.js +28 -0
- package/lib/types/client/preset-visibility.d.ts +109 -0
- package/lib/types/client/preset-visibility.js +123 -0
- package/lib/types/client/quiz-drafts.d.ts +29 -0
- package/lib/types/client/quiz-drafts.js +50 -0
- package/lib/types/client/reveal.d.ts +21 -0
- package/lib/types/client/reveal.js +31 -0
- package/lib/types/client/store.d.ts +111 -0
- package/lib/types/client/store.js +48 -0
- package/lib/types/client/titles.d.ts +17 -0
- package/lib/types/client/titles.js +23 -0
- package/lib/types/client/ui.d.ts +96 -0
- package/lib/types/client/ui.js +124 -0
- package/lib/types/client/views/book.d.ts +57 -0
- package/lib/types/client/views/book.js +164 -0
- package/lib/types/client/views/cards.d.ts +17 -0
- package/lib/types/client/views/cards.js +53 -0
- package/lib/types/client/views/home.d.ts +26 -0
- package/lib/types/client/views/home.js +263 -0
- package/lib/types/client/views/lesson.d.ts +42 -0
- package/lib/types/client/views/lesson.js +146 -0
- package/lib/types/client/views/path.d.ts +32 -0
- package/lib/types/client/views/path.js +64 -0
- package/lib/types/client/views/quiz.d.ts +42 -0
- package/lib/types/client/views/quiz.js +366 -0
- package/lib/types/client/views/station-quiz.d.ts +43 -0
- package/lib/types/client/views/station-quiz.js +203 -0
- package/lib/types/client/views/station.d.ts +18 -0
- package/lib/types/client/views/station.js +142 -0
- package/lib/types/image-refs.d.ts +35 -0
- package/lib/types/image-refs.js +71 -0
- package/lib/types/index.d.ts +51 -0
- package/lib/types/index.js +31 -0
- package/lib/types/mastery.d.ts +81 -0
- package/lib/types/mastery.js +110 -0
- package/lib/types/profiles.d.ts +31 -0
- package/lib/types/profiles.js +25 -0
- package/lib/types/remote.d.ts +355 -0
- package/lib/types/remote.js +332 -0
- package/lib/types/service/assess.d.ts +144 -0
- package/lib/types/service/assess.js +773 -0
- package/lib/types/service/bindings.d.ts +92 -0
- package/lib/types/service/bindings.js +198 -0
- package/lib/types/service/cards.d.ts +85 -0
- package/lib/types/service/cards.js +320 -0
- package/lib/types/service/core.d.ts +124 -0
- package/lib/types/service/core.js +169 -0
- package/lib/types/service/pages.d.ts +60 -0
- package/lib/types/service/pages.js +125 -0
- package/lib/types/service/roadmap.d.ts +99 -0
- package/lib/types/service/roadmap.js +284 -0
- package/lib/types/service/rules.d.ts +63 -0
- package/lib/types/service/rules.js +245 -0
- package/lib/types/service/shelf.d.ts +136 -0
- package/lib/types/service/shelf.js +422 -0
- package/lib/types/service.d.ts +42 -0
- package/lib/types/service.js +41 -0
- package/lib/types/store.d.ts +257 -0
- package/lib/types/store.js +1376 -0
- package/lib/types/tool-definition.d.ts +49 -0
- package/lib/types/tool-definition.js +49 -0
- package/lib/types/tool.d.ts +71 -0
- package/lib/types/tool.js +174 -0
- package/lib/types/tools/assess.d.ts +11 -0
- package/lib/types/tools/assess.js +82 -0
- package/lib/types/tools/binding.d.ts +11 -0
- package/lib/types/tools/binding.js +46 -0
- package/lib/types/tools/cards.d.ts +11 -0
- package/lib/types/tools/cards.js +100 -0
- package/lib/types/tools/kit.d.ts +215 -0
- package/lib/types/tools/kit.js +281 -0
- package/lib/types/tools/notes.d.ts +11 -0
- package/lib/types/tools/notes.js +40 -0
- package/lib/types/tools/page.d.ts +11 -0
- package/lib/types/tools/page.js +59 -0
- package/lib/types/tools/project.d.ts +11 -0
- package/lib/types/tools/project.js +67 -0
- package/lib/types/tools/quiz.d.ts +11 -0
- package/lib/types/tools/quiz.js +74 -0
- package/lib/types/tools/read.d.ts +11 -0
- package/lib/types/tools/read.js +127 -0
- package/lib/types/tools/resource.d.ts +11 -0
- package/lib/types/tools/resource.js +147 -0
- package/lib/types/tools/roadmap.d.ts +11 -0
- package/lib/types/tools/roadmap.js +102 -0
- package/lib/types/types.d.ts +752 -0
- package/lib/types/types.js +159 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judging the answers the sidebar can judge itself: a choice against its key
|
|
3
|
+
* (a letter, the option text, or a lettered option) and a fill-in against its
|
|
4
|
+
* key — the same text loosely (width, case, spaces, LaTeX vs unicode, the
|
|
5
|
+
* comma's width), or the same NUMBER (1/2 = 0.5 = \frac{1}{2}), part by part
|
|
6
|
+
* for a multi-blank answer; alternatives are written " / ", "|" or "或". What
|
|
7
|
+
* still disagrees, the learner may overrule (「我答对了」). Open answers are
|
|
8
|
+
* the coach's.
|
|
9
|
+
*
|
|
10
|
+
* @module @khorsheed/dsh-library/client/judge
|
|
11
|
+
*/
|
|
12
|
+
/** The common LaTeX commands as their unicode glyphs — a fill answer's spelling, never its notation, should decide. */
|
|
13
|
+
const MATH_COMMANDS = {
|
|
14
|
+
infty: '∞', pm: '±', times: '×', div: '÷', cdot: '·', sqrt: '√',
|
|
15
|
+
le: '≤', leq: '≤', ge: '≥', geq: '≥', ne: '≠', neq: '≠', approx: '≈',
|
|
16
|
+
cup: '∪', cap: '∩', in: '∈', subset: '⊂', supset: '⊃', emptyset: '∅',
|
|
17
|
+
to: '→', rightarrow: '→', leftarrow: '←', degree: '°',
|
|
18
|
+
pi: 'π', alpha: 'α', beta: 'β', gamma: 'γ', theta: 'θ', lambda: 'λ', mu: 'μ', sigma: 'σ', phi: 'φ', omega: 'ω',
|
|
19
|
+
};
|
|
20
|
+
/** Fold LaTeX notation onto its plain reading: $…$ delimiters, \sqrt / \frac, known commands, braces, \!/\, spacing. */
|
|
21
|
+
function mathFold(text) {
|
|
22
|
+
let folded = text.replace(/\$+/g, '').replace(/\\left|\\right/g, '');
|
|
23
|
+
// Innermost first, so \frac{\sqrt{3}}{2} reads as √3/2.
|
|
24
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
25
|
+
folded = folded
|
|
26
|
+
.replace(/\\sqrt\s*\{([^{}]*)\}/g, (_raw, body) => /^[\w.π]+$/.test(body) ? `√${body}` : `√(${body})`)
|
|
27
|
+
.replace(/\\[dt]?frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}/g, (_raw, top, bottom) => `${/^[\w.π√]+$/.test(top) ? top : `(${top})`}/${/^[\w.π√]+$/.test(bottom) ? bottom : `(${bottom})`}`);
|
|
28
|
+
}
|
|
29
|
+
return folded
|
|
30
|
+
.replace(/\\([a-zA-Z]+)/g, (_raw, cmd) => MATH_COMMANDS[cmd] ?? cmd)
|
|
31
|
+
.replace(/\\[^a-zA-Z]/g, '')
|
|
32
|
+
.replace(/[{}]/g, '');
|
|
33
|
+
}
|
|
34
|
+
/** Superscript / subscript digits as their ^ / _ spelling — NFKC alone would flatten (x-2)² into (x-2)2. */
|
|
35
|
+
const SCRIPTS = Object.fromEntries([
|
|
36
|
+
...[...'⁰¹²³⁴⁵⁶⁷⁸⁹'].map((char, digit) => [char, `^${digit}`]),
|
|
37
|
+
...[...'₀₁₂₃₄₅₆₇₈₉'].map((char, digit) => [char, `_${digit}`]),
|
|
38
|
+
['⁻', '^-'], ['ⁿ', '^n'],
|
|
39
|
+
]);
|
|
40
|
+
/** Compare answers loosely. */
|
|
41
|
+
function norm(text) {
|
|
42
|
+
return mathFold(text.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉⁻ⁿ]/g, char => SCRIPTS[char] ?? char).normalize('NFKC').replace(/\u2044/g, '/')).toLowerCase().replace(/\s+/g, ' ').trim().replace(/[。.!!??]+$/, '');
|
|
43
|
+
}
|
|
44
|
+
/** Looser still: no spaces, one minus sign, no wrapping quotes or trailing clause punctuation. */
|
|
45
|
+
function loose(text) {
|
|
46
|
+
return norm(text)
|
|
47
|
+
.replace(/\s+/g, '')
|
|
48
|
+
.replace(/[−–—]/g, '-')
|
|
49
|
+
.replace(/[×*·]/g, '×')
|
|
50
|
+
// Wrapping quotes go; brackets stay (an interval's brackets are the answer).
|
|
51
|
+
.replace(/^[「『“"'‘《]+|[」』”"'’》]+$/g, '')
|
|
52
|
+
.replace(/[,;:、]+$/, '');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The value of a plain numeric expression — digits, + - × / ^, parentheses,
|
|
56
|
+
* √, π, a trailing % — or undefined. A tiny recursive-descent parser, never
|
|
57
|
+
* eval: 1/2, 0.5, √3/2, 2π/3 and 50% all read as numbers.
|
|
58
|
+
*/
|
|
59
|
+
export function numericValue(text) {
|
|
60
|
+
const source = loose(text).replace(/^[a-zα-ω]\w*=/, '').replace(/,/g, '');
|
|
61
|
+
if (source === '' || !/^[\d.+\-×/^()√π%]+$/.test(source) || !/\d|π/.test(source))
|
|
62
|
+
return undefined;
|
|
63
|
+
let at = 0;
|
|
64
|
+
const peek = () => source[at] ?? '';
|
|
65
|
+
const expr = () => {
|
|
66
|
+
let value = term();
|
|
67
|
+
while (peek() === '+' || peek() === '-')
|
|
68
|
+
value = source[at++] === '+' ? value + term() : value - term();
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
const term = () => {
|
|
72
|
+
let value = power();
|
|
73
|
+
for (;;) {
|
|
74
|
+
if (peek() === '×') {
|
|
75
|
+
at += 1;
|
|
76
|
+
value *= power();
|
|
77
|
+
}
|
|
78
|
+
else if (peek() === '/') {
|
|
79
|
+
at += 1;
|
|
80
|
+
value /= power();
|
|
81
|
+
}
|
|
82
|
+
else if (/[\d.(√π]/.test(peek()))
|
|
83
|
+
value *= power(); // implicit: 2π, 2√3
|
|
84
|
+
else
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const power = () => {
|
|
89
|
+
const base = unary();
|
|
90
|
+
if (peek() !== '^')
|
|
91
|
+
return base;
|
|
92
|
+
at += 1;
|
|
93
|
+
return base ** unary();
|
|
94
|
+
};
|
|
95
|
+
const unary = () => {
|
|
96
|
+
if (peek() === '-') {
|
|
97
|
+
at += 1;
|
|
98
|
+
return -unary();
|
|
99
|
+
}
|
|
100
|
+
if (peek() === '+') {
|
|
101
|
+
at += 1;
|
|
102
|
+
return unary();
|
|
103
|
+
}
|
|
104
|
+
if (peek() === '√') {
|
|
105
|
+
at += 1;
|
|
106
|
+
return Math.sqrt(atom());
|
|
107
|
+
}
|
|
108
|
+
return atom();
|
|
109
|
+
};
|
|
110
|
+
const atom = () => {
|
|
111
|
+
if (peek() === '(') {
|
|
112
|
+
at += 1;
|
|
113
|
+
const value = expr();
|
|
114
|
+
if (peek() !== ')')
|
|
115
|
+
throw new Error('unbalanced');
|
|
116
|
+
at += 1;
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if (peek() === 'π') {
|
|
120
|
+
at += 1;
|
|
121
|
+
return Math.PI;
|
|
122
|
+
}
|
|
123
|
+
const match = /^\d*\.?\d+/.exec(source.slice(at));
|
|
124
|
+
if (match === null)
|
|
125
|
+
throw new Error('not a number');
|
|
126
|
+
at += match[0].length;
|
|
127
|
+
let value = Number(match[0]);
|
|
128
|
+
if (peek() === '%') {
|
|
129
|
+
at += 1;
|
|
130
|
+
value /= 100;
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const value = expr();
|
|
136
|
+
return at === source.length && Number.isFinite(value) ? value : undefined;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** One answer against one key: same text (loosely), or the same number. */
|
|
143
|
+
function sameAtom(given, want) {
|
|
144
|
+
if (norm(given) === norm(want) || loose(given) === loose(want))
|
|
145
|
+
return true;
|
|
146
|
+
const a = numericValue(given);
|
|
147
|
+
const b = numericValue(want);
|
|
148
|
+
return a !== undefined && b !== undefined && Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(b));
|
|
149
|
+
}
|
|
150
|
+
/** A multi-blank answer ("1/2, √3/2") matches part by part, in order. */
|
|
151
|
+
function sameAnswer(given, want) {
|
|
152
|
+
if (sameAtom(given, want))
|
|
153
|
+
return true;
|
|
154
|
+
const split = (text) => norm(text).split(/\s*[,;、]\s*/).filter(part => part !== '');
|
|
155
|
+
const parts = split(given);
|
|
156
|
+
const keys = split(want);
|
|
157
|
+
return keys.length > 1 && parts.length === keys.length && keys.every((key, index) => sameAtom(parts[index], key));
|
|
158
|
+
}
|
|
159
|
+
/** The option index an answer key names, or -1. */
|
|
160
|
+
export function choiceIndex(options, answer) {
|
|
161
|
+
const key = norm(answer);
|
|
162
|
+
if (/^[a-z]$/.test(key))
|
|
163
|
+
return key.charCodeAt(0) - 97;
|
|
164
|
+
const lettered = /^([a-z])[.、))::]\s*/.exec(key);
|
|
165
|
+
if (lettered !== null)
|
|
166
|
+
return lettered[1].charCodeAt(0) - 97;
|
|
167
|
+
const exact = options.findIndex(option => norm(option) === key);
|
|
168
|
+
if (exact >= 0)
|
|
169
|
+
return exact;
|
|
170
|
+
return options.findIndex(option => norm(option).includes(key) || key.includes(norm(option)));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Whether a fill-in answer matches the key. The whole key is tried first —
|
|
174
|
+
* "1/2" is a fraction, never two alternatives — then alternatives written
|
|
175
|
+
* " / " (spaced), "|" or "或".
|
|
176
|
+
*/
|
|
177
|
+
export function fillMatches(input, answer) {
|
|
178
|
+
if (norm(input) === '')
|
|
179
|
+
return false;
|
|
180
|
+
const alternatives = [answer, ...answer.split(/\s+\/\s+|\s*[||]\s*|\s*或\s*/)].filter(alternative => alternative.trim() !== '');
|
|
181
|
+
return alternatives.some(alternative => sameAnswer(input, alternative));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A fill-in judged by the project's profile: 'math' by text or value
|
|
185
|
+
* (fillMatches); 'spelling' also forgives a UK/US spelling or a plural.
|
|
186
|
+
*/
|
|
187
|
+
export function judgeFill(fill, input, answer) {
|
|
188
|
+
if (fillMatches(input, answer))
|
|
189
|
+
return true;
|
|
190
|
+
if (fill !== 'spelling')
|
|
191
|
+
return false;
|
|
192
|
+
return spellMatches(input, answer.split(/\s+\/\s+|\s*[||]\s*|\s*或\s*/));
|
|
193
|
+
}
|
|
194
|
+
/** The sentence with the term blanked, or undefined when the sentence does not hold it. */
|
|
195
|
+
export function clozeOf(sentence, term, aliases = []) {
|
|
196
|
+
const lower = sentence.toLowerCase();
|
|
197
|
+
for (const candidate of [term, ...aliases].filter(word => word.trim() !== '').sort((a, b) => b.length - a.length)) {
|
|
198
|
+
const at = lower.indexOf(candidate.toLowerCase());
|
|
199
|
+
if (at < 0)
|
|
200
|
+
continue;
|
|
201
|
+
// Take the whole word the match sits in (instalment → instalments).
|
|
202
|
+
let end = at + candidate.length;
|
|
203
|
+
while (end < sentence.length && /[A-Za-z'-]/.test(sentence[end]))
|
|
204
|
+
end += 1;
|
|
205
|
+
return { before: sentence.slice(0, at), after: sentence.slice(end), answer: sentence.slice(at, end) };
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Pick how to ask a card: a concept is explained; a word the learner already
|
|
211
|
+
* knows well (box 3+) is spelled; otherwise a word met in a sentence is a
|
|
212
|
+
* cloze or a meaning choice, alternating by its streak (the choice needs three
|
|
213
|
+
* other glosses for distractors).
|
|
214
|
+
*/
|
|
215
|
+
export function pickMode(card, otherGlosses) {
|
|
216
|
+
if (card.kind === 'concept')
|
|
217
|
+
return 'explain';
|
|
218
|
+
if (card.mastery.box >= 3)
|
|
219
|
+
return 'spell';
|
|
220
|
+
const hasCloze = card.contexts.some(context => clozeOf(context.sentence, card.term, card.aliases) !== undefined);
|
|
221
|
+
const canChoose = otherGlosses >= 3;
|
|
222
|
+
if (hasCloze && (!canChoose || card.mastery.streak % 2 === 0))
|
|
223
|
+
return 'cloze';
|
|
224
|
+
if (canChoose)
|
|
225
|
+
return 'choice';
|
|
226
|
+
return hasCloze ? 'cloze' : 'spell';
|
|
227
|
+
}
|
|
228
|
+
/** Fold the British / American spellings that should never decide an answer. */
|
|
229
|
+
function foldSpelling(word) {
|
|
230
|
+
return word.normalize('NFKC').toLowerCase().trim()
|
|
231
|
+
.replace(/[^a-z]/g, '')
|
|
232
|
+
.replace(/our\b/g, 'or')
|
|
233
|
+
.replace(/(?<=[a-z]{3})is(e|ed|es|ing|ation)$/g, 'iz$1')
|
|
234
|
+
.replace(/(?<=[a-z]{2})re$/g, 'er')
|
|
235
|
+
.replace(/ll(?=[a-z])/g, 'l')
|
|
236
|
+
.replace(/(?<=[aeiou])ll(ed|ing)$/g, 'l$1');
|
|
237
|
+
}
|
|
238
|
+
/** Whether a typed word is the term (case, width, a UK/US spelling and a plural apart). */
|
|
239
|
+
export function spellMatches(input, answers) {
|
|
240
|
+
const given = foldSpelling(input);
|
|
241
|
+
if (given === '')
|
|
242
|
+
return false;
|
|
243
|
+
return answers.some(answer => {
|
|
244
|
+
const want = foldSpelling(answer);
|
|
245
|
+
return want === given || want === given.replace(/e?s$/, '') || want.replace(/e?s$/, '') === given;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** Per-letter marks for a spelling attempt: true where the letter matches the target at that position. */
|
|
249
|
+
export function letterMarks(input, target) {
|
|
250
|
+
return [...input].map((char, index) => target[index]?.toLowerCase() === char.toLowerCase());
|
|
251
|
+
}
|
|
252
|
+
/** Three distractors from other cards' glosses, shuffled with the answer; `random` is injectable for tests. */
|
|
253
|
+
export function choiceOptions(answer, others, random = Math.random) {
|
|
254
|
+
const pool = [...new Set(others.filter(other => other !== answer))];
|
|
255
|
+
const picked = [];
|
|
256
|
+
while (picked.length < 3 && pool.length > 0)
|
|
257
|
+
picked.push(pool.splice(Math.floor(random() * pool.length), 1)[0]);
|
|
258
|
+
const options = [...picked];
|
|
259
|
+
const correct = Math.floor(random() * (options.length + 1));
|
|
260
|
+
options.splice(correct, 0, answer);
|
|
261
|
+
return { options, correct };
|
|
262
|
+
}
|