UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

813 lines (748 loc) 26 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): cache * * @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。 * * TODO:<code> </code> * * @since 2020/5/24 6:21:13 拆分自 CeL.application.net.wiki */ // More examples: see /_test suite/test.js // Wikipedia bots demo: https://github.com/kanasimi/wikibot 'use strict'; // 'use asm'; // -------------------------------------------------------------------------------------------- // 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。 typeof CeL === 'function' && CeL.run({ // module name name : 'application.net.wiki.cache', require : 'data.native.' // for library_namespace.get_URL + '|application.net.Ajax.' + '|application.net.wiki.' // load MediaWiki module basic functions + '|application.net.wiki.namespace.', // 設定不匯出的子函式。 no_extend : '*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION; // -------------------------------------------------------------------------------------------- /** {Object|Function}fs in node.js */ var node_fs; try { if (library_namespace.platform.nodejs) // @see https://nodejs.org/api/fs.html node_fs = require('fs'); if (typeof node_fs.readFile !== 'function') throw true; } catch (e) { // enumerate for wiki_API.cache // 模擬 node.js 之 fs,以達成最起碼的效果(即無 cache 功能的情況)。 library_namespace.warn(this.id + ': 無 node.js 之 fs,因此不具備 cache 或 SQL 功能。'); node_fs = { // library_namespace.storage.read_file() readFile : function(file_path, options, callback) { library_namespace.error('Cannot read file ' + file_path); if (typeof callback === 'function') callback(true); }, // library_namespace.storage.write_file() writeFile : function(file_path, data, options, callback) { library_namespace.error('Cannot write to file ' + file_path); if (typeof options === 'function' && !callback) callback = options; if (typeof callback === 'function') callback(true); } }; } // -------------------------------------------------------------------------------------------- /** * cache 相關函數: * * @see application.storage.file.get_cache_file * application.OS.Windows.file.cacher * application.net.Ajax.get_URL_cache<br /> * application.net.wiki<br /> * wiki_API.cache() CeL.wiki.cache() */ if (false) { // examples CeL.wiki.cache({ type : 'page', file_name : 'file_name', list : 'Wikipedia:Sandbox', operator : function(data) { console.log(data); } }, function callback(data) { console.log(data); }, { // default options === this // namespace : '0|1', // [KEY_SESSION] // session : wiki, // title_prefix : 'Template:', // cache path prefix prefix : 'base_directory/' }); CeL.set_debug(6); CeL.wiki.cache({ type : 'callback', file_name : 'file_name', list : function(callback) { callback([ 1, 2, 3 ]); }, operator : function(data) { console.log(data); } }, function callback(data) { console.log(data); }, { // default options === this // namespace : '0|1', // [KEY_SESSION] // session : wiki, // title_prefix : 'Template:', // cache path prefix prefix : './' }); CeL.set_debug(6); var wiki = Wiki(true); CeL.wiki.cache({ type : 'wdq', file_name : 'countries', list : 'claim[31:6256]', operator : function(list) { // console.log(list); result = list; } }, function callback(list) { // console.log(list); }, { // default options === this // namespace : '0|1', // [KEY_SESSION] session : wiki, // title_prefix : 'Template:', // cache path prefix prefix : './' }); } /** * cache 作業操作之輔助套裝函數。 * * 注意: only for node.js. 必須自行 include 'application.platform.nodejs'。 <code> CeL.run('application.platform.nodejs'); * </code><br /> * 注意: 需要自行先創建各 type 之次目錄,如 page, redirects, embeddedin, ...<br /> * 注意: 會改變 operation, _this! Warning: will modify operation, _this! * * 連續作業: 依照 _this 設定 {Object}default options,即傳遞於各 operator 間的 ((this))。<br /> * 依照 operation 順序個別執行單一項作業。 * * 單一項作業流程:<br /> * 設定檔名。<br /> * 若不存在此檔,則:<br /> * >>> 依照 operation.type 與 operation.list 取得資料。<br /> * >>> 若 Array.isArray(operation.list) 則處理多項列表作業:<br /> * >>>>>> 個別處理單一項作業,每次執行 operation.each() || operation.each_retrieve()。<br /> * >>> 執行 data = operation.retrieve(data),以其回傳作為將要 cache 之 data。<br /> * >>> 寫入cache。<br /> * 執行 operation.operator(data) * * TODO: file_stream<br /> * TODO: do not write file * * @param {Object|Array}operation * 作業設定。 * @param {Function}[callback] * 所有作業(operation)執行完後之回調函數。 callback(response data) * @param {Object}[_this] * 傳遞於各 operator 間的 ((this))。注意: 會被本函數更動! */ function wiki_API_cache(operation, callback, _this) { if (library_namespace.is_Object(callback) && !_this) { // 未設定/不設定 callback // shift arguments. _this = callback; callback = undefined; } var index = 0; /** * 連續作業時,轉到下一作業。 * * node.js v0.11.16: In strict mode code, functions can only be declared * at top level or immediately within another function. */ function next_operator(data) { library_namespace.debug('處理連續作業序列,轉到下一作業: ' + (index + 1) + '/' + operation.length, 2, 'wiki_API_cache.next_operator'); // [ {Object}operation, {Object}operation, ... ] // operation = { type:'embeddedin', operator:function(data) } if (index < operation.length) { var this_operation = operation[index++]; // console.log(this_operation); if (!this_operation) { // Allow null operation. library_namespace.debug('未設定 operation[' + (index - 1) + ']。Skip this operation.', 1, 'wiki_API_cache.next_operator'); next_operator(data); } else { if (!('list' in this_operation)) { // use previous data as list. library_namespace.debug( '未特別指定 list,以前一次之回傳 data 作為 list。', 3, 'wiki_API_cache.next_operator'); library_namespace.debug('前一次之回傳 data: ' + (data && JSON.stringify(data).slice(0, 180)) + '...', 3, 'wiki_API_cache.next_operator'); this_operation.list = data; } if (data) { library_namespace.debug('設定 .last_data_got: ' + (data && JSON.stringify(data).slice(0, 180)) + '...', 3, 'wiki_API_cache.next_operator'); this_operation.last_data_got = data; } // default options === _this: 傳遞於各 operator 間的 ((this))。 wiki_API_cache(this_operation, next_operator, _this); } } else if (typeof callback === 'function') { if (false && Array.isArray(data)) { // TODO: adapt to {Object}operation library_namespace.log('wiki_API_cache: Get ' + data.length + ' page(s).'); // 自訂list // data = [ '' ]; if (_this.limit >= 0) { // 設定此初始值,可跳過之前已經處理過的。 data = data.slice(0 * _this.limit, 1 * _this.limit); } library_namespace.debug(data.slice(0, 8).map( wiki_API.title_of).join('\n') + '\n...'); } // last 收尾 callback.call(_this, data); } } if (Array.isArray(operation)) { next_operator(); return; } // ---------------------------------------------------- /** * 以下為處理單一次作業。 */ library_namespace.debug('處理單一次作業。', 2, 'wiki_API_cache'); library_namespace.debug( 'using operation: ' + JSON.stringify(operation), 6, 'wiki_API_cache'); if (typeof _this !== 'object') { // _this: 傳遞於各 operator 間的 ((this))。 _this = Object.create(null); } var file_name = operation.file_name, /** 前一次之回傳 data。每次產出的 data。 */ last_data_got = operation.last_data_got; if (typeof file_name === 'function') { // @see wiki_API_cache.title_only file_name = file_name.call(_this, last_data_got, operation); } var /** {String}method to get data */ type = operation.type, /** {Boolean}是否自動嘗試建立目錄。 */ try_mkdir = typeof library_namespace.fs_mkdir === 'function' && operation.mkdir, // operator = typeof operation.operator === 'function' && operation.operator, // list = operation.list; if (!file_name) { // 若自行設定了檔名,則慢點執行 list(),先讀讀 cache。因為 list() 可能會頗耗時間。 // 基本上,設定 this.* 應該在 operation.operator() 中,而不是在 operation.list() 中。 if (typeof list === 'function') { // TODO: 允許非同步方法。 list = list.call(_this, last_data_got, operation); } if (!operation.postfix) { if (type === 'file') operation.postfix = '.txt'; else if (type === 'URL') operation.postfix = '.htm'; } // 自行設定之檔名 operation.file_name 優先度較 type/title 高。 // 需要自行創建目錄! file_name = _this[type + '_prefix'] || type; file_name = [ file_name // treat file_name as directory ? /[\\\/]/.test(file_name) ? file_name : file_name + '/' : '', // wiki_API.is_page_data(list) ? list.title // 若 Array.isArray(list),則 ((file_name = ''))。 : typeof list === 'string' && wiki_API.normalize_title(list, true) ]; if (file_name[1]) { file_name = file_name[0] // 正規化檔名。 + file_name[1].replace(/\//g, '_'); } else { // assert: node_fs.readFile('') 將執行 callback(error) file_name = ''; } } if (file_name) { if (!('postfix' in operation) && !('postfix' in _this) && /\.[a-z\d\-]+$/i.test(file_name)) { // 若已設定 filename extension,則不自動添加。 operation.postfix = ''; } file_name = [ 'prefix' in operation ? operation.prefix // _this.prefix: cache path prefix : 'prefix' in _this // ? _this.prefix : wiki_API_cache.prefix, file_name, // auto detect filename extension 'postfix' in operation ? operation.postfix // : 'postfix' in _this ? _this.postfix : wiki_API_cache.postfix ]; library_namespace.debug('Pre-normalized cache file name: [' + file_name + ']', 5, 'wiki_API_cache'); if (false) library_namespace.debug('file name param:' + [ operation.file_name, _this[type + '_prefix'], type, JSON.stringify(list) ].join(';'), 6, 'wiki_API_cache'); // 正規化檔名。 file_name = file_name.join('').replace(/[:*?<>]/g, '_'); } library_namespace.debug('Try to read cache file: [' + file_name + ']', 3, 'wiki_API_cache'); var /** * 採用 JSON<br /> * TODO: parse & stringify 機制 * * @type {Boolean} */ using_JSON = 'json' in operation ? operation.json : /\.json$/i .test(file_name), /** {String}file encoding for fs of node.js. */ encoding = _this.encoding || wiki_API.encoding; // list file path _this.file_name = file_name; // console.log('Read file: ' + file_name); node_fs.readFile(file_name, encoding, function(error, data) { /** * 結束作業。 */ function finish_work(data) { library_namespace.debug('finish work', 3, 'wiki_API_cache.finish_work'); last_data_got = data; if (operator) operator.call(_this, data, operation); library_namespace.debug('loading callback', 3, 'wiki_API_cache.finish_work'); if (typeof callback === 'function') callback.call(_this, data); } if (!operation.reget && !error && (data || // 當資料 Invalid,例如採用 JSON 卻獲得空資料時;則視為 error,不接受此資料。 ('accept_empty_data' in _this // ? _this.accept_empty_data : !using_JSON))) { // gettext_config:{"id":"using-cached-data"} library_namespace.debug('Using cached data.', 3, 'wiki_API_cache'); library_namespace.debug('Cached data: [' + (data && data.slice(0, 200)) + ']...', 5, 'wiki_API_cache'); if (using_JSON && data) { try { data = JSON.parse(data); } catch (e) { library_namespace.error( // error. e.g., "undefined" 'wiki_API_cache: Cannot parse as JSON: ' + data); // 注意: 若中途 abort,此時可能需要手動刪除大小為 0 的 cache file! data = undefined; } } finish_work(data); return; } library_namespace.debug( operation.reget ? 'Dispose cache. Reget again.' // ↑ operation.reget: 放棄 cache,重新取得資料。 : 'No valid cached data. Try to get data...', 3, 'wiki_API_cache'); /** * 寫入 cache 至檔案系統。 */ function write_cache(data) { if (operation.cache === false) { // 當設定 operation.cache: false 時,不寫入 cache。 library_namespace.debug( '設定 operation.cache === false,不寫入 cache。', 3, 'wiki_API_cache.write_cache'); } else if (/[^\\\/]$/.test(file_name)) { library_namespace.info('wiki_API_cache: ' + 'Write cache data to [' + file_name + '].' + (using_JSON ? ' (using JSON)' : '')); library_namespace.debug('Cache data: ' + (data && JSON.stringify(data).slice(0, 190)) + '...', 3, 'wiki_API_cache.write_cache'); var write = function() { // 為了預防需要建立目錄,影響到後面的作業, // 因此採用 fs.writeFileSync() 而非 fs.writeFile()。 node_fs.writeFileSync(file_name, using_JSON ? JSON .stringify(data) : data, encoding); }; try { write(); } catch (error) { // assert: 此 error.code 表示上層目錄不存在。 var matched = error.code === 'ENOENT' // 未設定 operation.mkdir 的話,預設會自動嘗試建立目錄。 && try_mkdir !== false // && file_name.match(/[\\\/][^\\\/]+$/); if (matched) { // 僅測試一次。設定 "已嘗試過" flag。 try_mkdir = false; // create parent directory library_namespace.fs_mkdir(file_name.slice(0, matched.index)); // re-write file again. try { write(); } catch (e) { library_namespace.error( // 'wiki_API_cache: Error to write cache data!'); library_namespace.error(e); } } } } finish_work(data); } // node.js v0.11.16: In strict mode code, functions can only be // declared // at top level or immediately within another function. /** * 取得並處理下一項 data。 */ function get_next_item(data) { if (index < list.length) { // 利用基本相同的參數以取得 cache。 _operation.list = list[index++]; var message = '處理多項列表作業: ' + type + ' ' + index + '/' + list.length; if (list.length > 8) { library_namespace.info('wiki_API_cache.get_next_item: ' + message); } else { library_namespace.debug(message, 1, 'wiki_API_cache.get_next_item'); } wiki_API_cache(_operation, get_next_item, _this); } else { // last 收尾 // All got. retrieve data. if (_operation.data_list) data = _operation.data_list; if (typeof operation.retrieve === 'function') data = operation.retrieve.call(_this, data); write_cache(data); } } if (typeof list === 'function' && type !== 'callback') { library_namespace.debug('Call .list()', 3, 'wiki_API_cache'); list = list.call(_this, last_data_got, operation); // 對於 .list() 為 asynchronous 函數的處理。 if (list === wiki_API_cache.abort) { library_namespace.debug('It seems the .list()' + ' is an asynchronous function.' + ' I will exit' + ' and wait for the .list() finished.', 3, 'wiki_API_cache'); return; } } if (list === wiki_API_cache.abort) { library_namespace .debug('Abort operation.', 1, 'wiki_API_cache'); finish_work(); return; } if (Array.isArray(list)) { if (!type) { library_namespace.debug('採用 list (length ' + list.length + ') 作為 data。', 1, 'wiki_API_cache'); write_cache(list); return; } if (list.length > 1e6) { library_namespace.warn( // 'wiki_API_cache: 警告: list 過長/超過限度 (length ' + list.length + '),將過於耗時而不實際!'); } /** * 處理多項列表作業。 */ var index = 0, _operation = Object.clone(operation); // 個別頁面不設定 .file_name, .end。 delete _operation.end; if (_operation.each_file_name) { _operation.file_name = _operation.each_file_name; delete _operation.each_file_name; } else { delete _operation.file_name; } if (typeof _operation.each === 'function') { // 每一項 list 之項目執行一次 .each()。 _operation.operator = _operation.each; delete _operation.each; } else { if (typeof _operation.each_retrieve === 'function') _operation.each_retrieve = _operation.each_retrieve .bind(_this); else delete _operation.each_retrieve; /** * 預設處理列表的函數。 */ _operation.operator = function(data) { if ('each_retrieve' in operation) // 資料事後處理程序 (post-processor): // 將以 .each_retrieve() 的回傳作為要處理的資料。 data = operation.each_retrieve.call(_this, data); if (_operation.data_list) { if (Array.isArray(data)) Array.prototype.push.apply( _operation.data_list, data); else if (data) _operation.data_list.push(data); } else { if (Array.isArray(data)) _operation.data_list = data; else if (data) _operation.data_list = [ data ]; } }; } library_namespace.debug('處理多項列表作業, using operation: ' + JSON.stringify(_operation), 5, 'wiki_API_cache'); get_next_item(); return; } // ------------------------------------------------ /** * 以下為處理單一項作業。 */ var to_get_data, list_type; if (// type in get_list.type wiki_API.list.type_list.includes(type)) { list_type = type; type = 'list'; } switch (type) { case 'callback': if (typeof list !== 'function') { library_namespace .warn('wiki_API_cache: list is not function!'); callback.call(_this, last_data_got); break; } // 手動取得資料。使用 list=function(callback){callback(list);} to_get_data = function(list, callback) { library_namespace.log('wiki_API_cache: ' + 'manually get data and then callback(list).'); if (typeof list === 'function') { // assert: (typeof list === 'function') 必須自己回 call! list.call(_this, callback, last_data_got, operation); } }; break; case 'file': // 一般不應用到。 // get file 內容。 to_get_data = function(file_path, callback) { library_namespace.log('wiki_API_cache: Get file [' + file_path + '].'); node_fs.readFile(file_path, operation.encoding, function( error, data) { if (error) library_namespace.error( // 'wiki_API_cache: Error get file [' + file_path + ']: ' + error); callback.call(_this, data); }); }; break; case 'URL': // get URL 頁面內容。 to_get_data = function(URL, callback) { library_namespace.log('wiki_API_cache: Get URL of [' + URL + '].'); library_namespace.get_URL(URL, callback); }; break; case 'wdq': to_get_data = function(query, callback) { if (_this[KEY_SESSION]) { if (!_this[KEY_SESSION].data_session) { _this[KEY_SESSION].set_data(); _this[KEY_SESSION].run(function() { // retry again to_get_data(query, callback); }); return; } operation[KEY_SESSION] // = _this[KEY_SESSION].data_session; } library_namespace.log('wiki_API_cache: Wikidata Query [' + query + '].'); // wikidata_query(query, callback, options) wiki_API.wdq(query, callback, operation); }; break; case 'page': // get page contents 頁面內容。 // title=(operation.title_prefix||_this.title_prefix)+operation.list to_get_data = function(title, callback) { library_namespace.log('wiki_API_cache: Get content of ' + wiki_API.title_link_of(title)); // 防止汙染。 var _options = library_namespace.new_options(_this, operation); // 包含 .list 時,wiki_API.page() 不會自動添加 .prop。 delete _options.list; wiki_API.page(title, function(page_data) { callback(page_data); }, _options); }; break; case 'redirects_here': // 取得所有重定向到(title重定向標的)之頁面列表,(title重定向標的)將會排在[0]。 // 注意: 無法避免雙重重定向問題! to_get_data = function(title, callback) { // wiki_API.redirects_here(title, callback, options) wiki_API.redirects_here(title, function(root_page_data, redirect_list) { if (!operation.keep_redirects && redirect_list && redirect_list[0]) { if (false) { console.assert(redirect_list[0].redirects // .join() === redirect_list.slice(1).join()); } // cache 中不需要此累贅之資料。 delete redirect_list[0].redirects; delete redirect_list[0].redirect_list; } callback(redirect_list); }, Object.assign({ // Making .redirect_list[0] the redirect target. include_root : true }, _this, operation)); }; break; case 'list': to_get_data = function(title, callback) { var options = Object.assign({ type : list_type }, _this, operation); wiki_API.list(title, function(pages) { if (!options.for_each_page || options.get_list) { library_namespace.log(list_type // allpages 不具有 title。 + (title ? ' ' // + wiki_API.title_link_of(title) : '') + ': ' + pages.length + ' page(s).'); } pages.query_title = title; // page list, title page_data callback(pages); }, options); }; break; default: if (typeof type === 'function') to_get_data = type.bind(Object.assign(Object.create(null), _this, operation)); else if (type) throw new Error('wiki_API_cache: Bad type: ' + type); else { library_namespace.debug('直接採用 list 作為 data。', 1, 'wiki_API_cache'); write_cache(list); return; } } // 回復 recover type // if (list_type) type = list_type; var title = list; if (typeof title === 'string') { // 可以用 operation.title_prefix 覆蓋 _this.title_prefix if ('title_prefix' in operation) { if (operation.title_prefix) title = operation.title_prefix + title; } else if (_this.title_prefix) title = _this.title_prefix + title; } library_namespace.debug('處理單一項作業: ' + wiki_API.title_link_of(title) + '。', 3, 'wiki_API_cache'); to_get_data(title, write_cache); }); } /** {String}預設 file encoding for fs of node.js。 */ wiki_API.encoding = 'utf8'; /** {String}檔名預設前綴。 */ wiki_API_cache.prefix = ''; /** {String}檔名預設後綴。 */ wiki_API_cache.postfix = '.json'; /** * 若 operation.list() return wiki_API_cache.abort,<br /> * 則將直接中斷離開 operation,不執行 callback。<br /> * 此時須由 operation.list() 自行處理 callback。 */ wiki_API_cache.abort = typeof Symbol === 'function' ? Symbol('ABORT_CACHE') // : { cache : 'abort' }; /** * 只取檔名,僅用在 operation.each_file_name。<br /> * <code>{ * each_file_name : CeL.wiki.cache.title_only, * }</code> * * @type {Function} */ wiki_API_cache.title_only = function(last_data_got, operation) { var list = operation.list; if (typeof list === 'function') { operation.list = list = list.call(this, last_data_got, operation); } return operation.type + '/' + remove_page_title_namespace(list); }; // ------------------------------------------------------------------------ // export 導出. // wiki_API.cache = wiki_API_cache; return wiki_API_cache; }