+function hashparams() {
+ // location.hash is not encoded in firefox
+ const encodedhash = (window.location.href.split('#'))[1] || '';
+ return decodeURIComponent(encodedhash).split('#');
+}
+
+class Words {
+ constructor(data, root = undefined) {
+ this.data = data;
+ this.selection = root || this.data[''][3];
+ this.visible = new Set(root || Object.keys(data).flatMap(id => id && parseInt(id)));
+ if (root) {
+ let children = root;
+ for (let loop = 0; children.length && loop < 20; loop++) {
+ for (let child of children) this.visible.add(child);
+ children = children.map(cat => data[cat][3]).filter(is => is).flat();
+ }
+ }
+ }
+
+ filter(f) {
+ // keep only matching entries, and root selection regardless
+ this.visible = new Set([...this.visible].filter(f).concat(this.selection));
+ }
+
+ *root() {
+ for (let i of this.selection) {
+ if (!this.has(i)) {
+ continue;
+ }
+ yield this.get(i);
+ }
+ }
+
+ *random() {
+ let order = [...this.visible.keys()].shuffle();
+ for (let i of order) {
+ if (!this.has(i)) {
+ continue;
+ }
+ yield this.get(i);
+ }
+ }
+
+ has(id) {
+ return this.visible.has(id);
+ }
+
+ subs(id) {
+ let refs = this.data[id][3];
+ if (!refs) {
+ return [];
+ }
+ for (let ref of refs) {
+ // retain orphaned references in grandparent categories
+ if (!this.has(ref)) {
+ refs = refs.concat(this.subs(ref));
+ }
+ }
+ return refs;
+ }
+
+ get(id) {
+ if (!this.has(id)) {
+ return;
+ }
+ const p = this;
+ const row = this.data[id];
+ return row && {
+ id: id,
+ title: row[0],
+ get label() {
+ return row[0].replace(/\/.*/, ''); // primary form
+ },
+ level: row[1],
+ imgid: row[2],
+ thumb(size = 32) {
+ return `/data/word/${size}/${row[2]}.jpg`;
+ },
+ get subs() {
+ return p.subs(id).map(e => p.get(e));
+ },
+ };
+ }
+}
+