Wanikani Open Framework - ItemData module

ItemData module for Wanikani Open Framework

当前为 2018-05-16 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/38580/597938/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.11
  6. // @copyright 2018+, 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(){var 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(/^\s+|\s*(,)\s*|\s+$/g, '$1').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. var endpoints = split_list(config)
  41. var config = {wk_items:{options:{}}};
  42. for (var idx in endpoints)
  43. config.wk_items.options[endpoints[idx]] = true;
  44. }
  45.  
  46. // Fetch the requested endpoints.
  47. var fetch_promise = promise();
  48. var items = [];
  49. var remaining = 0;
  50. for (var cfg_name in config) {
  51. var cfg = config[cfg_name];
  52. var 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. var 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. var cfg_options = config.options || {};
  87. options = options || {};
  88. var now = new Date().getTime();
  89.  
  90. // Endpoints that we can fetch (subjects MUST BE FIRST!!)
  91. var available_endpoints = ['subjects','assignments','review_statistics','study_materials'];
  92. var spec = wkof.ItemData.registry.sources.wk_items;
  93. for (var filter_name in config.filters) {
  94. var filter_spec = spec.filters[filter_name];
  95. if (!filter_spec || typeof filter_spec.set_options !== 'function') continue;
  96. var 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. var ep_promises = [];
  102. for (var idx in available_endpoints) {
  103. var 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 (var id in ep_data) {
  125. var record = ep_data[id];
  126. var 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. var prep_promises = [];
  137. var options = config.options || {};
  138. var filters = [];
  139. for (var filter_name in config.filters) {
  140. var filter_cfg = config.filters[filter_name];
  141. if (typeof filter_cfg !== 'object' || filter_cfg.value === undefined)
  142. filter_cfg = {value:filter_cfg};
  143. var filter_value = filter_cfg.value;
  144. var filter_spec = spec.filters[filter_name];
  145. if (filter_spec === undefined) throw new Error('wkof.ItemData.get_item() - Invalid filter "'+filter_name+'"');
  146. if (typeof filter_spec.filter_value_map === 'function')
  147. filter_value = filter_spec.filter_value_map(filter_cfg.value);
  148. if (typeof filter_spec.prepare === 'function') {
  149. var result = filter_spec.prepare(filter_value);
  150. if (result instanceof Promise) prep_promises.push(result);
  151. }
  152. filters.push({
  153. name: filter_name,
  154. func: filter_spec.filter_func,
  155. filter_value: filter_value,
  156. invert: (filter_cfg.invert === true)
  157. });
  158. }
  159. if (options.include_hidden !== true) {
  160. filters.push({
  161. name: 'remove_deleted',
  162. func: function(filter_value, item){return item.data.hidden_at === null;},
  163. filter_value: true,
  164. invert: false
  165. });
  166. }
  167.  
  168. return Promise.all(prep_promises).then(function(){
  169. var result = [];
  170. var max_level = Math.max(wkof.user.max_level_granted_by_subscription, wkof.user.override_max_level || 0);
  171. for (var item_idx in items) {
  172. var keep = true;
  173. var item = items[item_idx];
  174. if (item.data.level > max_level) continue;
  175. for (var filter_idx in filters) {
  176. var filter = filters[filter_idx];
  177. try {
  178. keep = filter.func(filter.filter_value, item);
  179. if (filter.invert) keep = !keep;
  180. if (!keep) break;
  181. } catch(e) {
  182. keep = false;
  183. break;
  184. }
  185. }
  186. if (keep) result.push(item);
  187. }
  188. return result;
  189. });
  190. }
  191.  
  192. //------------------------------
  193. // Return the items indexed by an indexing function.
  194. //------------------------------
  195. function get_index(items, index_name) {
  196. var index_func = wkof.ItemData.registry.indices[index_name];
  197. if (typeof index_func !== 'function') throw new Error('wkof.ItemData.index_by() - Invalid index function "'+index_name+'"');
  198. return index_func(items);
  199. }
  200.  
  201. //------------------------------
  202. // Register wk_items data source.
  203. //------------------------------
  204. wkof.ItemData.registry.sources['wk_items'] = {
  205. description: 'Wanikani',
  206. fetcher: get_wk_items,
  207. options: {
  208. assignments: {
  209. type: 'checkbox',
  210. label: 'Assignments',
  211. default: false,
  212. hover_tip: 'Include the "/assignments" endpoint (SRS status, burn status, progress dates)'
  213. },
  214. review_statistics: {
  215. type: 'checkbox',
  216. label: 'Review Statistics',
  217. default: false,
  218. hover_tip: 'Include the "/review_statistics" endpoint:\n * Per-item review count\n *Correct/incorrect count\n * Longest streak'
  219. },
  220. study_materials: {
  221. type: 'checkbox',
  222. label: 'Study Materials',
  223. default: false,
  224. hover_tip: 'Include the "/study_materials" endpoint:\n * User synonyms\n * User notes'
  225. },
  226. },
  227. filters: {
  228. item_type: {
  229. type: 'multi',
  230. label: 'Item type',
  231. content: {radical:'Radicals',kanji:'Kanji',vocabulary:'Vocabulary'},
  232. default: [],
  233. filter_value_map: item_type_to_arr,
  234. filter_func: function(filter_value, item){return filter_value[item.object] === true;},
  235. hover_tip: 'Filter by item type (radical, kanji, vocabulary)',
  236. },
  237. level: {
  238. type: 'text',
  239. label: 'Level',
  240. placeholder: '(e.g. "1..3,5")',
  241. default: '',
  242. filter_value_map: levels_to_arr,
  243. filter_func: function(filter_value, item){return filter_value[item.data.level] === true;},
  244. 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)',
  245. },
  246. srs: {
  247. type: 'multi',
  248. label: 'SRS Level',
  249. 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'},
  250. default: [],
  251. set_options: function(options){options.assignments = true;},
  252. filter_value_map: srs_to_arr,
  253. filter_func: function(filter_value, item){return filter_value[(item.assignments ? item.assignments.srs_stage : -1)] === true;},
  254. hover_tip: 'Filter by SRS level (Apprentice 1, Apprentice 2, ..., Burn)',
  255. },
  256. have_burned: {
  257. type: 'checkbox',
  258. label: 'Have burned',
  259. default: true,
  260. set_options: function(options){options.assignments = true;},
  261. filter_func: function(filter_value, item){return (item.assignments.burned_at !== null) === filter_value;},
  262. 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',
  263. },
  264. }
  265. };
  266.  
  267. //------------------------------
  268. // Macro to build a function to index by a specific field.
  269. // Set make_subarrays to true if more than one item can share the same field value (e.g. same item_type).
  270. //------------------------------
  271. function make_index_func(name, field, entry_type) {
  272. var fn = '';
  273. fn +=
  274. 'var index = {}, value;\n'+
  275. 'for (var idx in items) {\n'+
  276. ' var item = items[idx];\n'+
  277. ' try {\n'+
  278. ' value = '+field+';\n'+
  279. ' } catch(e) {continue;}\n'+
  280. ' if (value === null || value === undefined) continue;\n';
  281. if (entry_type === 'array') {
  282. fn +=
  283. ' if (index[value] === undefined) {\n'+
  284. ' index[value] = [item];\n'+
  285. ' continue;\n'+
  286. ' }\n';
  287. } else {
  288. fn +=
  289. ' if (index[value] === undefined) {\n'+
  290. ' index[value] = item;\n'+
  291. ' continue;\n'+
  292. ' }\n';
  293. if (entry_type === 'single_or_array') {
  294. fn +=
  295. ' if (!Array.isArray(index[value]))\n'+
  296. ' index[value] = [index[value]];\n';
  297. }
  298. }
  299. fn +=
  300. ' index[value].push(item);\n'+
  301. '}\n'+
  302. 'return index;'
  303. wkof.ItemData.registry.indices[name] = new Function('items', fn);
  304. }
  305.  
  306. // Build some index functions.
  307. make_index_func('item_type', 'item.object', 'array');
  308. make_index_func('level', 'item.data.level', 'array');
  309. make_index_func('slug', 'item.data.slug', 'single_or_array');
  310. make_index_func('srs_stage', 'item.assignments.srs_stage', 'array');
  311. make_index_func('srs_stage_name', 'item.assignments.srs_stage_name', 'array');
  312. make_index_func('subject_id', 'item.id', 'single');
  313.  
  314.  
  315. //------------------------------
  316. // Index by reading
  317. //------------------------------
  318. wkof.ItemData.registry.indices['reading'] = function(items) {
  319. var index = {};
  320. for (var idx in items) {
  321. var item = items[idx];
  322. if (!item.hasOwnProperty('data') || !item.data.hasOwnProperty('readings')) continue;
  323. if (!Array.isArray(item.data.readings)) continue;
  324. var readings = item.data.readings;
  325. for (var idx2 in readings) {
  326. var reading = readings[idx2].reading;
  327. if (reading === 'None') continue;
  328. if (!index[reading]) index[reading] = [];
  329. index[reading].push(item);
  330. }
  331. }
  332. return index;
  333. }
  334.  
  335. //------------------------------
  336. // Given an array of item type criteria (e.g. ['rad', 'kan', 'voc']), return
  337. // an array containing 'true' for each item type contained in the criteria.
  338. //------------------------------
  339. function item_type_to_arr(filter_value) {
  340. var xlat = {rad:'radical',kan:'kanji',voc:'vocabulary'};
  341. var arr = {}, value;
  342. if (typeof filter_value === 'string') filter_value = split_list(filter_value);
  343. if (typeof filter_value !== 'object') return {};
  344. if (Array.isArray(filter_value)) {
  345. for (var idx in filter_value) {
  346. value = filter_value[idx];
  347. value = xlat[value] || value;
  348. arr[value] = true;
  349. }
  350. } else {
  351. for (value in filter_value) {
  352. arr[xlat[value] || value] = (filter_value[value] === true);
  353. }
  354. }
  355. return arr;
  356. }
  357.  
  358. //------------------------------
  359. // Given an array of srs criteria (e.g. ['mast', 'enli', 'burn']), return an
  360. // array containing 'true' for each srs level contained in the criteria.
  361. //------------------------------
  362. function srs_to_arr(filter_value) {
  363. var index = ['init','appr1','appr2','appr3','appr4','guru1','guru2','mast','enli','burn'];
  364. var arr = [], value;
  365. if (typeof filter_value === 'string') filter_value = split_list(filter_value);
  366. if (typeof filter_value !== 'object') return {};
  367. if (Array.isArray(filter_value)) {
  368. for (var idx in filter_value) {
  369. value = Number(filter_value[idx]);
  370. if (isNaN(value)) value = index.indexOf(filter_value[idx]);
  371. arr[value] = true;
  372. }
  373. } else {
  374. for (value in filter_value) {
  375. arr[index.indexOf(value)] = (filter_value[value] === true);
  376. }
  377. }
  378. return arr;
  379. }
  380.  
  381. //------------------------------
  382. // Given an level criteria string (e.g. '1..3,5,8'), return an array containing
  383. // 'true' for each level contained in the criteria.
  384. //------------------------------
  385. function levels_to_arr(filter_value) {
  386. var levels = [], crit_idx, start, stop, lvl;
  387.  
  388. // Process each comma-separated criteria separately.
  389. var criteria = filter_value.split(',');
  390. for (crit_idx = 0; crit_idx < criteria.length; crit_idx++) {
  391. var crit = criteria[crit_idx];
  392. var value = true;
  393.  
  394. // Match '*' = all levels
  395. var match = crit.match(/^\s*(\*)\s*$/);
  396. if (match !== null) {
  397. start = to_num('1');
  398. stop = to_num('9999'); // All levels
  399. for (lvl = start; lvl <= stop; lvl++)
  400. levels[lvl] = value;
  401. continue;
  402. }
  403.  
  404. // Match 'a..b' = range of levels (or exclude if preceded by '!')
  405. match = crit.match(/^\s*(\!?)\s*((\+|-)?\d+)\s*(-|\.\.\.?|to)\s*((\+|-)?\d+)\s*$/);
  406. if (match !== null) {
  407. start = to_num(match[2]);
  408. stop = to_num(match[5]);
  409. if (match[1] === '!') value = false;
  410. for (lvl = start; lvl <= stop; lvl++)
  411. levels[lvl] = value;
  412. continue;
  413. }
  414.  
  415. // Match 'a' = specific level (or exclude if preceded by '!')
  416. match = crit.match(/^\s*(\!?)\s*((\+|-)?\d+)\s*$/);
  417. if (match !== null) {
  418. lvl = to_num(match[2]);
  419. if (match[1] === '!') value = false;
  420. levels[lvl] = value;
  421. continue;
  422. }
  423. var err = 'wkof.ItemData::levels_to_arr() - Bad filter criteria "'+filter_value+'"';
  424. console.log(err);
  425. throw err;
  426. }
  427. return levels;
  428.  
  429. //============
  430. function to_num(num) {
  431. num = (num[0] < '0' ? wkof.user.level : 0) + Number(num)
  432. return Math.min(Math.max(1, num), wkof.user.max_level_granted_by_subscription);
  433. }
  434. }
  435.  
  436. var registration_promise;
  437. var registration_timeout;
  438. var registration_counter = 0;
  439. //------------------------------
  440. // Ask clients to add items to the registry.
  441. //------------------------------
  442. function call_for_registration() {
  443. registration_promise = promise();
  444. wkof.set_state('wkof.ItemData.registry', 'ready');
  445. setTimeout(check_registration_counter, 1);
  446. registration_timeout = setTimeout(function(){
  447. registration_timeout = undefined;
  448. check_registration_counter(true /* force_ready */);
  449. }, 3000);
  450. return registration_promise;
  451. }
  452.  
  453. //------------------------------
  454. // Request to pause the 'ready' event.
  455. //------------------------------
  456. function pause_ready_event(value) {
  457. if (value === true) {
  458. registration_counter++;
  459. } else {
  460. registration_counter--;
  461. check_registration_counter();
  462. }
  463. }
  464.  
  465. //------------------------------
  466. // If registration is complete or timed out, mark it as resolved.
  467. //------------------------------
  468. function check_registration_counter(force_ready) {
  469. if (!force_ready && registration_counter > 0) return false;
  470. if (registration_timeout !== undefined) clearTimeout(registration_timeout);
  471. registration_promise.resolve();
  472. return true;
  473. }
  474.  
  475. //------------------------------
  476. // Notify listeners that we are ready.
  477. //------------------------------
  478. function notify_ready() {
  479. // Delay guarantees include() callbacks are called before ready() callbacks.
  480. setTimeout(function(){wkof.set_state('wkof.ItemData', 'ready');},0);
  481. }
  482. wkof.include('Apiv2');
  483. wkof.ready('Apiv2').then(call_for_registration).then(notify_ready);
  484.  
  485. })(this);