Wanikani Open Framework - ItemData module

ItemData module for Wanikani Open Framework

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greatest.deepsurf.us/scripts/38580/1187212/Wanikani%20Open%20Framework%20-%20ItemData%20module.js

  1. // ==UserScript==
  2. // @name Wanikani Open Framework - ItemData module
  3. // @namespace rfindley
  4. // @description ItemData module for Wanikani Open Framework
  5. // @version 1.0.19
  6. // @copyright 2018-2023, Robin Findley
  7. // @license MIT; http://opensource.org/licenses/MIT
  8. // ==/UserScript==
  9.  
  10. (function(global) {
  11.  
  12. //########################################################################
  13. //------------------------------
  14. // Published interface.
  15. //------------------------------
  16. global.wkof.ItemData = {
  17. presets: {},
  18. registry: {
  19. sources: {},
  20. indices: {},
  21. },
  22. get_items: get_items,
  23. get_index: get_index,
  24. pause_ready_event: pause_ready_event
  25. };
  26. //########################################################################
  27.  
  28. function promise(){let a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
  29. function split_list(str) {return str.replace(/、/g,',').replace(/[\s ]+/g,' ').trim().replace(/ *, */g, ',').split(',').filter(function(name) {return (name.length > 0);});}
  30.  
  31. //------------------------------
  32. // Get the items specified by the configuration.
  33. //------------------------------
  34. function get_items(config, global_options) {
  35. // Default to WK 'subjects' only.
  36. if (!config) config = {wk_items:{}};
  37.  
  38. // Allow comma-separated list of WK-only endpoints.
  39. if (typeof config === 'string') {
  40. let endpoints = split_list(config)
  41. config = {wk_items:{options:{}}};
  42. for (let idx in endpoints)
  43. config.wk_items.options[endpoints[idx]] = true;
  44. }
  45.  
  46. // Fetch the requested endpoints.
  47. let fetch_promise = promise();
  48. let items = [];
  49. let remaining = 0;
  50. for (let cfg_name in config) {
  51. let cfg = config[cfg_name];
  52. let spec = wkof.ItemData.registry.sources[cfg_name];
  53. if (!spec || typeof spec.fetcher !== 'function') {
  54. console.log('wkof.ItemData.get_items() - Config "'+cfg_name+'" not registered!');
  55. continue;
  56. }
  57. remaining++;
  58. spec.fetcher(cfg, global_options)
  59. .then(function(data){
  60. let filter_promise;
  61. if (typeof spec === 'object')
  62. filter_promise = apply_filters(data, cfg, spec);
  63. else
  64. filter_promise = Promise.resolve(data);
  65. filter_promise.then(function(data){
  66. items = items.concat(data);
  67. remaining--;
  68. if (!remaining) fetch_promise.resolve(items);
  69. });
  70. })
  71. .catch(function(e){
  72. if (e) throw e;
  73. console.log('wkof.ItemData.get_items() - Failed for config "'+cfg_name+'"');
  74. remaining--;
  75. if (!remaining) fetch_promise.resolve(items);
  76. });
  77. }
  78. if (remaining === 0) fetch_promise.resolve(items);
  79. return fetch_promise;
  80. }
  81.  
  82. //------------------------------
  83. // Get the wk_items specified by the configuration.
  84. //------------------------------
  85. function get_wk_items(config, options) {
  86. let cfg_options = config.options || {};
  87. options = options || {};
  88. let now = new Date().getTime();
  89.  
  90. // Endpoints that we can fetch (subjects MUST BE FIRST!!)
  91. let available_endpoints = ['subjects','assignments','review_statistics','study_materials'];
  92. let spec = wkof.ItemData.registry.sources.wk_items;
  93. for (let filter_name in config.filters) {
  94. let filter_spec = spec.filters[filter_name];
  95. if (!filter_spec || typeof filter_spec.set_options !== 'function') continue;
  96. let filter_cfg = config.filters[filter_name];
  97. filter_spec.set_options(cfg_options, filter_cfg.value);
  98. }
  99.  
  100. // Fetch all of the endpoints
  101. let ep_promises = [];
  102. for (let idx in available_endpoints) {
  103. let ep_name = available_endpoints[idx];
  104. if (ep_name === 'subjects' || cfg_options[ep_name] === true)
  105. ep_promises.push(
  106. wkof.Apiv2.get_endpoint(ep_name, options)
  107. .then(process_data.bind(null, ep_name))
  108. );
  109. }
  110. return Promise.all(ep_promises)
  111. .then(function(all_data){
  112. return all_data[0];
  113. });
  114.  
  115. //============
  116. function process_data(ep_name, ep_data) {
  117. if (ep_name === 'subjects') return ep_data;
  118. // Merge with 'subjects' when 'subjects' is done fetching.
  119. return ep_promises[0].then(cross_link.bind(null, ep_name, ep_data));
  120. }
  121.  
  122. //============
  123. function cross_link(ep_name, ep_data, subjects) {
  124. for (let id in ep_data) {
  125. let record = ep_data[id];
  126. let subject_id = record.data.subject_id;
  127. subjects[subject_id][ep_name] = record.data;
  128. }
  129. }
  130. }
  131.  
  132. //------------------------------
  133. // Filter the items array according to the specified filters and options.
  134. //------------------------------
  135. function apply_filters(items, config, spec) {
  136. let prep_promises = [];
  137. let options = config.options || {};
  138. let filters = [];
  139. let is_wk_items = (spec === wkof.ItemData.registry.sources.wk_items);
  140. for (let filter_name in config.filters) {
  141. let filter_cfg = config.filters[filter_name];
  142. if (typeof filter_cfg !== 'object' || filter_cfg.value === undefined)
  143. filter_cfg = {value:filter_cfg};
  144. let filter_value = filter_cfg.value;
  145. let filter_spec = spec.filters[filter_name];
  146. if (filter_spec === undefined) throw new Error('wkof.ItemData.get_item() - Invalid filter "'+filter_name+'"');
  147. if (typeof filter_spec.filter_value_map === 'function')
  148. filter_value = filter_spec.filter_value_map(filter_cfg.value);
  149. if (typeof filter_spec.prepare === 'function') {
  150. let result = filter_spec.prepare(filter_value);
  151. if (result instanceof Promise) prep_promises.push(result);
  152. }
  153. filters.push({
  154. name: filter_name,
  155. func: filter_spec.filter_func,
  156. filter_value: filter_value,
  157. invert: (filter_cfg.invert === true)
  158. });
  159. }
  160. if (is_wk_items && (options.include_hidden !== true)) {
  161. filters.push({
  162. name: 'remove_deleted',
  163. func: function(filter_value, item){return item.data.hidden_at === null;},
  164. filter_value: true,
  165. invert: false
  166. });
  167. }
  168.  
  169. return Promise.all(prep_promises).then(function(){
  170. let result = [];
  171. let max_level = Math.max(wkof.user.subscription.max_level_granted, wkof.user.override_max_level || 0);
  172. for (let item_idx in items) {
  173. let keep = true;
  174. let item = items[item_idx];
  175. if (is_wk_items && (item.data.level > max_level)) continue;
  176. for (let filter_idx in filters) {
  177. let filter = filters[filter_idx];
  178. try {
  179. keep = filter.func(filter.filter_value, item);
  180. if (filter.invert) keep = !keep;
  181. if (!keep) break;
  182. } catch(e) {
  183. keep = false;
  184. break;
  185. }
  186. }
  187. if (keep) result.push(item);
  188. }
  189. return result;
  190. });
  191. }
  192.  
  193. //------------------------------
  194. // Return the items indexed by an indexing function.
  195. //------------------------------
  196. function get_index(items, index_name) {
  197. let index_func = wkof.ItemData.registry.indices[index_name];
  198. if (typeof index_func !== 'function') throw new Error('wkof.ItemData.index_by() - Invalid index function "'+index_name+'"');
  199. return index_func(items);
  200. }
  201.  
  202. //------------------------------
  203. // Register wk_items data source.
  204. //------------------------------
  205. wkof.ItemData.registry.sources['wk_items'] = {
  206. description: 'Wanikani',
  207. fetcher: get_wk_items,
  208. options: {
  209. assignments: {
  210. type: 'checkbox',
  211. label: 'Assignments',
  212. default: false,
  213. hover_tip: 'Include the "/assignments" endpoint (SRS status, burn status, progress dates)'
  214. },
  215. review_statistics: {
  216. type: 'checkbox',
  217. label: 'Review Statistics',
  218. default: false,
  219. hover_tip: 'Include the "/review_statistics" endpoint:\n * Per-item review count\n *Correct/incorrect count\n * Longest streak'
  220. },
  221. study_materials: {
  222. type: 'checkbox',
  223. label: 'Study Materials',
  224. default: false,
  225. hover_tip: 'Include the "/study_materials" endpoint:\n * User synonyms\n * User notes'
  226. },
  227. },
  228. filters: {
  229. item_type: {
  230. type: 'multi',
  231. label: 'Item type',
  232. content: {radical:'Radicals',kanji:'Kanji',vocabulary:'Vocabulary',kana_vocabulary:'Kana Vocabulary'},
  233. default: [],
  234. filter_value_map: item_type_to_arr,
  235. filter_func: function(filter_value, item){return filter_value[item.object] === true;},
  236. hover_tip: 'Filter by item type (radical, kanji, vocabulary, kana_vocabulary)',
  237. },
  238. level: {
  239. type: 'text',
  240. label: 'Level',
  241. placeholder: '(e.g. "1..3,5")',
  242. default: '',
  243. filter_value_map: levels_to_arr,
  244. filter_func: function(filter_value, item){return filter_value[item.data.level] === true;},
  245. hover_tip: 'Filter by Wanikani level\nExamples:\n "*" (All levels)\n "1..3,5" (Levels 1 through 3, and level 5)\n "1..-1" (From level 1 to your current level minus 1)\n "-5..+0" (Your current level and previous 5 levels)\n "+1" (Your next level)',
  246. },
  247. srs: {
  248. type: 'multi',
  249. label: 'SRS Level',
  250. content: {lock:'Locked',init:'Initiate (Lesson Queue)',appr1:'Apprentice 1',appr2:'Apprentice 2',appr3:'Apprentice 3',appr4:'Apprentice 4',guru1:'Guru 1',guru2:'Guru 2',mast:'Master',enli:'Enlightened',burn:'Burned'},
  251. default: [],
  252. set_options: function(options){options.assignments = true;},
  253. filter_value_map: srs_to_arr,
  254. filter_func: function(filter_value, item){return filter_value[(item.assignments && item.assignments.unlocked_at ? item.assignments.srs_stage : -1)] === true;},
  255. hover_tip: 'Filter by SRS level (Apprentice 1, Apprentice 2, ..., Burn)',
  256. },
  257. have_burned: {
  258. type: 'checkbox',
  259. label: 'Have burned',
  260. default: true,
  261. set_options: function(options){options.assignments = true;},
  262. filter_func: function(filter_value, item){return ((item.assignments !== undefined) && (item.assignments.burned_at !== null)) === filter_value;},
  263. hover_tip: 'Filter items by whether they have ever been burned.\n * If checked, select burned items (including resurrected)\n * If unchecked, select items that have never been burned',
  264. },
  265. }
  266. };
  267.  
  268. //------------------------------
  269. // Macro to build a function to index by a specific field.
  270. // Set make_subarrays to true if more than one item can share the same field value (e.g. same item_type).
  271. //------------------------------
  272. function make_index_func(name, field, entry_type) {
  273. let fn = '';
  274. fn +=
  275. 'let index = {}, value;\n'+
  276. 'for (let idx in items) {\n'+
  277. ' let item = items[idx];\n'+
  278. ' try {\n'+
  279. ' value = '+field+';\n'+
  280. ' } catch(e) {continue;}\n'+
  281. ' if (value === null || value === undefined) continue;\n';
  282. if (entry_type === 'array') {
  283. fn +=
  284. ' if (index[value] === undefined) {\n'+
  285. ' index[value] = [item];\n'+
  286. ' continue;\n'+
  287. ' }\n';
  288. } else {
  289. fn +=
  290. ' if (index[value] === undefined) {\n'+
  291. ' index[value] = item;\n'+
  292. ' continue;\n'+
  293. ' }\n';
  294. if (entry_type === 'single_or_array') {
  295. fn +=
  296. ' if (!Array.isArray(index[value]))\n'+
  297. ' index[value] = [index[value]];\n';
  298. }
  299. }
  300. fn +=
  301. ' index[value].push(item);\n'+
  302. '}\n'+
  303. 'return index;'
  304. wkof.ItemData.registry.indices[name] = new Function('items', fn);
  305. }
  306.  
  307. // Build some index functions.
  308. make_index_func('item_type', 'item.object', 'array');
  309. make_index_func('level', 'item.data.level', 'array');
  310. make_index_func('slug', 'item.data.slug', 'single_or_array');
  311. make_index_func('srs_stage', '(item.assignments && item.assignments.unlocked_at ? item.assignments.srs_stage : -1)', 'array');
  312. make_index_func('srs_stage_name', '(item.assignments && item.assignments.unlocked_at ? item.assignments.srs_stage_name : "Locked")', 'array');
  313. make_index_func('subject_id', 'item.id', 'single');
  314.  
  315.  
  316. //------------------------------
  317. // Index by reading
  318. //------------------------------
  319. wkof.ItemData.registry.indices['reading'] = function(items) {
  320. let index = {};
  321. for (let idx in items) {
  322. let item = items[idx];
  323. if (!item.hasOwnProperty('data') || !item.data.hasOwnProperty('readings')) continue;
  324. if (!Array.isArray(item.data.readings)) continue;
  325. let readings = item.data.readings;
  326. for (let idx2 in readings) {
  327. let reading = readings[idx2].reading;
  328. if (reading === 'None') continue;
  329. if (!index[reading]) index[reading] = [];
  330. index[reading].push(item);
  331. }
  332. }
  333. return index;
  334. }
  335.  
  336. //------------------------------
  337. // Given an array of item type criteria (e.g. ['rad', 'kan', 'voc','kana_voc']), return
  338. // an array containing 'true' for each item type contained in the criteria.
  339. //------------------------------
  340. function item_type_to_arr(filter_value) {
  341. let xlat = {rad:'radical',kan:'kanji',voc:'vocabulary',kana_voc:'kana_vocabulary'};
  342. let arr = {}, value;
  343. if (typeof filter_value === 'string') filter_value = split_list(filter_value);
  344. if (typeof filter_value !== 'object') return {};
  345. if (Array.isArray(filter_value)) {
  346. for (let idx in filter_value) {
  347. value = filter_value[idx];
  348. value = xlat[value] || value;
  349. arr[value] = true;
  350. }
  351. } else {
  352. for (value in filter_value) {
  353. arr[xlat[value] || value] = (filter_value[value] === true);
  354. }
  355. }
  356. return arr;
  357. }
  358.  
  359. //------------------------------
  360. // Given an array of srs criteria (e.g. ['mast', 'enli', 'burn']), return an
  361. // array containing 'true' for each srs level contained in the criteria.
  362. //------------------------------
  363. function srs_to_arr(filter_value) {
  364. let index = ['lock','init','appr1','appr2','appr3','appr4','guru1','guru2','mast','enli','burn'];
  365. let arr = [], value;
  366. if (typeof filter_value === 'string') filter_value = split_list(filter_value);
  367. if (typeof filter_value !== 'object') return {};
  368. if (Array.isArray(filter_value)) {
  369. for (let idx in filter_value) {
  370. value = Number(filter_value[idx]);
  371. if (isNaN(value)) value = index.indexOf(filter_value[idx]) - 1;
  372. arr[value] = true;
  373. }
  374. } else {
  375. for (value in filter_value) {
  376. arr[index.indexOf(value) - 1] = (filter_value[value] === true);
  377. }
  378. }
  379. return arr;
  380. }
  381.  
  382. //------------------------------
  383. // Given an level criteria string (e.g. '1..3,5,8'), return an array containing
  384. // 'true' for each level contained in the criteria.
  385. //------------------------------
  386. function levels_to_arr(filter_value) {
  387. let levels = [], crit_idx, start, stop, lvl;
  388.  
  389. // Process each comma-separated criteria separately.
  390. let criteria = filter_value.split(',');
  391. for (crit_idx = 0; crit_idx < criteria.length; crit_idx++) {
  392. let crit = criteria[crit_idx];
  393. let value = true;
  394.  
  395. // Match '*' = all levels
  396. let match = crit.match(/^\s*["']?\s*(\*)\s*["']?\s*$/);
  397. if (match !== null) {
  398. start = to_num('1');
  399. stop = to_num('9999'); // All levels
  400. for (lvl = start; lvl <= stop; lvl++)
  401. levels[lvl] = value;
  402. continue;
  403. }
  404.  
  405. // Match 'a..b' = range of levels (or exclude if preceded by '!')
  406. match = crit.match(/^\s*["']?\s*(\!?)\s*((\+|-)?\d+)\s*(-|\.\.\.?|to)\s*((\+|-)?\d+)\s*["']?\s*$/);
  407. if (match !== null) {
  408. start = to_num(match[2]);
  409. stop = to_num(match[5]);
  410. if (match[1] === '!') value = false;
  411. for (lvl = start; lvl <= stop; lvl++)
  412. levels[lvl] = value;
  413. continue;
  414. }
  415.  
  416. // Match 'a' = specific level (or exclude if preceded by '!')
  417. match = crit.match(/^\s*["']?\s*(\!?)\s*((\+|-)?\d+)\s*["']?\s*$/);
  418. if (match !== null) {
  419. lvl = to_num(match[2]);
  420. if (match[1] === '!') value = false;
  421. levels[lvl] = value;
  422. continue;
  423. }
  424. let err = 'wkof.ItemData::levels_to_arr() - Bad filter criteria "'+filter_value+'"';
  425. console.log(err);
  426. throw err;
  427. }
  428. return levels;
  429.  
  430. //============
  431. function to_num(num) {
  432. num = (num[0] < '0' ? wkof.user.level : 0) + Number(num)
  433. return Math.min(Math.max(1, num), wkof.user.subscription.max_level_granted);
  434. }
  435. }
  436.  
  437. let registration_promise;
  438. let registration_timeout;
  439. let registration_counter = 0;
  440. //------------------------------
  441. // Ask clients to add items to the registry.
  442. //------------------------------
  443. function call_for_registration() {
  444. registration_promise = promise();
  445. wkof.set_state('wkof.ItemData.registry', 'ready');
  446. setTimeout(check_registration_counter, 1);
  447. registration_timeout = setTimeout(function(){
  448. registration_timeout = undefined;
  449. check_registration_counter(true /* force_ready */);
  450. }, 3000);
  451. return registration_promise;
  452. }
  453.  
  454. //------------------------------
  455. // Request to pause the 'ready' event.
  456. //------------------------------
  457. function pause_ready_event(value) {
  458. if (value === true) {
  459. registration_counter++;
  460. } else {
  461. registration_counter--;
  462. check_registration_counter();
  463. }
  464. }
  465.  
  466. //------------------------------
  467. // If registration is complete or timed out, mark it as resolved.
  468. //------------------------------
  469. function check_registration_counter(force_ready) {
  470. if (!force_ready && registration_counter > 0) return false;
  471. if (registration_timeout !== undefined) clearTimeout(registration_timeout);
  472. registration_promise.resolve();
  473. return true;
  474. }
  475.  
  476. //------------------------------
  477. // Notify listeners that we are ready.
  478. //------------------------------
  479. function notify_ready() {
  480. // Delay guarantees include() callbacks are called before ready() callbacks.
  481. setTimeout(function(){wkof.set_state('wkof.ItemData', 'ready');},0);
  482. }
  483. wkof.include('Apiv2');
  484. wkof.ready('Apiv2').then(call_for_registration).then(notify_ready);
  485.  
  486. })(this);