Wanikani Open Framework - ItemData module

ItemData module for Wanikani Open Framework

当前为 2018-02-22 提交的版本,查看 最新版本

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