UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,614 lines (1,430 loc) 168 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): task control * * @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.task', 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 : 'this,*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION, KEY_CORRESPOND_PAGE = wiki_API.KEY_CORRESPOND_PAGE; // @inner var is_api_and_title = wiki_API.is_api_and_title, add_session_to_options = wiki_API.add_session_to_options; var gettext = library_namespace.cache_gettext(function(_) { gettext = _; }); var /** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */ NOT_FOUND = ''.indexOf('_'); // ------------------------------------------------------------------------ // get_data_key(), get_data_page() function get_wikibase_key(id) { if (!id) return; if (id[KEY_CORRESPOND_PAGE]) id = id[KEY_CORRESPOND_PAGE]; return id.site && id.title && id; } // check if session.last_data is usable, 非過期資料。 function last_data_is_usable(session) { // When "servers are currently under maintenance", session.last_data is // a string. if (typeof session.last_data === 'object' && !session.last_data.error // 若是session.last_data與session.last_page連動,必須先確認是否沒變更過session.last_page,才能當作cache、跳過重新擷取entity之作業。 && (!(KEY_CORRESPOND_PAGE in session.last_data) // assert: // wiki_API.is_page_data(session.last_data[KEY_CORRESPOND_PAGE]) || session.last_page === session.last_data[KEY_CORRESPOND_PAGE])) { library_namespace.debug('Use cached data: [[' // + (KEY_CORRESPOND_PAGE in session.last_data // may use wiki_API.title_link_of() ? session.last_page.id : session.last_data.id) + ']]', 1, 'last_data_is_usable'); return true; } } // -------------------------------------------------------------------------------------------- // instance 實例相關函數。 // 在實例函數中,會依賴外部 promise 之後才繼續執行處設置監測點,以檢測是否需要手動執行 session.next()。 // e.g., // node 20201008.fix_anchor.js use_language=zh archives function set_up_if_needed_run_next(run_next_status) { run_next_status = Object.assign(run_next_status || Object.create(null), // this: wiki session { // actions : this.actions.slice(), // actions_length : this.actions.length, // waiting_callback_result_relying_on_this : this.actions[ // // wiki_API.KEY_waiting_callback_result_relying_on_this], last_action : this && this.running && this.actions.length > 0 && this.actions.at(-1) }); return run_next_status; } // 檢查上次 session.set_up_if_needed_run_next() 之後是否有新添加的 actions。 function check_if_needed_run_next(run_next_status) { if (!run_next_status) return; if (run_next_status.needed_run_next) return true; var last_action = run_next_status.last_action; // console.trace(run_next_status); // this: wiki session var index_of_old_tail = last_action && this.actions.length > 0 && this.actions.lastIndexOf(last_action) || undefined; // console.trace([ index_of_old_tail, this.actions.length ]); // 若有新添加的 actions,由於這些 actions 全被 push 進 queue,不會被執行到, // 因此必須手動執行 this.next()。 if (index_of_old_tail >= 0 ? this.actions.length - index_of_old_tail > 1 : this.actions.length > 0) { // console.trace(this.actions); run_next_status.needed_run_next = true; return true; } } /** * session.check_if_needed_run_next(run_next_status) 可以執行多次。 * session.check_and_run_next() 只能在 promise 之後馬上執行一次。 * * e.g., Inside session instance functions:<code> var run_next_status = session && session.set_up_if_needed_run_next(); // 某些可能會用到 session.* 並且回傳 {Promise} 的動作。 //... promise = promise.then(successive_function_after_promise); if (session) session.check_and_run_next(run_next_status, promise); // 直接跳出。之後會等 promise 出結果才繼續執行。 return; </code> */ function check_and_run_next(run_next_status, promise, no_check) { if (!run_next_status) return; if (!no_check) this.check_if_needed_run_next(run_next_status); if (!run_next_status.needed_run_next) { return; } if (!promise) promise = run_next_status.promise || Promise.resolve(); library_namespace.debug( '新增了不會被執行到的 actions。手動執行 session.next(promise)。', 2, 'check_and_run_next'); // console.trace(promise); // 重設 run_next_status 以供重複利用. delete run_next_status.needed_run_next; run_next_status = this.set_up_if_needed_run_next(run_next_status); this.next(promise); return run_next_status; } Object.assign(wiki_API.prototype, { set_up_if_needed_run_next : set_up_if_needed_run_next, check_if_needed_run_next : check_if_needed_run_next, check_and_run_next : check_and_run_next }); // ------------------------------------------------------------------------ /** * Register promise relying on wiki session actions. 設定依賴於本 wiki_API action * 的 promise。 * * @param promise * promise to set * * @example session.set_promise_relying(result); */ wiki_API.prototype.set_promise_relying = function set_promise_relying( promise) { // Promise.isPromise() if (library_namespace.is_thenable(promise) // no rely on wiki_API // && !promise.not_relying_on_wiki_API ) { // assert: promise 依賴於本 wiki_API action thread。 library_namespace.debug('設定依賴於本 wiki_API action 的 promise。', 3, 'set_promise_relying'); if (library_namespace.is_debug(3)) { console.trace([ this.running, promise, this.actions ]); // console.trace(promise); } this.actions.promise_relying = library_namespace .is_thenable(this.actions.promise_relying) ? this.actions.promise_relying .then(promise) : promise; return true; } }; wiki_API.prototype.test_promise_relying = function test_promise_relying() { // this.actions.promise_relying is relying on this action. // 為了偵測這些promise是否已fulfilled,必須先this.running,預防其他執行緒鑽空隙。 this.running = true; var _this = this; function status_handler(fulfilled, this_thenable) { if (this_thenable !== _this.actions.promise_relying) { library_namespace.debug( '有其他執行緒鑽空隙,執行了 .set_promise_relying()。需要再檢測一次。', 3, 'test_promise_relying'); _this.test_promise_relying(); return; } if (fulfilled) { delete _this.actions.promise_relying; if (0 < _this.actions.length) { // assert: other threads added _this.actions // after library_namespace.status_of_thenable() library_namespace.debug('有其他執行緒鑽空隙,設定了 .actions。', 3, 'test_promise_relying'); _this.next(); } else { library_namespace .debug( '依賴於本 wiki_API action 的 promise 皆已 fulfilled。本 action 結束。', 3, 'test_promise_relying'); _this.running = false; } return; } // incase session.next() will wait for this.actions.promise_relying // calling back if CeL.is_thenable(result). // e.g., await wiki.for_each_page(need_check_redirected_list, // ... @ 20200122.update_vital_articles.js // So we need to run `session.next()` manually. // await wiki.for_each_page(need_check_redirected_list, // ... @ 20200122.update_vital_articles.js: // 從 function work_page_callback() return 之後,會回到 function // wiki_API_edit()。 // `this_thenable` 會等待 push 進 session.actions 的 // this.page(this_slice, main_work, page_options), // 但 return 的話,會保持 session.running === true && // session.actions.length > 0 // 並且 abort。執行不到 this_thenable.then()。 if (0 < _this.actions.length) { // 有些 promise 依賴於本 wiki_API action,假如停下來的話將會導致直接 exit跳出。 if (false) { console .trace('test_promise_relying: Calling wiki_API.prototype.next() ' + [ _this.running, _this.actions.length ]); } _this.next(); } else { if (false) { console.trace('test_promise_relying: No .actions left! ' + [ _this.running, _this.actions.length ]); } // delete _this.actions.promise_relying; _this.running = false; } } library_namespace.status_of_thenable(this.actions.promise_relying, status_handler); }; // ------------------------------------------------------------------------ /** * 系統內定繁簡轉換無法獲得的字元,屬於一對多繁簡轉換詞彙。<br /> * 例如 "模板:規範控製" 可重定向到 "模板:規范控制",然而系統內定的繁簡轉換無法從"控制"獲得"控製"。因此在設定 * redirects_variants_patterns 時必須手動加入 "製"→"制"。 * * 這邊基本上只記錄用在模板的文字。 * * @see [[w:zh:簡繁轉換一對多列表]]。 */ var char_variants_hash = { // 维基大学计划 ⭠ 維基大學計畫 划 : [ '畫' ], // 規范控制 ⭠ 規範控製 制 : [ '製' ] }; function add_char_variants(char_Array) { // char_Array = char_Array.clone(); for ( var char_to_check in char_variants_hash) { if (char_Array.includes(char_to_check)) { var char_list_to_append = char_variants_hash[char_to_check]; char_list_to_append.forEach(function(char_to_append) { if (!char_Array.includes(char_to_append)) { if (false) { console.trace([ char_to_check, char_to_append, char_Array ]); } char_Array.push(char_to_append); } }); break; } } return char_Array; } // ------------------------------------------------------------------------ /** 代表欲自動設定 options.page_to_edit */ wiki_API.VALUE_set_page_to_edit = true; var KEY_first_char_list = typeof Symbol === 'function' ? Symbol('first_char_list') : '\0first_char_list'; // @inner function set_page_to_edit(options, page_data, error, page_title) { if (!options || options.page_to_edit !== wiki_API.VALUE_set_page_to_edit) { return; } if (page_title) { if (!options.page_title_to_edit) { options.page_title_to_edit = page_title; } else if (page_title !== options.page_title_to_edit) { library_namespace.info('set_page_to_edit: ' + '跳過改變頁面 .page_title_to_edit 的標題: ' + wiki_API.title_link_of(options.page_title_to_edit) + '→' + wiki_API.title_link_of(page_title)); } } if (options.multi === true && Array.isArray(page_data) && page_data.length === 1) { page_data = page_data[0]; } if (options.page_title_to_edit && wiki_API.title_link_of(options.page_title_to_edit) !== wiki_API .title_link_of(page_data.original_title || page_data.title)) { library_namespace.info('set_page_to_edit: ' + '所取得頁面 .page_to_edit 的標題改變: ' + wiki_API.title_link_of(options.page_title_to_edit) + '→' + wiki_API.title_link_of(page_data.original_title || page_data.title)); if (!page_data.title) { console.trace(page_title, page_data); } // console.trace(options); } options.page_to_edit = page_data; options.last_page_error = error; // options.last_page_options = options; } // @inner wiki_API.KEY_waiting_callback_result_relying_on_this = typeof Symbol === 'function' ? Symbol('waiting callback_result_relying_on_this') : '\0waiting callback_result_relying_on_this'; /** * 設定工作/添加新的工作。 * * 注意: 每個 callback 皆應在最後執行 session.next()。 * * 警告: 若 callback throw,可能導致工作中斷,不會自動復原,得要以 wiki.next() 重起工作。 * * 工作原理: 每個實體會hold住一個queue ({Array}this.actions)。 當設定工作時,就把工作推入佇列中。 * 另外內部會有另一個行程負責依序執行每一個工作。 * * @see wiki_API_prototype_method() @ CeL.application.net.wiki.list */ wiki_API.prototype.next = function next(callback_result_relying_on_this) { if (this.actions[wiki_API.KEY_waiting_callback_result_relying_on_this]) { // assert: 此時為 session.next() 中執行 callback。 // callback_result_relying_on_this 執行中應該只能 push 進 // session.actions,不可執行 session.next()! // e.g., 'edit_structured_data' 之 callback 直接採用 // _this.next(next[4], data, error); // 若 next[4] 會再次 call session.edit_structured_data(), // 可能造成執行 callback_result_relying_on_this 後, // 到 'structured_data' 跳出準備 wiki_API.data(), // 回到 callback_result_relying_on_this 主程序 // 就直接跑到 'edit_structured_data' 這邊來,結果選了錯誤的 this.last_page。 // e.g., check_structured_data() @ CeL.application.net.wiki.edit callback_result_relying_on_this = Array.prototype.slice .call(arguments); callback_result_relying_on_this.unshift('run'); this.actions.push(callback_result_relying_on_this); library_namespace .debug( '在 callback_result_relying_on_this 中 call this.next() 並且 waiting callback 而跳出。為避免造成多執行序,將執行權交予 call callback_result_relying_on_this() 之母執行序,子執行序這邊跳出。', 1, 'wiki_API.prototype.next'); // console.trace(this.actions.length); return; } // ------------------------------------------------ // 標註當前執行的執行緒正在 running。 this.running = true; var _this = this; // 所有呼叫 instance 功能的 callback 全部推至 this.actions queue, // 之後當前執行緒會繼續執行。藉以維持單一執行緒。 if (callback_result_relying_on_this) { var process_callback = function process_callback(callback) { // assert: this.running === true if (typeof callback !== 'function') { _this.set_promise_relying(callback); return; } // 標註正在執行 callback()。 // Will run this.next() after callback() finished. _this.actions[wiki_API.KEY_waiting_callback_result_relying_on_this] = true; // assert: this.running === true try { callback = callback // _this.next(callback, ...callback_arguments); .apply(_this, process_callback.args); } catch (e) { // Error.stackTraceLimit = Infinity; if (library_namespace.env.has_console) console.error(e); else library_namespace.error(e); } // assert: this.running === true delete _this.actions[wiki_API.KEY_waiting_callback_result_relying_on_this]; if (callback) _this.set_promise_relying(callback); }; process_callback.args = Array.prototype.slice.call(arguments, 1); // console.trace(process_callback.args); if (Array.isArray(callback_result_relying_on_this)) callback_result_relying_on_this.forEach(process_callback); else process_callback(callback_result_relying_on_this); // free / reset process_callback = callback_result_relying_on_this = null; } // assert: false === // library_namespace.is_thenable(callback_result_relying_on_this) // ------------------------------------------------------------------------------ this.running = 0 < this.actions.length; if (!this.running) { if (library_namespace.is_thenable(this.actions.promise_relying)) { this.test_promise_relying(); } else { // this.thread_count = 0; // delete this.current_action; library_namespace.debug('The queue is empty.', 2, 'wiki_API.prototype.next'); // console.trace(this.actions); } return; } // 繼續執行接下來的行動。 // ------------------------------------------------ library_namespace.debug('剩餘 ' + this.actions.length + ' action(s)', 2, 'wiki_API.prototype.next'); if (library_namespace.is_debug(3)) { console .trace([ this.running, this.actions.length, this.actions.promise_relying, this.actions[wiki_API.KEY_waiting_callback_result_relying_on_this], next ]); } if (library_namespace.is_debug(3) // .show_value() @ interact.DOM, application.debug && library_namespace.show_value) { library_namespace.show_value(this.actions.slice(0, 10)); } var next = this.actions.shift(); // 不改動 next。 var type = next[0], list_type; if (// type in get_list.type wiki_API.list.type_list.includes(type)) { list_type = type; type = 'list'; } // this.current_action = next; // console.trace(next); if (library_namespace.is_debug(3)) { library_namespace.debug( // '處理 ' + (this.token.lgname ? this.token.lgname + ' ' : '') + '[' // + next.map(function(arg) { // for function var message; if (arg && arg.toString) { message = arg.toString(); } else { try { message = JSON.stringify(arg); } catch (e) { // message = String(arg); message = library_namespace.is_type(arg); } } return message && message.slice(0, 80); }) + ']', 1, 'wiki_API.prototype.next'); } // ------------------------------------------------ // 若需改變,需同步更改 wiki_API.prototype.next.methods switch (type) { // setup options case 'set_URL': // next[1] : callback wiki_API.setup_API_URL(this /* session */, next[1]); this.next(); break; case 'set_language': // next[1] : callback wiki_API.setup_API_language(this /* session */, next[1]); this.next(); break; case 'set_data': // 設定 this.data_session。 // using @inner // setup_data_session(session, callback, API_URL, password, force) wiki_API.setup_data_session(this /* session */, // 確保 data_session login 了才執行下一步。 function() { // console.trace(_this); // console.trace(_this.data_session); // next[1] : callback of set_data _this.next(next[1]); }, next[2], next[3], next[4]); break; // ------------------------------------------------ // account case 'login': library_namespace.debug( '正 log in 中,當 login 後,會自動執行 .next(),處理餘下的工作。', 2, 'wiki_API.prototype.next'); // rollback this.actions.unshift(next); break; case 'logout': // 結束 // next[1] : callback wiki_API.logout(this /* session */, next[1]); // this.next(); break; // ------------------------------------------------ // page access case 'query': console.trace('use query'); throw new Error('Please use .query_API() instead of only .query()!'); library_namespace .error('Please use .query_API() instead of only .query()!'); case 'query_API': // wiki_API.query(post_data, callback, options) if (next[4] === undefined && library_namespace.is_Object(next[3]) && next[3].post_data_only) { // shift arguments next[4] = next[3]; next[3] = next[1]; next[1] = ''; } // wiki_API.query(action, callback, post_data, options) wiki_API.query(next[1], function query_API_callback(data, error) { // 再設定一次,預防有執行期中間再執行的情況。 // e.g., wiki.query_api(action,function(){wiki.page();}) // 注意: 這動作應該放在callback()執行完後設定。 // next[2] : callback _this.next(next[2], data, error); }, next[3], // next[4] : options add_session_to_options(this, next[4])); break; case 'siteinfo': // wiki.siteinfo(options, callback) // wiki.siteinfo(callback) if (typeof next[1] === 'function' && !next[2]) { // next[1] : callback next[2] = next[1]; next[1] = null; } wiki_API.siteinfo(add_session_to_options(this, next[1]), function( data, error) { // next[2] : callback // run next action _this.next(next[2], data, error); }); break; case 'page': // console.trace(next); // this.page(page data, callback, options); if (next[1] === null) { // console.trace(this.actions); } // Done @ wiki_API_prototype_methods() // @ CeL.application.net.wiki.list if (false && library_namespace.is_Object(next[2]) && !next[3]) { // 直接輸入 options,未輸入 callback。 next.splice(2, 0, null); } /** {Object}取得 next[1] 這個頁面的時候使用的 revisions parameters。 */ var revisions_parameters = wiki_API.is_page_data(next[1]) && next[1].revisions_parameters || Object.create(null); // → 此法會採用所輸入之 page data 作為 this.last_page,不再重新擷取 page。 if (wiki_API.is_page_data(next[1]) // && (!next[3] // 檢查是否非 cached 的內容。 || next[3].rvprop === revisions_parameters.rvprop // 重複編輯同一個頁面? && next[3].page_to_edit !== wiki_API.VALUE_set_page_to_edit) // 必須有頁面內容,要不可能僅有資訊。有時可能已經擷取過卻發生錯誤而沒有頁面內容,此時依然會再擷取一次。 && (wiki_API.content_of.has_content(next[1], // next[3] && next[3].rvlimit - 1) // 除非剛剛才取得,同一個執行緒中不需要再度取得內容。 || next[3] && next[3].allow_missing // 確認真的是不存在的頁面。預防一次擷取的頁面內容太多,或者其他出錯情況,實際上沒能成功取得頁面內容, // next[1].revisions:[] && (('missing' in next[1]) || ('invalid' in next[1])))) { library_namespace.debug('採用所輸入之 ' + wiki_API.title_link_of(next[1]) + ' 作為 this.last_page。', 2, 'wiki_API.prototype.next'); // console.trace(next, this.last_page_error); // console.trace(this.actions); this.last_page = next[1]; // console.trace(next[1]); set_page_to_edit(next[3], next[1], this.last_page, this.last_page_error); // next[2] : callback this.next(next[2], next[1]); break; } // free revisions_parameters = null; if (this.last_page_title === next[1] && this.last_page_options === next[3]) { // 這必須防範改動頁面之後重新取得。 library_namespace.debug('採用前次的回傳以避免重複取得頁面。', 2, 'wiki_API.prototype.next'); // console.trace('採用前次的回傳以避免重複取得頁面: ' + next[1]); set_page_to_edit(next[3], next[1], this.last_page_error); // next[2] : callback this.next(next[2], this.last_page, // @see "合併取得頁面的操作" this.last_page_error); break; } // ---------------------------------- if (typeof next[1] === 'function') { // this.page(callback): callback(last_page) set_page_to_edit(next[3], this.last_page, this.last_page_error); // next[1] : callback this.next(next[1], this.last_page, this.last_page_error); break; } // ---------------------------------- if (false) { console.trace(_this.thread_count + '/' + _this.actions.length + 'actions: ' + _this.actions.slice(0, 9).map(function(action) { return action[0]; })); // console.log(next); } // 準備擷取新的頁面。為了預防舊的頁面資料被誤用,因此將此將其刪除。 // 例如在 .edit() 的callback中再呼叫 .edit(): // wiki.page().edit(,()=>wiki.page().edit(,)) delete this.last_page; delete this.last_page_title; delete this.last_page_options; delete this.last_page_error; // console.trace(next[1]); // next[3] : options if (next[3]) { if (next[3].page_to_edit === wiki_API.VALUE_set_page_to_edit) { // page-edit 組合式操作。設定等待先前的取得頁面操作中。 next[3].waiting_for_previous_combination_operation = true; if (!next[3].page_title_to_edit) { // e.g., log_options @ wiki_API.prototype.work // Only section : 'new' next[3].page_title_to_edit = next[1]; } } if (library_namespace.is_debug(0)) { // 設定個僅 debug 用、無功能的註記。 next[3].actions_when_fetching_page = [ next ] .append(this.actions); next[3].actions_when_fetching_page.only_for_debug = true; } next[3].page_fetching = Date.now(); } // this.page(title, callback, options) // next[1] : title // next[3] : options // [ {String}API_URL, {String}title or {Object}page_data ] wiki_API.page(is_api_and_title(next[1]) ? next[1] : [ this.API_URL, next[1] ], // function wiki_API_next_page_callback(page_data, error) { if (false && next[3].page_fetching === 'unshift_edit') console.trace([ page_data, error, _this.actions ]); if (false) { if (Array.isArray(page_data)) { console.trace(page_data.length + ' pages get: ' + page_data.slice(0, 10).map( function(page_data) { return page_data.title; })); } else { console.trace([ page_data, error ]); } } // assert: 當錯誤發生,例如頁面不存在/已刪除,依然需要模擬出 page_data。 // 如此才能執行 .page().edit()。 _this.last_page // 正常情況。確保this.last_page為單頁面。需要使用callback以取得result。 = Array.isArray(page_data) ? page_data[0] : page_data; // 用於合併取得頁面的操作。 e.g., // node 20201008.fix_anchor.js use_language=zh archives _this.last_page_title = next[1]; _this.last_page_options = next[3]; _this.last_page_error = error; // next[3] : options set_page_to_edit(next[3], page_data, error, next[1]); var original_title = next[1]; // assert: typeof original_title === 'string' var next_action = _this.actions[0]; if (false && next_action // && next_action[0] === 'edit' // next_action[2]: options && typeof next_action[2] === 'object' // && (!next_action[2].page_to_edit // || next_action[2].page_to_edit // === wiki_API.VALUE_set_page_to_edit)) { if (!next_action[2].page_title_to_edit // || next_action[2].page_title_to_edit === original_title) { if (original_title !== page_data.title) { library_namespace.info( // 'wiki_API.prototype.next.page: ' + '所取得頁面的標題改變: ' + wiki_API.title_link_of(original_title) + '→' + wiki_API.title_link_of(page_data.title)); } // 手動指定要編輯的頁面。避免多執行續打亂 wiki.last_page。 next_action[2].page_to_edit = page_data; next_action[2].page_title_to_edit = original_title; next_action[2].last_page_options = next[3]; next_action[2].last_page_error = error; } else { library_namespace.error( // 'wiki_API.prototype.next.page: ' + '無法設定欲編輯的頁面資訊: ' + wiki_API.title_link_of(original_title) + '→' + wiki_API.title_link_of(page_data.title) + ' 不等於 ' + wiki_API.title_link_of( // next_action[2].page_title_to_edit)); console.trace([ next, next_action ]); } } if (!page_data) { // console.trace([ '' + next[2], page_data, error ]); } // console.trace(_this.actions); // next[2] : callback _this.next(next[2], page_data, error); }, // next[3] : options add_session_to_options(this, next[3])); break; case 'tracking_revisions': if (typeof next[3] === 'object') { // shift arguments next[4] = next[3]; next[3] = null; } wiki_API.tracking_revisions(next[1], next[2], function(revision, error) { _this.next(next[3], revision, error); }, // next[4] : options add_session_to_options(this, next[4])); break; case 'parse': // e.g., wiki.page('title', options).parse(callback, options); if (library_namespace.is_Object(next[1]) && !next[2]) { // 直接輸入 options,未輸入 callback。 next.splice(1, 0, null); } // next[2] : options var parsed = wiki_API.parser(this.last_page, add_session_to_options(this, next[2])).parse(); // next[3] : callback this.next(next[1], parsed); break; case 'purge': if (typeof next[1] === 'string' || typeof next[1] === 'number' || wiki_API.is_page_data(next[1])) { // purge() 可以直接輸入頁面,不必先 .page('Title') // wiki.purge('Title', callback, options) // wiki.purge('Title', options) // wiki.purge(pageid, callback, options) // wiki.purge('pageid|pageid', options) } else { // wiki.page('Title').purge() // wiki.page('Title').purge(callback, options) // wiki.page('Title').purge(options) next.splice(1, 0, this.last_page); } if (library_namespace.is_Object(next[2]) && !next[3]) { // 直接輸入 options,未輸入 callback。 next.splice(2, 0, null); } // next: [ 'purge', pages, callback, options ] if (!next[1]) { library_namespace .warn('wiki_API.prototype.next.purge: No page inputed!'); // next[3] : callback this.next(next[3], undefined, 'no page'); } else { wiki_API.purge([ this.API_URL, next[1] ], // function wiki_API_next_purge_callback(purge_pages, error) { // next[2] : callback _this.next(next[2], purge_pages, error); }, // next[3] : options add_session_to_options(this, next[3])); } break; case 'redirect_to': // this.redirect_to(page data, callback, options); if (library_namespace.is_Object(next[2]) && !next[3]) { // 直接輸入 options,未輸入 callback。 next.splice(2, 0, null); } // this.redirect_to(title, callback, options) // next[1] : title // next[3] : options // [ {String}API_URL, {String}title or {Object}page_data ] wiki_API.redirect_to(is_api_and_title(next[1]) ? next[1] : [ this.API_URL, next[1] ], // function wiki_API_next_redirect_to_callback(redirect_data, page_data, error) { // next[2] : callback _this.next(next[2], redirect_data, page_data, error); }, // next[3] : options add_session_to_options(this, next[3])); break; case 'list': // get_list(). e.g., 反向連結/連入頁面。 // next[1] : 大部分是 page title, // 但因為有些方法不需要用到頁面標題(recentchanges,allusers)因此對於這一些方法需要特別處理。 if (typeof next[1] === 'function' && typeof next[2] !== 'function') { next.splice(1, 0, ''); } // 注意: arguments 與 get_list() 之 callback 連動。 wiki_API[list_type]([ this.API_URL, next[1] ], // function wiki_API_next_list_callback(pages, error) { // [ page_data ] _this.last_pages = pages; if (typeof next[2] === 'function') { // 注意: arguments 與 get_list() 之 callback 連動。 callback_result_relying_on_this // next[2] : callback(pages, error) = next[2].call(_this, pages, error); } else if (next[2] && next[2].each) { // next[2] : 當作 work,處理積存工作。 if (pages) { _this.work(next[2]); } else { // 只有在本次有處理頁面時,才繼續下去。 library_namespace.info('無頁面可處理(已完成?),中斷跳出。'); } } _this.next(callback_result_relying_on_this); }, // next[3] : options add_session_to_options(this, next[3])); break; case 'random_categorymembers': // console.trace(next); wiki_API.random_categorymembers(next[1], function() { callback_result_relying_on_this // next[2] : callback(pages, target, options) = next[2].apply(_this, arguments); _this.next(callback_result_relying_on_this); }, // next[3] : options add_session_to_options(this, next[3])); break; // ------------------------------------------------------ // case 'category_tree': // @see wiki_API.prototype.category_tree @ application.net.wiki.list // register page alias. usually used for templates case 'register_redirects': // wiki.register_redirects(page_title_list, callback, options) // wiki.register_redirects(page_title_list, options) if (library_namespace.is_Object(next[2]) && !next[3]) { // 未設定/不設定 callback // shift arguments next.splice(2, 0, undefined); } var redirects_data; if (library_namespace.is_Object(next[1])) { // temp redirects_data = next[1]; if (!next[3].update_page_name_hash) { library_namespace .info('wiki_API.prototype.next.register_redirects: ' + '查詢傳入引數 Object 的 value 而非 key。'); } next[1] = Object.values(next[1]); } // next[3] : options next[3] = Object.assign({ // 取得定向的終點。 redirects : 1, // [KEY_SESSION] session : this, // Making .redirect_list[0] the redirect target. include_root : true, // converttitles: 1, // multiple pages multi : Array.isArray(next[1]) && next[1].length > 1 }, next[3]); if (next[3].update_page_name_hash === true && redirects_data) { // wiki.register_redirects({key:'page_name'},callback,{update_page_name_hash:true}); next[3].update_page_name_hash = redirects_data; } // next[1]: page_title if (next[3].namespace) next[1] = this.to_namespace(next[1], next[3].namespace); // console.trace(next[1]); next[1] = this.normalize_title(next[1]); if (!next[1]) { library_namespace.error([ // 'wiki_API.prototype.next.register_redirects: ', { // gettext_config:{"id":"invalid-title-$1"} T : [ 'Invalid title: %1', // wiki_API.title_link_of(next[1]) ] } ]); // next[2] : callback(root_page_data, error) this.next(next[2], next[1], new Error(gettext( // gettext_config:{"id":"invalid-title-$1"} 'Invalid title: %1', wiki_API.title_link_of(next[1])))); break; } redirects_data = next[3].register_target || this.redirects_data; if (next[3].reget) { } else if (Array.isArray(next[1])) { next[1] = next[1].filter(function(page_title) { if (false && redirects_data !== _this.redirects_data // Copy data: 這會漏掉 page_data.original_title, // page_data.redirect_from && (page_title in _this.redirects_data)) { redirects_data[page_title] // = _this.redirects_data[page_title]; } return page_title && !(page_title in redirects_data) // && !(page_title in _this.nonexistent_pages); }).unique(); if (next[1].length === 0) { // next[2] : callback(root_page_data, error) this.next(next[2]); break; } } else if (next[1] in redirects_data) { if (false) { console.trace('已處理過 have registered, use cache: ' + next[1] + '→' + redirects_data[next[1]]); } // next[2] : callback(root_page_data, error) this.next(next[2]); break; } else if (next[1] in this.nonexistent_pages) { // next[2] : callback(root_page_data, error) this.next(next[2]); break; } if (Array.isArray(next[1])) { // next[3] : options var slice_size = next[3].one_by_one ? 1 // 50: 避免 HTTP status 414: Request-URI Too Long : next[3].slice_size >= 1 ? Math.min(50, next[3].slice_size) : 50; while (next[1].length > slice_size) { _this.actions.unshift([ next[0], // keep request order slice_size === 1 ? next[1].pop() // : next[1].splice(next[1].length - slice_size, slice_size), next[2], next[3] ]); // remove callback: only run callback at the latest // time. next[2] = undefined; } } // console.trace(JSON.stringify(next[1])); // 解析出所有 next[1] 別名 // next[1]: page_title wiki_API.redirects_here(next[1], function(root_page_data, redirect_list, error) { // console.trace([ root_page_data, redirect_list, error ]); if (error) { // console.trace(error); // next[2] : callback(root_page_data, error) _this.next(next[2], null, error); return; } if (false) { console.trace(root_page_data); console.trace(redirect_list); console.assert(!redirect_list || redirect_list === root_page_data.redirect_list); } var page_list_to_check_variants // = Array.isArray(next[1]) ? next[1] : [ next[1] ]; // from: alias, to: 正式名稱 function register_title(from, to) { if (!from) { return; } // assert: from === // _this.normalize_title(from) // the namespace of from, to is normalized // from === to 亦登記,以供確認此頁面存在。 // 並方便 register_redirect_list_via_mapper() 中的 map_to 設定。 if (redirects_data[from]) { if (redirects_data[from] === to) return; // Should not fo to here. library_namespace .error('register_title: ' + from + '→' + redirects_data[from] + '\n\tchange to →' + to); } redirects_data[from] = to; page_list_to_check_variants.push(from); } function register_root_alias(page_data) { if (page_data.original_title) { // console.trace(page_data); register_title(page_data.original_title, // page_data.title); } if (page_data.redirect_from) { register_title(page_data.redirect_from, // page_data.title); } } function register_redirect_list(redirect_list, page_title) { // console.trace(redirect_list); // 本名 var target_page_title = redirect_list[0].title; var is_missing = !target_page_title || ('missing' in redirect_list[0]) || ('invalid' in redirect_list[0]); if (!is_missing) { redirect_list.forEach(function(page_data) { register_title(page_data.title, target_page_title); }); } if (next[3].no_message) { return; } var message = 'register_redirects: ' + (page_title === target_page_title ? '' : (wiki_API.title_link_of(page_title) // JSON.stringify(page_title) // Should not go to here || page_title) + ' → ') + wiki_API.title_link_of(target_page_title) + ': '; if (is_missing) { // 避免重複 call。 _this.nonexistent_pages[page_title] = true; message += 'Missing'; library_namespace.warn(message); return; } message += gettext(redirect_list.length === 1 // gettext_config:{"id":"no-page-redirects-to-this-page"} ? '無頁面重定向至本頁' // gettext_config:{"id":"total-$1-pages-redirected-to-this-page"} : '共有%1個{{PLURAL:%1|頁面}}重定向至本頁', redirect_list.length - 1); if (1 < redirect_list.length && redirect_list.length < 6) { message += ': ' // + redirect_list.slice(1).map(function(page_data) { // return page_data.title; return wiki_API.title_link_of(page_data); }).join(gettext('Comma-separator')); } library_namespace.info(message); } if (redirect_list) { // e.g., wiki_API.redirects_here({String}) // console.trace([ next[1], root_page_data ]); register_redirect_list(redirect_list, // Array.isArray(next[1]) ? // assert: next[1].length === 1 next[1][0] : next[1]); register_root_alias(root_page_data); } else { // e.g., wiki_API.redirects_here({Array}) root_page_data.forEach(function(page_data) { // console.trace(page_data.redirect_list); // console.trace(page_data.original_title); register_redirect_list(page_data.redirect_list || [ page_data ], page_data.original_title || page_data.title); register_root_alias(page_data); }); } // console.trace(redirects_data); if (library_namespace.is_Object( // 將 {Object}options.update_page_name_hash 之 value 視為 page name // 並更新其 value 成為 redirect target。 // TODO: 另外提出成為 function next[3].update_page_name_hash)) { var update_page_name_hash = next[3].update_page_name_hash; for ( var key in update_page_name_hash) { var page_name = _this.to_namespace( update_page_name_hash[key], next[3].namespace); if (typeof page_name !== 'string') continue; page_name = redirects_data[page_name]; if (!page_name) continue; if (next[3].namespace && _this.is_namespace( update_page_name_hash[key], 'main')) { // need_remove_namespace page_name = _this.remove_namespace(page_name); } update_page_name_hash[key] = page_name; } } if (false) { console.trace([ next, next[3].no_languagevariants, !_this.has_languagevariants ]); } if (next[3].no_languagevariants || !_this.has_languagevariants // /[\u4e00-\u9fa5]/: 匹配中文字 RegExp。 // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) // || !/[\u4e00-\u9fa5]/.test(next[1]) ) { // next[2] : callback(root_page_data, error) _this.next(next[2], root_page_data); return; } // -------------------------------------------------- // 處理 converttitles。 // console.trace('處理繁簡轉換問題: ' + page_list_to_check_variants); // console.trace(root_page_data); // console.trace(JSON.stringify(redirects_data)); // variants_of_target[register_target] // = [ 相對應的 variant list 1, 相對應的 variant list 2, ... ] // 相對應的 variant list = [ variants 1, variants 2 ] var variants_of_target = Object.create(null); var redirects_variants_patterns // redirects_variants_patterns[first char] = pattern_hash = _this.redirects_variants_patterns // pattern_hash = { pattern_id: pattern_data, ... } // pattern_data = [ {RegExp}pattern, redirects_to, namespace ] || (_this.redirects_variants_patterns = Object.create(null)); // {{規范控製}} → {{规范控制}} → {{Authority control}} // 為解決 "控製"也能引導到"控制" 此類模板繁體名稱採用繁簡共通字,因此就算 wiki.convert_Chinese() // 也不能獲得繁體字,但繁體字會自動 mapping 的問題;必須自行 // wiki.register_redirects(繁體字名稱)。 function register_variants_pattern() { // console.trace(variants_of_target); for ( var register_target in variants_of_target) { variants_of_target[register_target].forEach(function( variants_list) { if (variants_list.length === 1) { // 沒有任何變體。 return; } // assert: variants_list.length === 2: // [ 'zh-tw', 'zh-cn' ] or [ 'zh-hant', 'zh-hans' ] if (false) { console.trace(variants_list, variants_list[0] .chars()); } var char_list = variants_list.shift().chars() // 先取第一個為基準。 .map(function(char) { return [ char ]; }); if (variants_list.some(function(variant) { variant = variant.chars(); // 兩個變體的長度不同。 if (char_list.length !== variant.length) { return true; } variant.forEach(function(char, index) { if (!char_list[index].includes(char)) { char_list[index].push(char); } }); })) { library_namespace.warn( // 'register_variants_pattern: 跳過長度不同的變體 ' + register_target + ' ← ' // + char_list.join('') + '|' + variants_list.join('|')); return; } var pattern = char_list.map(function(chars) { chars = add_char_variants(chars); return chars.length === 1 ? chars : '[' + chars.join('') + ']'; }); pattern = pattern.join(''); var pattern_hash; var namespace = _this.namespace(register_target, { is_page_title : true, get_name : true }); if (false) { console.trace([ register_target, namespace, char_list ]); } // 跳過 namespace + ':' if (false && namespace) { if (!char_list[namespace.length] // || char_list[namespace.length].length !== 1 // || char_list[namespace.length][0] !== ':') { console.error([ register_target, namespace, char_list ]); throw new Error( 'register_variants_pattern: ' + '解析 namespace 出錯!'); } } (namespace ? char_list[namespace.length + 1] : char_list).forEach(function(first_char) { if (!redirects_variants_patterns[first_char]) { if (!pattern_hash) { pattern_hash = Object.create(null); pattern_hash[KEY_first_char_list] = []; } pattern_hash[KEY_first_char_list] .push(first_char); redirects_variants_patterns[first_char] // Initialization = pattern_hash; } else if (!pattern_hash) { pattern_hash // Copy = redirects_variants_patterns[first_char]; // assert: // pattern_hash[KEY_first_char_list].includes(first_char) } else if (!pattern_hash !== // merge redirects_variants_patterns[first_char]) { var first_char_list = // redirects_variants_patterns[first_char] // [KEY_first_char_list]; var old_first_char_list // = pattern_hash[KEY_first_char_list] // .filter(function(char) { if (first_char_list.includes(char)) return; first_char_list.push(char); return true; }); // assert: // first_char_list.includes(first_char); pattern_hash = Object.assign( // redirects_variants_patterns[first_char], // pattern_hash); redirects_variants_patterns[first_char] // recover [KEY_first_char_list] = first_char_list; // console.trace(old_first_char_list); old_first_char_list.forEach(function( first_char) { redirects_variants_patterns[first_char] // = pattern_hash; }); } if (pattern_hash[pattern] // && pattern_hash[pattern][1] // !== register_target) { library_namespace.warn( // 'register_variants_pattern: 相同變體重定向到不同頁面: ' + pattern + ' → ' + pattern_hash[pattern] + ', ' + register_target); } pattern_hash[pattern] = [ new RegExp('^' + pattern + '$' // , 'u' ), register_target, _this.namespace(register_target) ]; }); }); } } function register_redirect_list_via_mapper(original_list, variants_list, error) { if (false) { console.trace([ next[3].uselang, original_list, variants_list ]); } if (!Array.isArray(variants_list)) { library_namespace .error('register_redirect_list_via_mapper: ' + '無法繁簡轉換: ' + (error || '未知的錯誤')); return; } // assert: variants_list.length === original_list.length if (/* check_char_variants */false) { original_list = original_list.slice(1); variants_list = variants_list.slice(1); } variants_list.forEach(function(map_from, index) { // if (map_from in redirects_data) return; var map_to // = redirects_data[original_list[index]]; if (original_list.start_index) { index += original_list.start_index; } if (false) { library_namespace .log('register_redirect_list_via_mapper: [' + variants_list.uselang + '] ' + '[' + index + '] ' + map_from + ' → ' + map_to); } redirects_data[map_from] = map_to; var variants_map = variants_of_target[map_to]; if (!variants_map) variants_map = variants_of_target[map_to] = []; if (!variants_map[index]) { variants_map[index] = [ map_from ]; } else if (!variants_map[index].includes(map_from)) { variants_map[index].push(map_from); } }); // console.trace(variants_of_target); } var promise = Promise.resolve(); function add_variant_of_page_list(uselang) { // next[3] : options var options = Object.assign(Object.clone(next[3]), { uselang : uselang }); promise = promise.then(function() { // console.trace(uselang); return new Promise( /* executor */function(resolve, reject) { // console.trace(uselang); wiki_API.convert_Chinese( // page_list_to_check_variants, // function(converted_page_list, error) { if (false) { console.trace([ // page_list_to_check_variants, // uselang, converted_page_list, error ]); } register_redirect_list_via_mapper( page_list_to_check_variants, converted_page_list, error); // console.trace(variants_of_target, promise); resolve(); }, options); }); }); } function check_big_variations() { for ( var register_target in variants_of_target) { variants_of_target[register_target] // = variants_of_target[register_target].filter(function( variants_list) { if (variants_list.length === 1) { // 沒有任何變體。 return; } var length = variants_list[0].length; for (var index = 1; // index < variants_list.length; index++) { if (variants_list[index].length !== length) { // 有地區化語詞。 page_list_to_check_variants .append(variants_list); return; // break; } } return true; }); } } page_list_to_check_variants = page_list_to_check_variants // .filter(function(page_title) { // 去掉全英文字母的頁面名稱。 return !/^[\w\s:\-]+$/.test(page_title); }); // console.trace(page_list_to_check_variants); if (/* check_char_variants */false) { page_list_to_check_variants // .unshift(page_list_to_check_variants.join('') // 這些字元不該有變體。 .replace(/[\w\s:"']/g, '').chars().unique().join(',')); } var accumulated_length // 完成一階段,reset 前必須設定 .start_index = 累積的長度 accumulated_length,預防 // register_redirect_list_via_mapper() 登記到先前的 index。 = page_list_to_check_variants.length; [ 'zh-tw', // zh-cn: e.g., "Template:軟體專題" ⭠ "Template:软件专题" 'zh-cn' ].forEach(add_variant_of_page_list); promise.then(function() { // reset page_list_to_check_variants = []; page_list_to_check_variants // .start_index = accumulated_length; check_big_variations(); if (page_list_to_check_variants.length > 0) { // e.g., "Template:独联体专题" ⭠ "Template:獨立國協專題" // console.trace(page_list_to_chec