Wanikani Open Framework - ItemData module

ItemData module for Wanikani Open Framework

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

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/38580/265422/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.4
  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. };
  25. //########################################################################
  26.  
  27. function promise(){var a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
  28. function split_list(str) {return str.replace(/^\s+|\s*(,)\s*|\s+$/g, '$1').split(',').filter(function(name) {return (name.length > 0);});}
  29.  
  30. //------------------------------
  31. // Get the items specified by the configuration.
  32. //------------------------------
  33. function get_items(config, global_options) {
  34. // Default to WK 'subjects' only.
  35. if (!config) config = {wk_items:{}};
  36.  
  37. // Allow comma-separated list of WK-only endpoints.
  38. if (typeof config === 'string') {
  39. var endpoints = split_list(config)
  40. var config = {wk_items:{options:{}}};
  41. for (var idx in endpoints)
  42. config.wk_items.options[endpoints[idx]] = true;
  43. }
  44.  
  45. // Fetch the requested endpoints.
  46. var fetch_promise = promise();
  47. var items = [];
  48. var remaining = 0;
  49. for (var cfg_name in config) {
  50. var cfg = config[cfg_name];
  51. var spec = wkof.ItemData.registry.sources[cfg_name];
  52. if (!spec || typeof spec.fetcher !== 'function') {
  53. console.log('wkof.ItemData.get_items() - Config "'+cfg_name+'" not registered!');
  54. continue;
  55. }
  56. remaining++;
  57. spec.fetcher(cfg, global_options)
  58. .then(function(data){
  59. var filter_promise;
  60. if (typeof spec === 'object')
  61. filter_promise = apply_filters(data, cfg, spec);
  62. else
  63. filter_promise = Promise.resolve(data);
  64. filter_promise.then(function(data){
  65. items = items.concat(data);
  66. remaining--;
  67. if (!remaining) fetch_promise.resolve(items);
  68. });
  69. })
  70. .catch(function(e){
  71. if (e) throw e;
  72. console.log('wkof.ItemData.get_items() - Failed for config "'+cfg_name+'"');
  73. remaining--;
  74. if (!remaining) fetch_promise.resolve(items);
  75. });
  76. }
  77. if (remaining === 0) fetch_promise.resolve(items);
  78. return fetch_promise;
  79. }
  80.  
  81. //------------------------------
  82. // Get the wk_items specified by the configuration.
  83. //------------------------------
  84. function get_wk_items(config, options) {
  85. var cfg_options = config.options || {};
  86. options = options || {};
  87. var now = new Date().getTime();
  88.  
  89. // Endpoints that we can fetch (subjects MUST BE FIRST!!)
  90. var available_endpoints = ['subjects','assignments','review_statistics','study_materials'];
  91. var spec = wkof.ItemData.registry.sources.wk_items;
  92. for (var filter_name in config.filters) {
  93. var filter_spec = spec.filters[filter_name];
  94. if (!filter_spec || typeof filter_spec.set_options !== 'function') continue;
  95. var filter_cfg = config.filters[filter_name];
  96. filter_spec.set_options(cfg_options, filter_cfg.value);
  97. }
  98.  
  99. // Fetch all of the endpoints
  100. var ep_promises = [];
  101. for (var idx in available_endpoints) {
  102. var ep_name = available_endpoints[idx];
  103. if (ep_name === 'subjects' || cfg_options[ep_name] === true)
  104. ep_promises.push(
  105. wkof.Apiv2.get_endpoint(ep_name, options)
  106. .then(process_data.bind(null, ep_name))
  107. );
  108. }
  109. return Promise.all(ep_promises)
  110. .then(function(all_data){
  111. return all_data[0];
  112. });
  113.  
  114. //============
  115. function process_data(ep_name, ep_data) {
  116. if (ep_name === 'subjects') return ep_data;
  117. // Merge with 'subjects' when 'subjects' is done fetching.
  118. return ep_promises[0].then(cross_link.bind(null, ep_name, ep_data));
  119. }
  120.  
  121. //============
  122. function cross_link(ep_name, ep_data, subjects) {
  123. for (var id in ep_data) {
  124. var record = ep_data[id];
  125. var subject_id = record.data.subject_id;
  126. subjects[subject_id][ep_name] = record.data;
  127. }
  128. }
  129. }
  130.  
  131. //------------------------------
  132. // Filter the items array according to the specified filters and options.
  133. //------------------------------
  134. function apply_filters(items, config, spec) {
  135. var prep_promises = [];
  136. var options = config.options || {};
  137. var filters = [];
  138. for (var filter_name in config.filters) {
  139. var filter_cfg = config.filters[filter_name];
  140. if (typeof filter_cfg !== 'object' || filter_cfg.value === undefined)
  141. filter_cfg = {value:filter_cfg};
  142. var filter_value = filter_cfg.value;
  143. var filter_spec = spec.filters[filter_name];
  144. if (filter_spec === undefined) throw new Error('wkof.ItemData.get_item() - Invalid filter "'+filter_name+'"');
  145. if (typeof filter_spec.filter_value_map === 'function')
  146. filter_value = filter_spec.filter_value_map(filter_cfg.value);
  147. if (typeof filter_spec.prepare === 'function') {
  148. var result = filter_spec.prepare(filter_value);
  149. if (result instanceof Promise) prep_promises.push(result);
  150. }
  151. filters.push({
  152. name: filter_name,
  153. func: filter_spec.filter_func,
  154. filter_value: filter_value,
  155. invert: (filter_cfg.invert === true)
  156. });
  157. }
  158.  
  159. return Promise.all(prep_promises).then(function(){
  160. var result = [];
  161. for (var item_idx in items) {
  162. var keep = true;
  163. var item = items[item_idx];
  164. for (var filter_idx in filters) {
  165. var filter = filters[filter_idx];
  166. try {
  167. keep = filter.func(filter.filter_value, item);
  168. if (filter.invert) keep = !keep;
  169. if (!keep) break;
  170. } catch(e) {
  171. keep = false;
  172. break;
  173. }
  174. }
  175. if (keep) result.push(item);
  176. }
  177. return result;
  178. });
  179. }
  180.  
  181. //------------------------------
  182. // Return the items indexed by an indexing function.
  183. //------------------------------
  184. function get_index(items, index_name) {
  185. var index_func = wkof.ItemData.registry.indices[index_name];
  186. if (typeof index_func !== 'function') throw new Error('wkof.ItemData.index_by() - Invalid index function "'+index_name+'"');
  187. return index_func(items);
  188. }
  189.  
  190. //------------------------------
  191. // Register wk_items data source.
  192. //------------------------------
  193. wkof.ItemData.registry.sources['wk_items'] = {
  194. description: 'Wanikani',
  195. fetcher: get_wk_items,
  196. options: {
  197. assignments: {
  198. type: 'checkbox',
  199. label: 'Assignments',
  200. default: false,
  201. hover_tip: 'Include the "/assignments" endpoint (SRS status, burn status, progress dates)'
  202. },
  203. review_statistics: {
  204. type: 'checkbox',
  205. label: 'Review Statistics',
  206. default: false,
  207. hover_tip: 'Include the "/review_statistics" endpoint:\n * Per-item review count\n *Correct/incorrect count\n * Longest streak'
  208. },
  209. study_materials: {
  210. type: 'checkbox',
  211. label: 'Study Materials',
  212. default: false,
  213. hover_tip: 'Include the "/study_materials" endpoint:\n * User synonyms\n * User notes'
  214. },
  215. },
  216. filters: {
  217. item_type: {
  218. type: 'multi',
  219. label: 'Item type',
  220. content: {radical:'Radicals',kanji:'Kanji',vocabulary:'Vocabulary'},
  221. default: [],
  222. filter_value_map: item_type_to_arr,
  223. filter_func: function(filter_value, item){return filter_value[item.object] === true;},
  224. hover_tip: 'Filter by item type (radical, kanji, vocabulary)',
  225. },
  226. level: {
  227. type: 'text',
  228. label: 'Level',
  229. placeholder: '(e.g. "1-3,5")',
  230. default: '',
  231. filter_value_map: levels_to_arr,
  232. filter_func: function(filter_value, item){return filter_value[item.data.level] === true;},
  233. 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)',
  234. },
  235. srs: {
  236. type: 'multi',
  237. label: 'SRS Level',
  238. content: {appr1:'Apprentice 1',appr2:'Apprentice 2',appr3:'Apprentice 3',appr4:'Apprentice 4',guru1:'Guru 1',guru2:'Guru 2',mast:'Master',enli:'Enlightened',burn:'Burned'},
  239. default: [],
  240. set_options: function(options){options.assignments = true;},
  241. filter_value_map: srs_to_arr,
  242. filter_func: function(filter_value, item){return filter_value[item.assignments.srs_stage] === true;},
  243. hover_tip: 'Filter by SRS level (Apprentice 1, Apprentice 2, ..., Burn)',
  244. },
  245. have_burned: {
  246. type: 'checkbox',
  247. label: 'Have burned',
  248. default: true,
  249. set_options: function(options){options.assignments = true;},
  250. filter_func: function(filter_value, item){return (item.assignments.burned_at !== null) === filter_value;},
  251. 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',
  252. },
  253. }
  254. };
  255.  
  256. //------------------------------
  257. // Macro to build a function to index by a specific field.
  258. // Set make_subarrays to true if more than one item can share the same field value (e.g. same item_type).
  259. //------------------------------
  260. function make_index_func(name, field, entry_type) {
  261. var fn = '';
  262. fn +=
  263. 'var index = {}, value;\n'+
  264. 'for (var idx in items) {\n'+
  265. ' var item = items[idx];\n'+
  266. ' try {\n'+
  267. ' value = '+field+';\n'+
  268. ' } catch(e) {continue;}\n'+
  269. ' if (value === null || value === undefined) continue;\n';
  270. if (entry_type === 'array') {
  271. fn +=
  272. ' if (index[value] === undefined) {\n'+
  273. ' index[value] = [item];\n'+
  274. ' continue;\n'+
  275. ' }\n';
  276. } else {
  277. fn +=
  278. ' if (index[value] === undefined) {\n'+
  279. ' index[value] = item;\n'+
  280. ' continue;\n'+
  281. ' }\n';
  282. if (entry_type === 'single_or_array') {
  283. fn +=
  284. ' if (!Array.isArray(index[value]))\n'+
  285. ' index[value] = [index[value]];\n';
  286. }
  287. }
  288. fn +=
  289. ' index[value].push(item);\n'+
  290. '}\n'+
  291. 'return index;'
  292. wkof.ItemData.registry.indices[name] = new Function('items', fn);
  293. }
  294.  
  295. // Build some index functions.
  296. make_index_func('item_type', 'item.object', 'array');
  297. make_index_func('level', 'item.data.level', 'array');
  298. make_index_func('slug', 'item.data.slug', 'single_or_array');
  299. make_index_func('srs_stage', 'item.assignments.srs_stage', 'array');
  300. make_index_func('srs_stage_name', 'item.assignments.srs_stage_name', 'array');
  301. make_index_func('subject_id', 'item.id', 'single');
  302.  
  303. //------------------------------
  304. // Index by reading
  305. //------------------------------
  306. wkof.ItemData.registry.indices['reading'] = function(items) {
  307. var index = {};
  308. for (var idx in items) {
  309. var item = items[idx];
  310. if (!item.hasOwnProperty('data') || !item.data.hasOwnProperty('readings')) continue;
  311. if (!Array.isArray(item.data.readings)) continue;
  312. var readings = item.data.readings;
  313. for (var idx2 in readings) {
  314. var reading = readings[idx2].reading;
  315. if (reading === 'None') continue;
  316. if (!index[reading]) index[reading] = [];
  317. index[reading].push(item);
  318. }
  319. }
  320. return index;
  321. }
  322.  
  323. //------------------------------
  324. // Given an array of item type criteria (e.g. ['rad', 'kan', 'voc']), return
  325. // an array containing 'true' for each item type contained in the criteria.
  326. //------------------------------
  327. function item_type_to_arr(filter_value) {
  328. var xlat = {rad:'radical',kan:'kanji',voc:'vocabulary'};
  329. var arr = {}, value;
  330. if (typeof filter_value === 'string') filter_value = split_list(filter_value);
  331. if (typeof filter_value !== 'object') return {};
  332. if (Array.isArray(filter_value)) {
  333. for (var idx in filter_value) {
  334. value = filter_value[idx];
  335. value = xlat[value] || value;
  336. arr[value] = true;
  337. }
  338. } else {
  339. for (value in filter_value) {
  340. arr[xlat[value] || value] = (filter_value[value] === true);
  341. }
  342. }
  343. return arr;
  344. }
  345.  
  346. //------------------------------
  347. // Given an array of srs criteria (e.g. ['mast', 'enli', 'burn']), return an
  348. // array containing 'true' for each srs level contained in the criteria.
  349. //------------------------------
  350. function srs_to_arr(filter_value) {
  351. var index = ['init','appr1','appr2','appr3','appr4','guru1','guru2','mast','enli','burn'];
  352. var arr = [], value;
  353. if (typeof filter_value === 'string') filter_value = split_list(filter_value);
  354. if (typeof filter_value !== 'object') return {};
  355. if (Array.isArray(filter_value)) {
  356. for (var idx in filter_value) {
  357. value = Number(filter_value[idx]);
  358. if (isNaN(value)) value = index.indexOf(filter_value[idx]);
  359. arr[value] = true;
  360. }
  361. } else {
  362. for (value in filter_value) {
  363. arr[index.indexOf(value)] = (filter_value[value] === true);
  364. }
  365. }
  366. return arr;
  367. }
  368.  
  369. //------------------------------
  370. // Given an level criteria string (e.g. '1-3,5,8'), return an array containing
  371. // 'true' for each level contained in the criteria.
  372. //------------------------------
  373. function levels_to_arr(filter_value) {
  374. var levels = [], crit_idx, start, stop, lvl;
  375.  
  376. // Process each comma-separated criteria separately.
  377. var criteria = filter_value.split(',');
  378. for (crit_idx = 0; crit_idx < criteria.length; crit_idx++) {
  379. var crit = criteria[crit_idx];
  380. var value = true;
  381.  
  382. // Match '*' = all levels
  383. var match = crit.match(/^\s*(\*)\s*$/);
  384. if (match !== null) {
  385. start = to_num('1');
  386. stop = to_num('9999'); // All levels
  387. for (lvl = start; lvl <= stop; lvl++)
  388. levels[lvl] = value;
  389. continue;
  390. }
  391.  
  392. // Match 'a-b' = range of levels (or exclude if preceded by '!')
  393. match = crit.match(/^\s*(\!?)\s*((\+|-)?\d+)\s*-\s*((\+|-)?\d+)\s*$/);
  394. if (match !== null) {
  395. start = to_num(match[2]);
  396. stop = to_num(match[4]);
  397. if (match[1] === '!') value = false;
  398. for (lvl = start; lvl <= stop; lvl++)
  399. levels[lvl] = value;
  400. continue;
  401. }
  402.  
  403. // Match 'a' = specific level (or exclude if preceded by '!')
  404. match = crit.match(/^\s*(\!?)\s*((\+|-)?\d+)\s*$/);
  405. if (match !== null) {
  406. lvl = to_num(match[2]);
  407. if (match[1] === '!') value = false;
  408. levels[lvl] = value;
  409. continue;
  410. }
  411. var err = 'wkof.ItemData::levels_to_arr() - Bad filter criteria "'+filter_value+'"';
  412. console.log(err);
  413. throw err;
  414. }
  415. return levels;
  416.  
  417. //============
  418. function to_num(num) {
  419. num = (num[0] < '0' ? wkof.user.level : 0) + Number(num)
  420. return Math.min(Math.max(1, num), wkof.user.max_level_granted_by_subscription);
  421. }
  422. }
  423.  
  424. //------------------------------
  425. // Notify listeners that we are ready.
  426. //------------------------------
  427. function notify_ready() {
  428. // Delay guarantees include() callbacks are called before ready() callbacks.
  429. setTimeout(function(){wkof.set_state('wkof.ItemData', 'ready');},0);
  430. }
  431. wkof.include('Apiv2');
  432. wkof.ready('Apiv2').then(notify_ready);
  433.  
  434. })(this);