qb-answer-checker 1.0.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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ This document specifies the kind of (quizbowl) answerlines that the program is designed to parse.
2
+
3
+ **Answerlines** should be formatted as follows:
4
+
5
+ ```
6
+ <main answerline> [<sub-answerline>]
7
+ ```
8
+
9
+ where the **sub-answerline** is a string of clauses separated by semicolons of the form:
10
+
11
+ ```
12
+ (<special directives>;)? <clause> (; <clause>; ...)?
13
+ ```
14
+
15
+ where each **clause** is a string of answers separated by the word "or" of the form:
16
+
17
+ ```
18
+ <directive>? (on)? <answer>((or|,) <answer>(or|,) <answer> ...)? (by asking|with <directed prompt>)?
19
+ ```
20
+
21
+ **Deprecated:** answers can also be separated by commas instead of "or", but this is deprecated and serves mostly to support old answerlines.
22
+
23
+ Each **directive** should be one of:
24
+
25
+ - "accept"
26
+ - "prompt"
27
+ - "reject"
28
+ - "anti-prompt"
29
+ - some sets use "antiprompt" (no hyphen)
30
+
31
+ and "on" and "by asking/with" are optional and indicate that there should be a directed prompt.
32
+
33
+ ## Special Directives
34
+
35
+ **Special directives** should be one of the following and affect the main answerline only:
36
+
37
+ - "accept either" or "accept any": accept any individual word of the main answer
38
+ - For example, if the entire answerline is `<b><u>Grover Underwood</u></b> [accept either]`, then "Grover", "Underwood", and "Grover Underwood" would be accepted.
39
+ - "prompt on partial": prompt on any individual word of the main answer
40
+ - For example: `<b><u>John</u></b> [prompt on partial]` would prompt on "John" and "John Smith", but not "John Smithson".
41
+
42
+ **Note:** special directives should be the first phrase in the sub-answerline, but this program will recognize them anywhere in the sub-answerline.
43
+
44
+ ## Additional Info
45
+
46
+ For more information about how answerlines should be formatted, see <https://minkowski.space/quizbowl/manuals/style/answerlines.html>.
47
+ Note that the linked guide is more useful for explaining how answerlines should be formatted from a sylistic/quizbowl sense, while this specification only describes how they should be formatted in a way that computers can understand.
@@ -0,0 +1,348 @@
1
+ import * as utils from './utils.js';
2
+ import getEquivalentAnswers from './equivalent-answers.js';
3
+ import standardizeTokens from './standardize-tokens.js';
4
+
5
+ import { distance } from 'damerau-levenshtein-js';
6
+ import numberToWords from 'number-to-words';
7
+ import { toArabic } from 'roman-numerals';
8
+ import { stemmer } from 'stemmer';
9
+
10
+ const { toWords } = numberToWords;
11
+
12
+ /**
13
+ * Splits a string into the main answer and the sub-answer (everything in brackets or parentheses),
14
+ * while intelligently detecting whether to keep the part in parentheses, if present.
15
+ * @param {string} string
16
+ * @returns {{ mainAnswer: string, subAnswer: string }}
17
+ */
18
+ const splitMainAnswer = (string) => {
19
+ const bracketsSubAnswer = (string.match(/(?<=\[)[^\]]*(?=\])/g) ?? ['']).pop();
20
+ const parenthesesSubAnswer = (string.match(/(?<=\()[^)]*(?=\))/g) ?? ['']).pop();
21
+
22
+ const mainAnswer = utils.removeParentheses(string);
23
+
24
+ if (bracketsSubAnswer.length !== 0)
25
+ return { mainAnswer, subAnswer: bracketsSubAnswer };
26
+
27
+ for (const directive of ['or', 'prompt', 'antiprompt', 'anti-prompt', 'accept', 'reject', 'do not accept']) {
28
+ if (parenthesesSubAnswer.startsWith(directive))
29
+ return { mainAnswer, subAnswer: parenthesesSubAnswer };
30
+ }
31
+
32
+ return { mainAnswer, subAnswer: '' };
33
+ };
34
+
35
+ /**
36
+ * Split either the main- or sub-answerline into clauses.
37
+ * @param {string} string
38
+ * @returns {string[]} the clauses in the string
39
+ */
40
+ const splitAnswerlineIntoClauses = (string) => {
41
+ return string.split(';').map(token => token.trim());
42
+ };
43
+
44
+ /**
45
+ *
46
+ * @param {string} clause
47
+ * @returns {{directive: "accept" | "reject" | "prompt", answers: string[], directedPrompt: string?}} the answers in the clause
48
+ */
49
+ const splitClauseIntoAnswers = (clause) => {
50
+ let directive = 'accept'; // by default, this clause accepts answers that match to it
51
+ if (clause.startsWith('prompt')) {
52
+ directive = 'prompt';
53
+ } else if (clause.startsWith('antiprompt') || clause.startsWith('anti-prompt')) {
54
+ directive = 'accept';
55
+ } else if (clause.startsWith('reject') || clause.startsWith('do not accept')) {
56
+ directive = 'reject';
57
+ }
58
+
59
+ let directedPrompt = null;
60
+ if (directive === 'prompt') {
61
+ for (const key of ['by asking', 'with']) {
62
+ const index = clause.indexOf(key);
63
+
64
+ if (index < 0) {
65
+ continue;
66
+ }
67
+
68
+ directedPrompt = utils.extractQuotes(clause.slice(index + key.length));
69
+ clause = clause.slice(0, index);
70
+ break;
71
+ }
72
+ }
73
+
74
+ clause = clause.replace(/^(or|prompt|prompt on|antiprompt|antiprompt on|anti-prompt|anti-prompt on|accept|reject|do not accept or prompt on|do not accept)/, '').trim();
75
+
76
+ const answers = clause.split(/,? or |, /).map(token => token.trim()).filter(token => token.length > 0);
77
+
78
+ return { directive, answers, directedPrompt };
79
+ };
80
+
81
+
82
+ /**
83
+ * Parses the answerline, returning the acceptable, promptable, and rejectable answers.
84
+ * @param {string} answerline
85
+ */
86
+ function parseAnswerline(answerline) {
87
+ answerline = utils.removeItalics(answerline);
88
+ answerline = utils.replaceSpecialCharacters(answerline);
89
+ answerline = utils.replaceSpecialSubstrings(answerline);
90
+
91
+ const { mainAnswer, subAnswer } = splitMainAnswer(answerline);
92
+ const mainAnswers = mainAnswer.split(' or ').map(token => token.trim()).filter(token => token.length > 0);
93
+
94
+ /**
95
+ * @type {{ "accept": String[], "prompt": String[][2], "reject": String[] }}
96
+ */
97
+ const parsedAnswerline = {
98
+ accept: [],
99
+ prompt: [],
100
+ reject: [],
101
+ };
102
+
103
+ for (const answer of mainAnswers) {
104
+ parsedAnswerline.accept.push(utils.extractUnderlining(answer), utils.extractKeyWords(answer), utils.extractQuotes(answer));
105
+ }
106
+
107
+ if (utils.getAbbreviation(mainAnswer).length > 1) {
108
+ parsedAnswerline.accept.push(utils.getAbbreviation(mainAnswer));
109
+ }
110
+
111
+ if (utils.getAbbreviation(utils.extractUnderlining(mainAnswer)).length > 1) {
112
+ parsedAnswerline.accept.push(utils.getAbbreviation(utils.extractUnderlining(mainAnswer)));
113
+ }
114
+
115
+ if (/[[(]accept either/i.test(answerline) || /[[(]accept any/i.test(answerline)) {
116
+ for (const answer of parsedAnswerline.accept[0].split(' ')) {
117
+ parsedAnswerline.accept.push(answer);
118
+ }
119
+ }
120
+
121
+ if (/prompt on (a )?partial/.test(answerline)) {
122
+ for (const answer of parsedAnswerline.accept[0].split(' ')) {
123
+ parsedAnswerline.prompt.push([answer, null]);
124
+ }
125
+ }
126
+
127
+ for (const answer of parsedAnswerline.accept) {
128
+ const equivalentAnswers = getEquivalentAnswers(answer);
129
+ parsedAnswerline.accept = parsedAnswerline.accept.concat(equivalentAnswers);
130
+ }
131
+
132
+ const clauses = splitAnswerlineIntoClauses(subAnswer);
133
+ clauses.forEach(clause => {
134
+ if (clause.length === 0)
135
+ return;
136
+
137
+ const { directive, answers, directedPrompt } = splitClauseIntoAnswers(clause);
138
+
139
+ for (const answer of answers) {
140
+ switch (directive) {
141
+ case 'accept':
142
+ parsedAnswerline[directive].push(utils.extractUnderlining(answer), utils.extractKeyWords(answer), utils.extractQuotes(answer));
143
+ break;
144
+ case 'prompt': {
145
+ parsedAnswerline[directive].push([utils.extractUnderlining(answer), directedPrompt]);
146
+ parsedAnswerline[directive].push([utils.extractKeyWords(answer), directedPrompt]);
147
+ parsedAnswerline[directive].push([utils.extractQuotes(answer), directedPrompt]);
148
+ break;
149
+ }
150
+ case 'reject':
151
+ parsedAnswerline[directive].push(utils.extractQuotes(answer));
152
+ break;
153
+ }
154
+ }
155
+ });
156
+
157
+ return parsedAnswerline;
158
+ }
159
+
160
+
161
+ const generateTokens = (string) => {
162
+ const tokens = string.split(' ')
163
+ .filter(token => token.length > 0)
164
+ .map(string => standardizeTokens(string));
165
+
166
+ for (let i = tokens.length - 1; i >= 0; i--) {
167
+ if (tokens[i].endsWith('s')) {
168
+ tokens[i] = tokens[i].slice(0, -1);
169
+ }
170
+
171
+ try {
172
+ tokens[i] = toArabic(tokens[i]);
173
+ } catch (e) {
174
+ if (e.message !== 'toArabic expects a valid roman number' && !(e instanceof TypeError)) {
175
+ throw e;
176
+ }
177
+ }
178
+
179
+ if (isFinite(tokens[i])) {
180
+ tokens[i] = parseInt(tokens[i]);
181
+ } else {
182
+ continue;
183
+ }
184
+
185
+ if (tokens[i] <= 100) {
186
+ tokens[i] = toWords(tokens[i]);
187
+ } else {
188
+ tokens[i] = tokens[i].toString();
189
+ }
190
+ }
191
+
192
+ return tokens;
193
+ };
194
+
195
+
196
+ /**
197
+ * Helper method to check if every token in `given` is present in `reference`.
198
+ * @param {string[]} given
199
+ * @param {string[]} reference
200
+ * @param {boolean} acceptSubstring
201
+ * @param {number} strictness
202
+ * @param {boolean} useStemmer
203
+ * @returns
204
+ */
205
+ function tokenListsMatch(given, reference, acceptSubstring, strictness, useStemmer) {
206
+ let j = 0;
207
+ for (let i = 0; i < given.length; i++) {
208
+ let matches = false;
209
+
210
+ while (j < reference.length && matches === false) {
211
+ let errors;
212
+
213
+ if (useStemmer) {
214
+ errors = distance(stemmer(given[i]), stemmer(reference[j]));
215
+ } else {
216
+ errors = distance(given[i], reference[j]);
217
+ }
218
+
219
+ if (strictness * errors <= reference[j].length) {
220
+ matches = true;
221
+ }
222
+
223
+ if (acceptSubstring && reference[j].includes(given[i])) {
224
+ matches = true;
225
+ }
226
+
227
+ j++;
228
+ }
229
+
230
+ if (!matches) {
231
+ return false;
232
+ }
233
+ }
234
+
235
+ return true;
236
+ }
237
+
238
+ /**
239
+ * Returns true if and only if every token in `string` is present in `reference`.
240
+ * @param {String} string
241
+ * @param {String} reference
242
+ * @param {Number} strictness - the number of characters per error allowed for two tokens to match.
243
+ * @param {Boolean} acceptSubstring - whether or not to accept substrings.
244
+ * @param {Boolean} useStemmer - whether or not to use a stemmer.
245
+ * @param {Boolean} respectOrder - whether or not to respect the order of the tokens (i.e. "a b" is not the same as "b a").
246
+ * @returns {Boolean}
247
+ */
248
+ function stringMatchesReference({ string, reference, strictness = 5, acceptSubstring = false, useStemmer = true, respectOrder = false }) {
249
+ if (string === null || string === undefined || reference === null || reference === undefined)
250
+ return false;
251
+
252
+ if (string.length === 0)
253
+ return false;
254
+
255
+ string = utils.removePunctuation(string).trim();
256
+ reference = utils.removePunctuation(reference).trim();
257
+
258
+ let stringTokenLists = [];
259
+ let referenceTokenLists = [];
260
+
261
+ if (/-/.test(string)) {
262
+ stringTokenLists.push(generateTokens(string.replace(/-/g, ' ')));
263
+ stringTokenLists.push(generateTokens(string.replace(/-/g, '')));
264
+ } else {
265
+ stringTokenLists.push(generateTokens(string));
266
+ }
267
+
268
+ if (/-/.test(reference)) {
269
+ referenceTokenLists.push(generateTokens(reference.replace(/-/g, ' ')));
270
+ referenceTokenLists.push(generateTokens(reference.replace(/-/g, '')));
271
+ } else {
272
+ referenceTokenLists.push(generateTokens(reference));
273
+ }
274
+
275
+ if (!respectOrder) {
276
+ stringTokenLists = stringTokenLists.map(tokenList => tokenList.sort());
277
+ referenceTokenLists = referenceTokenLists.map(tokenList => tokenList.sort());
278
+ }
279
+
280
+ // check if every token in the string is in the reference
281
+ for (const stringTokenList of stringTokenLists) {
282
+ for (const referenceTokenList of referenceTokenLists) {
283
+ if (tokenListsMatch(stringTokenList, referenceTokenList, acceptSubstring, strictness, useStemmer)) {
284
+ return true;
285
+ }
286
+ }
287
+ }
288
+
289
+ return false;
290
+ }
291
+
292
+ /**
293
+ * Check if the given answer matches the answerline.
294
+ * @param {String} answerline
295
+ * @param {String} givenAnswer
296
+ * @returns {{
297
+ * directive: 'accept' | 'prompt' | 'reject',
298
+ * directedPrompt: String | null
299
+ * }}
300
+ */
301
+ function checkAnswer(answerline, givenAnswer) {
302
+ answerline = answerline.toLowerCase();
303
+ givenAnswer = utils.replaceSpecialCharacters(givenAnswer.toLowerCase());
304
+ const answerlineIsFormatted = answerline.includes('<u>');
305
+
306
+ const answerWorks = (answerline, givenAnswer) => {
307
+ if (answerlineIsFormatted) {
308
+ return stringMatchesReference({ string: answerline, reference: givenAnswer });
309
+ } else {
310
+ return stringMatchesReference({ string: givenAnswer, reference: answerline, acceptSubstring: true });
311
+ }
312
+ };
313
+
314
+ const parsedAnswerline = parseAnswerline(answerline);
315
+
316
+ if (!answerlineIsFormatted && parsedAnswerline.accept[0].length > 1 && givenAnswer.length === 1 && isNaN(givenAnswer))
317
+ return { directive: 'reject', directedPrompt: null };
318
+
319
+ for (const answer of parsedAnswerline.reject) {
320
+ const useStemmer = (stemmer(answer) !== stemmer(parsedAnswerline.accept[0]));
321
+
322
+ if (!stringMatchesReference({ string: answer, reference: givenAnswer, strictness: 11, useStemmer }))
323
+ continue;
324
+
325
+ if (!stringMatchesReference({ string: givenAnswer, reference: answer, strictness: 11, useStemmer }))
326
+ continue;
327
+
328
+ return { directive: 'reject', directedPrompt: null };
329
+ }
330
+
331
+ for (const answer of parsedAnswerline.accept) {
332
+ if (answerWorks(answer, givenAnswer)) {
333
+ return { directive: 'accept', directedPrompt: null };
334
+ }
335
+ }
336
+
337
+ for (const answer of parsedAnswerline.prompt) {
338
+ const directedPrompt = answer[1];
339
+ if (answerWorks(answer[0], givenAnswer)) {
340
+ return { directive: 'prompt', directedPrompt: directedPrompt };
341
+ }
342
+ }
343
+
344
+ return { directive: 'reject', directedPrompt: null };
345
+ }
346
+
347
+
348
+ export default checkAnswer;
@@ -0,0 +1,62 @@
1
+ import { removeHTMLTags } from './utils.js';
2
+
3
+ /**
4
+ * Given an answer, return an array of equivalent answers (i.e. answers that should always match).
5
+ * @param {string} answer
6
+ * @returns {string[]} An array of equivalent answers.
7
+ */
8
+ function getEquivalentAnswers(answer) {
9
+ answer = answer.toLowerCase();
10
+ answer = removeHTMLTags(answer);
11
+ switch (answer) {
12
+ case 'atomic bombs':
13
+ case 'nuclear weapons':
14
+ case 'nukes':
15
+ return ['atomic bombs', 'atomic weapons', 'nuclear bombs', 'nuclear weapons', 'nukes', 'fission bombs', 'A-bombs'];
16
+ case 'house':
17
+ return ['home'];
18
+ case 'mouse':
19
+ return ['mice'];
20
+ case 'rail':
21
+ case 'railroad':
22
+ return ['rail', 'railroad'];
23
+ case 'nineteen eighty-four':
24
+ case 'nineteen eighty four':
25
+ return ['1984'];
26
+ case 'oxidation number':
27
+ case 'oxidation state':
28
+ return ['oxidation number', 'oxidation state'];
29
+ case 'ralph vaughan-williams':
30
+ return ['rvw'];
31
+ case 'spacewalk':
32
+ return ['space walk'];
33
+ case 'sugar cane':
34
+ case 'sugarcane':
35
+ return ['sugar cane', 'sugarcane'];
36
+ case 'wavefunction':
37
+ case 'wave function':
38
+ return ['wave function', 'wavefunction'];
39
+ case 'world war 1':
40
+ case 'world war i':
41
+ case 'world war one':
42
+ return [
43
+ 'first world war',
44
+ 'great war',
45
+ ];
46
+ case 'world war ii':
47
+ case 'world war two':
48
+ case 'world war 2':
49
+ return [
50
+ 'ww2',
51
+ 'wwii',
52
+ 'world war ii',
53
+ 'world war 2',
54
+ 'world war two',
55
+ 'second world war',
56
+ ];
57
+ }
58
+
59
+ return [];
60
+ }
61
+
62
+ export default getEquivalentAnswers;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Given a token, convert it to a standard form when it can be represented in multiple ways.
3
+ * @param {string} token
4
+ * @returns {string}
5
+ */
6
+ function standardizeTokens(token) {
7
+ switch (token) {
8
+ case 'dr':
9
+ case 'dr.':
10
+ return 'doctor';
11
+
12
+ case 'st':
13
+ case 'st.':
14
+ return 'saint';
15
+
16
+ // ordinals
17
+ case '1st':
18
+ return 'first';
19
+ case '2nd':
20
+ return 'second';
21
+ case '3rd':
22
+ return 'third';
23
+ case '4th':
24
+ return 'fourth';
25
+ case '5th':
26
+ return 'fifth';
27
+ case '6th':
28
+ return 'sixth';
29
+ case '7th':
30
+ return 'seventh';
31
+ case '8th':
32
+ return 'eighth';
33
+ case '9th':
34
+ return 'ninth';
35
+ case '10th':
36
+ return 'tenth';
37
+
38
+ // units
39
+ case 'cm':
40
+ return 'centimeter';
41
+ case 'mm':
42
+ return 'millimeter';
43
+
44
+ // typoes
45
+ case 'contentinal':
46
+ return 'continental';
47
+ }
48
+
49
+ return token;
50
+ }
51
+
52
+ export default standardizeTokens;
package/lib/utils.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Get all words which are partially or wholly underlined.
3
+ * @param {string} string
4
+ * @returns {string}
5
+ */
6
+ function extractKeyWords(string) {
7
+ const requiredWords = extractUnderlining(string).split(' ');
8
+
9
+ string = string
10
+ .split(' ')
11
+ .filter(token => token.length > 0)
12
+ .filter(token => token.match(/<\/?u>/) || requiredWords.includes(token))
13
+ .reduce((prev, curr) => prev + curr + ' ', '')
14
+ .trim();
15
+
16
+ return removeHTMLTags(string);
17
+ }
18
+
19
+
20
+ /**
21
+ * Extracts the text in quotes from a given string.
22
+ * @param {string} string - The input string.
23
+ * @returns {string} - The extracted quotes or the string without HTML tags.
24
+ */
25
+ function extractQuotes(string) {
26
+ const matches = string.match(/(?<=["])[^"]*(?=["])/g);
27
+
28
+ if (matches) {
29
+ return matches.reduce((prev, curr) => prev + ' ' + curr, '').trim();
30
+ } else {
31
+ return string;
32
+ }
33
+ }
34
+
35
+
36
+ /**
37
+ * Extracts the underlined text from a string.
38
+ * If no underlined text is found, it removes HTML tags from the string.
39
+ * @param {string} string - The input string.
40
+ * @returns {string} - The extracted underlined text or the string without HTML tags.
41
+ */
42
+ function extractUnderlining(string) {
43
+ const matches = string.match(/(?<=<u>)[^<]*(?=<\/u>)/g);
44
+
45
+ if (matches) {
46
+ return removeHTMLTags(matches.reduce((prev, curr) => prev + curr + ' ', '').trim());
47
+ } else {
48
+ return removeHTMLTags(string);
49
+ }
50
+ }
51
+
52
+
53
+ /**
54
+ * Get the abbreviation of a string by taking the first letter of each word.
55
+ * For example, "World Health Organization" becomes "WHO".
56
+ * @param {string} string
57
+ * @returns {string}
58
+ */
59
+ function getAbbreviation(string) {
60
+ return string
61
+ .split(' ')
62
+ .filter(token => token.length > 0)
63
+ .map(token => removeHTMLTags(token).charAt(0))
64
+ .reduce((a, b) => a + b, '')
65
+ .trim();
66
+ }
67
+
68
+
69
+ /**
70
+ * Removes HTML tags from a string.
71
+ * @param {string} string
72
+ * @returns {string}
73
+ */
74
+ function removeHTMLTags(string) {
75
+ return string.replace(/<[^>]*>/g, '');
76
+ }
77
+
78
+
79
+ /**
80
+ *
81
+ * @param {string} string
82
+ * @returns {string}
83
+ */
84
+ function removeItalics(string) {
85
+ return string.replace(/<\/?i>/g, '');
86
+ }
87
+
88
+
89
+ /**
90
+ * Removes parentheses and square brackets from a string.
91
+ * @param {string} string - The input string.
92
+ * @returns {string} The string with parentheses and square brackets removed.
93
+ */
94
+ function removeParentheses(string) {
95
+ return string
96
+ .replace(/\([^)]*\)/g, '')
97
+ .replace(/\[[^\]]*\]/g, '');
98
+ }
99
+
100
+
101
+ function removePunctuation(string) {
102
+ return string.replace(/[.,!;:'"\\/?@#$%^&*_~’]/g, '');
103
+ }
104
+
105
+
106
+ function replaceSpecialCharacters(string) {
107
+ return string
108
+ .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
109
+ .replace(/["“‟❝”❞]/g, '"');
110
+ }
111
+
112
+
113
+ function replaceSpecialSubstrings(string) {
114
+ return string
115
+ .replace(/\(s\)/g, 's')
116
+ .replace(/\p{Pd}/gu, '-'); // replace all dashes with the same dash
117
+ }
118
+
119
+
120
+ export {
121
+ extractKeyWords,
122
+ extractQuotes,
123
+ extractUnderlining,
124
+ getAbbreviation,
125
+ removeHTMLTags,
126
+ removeItalics,
127
+ removeParentheses,
128
+ removePunctuation,
129
+ replaceSpecialCharacters,
130
+ replaceSpecialSubstrings,
131
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "qb-answer-checker",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "A package to automatically check/judge answers against quizbowl answerlines.",
6
+ "main": "lib/check-answer.js",
7
+ "scripts": {
8
+ "test": "mocha test/script.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/qbreader/qb-answer-checker.git"
13
+ },
14
+ "keywords": [
15
+ "quizbowl",
16
+ "qb"
17
+ ],
18
+ "author": "Geoffrey Wu",
19
+ "license": "ISC",
20
+ "bugs": {
21
+ "url": "https://github.com/qbreader/qb-answer-checker/issues"
22
+ },
23
+ "homepage": "https://github.com/qbreader/qb-answer-checker#readme",
24
+ "devDependencies": {
25
+ "chai": "^4.3.10",
26
+ "mocha": "^10.2.0"
27
+ },
28
+ "dependencies": {
29
+ "damerau-levenshtein-js": "^1.1.8",
30
+ "number-to-words": "^1.2.4",
31
+ "roman-numerals": "^0.3.2",
32
+ "stemmer": "^2.0.1"
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ // A class that contains bash escape sequences for common colors.
2
+ // Shamelessly copied from https://svn.blender.org/svnroot/bf-blender/trunk/blender/build_files/scons/tools/bcolors.py
3
+
4
+
5
+
6
+ const HEADER = '\x1b[95m';
7
+ const OKBLUE = '\x1b[94m';
8
+ const OKCYAN = '\x1b[96m';
9
+ const OKGREEN = '\x1b[92m';
10
+ const WARNING = '\x1b[93m';
11
+ const FAIL = '\x1b[91m';
12
+ const ENDC = '\x1b[0m';
13
+ const BOLD = '\x1b[1m';
14
+ const UNDERLINE = '\x1b[4m';
15
+
16
+ export {
17
+ HEADER,
18
+ OKBLUE,
19
+ OKCYAN,
20
+ OKGREEN,
21
+ WARNING,
22
+ FAIL,
23
+ ENDC,
24
+ BOLD,
25
+ UNDERLINE,
26
+ };