UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,564 lines (1,405 loc) 101 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): list * * @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。 * * TODO:<code> </code> * * @since 2019/10/10 拆分自 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.list', require : 'application.net.wiki.' // load MediaWiki module basic functions + '|application.net.wiki.namespace.' // for library_namespace.get_URL() + '|application.net.Ajax.' // + '|application.net.wiki.query.' // wiki_API.parse.redirect() // + '|application.net.wiki.parser.' , // 設定不匯出的子函式。 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; // @inner var is_api_and_title = wiki_API.is_api_and_title, normalize_title_parameter = wiki_API.normalize_title_parameter, set_parameters = wiki_API.set_parameters; var gettext = library_namespace.cache_gettext(function(_) { gettext = _; }); // ------------------------------------------------------------------------ var KEY_generator_title = typeof Symbol === 'function' ? Symbol('generator title') : 'generator title'; /** * 生成用於 wiki query 的生成器參數 parameters。 * * <code> 對 generator=prop模塊,須指定 "titles" / "pageids" parameter。 https://en.wikipedia.org/wiki/Special:ApiSandbox#action=query&format=json&prop=categories&titles=ABC%7CABC's%7CABC's%20Coverage%20of%20the%20NBA%20on%20ESPN&generator=links&formatversion=2&cllimit=2&gpllimit=3 對 generator=all*,"generator" parameter 基本上取代 "titles" / "pageids" parameter,並且優先度更高。 https://en.wikipedia.org/wiki/Special:ApiSandbox#action=query&format=json&prop=links%7Ccategories&generator=allpages&formatversion=2&pllimit=2&cllimit=2&gapfrom=ABC&gaplimit=3 https://en.wikipedia.org/wiki/Special:ApiSandbox#action=query&format=json&prop=links%7Ccategories&titles=ABC%7CABC's%7CABC's%20Coverage%20of%20the%20NBA%20on%20ESPN&formatversion=2&pllimit=2&cllimit=2 對 generator=其他links模塊&prop=prop模塊,可能必須指定 generator titles / pageids。 https://en.wikipedia.org/wiki/Special:ApiSandbox#action=query&format=json&prop=links%7Ccategories&generator=categorymembers&formatversion=2&pllimit=2&cllimit=2&gcmtitle=Category%3ACountries&gcmlimit=3 </code> * * @param {String}generator * 生成器參數。 * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項。 * @return {Object}正規化後,用於 wiki query 的生成器參數。 * * @see https://www.mediawiki.org/wiki/API:Query#Example_6:_Generators * https://www.mediawiki.org/w/api.php?action=help&modules=query */ function generator_parameters(generator, options) { if (typeof options === 'string') { options = { title : options }; } var parameters = { generator : generator, limit : 'max' }; var prefix = get_list.type[generator]; if (Array.isArray(prefix)) prefix = prefix[0]; var session = wiki_API.session_of_options(options); for ( var parameter in options) { var value = options[parameter]; if (parameter === 'namespace') value = (session || wiki_API).namespace(value); if (parameter.startsWith('g' + prefix)) { parameters[parameter] = value; continue; } if (parameter.startsWith(prefix) && !(('g' + parameter) in options)) { parameters['g' + parameter] = value; continue; } parameter = 'g' + prefix + parameter; if (!(parameter in parameters)) parameters[parameter] = value; } // Using (KEY_generator_title in parameters) to test if parameters is a // generator title. // Warning: The value may be undefined for generator=all*. parameters[KEY_generator_title] = parameters['g' + prefix + 'title']; return parameters; } wiki_API.generator_parameters = generator_parameters; wiki_API.KEY_generator_title = KEY_generator_title; // ------------------------------------------------------------------------ /** * 自 title 頁面取得後續檢索用索引值 (continuation data)。<br /> * e.g., 'continue' * * @param {String|Array}title * the page title to search continue information * @param {Function|Object}callback * 回調函數 or options。 callback({Object} continue data); * * @see https://www.mediawiki.org/wiki/API:Query#Continuing_queries */ function get_continue(title, callback) { var options; if (library_namespace.is_Object(callback)) { callback = (options = callback).callback; } else { // 前置處理。 options = Object.create(null); } wiki_API.page(title, function(page_data) { var matched, done, content = wiki_API.content_of(page_data), // {RegExp}[options.pattern]: // content.match(pattern) === [ , '{type:"continue"}' ] pattern = options.pattern, // {Object} continue data data = Object.create(null); if (!pattern) { pattern = new RegExp(library_namespace.to_RegExp_pattern( // (options.continue_key || wiki_API.prototype.continue_key) .trim()) + ' *:? *({[^{}]{0,80}})', 'g'); } library_namespace.debug('pattern: ' + pattern, 2, 'get_continue'); while (matched = pattern.exec(content)) { library_namespace.debug('continue data: [' + matched[1] + ']', 2, 'get_continue'); if (!(done = /^{\s*}$/.test(matched[1]))) data = Object.assign(data, JSON.parse(matched[1])); } // options.get_all: get all continue data. if (!options.get_all) if (done) { library_namespace.debug('最後一次之後續檢索用索引值為空,可能已完成?', 1, 'get_continue'); data = null; } else { // {String|Boolean}[options.type]: what type to search. matched = options.type; if (matched in get_list.type) matched = get_list.type[matched] + 'continue'; content = data; data = Object.create(null); if (matched in content) { data[matched] = content[matched]; } } // callback({Object} continue data); callback(data || Object.create(null)); }, options); } // ------------------------------------------------------------------------ if (false) { // 若是想一次取得所有 list,不應使用單次版: // 注意: arguments 與 get_list() 之 callback 連動。 wiki.categorymembers('Category_name', function(pages, error) { console.log(pages.length); }, { limit : 'max' }); // 而應使用循環取得資料版: // method 1: using wiki_API_list() CeL.wiki.list(title, function(list/* , target, options */) { // assert: Array.isArray(list) if (list.error) { ; } else { CeL.log('Get ' + list.length + ' item(s).'); } }, Object.assign({ // [KEY_SESSION] session : wiki, type : list_type }, options)); // method 2: CeL.wiki.cache({ type : 'categorymembers', list : 'Category_name' }, function(list) { CeL.log('Get ' + list.length + ' item(s).'); }, { // default options === this // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bcategorymembers namespace : '0|1', // [KEY_SESSION] session : wiki, // title_prefix : 'Template:', // cache path prefix prefix : base_directory, // Do not write cache file to disk. cache : false }); } function combine_by_page(pages, unique_attribute) { var hash = Object.create(null); pages.forEach(function(page_data) { var value = page_data[unique_attribute]; if (page_data.title in hash) { hash[page_data.title][unique_attribute].push(value); } else { page_data[unique_attribute] = [ value ]; hash[page_data.title] = page_data; } }); return Object.values(hash); } // allow async functions // https://github.com/tc39/ecmascript-asyncawait/issues/78 var get_list_async_code = '(async function() {' + ' try { if (wiki_API_list.exit === await options.for_each_page(item)) options.abort_operation = true; }' + ' catch(e) { library_namespace.error(e); }' + ' })();'; /** * get list. 檢索/提取列表<br /> * 注意: 可能會改變 options! * * TODO: options.get_sub options.ns * * TODO: using iterable protocol * https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Iteration_protocols * * @param {String}type * one of get_list.type * @param {String}[title] * page title 頁面標題。 * @param {Function}callback * 回調函數。 callback(pages, error)<br /> * 注意: arguments 與 get_list() 之 callback 連動。 * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項。 options.page_filter(): A function to * filter result pages. Return `true` if you want to keep the * element. filter result pages. */ function get_list(type, title, callback, options) { // console.trace(title); // console.trace(options.for_each_page); library_namespace.debug(type + (title ? ' ' + wiki_API.title_link_of(title) : '') + ', callback: ' + callback, 3, 'get_list'); var parameter, // 預處理器 title_preprocessor, /** {String} 前置字首。 */ prefix = get_list.type[type]; library_namespace.debug('parameters: ' + JSON.stringify(prefix), 3, 'get_list'); if (Array.isArray(prefix)) { // list_type : [ {String}prefix, {String}:query=prop|list, // {Function}title_preprocessor ] parameter = prefix[1] || get_list.default_parameter; title_preprocessor = prefix[2]; prefix = prefix[0]; } else { parameter = get_list.default_parameter; } var action = { action : 'query' }; action[parameter] = type; if (wiki_API.need_get_API_parameters(action, options, get_list, arguments)) { return; } if (typeof options === 'string' || typeof options === 'number') { // 當作 namespace。 options = { // {ℕ⁰:Natural+0|String|Object}namespace // one of wiki_API.namespace.hash. namespace : options }; } else if (!library_namespace.is_Object(options)) { options = { // original option namespace : options }; } if ('namespace' in options) { // console.trace('檢查 options.namespace: ' + options.namespace); options.namespace = wiki_API.namespace(options.namespace, options); // console.trace(options.namespace); if (options.namespace === undefined) { library_namespace .warn('get_list: options.namespace 非正規 namespace!將被忽略!'); delete options.namespace; } } if (typeof options.for_each_slice === 'function' && !options.for_each_page) { // 設定 options.for_each_page 以簡化檢測特定操作的流程。 options.for_each_page = options.for_each_slice; } if (is_api_and_title(title, true)) { // 處理 [ {String}API_URL, {String}title or {Object}page_data ] action = title.clone(); } else { // assert: {String}title action = [ , title ]; } var continue_from = prefix + 'continue'; var session = wiki_API.session_of_options(options); // 注意: 這裡會改變 options! if (!options.next_mark) { // 紀錄各種後續檢索用索引值。應以 append,而非整個換掉的方式更改。 // 對舊版本須用到 for (in .next_mark) library_namespace.debug('未傳入後續檢索用索引值。', 4, 'get_list'); // initialization options.next_mark = Object.create(null); } else if (false && Object.keys(options.next_mark).length > 0) { // assert: library_namespace.is_Object(options.next_mark) // e.g., called by function wiki_API_list() // console.trace([ type, title, options.next_mark ]); library_namespace .debug( '直接傳入了 options.next_mark;可延續使用上次的後續檢索用索引值,避免重複 loading page。', 4, 'get_list'); // Object.assign(options, options.next_mark); for ( var next_mark_key in options.next_mark) { if (next_mark_key !== 'continue') { options[next_mark_key] // {String}options.next_mark[next_mark_key]: // 後續檢索用索引值。後続の索引。 = options.next_mark[next_mark_key]; // 經由,經過,通過來源 library_namespace.debug('continue from [' + options[next_mark_key] + '] via options', 1, 'get_list'); } delete options.next_mark[next_mark_key]; } // 刪掉標記,避免無窮迴圈。 delete options.get_continue; console.trace(options); // usage: // options: { next_mark : {} , get_continue : log_to } if (false && (continue_from in options.next_mark)) { // {String}options.next_mark[continue_from]: // 後續檢索用索引值。後続の索引。 options[continue_from] = options.next_mark[continue_from]; // 經由,經過,通過來源 library_namespace.debug('continue from [' + options[continue_from] + '] via options', 1, 'get_list'); // 刪掉標記,避免無窮迴圈。 delete options.get_continue; } } // 若未設定 .next_mark,才會自 options.get_continue 取得後續檢索用索引值。 if (typeof options.get_continue === 'string') { // 設定好 options.get_continue,以進一步從 page 取得後續檢索用索引值。 // 採用 session 之 domain。 options.get_continue = [ session.API_URL, options.get_continue ]; } // options.get_continue: 用以取用後續檢索用索引值之 title。 // {String}title || {Array}[ API_URL, title ] if (options.get_continue) { // 在多人共同編輯的情況下,才需要每次重新 load page。 get_continue(Array.isArray(options.get_continue) // ? options.get_continue : [ action[0], options.get_continue ], { type : type, session : session, continue_key : session.continue_key, callback : function(continuation_data) { if (continuation_data = continuation_data[continue_from]) { library_namespace.info('get_list: continue from [' + continuation_data + '] via page'); // 注意: 這裡會改變 options! // 刪掉標記,避免無窮迴圈。 delete options.get_continue; // 設定/紀錄後續檢索用索引值,避免無窮迴圈。 options.next_mark // [continue_from] = continuation_data; // console.trace(options.next_mark); get_list(type, title, callback, options); } else { // delete options[continue_from]; library_namespace.debug('Nothing to continue!', 1, 'get_list'); if (typeof callback === 'function') { callback(undefined, new Error( 'Nothing to continue!')); } } } }); return; } continue_from = options[continue_from]; if (false) { library_namespace.debug(type + (title ? ' ' + wiki_API.title_link_of(title) : '') + ': start from ' + continue_from, 2, 'get_list'); } // ------------------------------------------------ // 處理輸入過長的列表。 if (Array.isArray(action[1]) && (options.no_post_data // ? encodeURIComponent(action[1].map(function(page_data) { return wiki_API.title_of(page_data); }).join('|')).length > get_list.slice_chars // : action[1].length > // wiki_API.max_slice_size(session, options/* , action[1] */))) { options.next_title_index = 0; // multiple pages options.multi = true; options.starting_time = Date.now(); var get_next_batch = function(pages, error) { if (error) { callback(null, error); return; } if (!pages) { // The first time running } else if (options.overall_pages) { options.overall_pages.append(pages); } else { pages.titles = action[1]; options.overall_pages = pages; } var latest_batch_title_index = options.next_title_index; if (!(latest_batch_title_index < action[1].length)) { pages = options.overall_pages; delete options.overall_pages; // delete options.multi; callback(pages); return; } if (options.no_post_data) { var slice_chars = 0; do { slice_chars += encodeURIComponent(wiki_API .title_of(action[1][options.next_title_index++]) + '|').length; if (slice_chars > get_list.slice_chars) { options.next_title_index--; if (latest_batch_title_index === options.next_title_index) { library_namespace.error('第一個元素的長度過長!'); options.next_title_index++; } break; } } while (options.next_title_index < action[1].length); } else { options.next_title_index += wiki_API.max_slice_size( session, options); if (options.next_title_index > action[1].length) options.next_title_index = action[1].length; } library_namespace.log_temporary('get_list: ' + type + ' ' + latest_batch_title_index + '/' + action[1].length + wiki_API.estimated_message(latest_batch_title_index, action[1].length, options.starting_time)); get_list(type, [ action[0], action[1].slice(latest_batch_title_index, options.next_title_index) ], get_next_batch, options); }; get_next_batch(); return; } // ------------------------------------------------ if (!library_namespace.is_Object(action[1]) || wiki_API.is_page_data(action[1])) { action[1] = action[1] ? '&' // allpages 不具有 title。 + (parameter === get_list.default_parameter ? prefix : '') // 不能設定 wiki_API.query.title_param(action, true),有些用 title 而不用 // titles。 // e.g., 20150916.Multiple_issues.v2.js + wiki_API.query.title_param(action[1]/* , true, options.is_id */) : ''; if (typeof title_preprocessor === 'function') { // title_preprocessor(title_parameter) library_namespace.debug('title_parameter: [' + action[1] + ']', 3, 'get_list'); action[1] = title_preprocessor(action[1], options); library_namespace.debug('→ [' + action[1] + ']', 3, 'get_list'); } } else if (!(KEY_generator_title in action[1]) && !type.startsWith('all')) { // Should be a generator title library_namespace .error('get_list: You should use generator_parameters() to create a generator title!'); console.trace([ type, action ]); } action[1] = library_namespace.Search_parameters(action[1]); // console.trace(action); action[1].action = 'query'; action[1][parameter] = type; // 處理數目限制 limit。 // No more than 500 (5,000 for bots) allowed. if (options.limit >= 0 || options.limit === 'max') { // @type integer or 'max' // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Brevisions action[1][prefix + 'limit'] = options.limit; } // next start from here. if (false && continue_from) { // allpages 的 apcontinue 為 title,需要 encodeURIComponent()。 action[1][prefix + 'continue'] // 未處理 allpages 的 escape 可能造成 HTTP status 400。 // = encodeURIComponent(continue_from); continue_from; } // console.trace(options.next_mark); for (continue_from in options.next_mark) { if (continue_from !== 'continue') { action[1][continue_from] = options.next_mark[continue_from]; } delete options.next_mark[continue_from]; } if ('namespace' in options) { action[1][prefix + 'namespace'] = options.namespace; } if (options.redirects) { // 舊版毋須 '&redirects=1','&redirects' 即可。 action[1].redirects = 1; } if (options.converttitles) { action[1].converttitles = 1; } // console.trace(options); for ( var parameter in options) { if (parameter.startsWith(prefix)) { var value = options[parameter]; if (library_namespace.is_Date(value)) { // https://www.mediawiki.org/w/api.php?action=help&modules=main#main/datatype/timestamp value = value.toISOString(); } // value = encodeURIComponent(value); action[1][parameter] = value; } } // console.trace(action); set_parameters(action, options); // action = wiki_API.extract_parameters(options, action, true); // console.trace(action); // TODO: 直接以是不是 .startsWith(prefix) 來判定是不是該加入 parameters。 if (!action[0]) action = action[1]; // console.log('get_list: title: ' + title); if (typeof callback !== 'function') { library_namespace.error('callback is NOT function! callback: [' + callback + ']'); library_namespace.debug('可能是想要當作 wiki instance,卻未設定好,直接呼叫了 ' // TODO: use module_name + library_namespace.Class + '.wiki?\ne.g., 想要 var wiki = ' + library_namespace.Class + '.wiki(user, password) 卻呼叫了 var wiki = ' + library_namespace.Class + '.wiki?', 3); return; } // console.trace(action); var post_data; if (!options.no_post_data) { post_data = action[1]; action[1] = undefined; } // console.trace([ action, post_data ]); wiki_API.query(action, // treat as {Function}callback or {Object}wiki_API.work config. function(data, error) { if (false) { console.trace(data && (JSON.stringify(data).slice(0, 200) + '... (' + JSON.stringify(data).length + ')'), data, error); } if (library_namespace.is_debug(2) // .show_value() @ interact.DOM, application.debug && library_namespace.show_value) { library_namespace.show_value(data, 'get_list: ' + type); } if (error) { callback(undefined, error); } var // {Array}page_list pages = [], // 取得列表後,設定/紀錄新的後續檢索用索引值。 // https://www.mediawiki.org/wiki/API:Query#Backwards_compatibility_of_continue // {Object}next_index: 後續檢索用索引值。 next_index = data && (data['continue'] || data['query-continue']); if (library_namespace.is_Object(next_index)) { pages.next_index = next_index; if ('query-continue' in data) { // style of 2014 CE. 例如: // {backlinks:{blcontinue:'[0|12]'}} for ( var type_index in next_index) { Object .assign(options.next_mark, next_index[type_index]); } } else { // 2021 CE. e.g., // {continue: { blcontinue: '0|123', continue: '-||' }} Object.assign(options.next_mark, next_index); // 因為新的 options.next_mark 無法傳遞到 caller,因此不可使用: // options.next_mark = next_index; } library_namespace.debug('next index of ' + type + ': ' + JSON.stringify(options.next_mark), 2, 'get_list'); if (library_namespace.is_debug(2) // .show_value() @ interact.DOM, application.debug && library_namespace.show_value) { library_namespace.show_value(next_index, 'get_list: get the continue value'); } if (options.limit === 'max' && type.includes('users')) { library_namespace.debug( // 'Too many users so we do not get full list' // + (options.augroup ? ' of [' + options.augroup + ']' : '') + '!', 1, 'get_list'); // 必須重複手動呼叫。 } } else if (library_namespace.is_Object(data) // ↑ 在 503 的時候 data 可能是字串。 && ('batchcomplete' in data)) { // ↑ check "batchcomplete" var keyword_continue = get_list.type[type]; if (keyword_continue) { if (Array.isArray(keyword_continue)) { keyword_continue = keyword_continue[0]; } // e.g., "cmcontinue" keyword_continue += 'continue'; if (keyword_continue in options.next_mark) { library_namespace.debug('去除已經不需要的檢索用索引值。', 3, 'get_list'); // needless. delete options.next_mark[keyword_continue]; } } } // 紀錄清單類型。 // assert: overwrite 之屬性不應該是原先已經存在之屬性。 pages.list_type = type; if ('namespace' in options) pages.namespace = options.namespace; if (is_api_and_title(title, true)) { title = title[1]; } if (wiki_API.is_page_data(title)) { title = title.title; } if (!Array.isArray(title)) { // 包含 {generator:'categorymembers',gcmtitle:'Category:name'} pages.title = title; } if (!data || !data.query) { if (data && ('batchcomplete' in data) && Array.isArray(title) && title.length === 0) { library_namespace.debug('No page input for ' + type, 3, 'get_list'); callback(pages); return; } library_namespace.error('get_list: Unknown response: [' + (typeof data === 'object' && typeof JSON !== 'undefined' ? JSON .stringify(data) : data) + ']'); callback(pages, data); return; } function run_for_each(error) { // console.trace(error); if (false) { console.trace([ options.abort_operation, options.for_each_page ]); } if (options.abort_operation || typeof options.for_each_page !== 'function') { callback(pages, error); return; } // console.trace(pages); var promises = []; // run for each item function set_promises(item, operator) { if (typeof operator !== 'string' || typeof options[operator] !== 'function') { operator = 'for_each_page'; } // assert: typeof options[operator] === 'function' try { if (false && library_namespace .is_async_function(options.for_each_page)) { // eval(get_list_async_code); // console.log(options); return options.abort_operation; } var result = options[operator](item); // console.trace(result); if (result === wiki_API_list.exit) { options.abort_operation = true; return true; } if (library_namespace.is_thenable(result)) { promises.push(result); } } catch (e) { library_namespace.error(e); error = error || e; return true; } } // console.trace(options.for_each_slice); if (options.for_each_slice) { set_promises(pages, 'for_each_slice'); } if (options.for_each_page !== options.for_each_slice && !options.abort_operation) { pages.some(set_promises); } // console.trace(promises); // 注意: arguments 與 get_list() 之 callback 連動。 // 2016/6/22 change API 應用程式介面變更 of callback(): // (title, titles, pages) → (pages, titles, title) // 2019/8/7 change API 應用程式介面變更 of callback(): // (pages, titles, title) → (pages, error) // 按照需求程度編配/編排 arguments。 // 因為 callback 所欲知最重要的資訊是 pages,因此將 pages 置於第一 argument。 if (promises.length === 0) { // console.trace(error); callback(pages, error); return; } // library_namespace.set_debug(3); var _promise = Promise.all(promises).then(function(results) { // console.trace(results); if (results.some(function(result) { return result === wiki_API_list.exit; })) { options.abort_operation = true; } }, function(e) { // `error` will record the first error. error = error || e; }).then(function() { // console.trace(promises); return Promise.allSettled(promises); }).then(function(results) { // console.trace(results); if (results.some(function(result) { return result.value === wiki_API_list.exit; })) { options.abort_operation = true; } // console.trace(promises); // console.trace(session.running); // console.trace(session); // console.trace(error); callback(pages, error); }); session.next(_promise); // console.trace(session); } /** * for redirects: e.g., <code> {"batchcomplete":"","query":{"redirects":[{"from":"Category:言語別分類","to":"Category:言語別"}],"pages":{"1664588":{"pageid":1664588,"ns":14,"title":"Category:言語別","redirects":[{"pageid":4005079,"ns":14,"title":"Category:言語別分類"}]}}},"limits":{"redirects":5000}} </code> */ if (type !== 'redirects' && data.query[type]) { data = data.query[type]; // 一般情況。 if (Array.isArray(data)) { // console.log(options); var page_filter = options.page_filter // 不採用 `options.filter`,預防誤用。 // || options.filter ; if (page_filter) { // console.trace(page_filter); } if (typeof page_filter === 'function') { // function page_filter(page_data){return passed;} data = data.filter(page_filter); } if (type === 'exturlusage' && options.combine_pages) { // 處理有同一個頁面多個網址的情況。 data = combine_by_page(data, 'url'); } pages = Object.assign(data, pages); // console.assert(Array.isArray(pages)); } else if (data.results) { // e.g., // https://en.wikipedia.org/w/api.php?action=query&list=querypage&qppage=MediaStatistics&qplimit=max&format=json&utf8 if (typeof page_filter === 'function') { // function page_filter(page_data){return passed;} data.results = data.results.filter(page_filter); } pages = Object.assign(data.results, pages); pages.data = data; // console.assert(Array.isArray(pages)); } else { // e.g., .userinfo('*') pages = Object.assign(data, pages); // console.assert(library_namespace.is_Object(pages)); } if (get_list.post_processor[type]) { if (Array.isArray(pages)) pages.forEach(get_list.post_processor[type]); else get_list.post_processor[type](pages); } if (Array.isArray(pages)) { library_namespace.debug(wiki_API.title_link_of(title) + ': ' + pages.length + ' page(s)', 2, 'get_list'); } run_for_each(); return; } if (data.query.normalized) pages.normalized = data.query.normalized; // console.log(data.query); data = data.query.pages; // console.trace(data); // console.trace(options.page_filter); for ( var pageid in data) { var page = data[pageid]; if (typeof options.page_filter === 'function' && !options.page_filter(page)) { continue; } if (!(type in page)) { // error! continue; } var page_list = page[type]; // usually Array.isArray(page_list); // library_namespace.is_Object(page_list) for categoryinfo if (Array.isArray(page_list)) { // page_list.title = page.title; Object.assign(page_list, page); delete page_list[type]; } else { page_list = page; } pages.push(page_list); library_namespace.debug('[' + page.title + ']: ' + page_list.length + ' page(s)', 1, 'get_list'); } // console.trace(pages); if (pages.length === 1 && !options.multi) { // Object.assign(pages[0], pages); Object.keys(pages).forEach(function(key) { if (key !== '0') pages[0][key] = pages[key]; }); pages = pages[0]; run_for_each(); return; } if (pages.length === 0) { library_namespace.debug('No [' + type + '] of ' + wiki_API.title_link_of(title), 1, 'get_list'); // console.trace(data); callback(pages/* , new Error('No page got!') */); return; } // For multi-page-list library_namespace.debug(pages.length + ' ' + type + ' got!', 1, 'get_list'); // 紀錄 titles。 .original_title if (pages.title !== title) pages.titles = title; run_for_each(); }, post_data, options); } get_list.slice_chars = 7800; get_list.post_processor = { usercontribs : function(item, index, pages) { var comment = item.comment; if (!comment) return; // https://translatewiki.net/wiki/MediaWiki:Logentry-move-move_redir/en // https://translatewiki.net/wiki/MediaWiki:Logentry-move-move/en // "User moved page [[From]] to [[To]] over redirect: summary" var matched = comment .match(/ moved page \[\[(.+?)\]\] to \[\[(.+?)\]\]( over redirect)?/); if (!matched) return; if (item.from || item.to) { library_namespace .warn('usercontribs: There is already item.from or item.to!'); return; } item.from = matched[1]; item.to = matched[2]; if (matched[3]) item.redirect = true; } }; // const: 基本上與程式碼設計合一,僅表示名義,不可更改。 get_list.default_parameter = 'list'; // 把單數改成複數。 function title_to_plural(title_parameter/* , options */) { // console.trace(title_parameter); return title_parameter.replace(/^&title=/, '&titles=') // .replace(/^&pageid=/, '&pageids='); } /** * All MediaWiki list types supported by this library. * * @type {Object} * * @see https://www.mediawiki.org/wiki/API:Lists/All * https://www.mediawiki.org/w/api.php?action=help&modules=query */ get_list.type = { // list_type : [ {String}prefix, {String}:query=prop|list, // {Function}title_preprocessor ] // 'type name' : 'abbreviation 縮寫 / prefix' (parameter : // get_list.default_parameter) // 按標題排序列出指定的 namespace 的頁面 title。 // 可用來遍歷所有頁面。 // includes redirection 包含重定向頁面. // @see traversal_pages() // https://www.mediawiki.org/wiki/API:Allpages // 警告: 不在 Wikimedia Toolforge 上執行 allpages 速度太慢。但若在 // Wikimedia Toolforge,當改用 database。 allpages : 'ap', // https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Ballimages // .allimages(['from','to']) // .allimages('from') // .allimages([,'to']) // .allimages(['2011-08-01T01:39:45Z','2011-08-01T01:45:45Z']) // .allimages('2011-08-01T01:39:45Z') // .allimages([,'2011-08-01T01:45:45Z']) allimages : [ 'ai', , function(title_parameter) { // console.trace([title_parameter]); // e.g., .allimages('2011-08-01T01:39:45Z'): // '&aititle=2011-08-01T01%3A39%3A45Z' // .allimages(['2011-08-01T01:39:45Z','2011-08-01T01:45:45Z']): // '&aititle=2011-08-01T01%3A39%3A45Z%7C2011-08-01T01%3A45%3A45Z' return title_parameter.replace(/^&aititle=([^&]+)/, // function(all, parameter) { parameter = decodeURIComponent(parameter); var matched = parameter.split('|'); // console.trace(matched); if (matched.length !== 1 && matched.length !== 2) { return all; } if (matched[0] && !Date.parse(matched[0]) // || matched[1] && !Date.parse(matched[1])) { return (matched[0] ? '&aifrom=' // + encodeURIComponent(matched[0]) : '') // + (matched[1] ? '&aito=' // + encodeURIComponent(matched[1]) : ''); } return '&aisort=timestamp' + (matched[0] ? '&aistart=' // + new Date(matched[0]).toISOString() : '') // + (matched[1] ? '&aiend=' // + new Date(matched[1]).toISOString() : ''); }); } ], // https://www.mediawiki.org/wiki/API:Alllinks // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Balllinks alllinks : 'al', // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Ballusers allusers : 'au', // TODO: // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Ballcategories allcategories : 'ac', // TODO: // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Ballredirects allredirects : 'ar', /** * 為頁面標題執行前綴搜索。ページ名の先頭一致検索を行います。<br /> * <code> // 注意: arguments 與 get_list() 之 callback 連動。 CeL.wiki.prefixsearch('User:Cewbot/log/20151002/', function(pages, error){ console.log(pages); }, {limit:'max'}); wiki_instance.prefixsearch('User:Cewbot', function(pages, error){ console.log(pages); }, {limit:'max'}); * </code> * * @see https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bprefixsearch */ prefixsearch : [ 'ps', , function(title_parameter) { return title_parameter.replace(/^&pstitle=/, '&pssearch='); } ], // 取得連結到 [[title]] 的頁面。 // リンク元 // e.g., [[name]], [[:Template:name]]. // https://www.mediawiki.org/wiki/API:Backlinks backlinks : 'bl', // Find all pages that embed (transclude) the given title. // 取得所有[[w:zh:Wikipedia:嵌入包含]] title 的頁面。 (transclusion, inclusion) // 参照読み込み // e.g., {{Template name}}, {{/title}}. // 設定 title 'Template:tl' 可取得使用指定 Template 的頁面。 // https://en.wikipedia.org/wiki/Wikipedia:Transclusion // https://www.mediawiki.org/wiki/API:Embeddedin // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bembeddedin embeddedin : 'ei', // 回傳連結至指定頁面的所有重新導向。 Returns all redirects to the given pages. // 転送ページ // Warning: 採用 wiki_API.redirects_here(title) 才能追溯重新導向的標的。 // wiki.redirects() 無法追溯重新導向的標的! // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bredirects // @since 2019/9/11 redirects : [ 'rd', 'prop', title_to_plural ], // 取得所有使用 file 的頁面。 // title 必須包括File:前綴。 // e.g., [[File:title.jpg]]. // https://www.mediawiki.org/wiki/API:Imageusage imageusage : 'iu', // https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bimageinfo imageinfo : [ 'ii', 'prop', title_to_plural ], // https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bstashimageinfo stashimageinfo : [ 'sii', 'prop', title_to_plural ], // https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Bvideoinfo videoinfo : [ 'vi', 'prop', title_to_plural ], // https://commons.wikimedia.org/w/api.php?action=help&modules=query%2Btranscodestatus // 列出在指定分類中的所有頁面。 // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bcategorymembers // @see [[mw:Help:Tracking categories|追蹤分類]] categorymembers : [ 'cm', , function(title_parameter) { // 要列舉的分類(必需)。必須包括Category:前綴。不能與cmpageid一起使用。 if (/^&cmtitle=(Category|分類|分类|カテゴリ|분류)%3A/ig // @see PATTERN_category @ CeL.wiki .test(title_parameter)) { return title_parameter; } return title_parameter.replace(/^&cmtitle=/, '&cmtitle=Category:'); } ], // Returns information about the given categories. // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bcategoryinfo categoryinfo : [ 'ci', 'prop', function(title_parameter, options) { // There is no cilimit. delete options.limit; return title_to_plural(title_parameter); } ], // List all categories the pages belong to. // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bcategories categories : [ 'cl', 'prop', title_to_plural ], // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Brecentchanges recentchanges : 'rc', // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Busercontribs // wiki.usercontribs(user_name,function(list){console.log(list);},{limit:80}); // get new → old usercontribs : [ 'uc', , function(title_parameter, options) { if (!options.ucdir && options.ucend - options.ucstart > 0) { library_namespace.warn( // 'usercontribs: Change ucdir to "newer", oldest first.'); options.ucdir = 'newer'; // console.trace(title_parameter, options); } return title_parameter.replace(/^&uctitle=/, '&ucuser='); } ], // 'type name' : [ 'abbreviation 縮寫 / prefix', 'parameter' ] // ** 可一次處理多個標題,但可能較耗資源、較慢。 // TODO: // **暫時使用wiki_API.langlinks(),因為尚未整合,在跑舊程式時會有問題。 NYI_langlinks : [ 'll', 'prop', function(title_parameter, options) { // console.trace(title_parameter); if (options && options.lang && typeof options.lang === 'string') { return title_parameter + '&lllang=' + options.lang; } return title_parameter; } ], // linkshere: 取得連結到 [[title]] 的頁面。 // [[Special:Whatlinkshere]] // [[使用說明:連入頁面]] // https://zh.wikipedia.org/wiki/Help:%E9%93%BE%E5%85%A5%E9%A1%B5%E9%9D%A2 linkshere : [ 'lh', 'prop', title_to_plural ], // 取得所有使用 title (e.g., [[File:title.jpg]]) 的頁面。 // 基本上同 imageusage。 fileusage : [ 'fu', 'prop', title_to_plural ], // 列舉包含指定 URL 的頁面。 [[Special:LinkSearch]] // https://www.mediawiki.org/wiki/API:Exturlusage // 注意: 可能會有同一個頁面多個網址的情況!可使用 options.combine_pages。 exturlusage : [ 'eu', , function(title_parameter) { // console.log(decodeURIComponent(title_parameter)); return title_parameter.replace(/^&eutitle=([^=&]*)/, // function($0, link) { if (link) { var matched = decodeURIComponent(link) // .match(/^([a-z]+):\/\/(.+)$/i); if (matched) { // `http://www.example.com/path/` // → http + `www.example.com` link = matched[2].replace(/\/.*$/, '') + '&euprotocol=' // + encodeURIComponent(matched[1]); } } else { link = ''; } return '&euquery=' + link; }); } ], // 回傳指定頁面的所有連結。 // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Blinks links : [ 'pl', 'prop', title_to_plural ], // 取得透過特殊頁面 QueryPage-based 所提供的清單。 querypage : [ 'qp', , function(title_parameter) { return title_parameter.replace(/^&qptitle=/, '&qppage='); } ], // [[Help:Magic words]] 列出所有在 wiki 使用的頁面屬性名稱。 pagepropnames : 'ppn', // 列出使用到指定頁面屬性的所有頁面。 pageswithprop : [ 'pwp', , function(title_parameter) { return title_parameter.replace(/^&pwptitle=/, '&pwppropname='); } ], // 列出變更標記。 tags : [ 'tg', , function(title_parameter) { if (!title_parameter) return '&tgprop=displayname|description' // all 要取得的屬性。 + '|hitcount|defined|source|active'; return title_parameter.replace(/^&tgtitle=/, '&tgprop='); } ], // 取得有關使用者清單的資訊。 // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Busers users : [ 'us', , function(title_parameter) { return title_parameter.replace(/^&ustitle=/, '&ususers='); } ], // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bglobaluserinfo globaluserinfo : [ 'gui', 'meta', function(title_parameter) { // console.trace(title_parameter); return title_parameter.replace(/^&title=/, '&guiuser='); } ], // .userinfo(['rights']) // Get information about the current user. userinfo : [ 'ui', 'meta', function(title_parameter) { // console.trace(title_parameter); return title_parameter.replace(/^&title=/, '&uiprop='); } ], // 從日誌中獲取事件。 // result: new → old logevents : 'le' }; // ------------------------------------------------------------------------ var KEY_page_list = typeof Symbol === 'function' ? Symbol('page list') : 'page list'; /** * 取得完整 list 後才作業。 * * @param {String}target * page title 頁面標題。 * @param {Function}callback * 回調函數。 callback(pages, target, options) * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 */ function wiki_API_list(target, callback, options) { // 前置處理。 options = library_namespace.new_options(options); var session = wiki_API.session_of_options(options); if (!options.initialized) { // console.trace(options); if (!session) { session = new wiki_API; } if (!options.type) { options.type = wiki_API_list.default_type; } options.initialized = true; } if (!options.limit) options.limit = 'max'; if (!options.next_mark) { // initialization options.next_mark = Object.create(null); } // 對於太大的 {Array}target,會在 get_list() 中自行處理。 // console.trace(target, options); session[options.type](target, // 注意: arguments 與 get_list() 之 callback 連動。 function wiki_API_list_callback(pages, error) { // console.trace([ target, pages ]); if (pages) { library_namespace.debug('Get ' + pages.length + ' ' + options.type + ' pages of ' + pages.title, 2, 'wiki_API_list'); } else { // has error! pages = []; } if (error) { console.trace(error); pages.error = error; } if (typeof options.callback === 'function') { // options.callback() 為取得每一階段清單時所會被執行的函數。 // 注意: arguments 與 get_list() 之 callback 連動。 // page_list options.callback(pages, target, options); } // 設定了 options.for_each_page 時,callback() 不會傳入 list! // 用意在省記憶體。options.for_each_page() 執行過就不用再記錄了。 if (Array.isArray(options[KEY_page_list])) { if (!options.for_each_page || options.get_list) { options[KEY_page_list].append(pages); // console.trace([ pages.title, pages[0], // wiki_API.title_link_of(pages[0]) ]); var message = '[' + options.type + '] '; if (!target) { // e.g., allcategories } else if (Array.isArray(target)) { message += target.length + ' targets:'; } else if (target[wiki_API.KEY_generator_title]) { message += 'of [' + target.generator + '] ' // + wiki_API.title_link_of( // target[wiki_API.KEY_generator_title]); } else { message += wiki_API.title_link_of(target); } message += ' ' // gettext_config:{"id":"$1-results"} + gettext('%1 {{PLURAL:%1|result|results}}', // options[KEY_page_list].length) // + (options.page_filter ? ' (filtered)' : '') // + ': +' + pages.length; if (pages.title && pages.length > 0) { message += ' ' + wiki_API.title_link_of( // pages[0].title || pages[0]); if (pages.length > 1) { message += '–' + wiki_API.title_link_of( // pages.at(-1).title || pages.at(-1)); } } if (pages.length === 0 && options.next_mark) { // 增加辨識度。 for ( var continue_from in options.next_mark) { if (continue_from !== 'continue') { message += ' (' + continue_from + ': ' // + String(options.next_mark[continue_from]) // .replace(/^(.{10})[\s\S]*?(.{8})$/, '$1...$2') + ')'; break; } } } // library_namespace.log_temporary() library_namespace.info(message); } else { // Only preserve length property. options[KEY_page_list].length += pages.length; } } else if (!options[KEY_page_list] || options[KEY_page_list].length === 0) { if (!options.for_each_page || options.get_list) { } else { // Only preserve length property. var length = pages.length; pages.truncate(); pages.length = length; } if (!options[KEY_page_list]) { options[KEY_page_list] = pages; } else { // assert: options[KEY_page_list].length === 0 Object.assign(options[KEY_page_list], pages); } } else if (!pages.error) { pages.error = new Error( 'options[KEY_page_list] has been set up!'); } // console.log(pages.next_index); // console.log(options.next_mark); if (pages.next_index && !options.abort_operation && !(options[KEY_page_list].length >= options.limit)) { library_namespace.debug(wiki_API.title_link_of(target) // + ': 尚未取得所有清單,因此繼續取得下一階段清單。', 1, 'wiki_API_list'); if (false) { console.trace([ wiki_API.title_link_of(target), options.next_mark ]); } setImmediate(wiki_API_list, target, callback, options); } else { library_namespace.debug(wiki_API.title_link_of(target) // + ': run callback after all list got or abort operation.', 1, 'wiki_API_list'); // reset .next_mark // session.next_mark = Object.create(null); // console.trace(options.for_each_page); // 警告: options[KEY_page_list] 與 target 並非完全一對一對應! if (!options.for_each_page) { callback(options[KEY_page_list], target, options); } else { // `options.for_each_page` 可能還在執行中,例如正在取得頁面內容; // 等到 `options.for_each_page` 完成之後才執行 callback。 session.run(callback, options[KEY_page_list], target, options); } } }, // 引入 options,避免 get_list() 不能確實僅取指定 namespace。 options); } // `options.for_each_page` 設定直接跳出。 `CeL.wiki.list.exit` wiki_API_list.exit = [ 'wiki_API_list.exit: abort the operation' ]; wiki_API_list.default_type = 'embeddedin'; // supported type list wiki_API_list.type_list = []; // ------------------------------------------------------------------------ // setup wiki_API.prototype.methods (function wiki_API_prototype_methods() { // 登記 methods。 var methods = wiki_API.prototype.next.methods; for ( var name in get_list.type) { methods.push(name); wiki_API_list.type_list.push(name); wiki_API[name] = get_list.bind(null, name); } // add method to wiki_API.prototype // setup other wiki_API.prototype methods. methods.forEach(function(method) { library_namespace.debug('add action to wiki_API.prototype: ' + method, 2); wiki_API.prototype[method] = function wiki_API_prototype_method() { // assert: 不可改動 method @ IE! var args = [ method ]; Array.prototype.push.apply(args, arguments); if (library_namespace.is_debug() && !this.running) { // console.trace(method + ': ' + this.running); } if (this.run_after_initializing) { library_namespace.debug('It is now initializing. 添加初始程序: ' + args[0], 1, 'wiki_API_prototype_methods'); } // ---------------------------------------- if (library_namespace.is_debug(3)) { try { library_namespace.debug('add action: ' + args.map(JSON.stringify).join('<br />\n'), 3, 'wiki_API.prototype.' + method); } catch (e) { // TODO: handle exception } } var previous_action = this.actions.at(-1); this.actions.push(args); // console.trace([ this.running, this.actions.length, args ]); // ---------------------------------------- // 對於各種連續操作的處理。 // 做個預先處理,以保證 previous_action[3] 是options。 if (method === 'page' // @see wiki_API.prototype.next.page && library_namespace.is_Object(args[2]) && !args[3]) { // 直接輸入 options,未輸入 callback。 args.splice(2, 0, null); } if (method === 'edit' && (!args[2] || !('page_to_edit' in args[2]))) { // console.trace('No options.page_to_edit set!'); // console.log(this.actions); if (!args[2]) { args[2] = previous_action[0] === 'page' && previous_action[3] || Object.create(null); } // 自動配給一個。 // @see set_page_to_edit(options, page_data) args[2].page_to_edit = wiki_API.VALUE_set_page_to_edit; } if (method === 'edit' &