index: release v1.18 with only altgr index linked
[sheet.git] / word / quiz.js
1 Array.prototype.shuffle = function () {
2         for (let i = this.length - 1; i > 0; i--) {
3                 const j = Math.floor(Math.random() * (i + 1)); // random index 0..i
4                 [this[i], this[j]] = [this[j], this[i]]; // swap elements
5         }
6         return this;
7 };
8
9 function hashparams() {
10         const encodedhash = window.location.href.split('#').slice(1) || '';
11         if (encodedhash.length == 1) {
12                 // location.hash is not encoded in firefox
13                 return decodeURIComponent(encodedhash).split('#');
14         }
15         return encodedhash;
16 }
17
18 class Words {
19         constructor(data, root = undefined) {
20                 this.data = data;
21                 this.selection = root || this.data[''][3];
22                 this.visible = new Set(root || Object.keys(data).flatMap(id => id && parseInt(id)));
23                 if (root) {
24                         let children = root;
25                         for (let loop = 0; children.length && loop < 20; loop++) {
26                                 for (let child of children) this.visible.add(child);
27                                 children = children.map(cat => data[cat][3]).filter(is => is).flat();
28                         }
29                 }
30         }
31
32         filter(f) {
33                 // keep only matching entries, and root selection regardless
34                 this.visible = new Set([...this.visible].filter(f).concat(this.selection));
35         }
36
37         *root() {
38                 for (let i of this.selection) {
39                         if (!this.has(i)) {
40                                 continue;
41                         }
42                         yield this.get(i);
43                 }
44         }
45
46         *random() {
47                 let order = [...this.visible.keys()].shuffle();
48                 for (let i of order) {
49                         if (!this.has(i)) {
50                                 continue;
51                         }
52                         yield this.get(i);
53                 }
54         }
55
56         has(id) {
57                 return this.visible.has(id);
58         }
59
60         subs(id) {
61                 let refs = this.data[id][3];
62                 if (!refs) {
63                         return [];
64                 }
65                 for (let ref of refs) {
66                         // retain orphaned references in grandparent categories
67                         if (!this.has(ref)) {
68                                 refs = refs.concat(this.subs(ref));
69                         }
70                 }
71                 return refs;
72         }
73
74         get(id) {
75                 if (!this.has(id)) {
76                         return;
77                 }
78                 const p = this;
79                 const row = this.data[id];
80                 return row && {
81                         id: id,
82                         title: row[0],
83                         get label() {
84                                 return row[0].replace(/\/.*/, ''); // primary form
85                         },
86                         get html() {
87                                 let aliases = this.title.split('/');
88                                 let html = aliases.shift();
89                                 html = html.replace(/\((.+)\)/, '<small>$1</small>');
90                                 for (let alias of aliases) {
91                                         html += ` <small>(${alias})</small>`;
92                                 }
93                                 return html;
94                         },
95                         level: row[1],
96                         imgid: row[2],
97                         thumb(size = 32) {
98                                 return `/data/word/${size}/${row[2]}.jpg`;
99                         },
100                         get subs() {
101                                 return p.subs(id).map(e => p.get(e));
102                         },
103                 };
104         }
105 }
106
107 class WordQuiz {
108         dataselect(json) {
109                 this.data = this.datafilter(json);
110                 return [...this.data.random()];
111         }
112
113         datafilter(json) {
114                 // find viable rows from json data
115                 const selection = new Words(json, this.preset.cat);
116
117                 if (this.preset.images) {
118                         selection.filter(id => json[id][2]);
119                 }
120                 if (this.preset.level !== undefined) {
121                         selection.filter(id => json[id][1] <= this.preset.level);
122                 }
123
124                 if (this.preset.distinct) {
125                         // remove referenced categories
126                         selection.filter(id => !selection.get(id).subs.length);
127                 }
128
129                 return selection;
130         }
131
132         configure(params = hashparams()) {
133                 const opts = new Map(params.map(arg => arg.split(/[:=](.*)/)));
134                 for (let [query, val] of opts) {
135                         if (query.match(/^\d+$/)) {
136                                 this.preset.cat = [parseInt(query)];
137                         }
138                         else if (query === 'level') {
139                                 this.preset.level = parseInt(val);
140                         }
141                         else if (query === 'debug') {
142                                 this.preset.debug = true;
143                         }
144                         else {
145                                 this.preset[query] = val;
146                         }
147                 }
148                 this.preset.dataurl = `/data/wordlist.${this.preset.lang}.json`
149         }
150
151         setup() {
152                 this.form = document.getElementById('quiz');
153         }
154
155         load() {
156                 this.configure();
157                 fetch(this.preset.dataurl).then(res => res.json()).then(json => {
158                         this.words = this.dataselect(json)
159                         this.setup();
160                 });
161         }
162
163         log(...args) {
164                 this.history.push([new Date().toISOString(), ...args]);
165         }
166
167         stop(...args) {
168                 this.log(...args);
169                 window.onbeforeunload = null;
170                 fetch('/word/report', {method: 'POST', body: JSON.stringify(this.history)});
171         }
172
173         constructor() {
174                 this.preset = {images: true, lang: 'en'};
175                 this.load();
176                 this.history = [];
177                 window.onbeforeunload = e => {
178                         this.stop('abort');
179                 };
180                 window.onhashchange = e => {
181                         this.load();
182                 };
183         }
184 }