UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,668 lines (1,496 loc) 163 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): page, revisions * * @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.page', require : 'data.native.' // CeL.data.fit_filter() + '|data.' // CeL.date.String_to_Date(), Julian_day(), .to_millisecond(): CeL.data.date + '|data.date.' // for library_namespace.directory_exists + '|application.storage.' // for library_namespace.get_URL + '|application.net.Ajax.' + '|application.net.wiki.' // load MediaWiki module basic functions + '|application.net.wiki.namespace.' // for wiki_API.estimated_message() // + '|application.net.wiki.task.' // + '|application.net.wiki.query.|application.net.wiki.Flow.', // 設定不匯出的子函式。 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 /** node.js file system module */ node_fs = library_namespace.platform.nodejs && require('fs'); var /** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */ NOT_FOUND = ''.indexOf('_'); var gettext = library_namespace.cache_gettext(function(_) { gettext = _; }); // ------------------------------------------------------------------------ // wiki.page() 範例。 if (false) { CeL.wiki.page('史記', function(page_data) { CeL.show_value(page_data); }); wiki.page('巴黎協議 (消歧義)', { query_props : 'pageprops' }); // wiki.last_page // for "Date of page creation" 頁面建立日期 @ Edit history 編輯歷史 @ 頁面資訊 // &action=info wiki.page('巴黎協議', function(page_data) { // e.g., '2015-12-17T12:10:18.000Z' console.log(CeL.wiki.content_of.edit_time(page_data)); }, { rvdir : 'newer', rvprop : 'timestamp', rvlimit : 1 }); wiki.page('巴黎協議', function(page_data) { // {Date}page_data.creation_Date console.log(page_data); }, { get_creation_Date : true }); // for many pages, e.g., more than 200, please use: wiki.work({ // redirects : 1, each : for_each_page_data, last : last_operation, no_edit : true, page_options : { // multi : 'keep index', // converttitles : 1, redirects : 1 } }, page_list); // 組合以取得資訊。 wiki.page(title, function(page_data) { console.log(page_data); }, { prop : 'revisions|info', // rvprop : 'ids|timestamp', // https://www.mediawiki.org/w/api.php?action=help&modules=query%2Binfo // https://www.mediawiki.org/wiki/API:Info additional_query : 'inprop=talkid|subjectid' + '|preload|displaytitle|varianttitles' }); // 組合以取得資訊。 wiki.page(title, function(page_data) { console.log(page_data); if ('read' in page_data.actions) console.log('readable'); }, { prop : 'info', // https://www.mediawiki.org/wiki/API:Info additional_query : 'inprop=intestactions&intestactions=read' // + '&intestactionsdetail=full' }); // Get all summaries <del>and diffs</del> wiki.page('Heed (cat)', function(page_data) { console.log(page_data); }, { rvprop : 'ids|timestamp|comment', rvlimit : 'max' }); } // assert: !!KEY_KEEP_INDEX === true var KEY_KEEP_INDEX = 'keep index', // assert: !!KEY_KEEP_ORDER === true KEY_KEEP_ORDER = 'keep order'; // https://www.mediawiki.org/wiki/API:Query#Query_modules function setup_query_modules(title, callback, options) { var session = wiki_API.session_of_options(options); // console.trace(session.API_parameters.query); wiki_API_page.query_modules = []; session.API_parameters.query.parameter_Map // Should be [ 'prop', 'list', 'meta', ... ] .forEach(function(parameters, key) { if (parameters.limit && parameters.submodules) wiki_API_page.query_modules.push(key); }); library_namespace.info([ // 'setup_query_modules: ' + wiki_API.site_name(session) + ': ', { T : [ // gettext_config:{"id":"found-$2-query-modules-$1"} 'Found %2 query {{PLURAL:%2|module|modules}}: %1', // gettext_config:{"id":"Comma-separator"} wiki_API_page.query_modules.join(gettext('Comma-separator')), // wiki_API_page.query_modules.length ] } ]); wiki_API_page.apply(this, arguments); } // ---------------------------------------------------- function set_invalid_page(query_result_buffer, query_result, value) { if (!(query_result_buffer.next_invalid_page < 0)) { // invalid page id starts from -1 query_result_buffer.next_invalid_page = -1; } while (query_result_buffer.next_invalid_page in query_result.pages) query_result_buffer.next_invalid_page--; // assert: 之前已經有無效頁面存在,因此 .next_invalid_page < -1 // console.trace(query_result_buffer.next_invalid_page, value); query_result.pages[query_result_buffer.next_invalid_page--] = value; } // merge_query_results() function combine_query_results(query_result_buffer) { var query_result; // assert: Array.isArray(query_result_buffer) while (query_result_buffer.length > 0) { var this_query_result = query_result_buffer.shift(); if (!query_result) { query_result = this_query_result; continue; } // assert: {Object}query_result for ( var property_name in this_query_result) { var value = this_query_result[property_name]; if (!(property_name in query_result)) { query_result[property_name] = value; continue; } if (typeof value !== 'object') { query_result.error = new Error( 'combine_query_results: 獲得了 {' + typeof value + '},非 {Object} 的資料!'); return query_result; } if (Array.isArray(value)) { if (!query_result[property_name]) { query_result.error = new Error( 'combine_query_results: 資料型態從' + typeof value + '}轉成了 Array!'); return query_result; } query_result[property_name].append(value); continue; } for ( var key in value) { // Object.assign(query_result[property_name], value); if (key in query_result[property_name]) { // console.trace(query_result); // console.trace(this_query_result); if (property_name === 'pages' && key < 0) { // 無效的頁面可以直接換個id填入。 set_invalid_page(query_result_buffer, query_result, value[key]); continue; } if (JSON.stringify(query_result[property_name][key]) !== JSON .stringify(value[key])) { library_namespace.warn('combine_query_results: ' + '以新的資料覆蓋舊的 query.' + property_name + '[' + key + ']'); console.trace(query_result[property_name][key], '→', value[key]); } } query_result[property_name][key] = value[key]; } } } return query_result; } // ---------------------------------------------------- /** * 讀取頁面內容,取得頁面源碼。可一次處理多個標題。 * * 前文有 wiki.page() 範例。 * * 注意: 用太多 CeL.wiki.page() 並行處理,會造成 error.code "EMFILE"。 * * TODO: * https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates * or https://www.mediawiki.org/w/api.php?action=help&modules=parse * * @example <code> // 前文有 wiki.page() 範例。 </code> * * @param {String|Array}title * title or [ {String}API_URL, {String}title or {Object}page_data ] * @param {Function}[callback] * 回調函數。 callback(page_data, error) { page_data.title; var * content = CeL.wiki.content_of(page_data); } * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @see https://www.mediawiki.org/w/api.php?action=help&modules=query%2Brevisions */ function wiki_API_page(title, callback, options) { if (wiki_API.need_get_API_parameters('query+revisions', options, wiki_API_page, arguments)) { return; } var action = { action : 'query' }; if (wiki_API.need_get_API_parameters(action, options, setup_query_modules, arguments)) { return; } if (typeof callback === 'object' && options === undefined) { // shift arguments options = callback; callback = undefined; } // 正規化並提供可隨意改變的同內容參數,以避免修改或覆蓋附加參數。 options = library_namespace.new_options(options); if (false && library_namespace.is_Set(title)) { title = Array.from(title); } // console.log('title: ' + JSON.stringify(title)); if (options.get_creation_Date) { // 警告:僅適用於單一頁面。 wiki_API_page(title, function(page_data, error) { if (error || !wiki_API.content_of.page_exists(page_data)) { // console.trace('error? 此頁面不存在/已刪除。'); callback(page_data, error); return; } // e.g., '2015-12-17T12:10:18.000Z' // page_data.revisions[0].timestamp; page_data.creation_Date // CeL.wiki.content_of.edit_time(page_data) = wiki_API.content_of.edit_time(page_data); if (typeof options.get_creation_Date === 'function') { options.get_creation_Date(page_data.creation_Date, page_data); } if (false) { console.log(page_data.creation_Date.format('%Y/%m/%d')); } delete options.get_creation_Date; // 去掉僅有timestamp,由舊至新的.revisions。 delete page_data.revisions; // 若有需要順便取得頁面內容,需要手動設定如: // {get_creation_Date:true,prop:'revisions'} if (('query_props' in options) || ('prop' in options)) { wiki_API_page(title, function(_page_data, error) { // console.trace(title); callback(Object.assign(page_data, _page_data), error); }, options); } else { // console.trace(title); callback(page_data); } }, { rvdir : 'newer', rvprop : 'timestamp', rvlimit : 1 }); return; } if (options.query_props) { var query_props = options.query_props, page_data, // get_properties = function(page) { if (page) { if (page_data) Object.assign(page_data, page); else page_data = page; } var prop; while (query_props.length > 0 // && !(prop = query_props.shift())) ; if (!prop || page_data // && (('missing' in page_data) || ('invalid' in page_data))) { // 此頁面不存在/已刪除。 callback(page_data); } else { library_namespace.debug('Get property: [' + prop + ']', 1, 'wiki_API_page'); options.prop = prop; wiki_API_page(title, get_properties, options); } }; delete options.query_props; if (typeof query_props === 'string') { query_props = query_props.split('|'); } if (Array.isArray(query_props)) { if (!options.no_content) query_props.push('revisions'); get_properties(); } else { library_namespace.error([ 'wiki_API_page: ', { // gettext_config:{"id":"invalid-parameter-$1"} T : [ 'Invalid parameter: %1', '.query_props' ] } ]); throw new Error('wiki_API_page: ' // gettext_config:{"id":"invalid-parameter-$1"} + gettext('Invalid parameter: %1', '.query_props')); } return; } // console.trace(title, arguments); // modules=query&titles= overwrite multi=false options.multi_param = true; action = normalize_title_parameter(title, options); // console.trace(action); if (!action) { library_namespace.error([ 'wiki_API_page: ', { // gettext_config:{"id":"invalid-title-$1"} T : [ 'Invalid title: %1', wiki_API.title_link_of(title) ] } ]); // console.trace(title); callback(undefined, new Error(gettext( // gettext_config:{"id":"invalid-title-$1"} 'Invalid title: %1', wiki_API.title_link_of(title)))); return; throw new Error('wiki_API_page: ' // gettext_config:{"id":"invalid-title-$1"} + gettext('Invalid title: %1', wiki_API.title_link_of(title))); } // console.trace(action); // console.trace(options); if (!wiki_API_page.query_modules // || !wiki_API_page.query_modules.some(function(module) { return module in options; })) { options.prop = 'revisions'; } var get_content = options.prop // {String|Array} && options.prop.includes('revisions'); if (get_content) { var session = wiki_API.session_of_options(options); // 2019 API: // https://www.mediawiki.org/wiki/Manual:Slot // https://www.mediawiki.org/wiki/API:Revisions // 檢測有沒有此項參數。 if (!session || session.API_parameters['query+revisions'].slots) { action[1].rvslots = options.rvslots || 'main'; } // 處理數目限制 limit。單一頁面才能取得多 revisions。多頁面(≤50)只能取得單一 revision。 // https://www.mediawiki.org/w/api.php?action=help&modules=query // titles/pageids: Maximum number of values is 50 (500 for bots). if ('rvlimit' in options) { if (options.rvlimit > 0 || options.rvlimit === 'max') action[1].rvlimit = options.rvlimit; } else if (!action[1].titles && !action[1].pageids) { // assert: action[1].title || action[1].pageid // || action[1].pageid === 0 // default: 僅取得單一 revision。 action[1].rvlimit = 1; } // Which properties to get for each revision get_content = Array.isArray(options.rvprop) // && options.rvprop.join('|') // || options.rvprop || wiki_API_page.default_rvprop; action[1].rvprop = get_content; get_content = get_content.includes('content'); } // 自動搜尋/轉換繁簡標題。 if (!('converttitles' in options)) { options.converttitles = wiki_API.site_name(options, { get_all_properties : true }).language; if (!wiki_API_page.auto_converttitles .includes(options.converttitles)) { delete options.converttitles; } else { options.converttitles = 1; } } if (typeof options.prop === 'string') options.prop = options.prop.split(/[,;|]/); // Which properties to get for the queried pages // 輸入 prop:'' 或再加上 redirects:1 可以僅僅確認頁面是否存在,以及頁面的正規標題。 if (Array.isArray(options.prop)) { options.prop = options.prop.map(function(submodule) { return submodule && String(submodule).trim(); }).filter(function(submodule) { return !!submodule; }); var _arguments = arguments; if (options.prop.some(function(submodule) { return wiki_API.need_get_API_parameters('query+' + submodule, options, wiki_API_page, _arguments); })) { return; } options.prop = options.prop.join('|'); } for ( var parameter in { // e.g., rvdir=newer // Get first revisions rvdir : true, rvcontinue : true, converttitles : true, // e.g., prop=info|revisions // e.g., prop=pageprops|revisions // 沒 .pageprops 的似乎大多是沒有 Wikidata entity 的? prop : true }) { if (parameter in options) { action[1][parameter] = options[parameter]; } } // options.handle_continue_response = true; if (false && library_namespace.is_Object(options.page_options_continue)) { Object.assign(action[1], options.page_options_continue); } // console.trace(action); set_parameters(action, options); // console.trace(action); action[1].action = 'query'; action[1] = wiki_API.extract_parameters(options, action[1], true); // console.trace([ options, action ]); // TODO: // wiki_API.extract_parameters(options, action, true); library_namespace.debug('get url token: ' + action, 5, 'wiki_API_page'); // console.trace([ action, options ]); var post_data = library_namespace.Search_parameters(); // 將<s>過長的</s>標題列表改至 POST,預防 "414 Request-URI Too Long"。 // https://github.com/kanasimi/wikibot/issues/32 // 不同 server 可能有不同 GET 請求長度限制。不如直接改成 POST。 if (Array.isArray(action[1].pageids)) { post_data.pageids = action[1].pageids; delete action[1].pageids; } if (Array.isArray(action[1].titles)) { post_data.titles = action[1].titles; delete action[1].titles; } // console.trace(wiki_API.session_of_options(options)); // console.trace(action); wiki_API.query(action, typeof callback === 'function' // && function process_page(data) { // console.trace('Get page: ' + title); if (library_namespace.is_debug(2) // .show_value() @ interact.DOM, application.debug && library_namespace.show_value) { library_namespace.show_value(data, 'wiki_API_page: data'); } var error = data && data.error; // 檢查 MediaWiki 伺服器是否回應錯誤資訊。 if (error) { library_namespace.error('wiki_API_page: [' // + error.code + '] ' + error.info); /** * e.g., Too many values supplied for parameter 'pageids': the * limit is 50 */ if (data.warnings && data.warnings.query // && data.warnings.query['*']) { library_namespace.warn( // 'wiki_API_page: ' + data.warnings.query['*']); } if (error.code === 'toomanyvalues' && error.limit > 0 // 嘗試自動將所要求的 query 切成小片。 // TODO: 此功能應放置於 wiki_API.query() 中。 // TODO: 將 title 切成 slice,重新 request。 && options.try_cut_slice && Array.isArray(title) // 2: 避免 is_api_and_title(title) && title.length > 2) { var session = wiki_API.session_of_options(options); if (session && !(session.slow_query_limit < error.limit)) { library_namespace.warn([ 'wiki_API_page: ', { // gettext_config:{"id":"reduce-the-maximum-number-of-pages-per-fetch-to-a-maximum-of-$1-pages"} T : [ '調降取得頁面的上限,改成每次最多 %1 個頁面。', error.limit ] } ]); // https://www.mediawiki.org/w/api.php // slow queries: 500; fast queries: 5000 // The limits for slow queries also apply to multivalue // parameters. session.slow_query_limit = error.limit; } options.multi = true; options.slice_size = error.limit; // console.trace(title); wiki_API_page(title, callback, options); return; } callback(data, error); return; } if (false && data.warnings && data.warnings.result /** * <code> // e.g., 2021/5/23: { continue: { rvcontinue: '74756|83604874', continue: '||' }, warnings: { result: { '*': 'This result was truncated because it would otherwise be larger than the limit of 12,582,912 bytes.' } }, query: { pages: { '509': [Object], ... } } } </code> * limit: 12 MB. 此時應該有 .continue。 */ && data.warnings.result['*']) { if (false && data.warnings.result['*'].includes('truncated')) data.truncated = true; library_namespace.warn( // 'wiki_API_page: ' + data.warnings.result['*']); } if (!data || !data.query // assert: data.cached_response && data.query.pages || !data.query.pages && data.query.redirects /** * <code> // e.g., { batchcomplete: '', warnings: { info: { '*': 'Unrecognized value for parameter "inprop": info' } }, query: { interwiki: [ [Object], [Object], [Object] ] } } </code> */ && !data.query.interwiki) { // e.g., 'wiki_API_page: Unknown response: // [{"batchcomplete":""}]' library_namespace.warn([ 'wiki_API_page: ', { // gettext_config:{"id":"unknown-api-response-$1"} T : [ 'Unknown API response: %1', (typeof data === 'object' // && typeof JSON !== 'undefined' // ? JSON.stringify(data) : data) ] } ]); // console.trace(data); // library_namespace.set_debug(6); if (library_namespace.is_debug() // .show_value() @ interact.DOM, application.debug && library_namespace.show_value) library_namespace.show_value(data); callback(undefined, 'Unknown response'); return; } if (options.titles_left) { // console.trace(data); // e.g., Template:Eulipotyphla @ // 20230418.Fix_redirected_wikilinks_of_templates.js if (!options.query_result_buffer) options.query_result_buffer = []; options.query_result_buffer.push(data.query); if (false) { console.trace('get next page slices (' // + options.slice_size + '): ' + options.titles_left); } wiki_API_page(null, callback, options); return; } if (Array.isArray(options.query_result_buffer)) { options.query_result_buffer.push(data.query); data.query // = combine_query_results(options.query_result_buffer); if (data.query.error) { callback(undefined, data.query.error); return; } } // -------------------------------------------- var page_list = [], // index_of_title[title] = index in page_list index_of_title = page_list.index_of_title = Object.create(null), // 標題→頁面資訊映射。 title_data_map[title] = page_data title_data_map = page_list.title_data_map = Object.create(null), // library_namespace.storage.write_file() page_cache_prefix = library_namespace.write_file // && options.page_cache_prefix; var continue_id; if ('continue' in data) { // console.trace(data['continue']); // page_list['continue'] = data['continue']; if (data['continue'] // && typeof data['continue'].rvcontinue === 'string' // && (continue_id = data['continue'].rvcontinue // assert: page_list['continue'].rvcontinue = 'date|oldid'。 .match(/\|([1-9]\d*)$/))) { continue_id = Math.floor(continue_id[1]); } if (false && data.truncated) page_list.truncated = true; } // ------------------------ // https://zh.wikipedia.org/w/api.php?action=query&prop=info&converttitles=zh&titles=A&redirects=&maxlag=5&format=json&utf8=1 // 2020/10/9: for [[A]]→[[B]]→[[A]], we will get // {"batchcomplete":"","query":{"redirects":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}} // 找尋順序應為: // query.normalized[原標題]=正規化後的標題/頁面名稱 // data.query.converted[正規化後的標題||原標題]=繁簡轉換後的標題 // data.query.redirects[繁簡轉換後的標題||正規化後的標題||原標題]=重定向後的標題=必然存在的正規標題 var redirect_from; if (data.query.redirects) { page_list.redirects = data.query.redirects; if (Array.isArray(data.query.redirects)) { page_list.redirect_from // 記錄經過重導向的標題。 = redirect_from = Object.create(null); page_list.redirects.map = Object.create(null); data.query.redirects.forEach(function(item) { redirect_from[item.to] = item.from; page_list.redirects.map[item.from] = item; }); if (!data.query.pages) { data.query.pages = { title : data.query.redirects[0].from }; if (data.query.pages.title === // redirect_from[data.query.redirects[0].to]) { library_namespace.warn([ 'wiki_API_page: ', { // gettext_config:{"id":"circular-redirect-$1↔$2"} T : [ 'Circular redirect: %1↔%2', // wiki_API.title_link_of( // data.query.pages.title), // wiki_API.title_link_of( // data.query.redirects[0].to) ] } ]); data.query.pages.redirect_loop = true; } data.query.pages = { // [wiki_API.run_SQL.KEY_additional_row_conditions] '' : data.query.pages }; } } } var convert_from; if (data.query.converted) { page_list.converted = data.query.converted; if (Array.isArray(data.query.converted)) { page_list.convert_from = convert_from // 記錄經過轉換的標題。 = Object.create(null); page_list.converted.map = Object.create(null); data.query.converted.forEach(function(item) { convert_from[item.to] = item.from; page_list.converted.map[item.from] = item; if (page_list.redirects // && page_list.redirects.map[item.to]) { page_list.redirects.map[item.from] // = page_list.redirects.map[item.to]; } }); } } if (data.query.normalized) { page_list.normalized = data.query.normalized; // console.log(data.query.normalized); page_list.convert_from = convert_from // 記錄經過轉換的標題。 || (convert_from = Object.create(null)); page_list.normalized.map = Object.create(null); data.query.normalized.forEach(function(item) { convert_from[item.to] = item.from; page_list.normalized.map[item.from] = item; if (page_list.redirects // && page_list.redirects.map[item.to]) { page_list.redirects.map[item.from] // = page_list.redirects.map[item.to]; } }); } if (data.query.interwiki) { page_list.interwiki = data.query.interwiki; if (!data.query.pages) data.query.pages = Object.create(null); } // ------------------------ var pages = data.query.pages; // console.log(options); var need_warn = /* !options.no_warning && */!options.allow_missing // 其他 .prop 本來就不會有內容。 && get_content; for ( var pageid in pages) { // 對於 invalid title,pageid 會從 -1 開始排,-2, -3, ...。 var page_data = pages[pageid]; if (!wiki_API.content_of.has_content(page_data)) { if (continue_id && continue_id === page_data.pageid) { // 找到了 page_list.continue 所指之 index。 // effect length page_list.OK_length = page_list.length; // 當過了 continue_id 之後,表示已經被截斷,則不再警告。 need_warn = false; } if (need_warn) { /** * <code> {"title":"","invalidreason":"The requested page title is empty or contains only the name of a namespace.","invalid":""} </code> */ // console.trace(page_data); library_namespace.warn([ 'wiki_API_page: ', { T : [ 'invalid' in page_data // gettext_config:{"id":"invalid-title-$1"} ? 'Invalid title: %1' // 此頁面不存在/已刪除。Page does not exist. Deleted? : 'missing' in page_data // gettext_config:{"id":"does-not-exist"} ? 'Does not exist: %1' // gettext_config:{"id":"no-content"} : 'No content: %1', // (page_data.title // ? wiki_API.title_link_of(page_data) // : 'id ' + page_data.pageid) // + (page_data.invalidreason // ? '. ' + page_data.invalidreason : '') ] } ]); } } else if (page_cache_prefix) { library_namespace.write_file(page_cache_prefix // + page_data.title + '.json', /** * 寫入cache。 * * 2016/10/28 21:44:8 Node.js v7.0.0 <code> DeprecationWarning: Calling an asynchronous function without callback is deprecated. </code> */ JSON.stringify(pages), wiki_API.encoding, function() { // 因為此動作一般說來不會影響到後續操作,因此採用同時執行。 library_namespace.debug( // gettext_config:{"id":"the-cache-file-is-saved"} 'The cache file is saved.', 1, 'wiki_API_page'); }); } title_data_map[page_data.title] = page_data; if (redirect_from && redirect_from[page_data.title] // && !page_data.redirect_loop) { page_data.original_title = page_data.redirect_from // .from_title, .redirect_from_title = redirect_from[page_data.title]; // e.g., "研究生教育" redirects to → "學士後" // redirects to → "深造文憑" while (redirect_from[page_data.original_title]) { page_data.original_title // = redirect_from[page_data.original_title]; } } // 可以利用 page_data.convert_from // 來判別標題是否已經過繁簡轉換與 "_" → " " 轉換。 if (convert_from) { if (convert_from[page_data.title]) { page_data.convert_from // .from_title, .convert_from_title = convert_from[page_data.title]; // 注意: 這邊 page_data.original_title // 可能已設定為 redirect_from[page_data.title] if (!page_data.original_title // 通常 wiki 中,redirect_from 會比 convert_from 晚處理, // 照理來說不應該會到 !convert_from[page_data.original_title] 這邊, // 致使重設 `page_data.original_title`? || !convert_from[page_data.original_title]) { page_data.original_title = page_data.convert_from; } } // e.g., "人民法院_(消歧义)" converted → "人民法院 (消歧义)" // converted → "人民法院 (消歧義)" redirects → "人民法院" while (convert_from[page_data.original_title]) { page_data.original_title // .from_title, .convert_from_title = convert_from[page_data.original_title]; } } index_of_title[page_data.title] = page_list.length; // 注意: 這可能註冊多種不同的標題。 if (page_data.original_title) { // 對於 invalid title,.original_title 可能是 undefined。 title_data_map[page_data.original_title] = page_data; } page_list.push(page_data); } if (page_list.redirects) { page_list.redirects.forEach(function(data) { var to = data.to; while (to in page_list.redirects.map) { // e.g., 美國法典第10卷: [美國法典第十編]→[美國法典第10編] @ [[Template:US // military navbox']] @ // 20230418.Fix_redirected_wikilinks_of_templates.js library_namespace.log('wiki_API_page: ' // + data.from + ': [' + to + ']→[' // + page_list.redirects.map[to].to + ']'); var next__to = page_list.redirects.map[to].to; if (to === next__to) { // e.g., [[愛愛內含光]] 2024/2/12 自己連到自己 break; } to = next__to; } if (!title_data_map[to]) { // console.trace(page_list); error = error // || new Error('No redirects title data: [' // + to + ']←[' + data.from + ']'); return; } // 注意: 這可能註冊多種不同的標題。 title_data_map[data.from] = title_data_map[to]; }); } if (page_list.converted) { page_list.converted.forEach(function(data) { if (!title_data_map[data.to]) { error = error // || new Error('No converted title data: [' // + data.to + ']←[' + data.from + ']'); return; } // 注意: 這可能註冊多種不同的標題。 title_data_map[data.from] = title_data_map[data.to]; }); } if (page_list.normalized) { page_list.normalized.forEach(function(data) { if (!title_data_map[data.to]) { // e.g., '#...' → '' if (!data.to || /^[^:]+:/.test(data.to)) { // e.g. [[commons:title]] return; } console.trace(pages); // console.trace(page_list); error = error // || new Error('No normalized title data: [' // + data.to + ']←[' + data.from + ']'); return; } // 注意: 這可能註冊多種不同的標題。 title_data_map[data.from] = title_data_map[data.to]; }); } if (data.warnings && data.warnings.query // && typeof data.warnings.query['*'] === 'string') { if (need_warn) { library_namespace.warn( // 'wiki_API_page: ' + data.warnings.query['*']); // console.log(data); } /** * 2016/6/27 22:23:25 修正: 處理當非 bot 索求過多頁面時之回傳。<br /> * e.g., <code> * { batchcomplete: '', warnings: { query: { '*': 'Too many values supplied for parameter \'pageids\': the limit is 50' } }, * query: { pages: { '0000': [Object],... '0000': [Object] } } } * </code> */ if (data.warnings.query['*'].includes('the limit is ')) { // TODO: 注記此時真正取得之頁面數。 // page_list.OK_length = page_list.length; page_list.truncated = true; } } // options.multi: 明確指定即使只取得單頁面,依舊回傳 Array。 if (!options.multi) { if (page_list.length <= 1) { // e.g., pages: { '1850031': [Object] } library_namespace.debug('只取得單頁面 ' // + wiki_API.title_link_of(page_list) // + ',將回傳此頁面內容,而非 Array。', 2, 'wiki_API_page'); page_list = page_list[0]; // 警告: `page_list`可能是 undefined。 if (is_api_and_title(title, true)) { title = title[1]; } if (!options.do_not_import_original_page_data // && wiki_API.is_page_data(title)) { // 去除掉可能造成誤判的錯誤標記 'missing'。 // 即使真有錯誤,也由page_list提供即可。 if ('missing' in title) { delete title.missing; // 去掉該由page_list提供的資料。因為下次呼叫時可能會被利用到。例如之前找不到頁面,.pageid被設成-1,下次呼叫被利用到就會出問題。 // ** 照理說這兩者都必定會出現在page_list。 // delete title.pageid; // delete title.title; } // import data to original page_data. 盡可能多保留資訊。 page_list = Object.assign(title, page_list); } if (page_list && get_content // && (page_list.is_Flow = wiki_API.Flow.is_Flow(page_list)) // e.g., { flow_view : 'header' } && options.flow_view) { // Flow_page() wiki_API.Flow.page(page_list, callback, options); return; } } else { library_namespace.debug('Get ' + page_list.length // + ' page(s)! The pages will all ' // + 'passed to the callback as Array!', 2, 'wiki_API_page'); } } else if ((options.multi === KEY_KEEP_INDEX // options.keep_order || options.multi === KEY_KEEP_ORDER) // && is_api_and_title(title, true) // && Array.isArray(title[1]) && title[1].length >= 2) { var order_hash = title[1].map(function(page_data) { return options.is_id ? page_data.pageid // || page_data : wiki_API.title_of(page_data); }).to_hash(), ordered_list = []; // console.log(title[1].join('|')); // console.log(order_hash); if (false) { // another method // re-sort page list page_list.sort(function(page_data_1, page_data_2) { return order_hash[page_data_1.original_title // || page_data_1.title] // - order_hash[page_data_2.original_title // || page_data_2.title]; }); console.log(page_list.map(function(page_data) { return page_data.original_title // || page_data.title; }).join('|')); throw new Error('Reorder the list of pages'); } // 維持頁面的順序與輸入的相同。 page_list.forEach(function(page_data) { var original_title = page_data.original_title // || page_data.title; if (original_title in order_hash) { ordered_list[order_hash[original_title]] = page_data; } else { console.log(order_hash); console.log(original_title); console.log('-'.repeat(70)); console.log('Page list:'); console.log(title[1].map(function(page_data) { return wiki_API.title_of(page_data); }).join('\n')); console.log(page_data); throw new Error('wiki_API_page: 取得了未指定的頁面: ' // + wiki_API.title_link_of(original_title)); } }); // 緊湊化,去掉沒有設定到的頁面。 if (options.multi === KEY_KEEP_ORDER) { ordered_list = ordered_list.filter(function(page_data) { return !!page_data; }); } // copy attributes form original page_list [ 'OK_length', 'truncated', 'normalized', // 'index_of_title', 'title_data_map', // 'redirects', 'redirect_from', 'converted', 'convert_from' ] // 需要注意page_list可能帶有一些已經設定的屬性值,因此不能夠簡單的直接指派到另外一個值。 .forEach(function(attribute_name) { if (attribute_name in page_list) { ordered_list[attribute_name] // = page_list[attribute_name]; } }); page_list = ordered_list; } // 警告: `page_list`可能是 undefined。 if (page_list && options.save_response) { // 附帶原始回傳查詢資料。 // save_data, query_data // assert: !('response' in page_list) page_list.response = data; } if (options.expandtemplates) { if (options.titles_left) { error = error // || new Error('There are options.titles_left!'); } // 需要expandtemplates的情況。 if (!Array.isArray(page_list)) { // TODO: test var revision = wiki_API.content_of.revision(page_list); // 出錯時 revision 可能等於 undefined。 if (!revision) { callback(page_list, error); return; } wiki_API_expandtemplates( // wiki_API.revision_content(revision), function() { callback(page_list, error); }, Object.assign({ page : page_list, title : page_data.title, revid : revision.revid, includecomments : options.includecomments, session : options[KEY_SESSION] }, options.expandtemplates)); return; } // TODO: test page_list.run_serial(function(run_next, page_data, index) { var revision = wiki_API.content_of.revision(page_data); wiki_API_expandtemplates( // wiki_API.revision_content(revision), // run_next, Object.assign({ page : page_data, title : page_data.title, revid : revision && revision.revid, includecomments : options.includecomments, session : options[KEY_SESSION] }, options.expandtemplates)); }, function() { callback(page_list, error); }); return; } // 一般正常回傳。 if (page_list) { if (false && page_list.title) { console.trace('Get page and callback: ' + page_list.title); } page_list.revisions_parameters = action[1]; } if (library_namespace.is_debug(9)) { // console.trace(page_list); // console.trace(options); } // page 之 structure 將按照 wiki API 本身之 return! // page_data = {pageid,ns,title,revisions:[{timestamp,'*'}]} callback(page_list, error); }, post_data, options); } // default properties of revisions // ids, timestamp 是為了 wiki_API_edit.set_stamp 檢查編輯衝突用。 wiki_API_page.default_rvprop = 'ids|timestamp|content'; // @see https://www.mediawiki.org/w/api.php?action=help&modules=query wiki_API_page.auto_converttitles = 'zh,gan,iu,kk,ku,shi,sr,tg,uz' .split(','); // ------------------------------------------------------------------------ /** * 回溯看看是哪個 revision 增加/刪除了標的文字。 * * @param {String}title * page title * @param to_search * filter / text to search.<br /> * to_search(diff, revision, old_revision):<br /> * `diff` 為從舊的版本 `old_revision` 改成 `revision` 時的差異。 * @param {Function}callback * 回調函數。 * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 */ function tracking_revisions(title, to_search, callback, options) { options = Object.assign({ rvlimit : 20 }, options, { save_response : true }); if (options.search_diff && typeof to_search !== 'function') { throw new TypeError( 'Only {Function}filter to search for .search_diff=true!'); } function do_search(revision, old_revision) { var value = revision.revid ? wiki_API.revision_content(revision) : revision; if (!value) return; if (typeof to_search === 'string') return value.includes(to_search); if (options.search_diff) return to_search([ , value ], revision, old_revision); // return found; return library_namespace.fit_filter(to_search, value); } var session = wiki_API.session_of_options(options); var run_next_status = session && session.set_up_if_needed_run_next(); var newer_revision, revision_count = 0; function search_revisions(page_data, error) { // console.trace(page_data, error); if (error) { callback(null, page_data, error); return; } var revision_index = 0, revisions = page_data.revisions; if (!newer_revision && revisions.length > 0) { newer_revision = revisions[revision_index++]; newer_revision.lines = wiki_API .revision_content(newer_revision).split('\n'); // console.trace([do_search(newer_revision),options]); if (!options.search_diff && !options.search_deleted) { var result = do_search(newer_revision); if (!result) { // 最新版本就已經不符合需求。 callback(null, page_data); return; } if (library_namespace.is_thenable(result)) { result = result.then(function(result) { if (!result) { // 最新版本就已經不符合需求。 callback(null, page_data); return; } search_next_revision(); }); if (session) session.check_and_run_next(run_next_status, result); // 直接跳出。之後會等 promise 出結果才繼續執行。 return; } } } // console.log(revisions.length); search_next_revision(); function search_next_revision() { // console.trace(revision_index + '/' + revisions.length); if (revision_index === revisions.length) { finish_search(); return; } var this_revision = revisions[revision_index++]; // MediaWiki using line-diff this_revision.lines = wiki_API.revision_content(this_revision) .split('\n'); var diff_list; try { diff_list = newer_revision.diff_list // = library_namespace.LCS(this_revision.lines, // newer_revision.lines, { diff : true, // MediaWiki using line-diff line : true, treat_as_String : true }); } catch (e) { // e.g., RangeError: Maximum call stack size exceeded @ // backtrack() callback(null, page_data, e); return; } // console.trace(diff_list); var found, diff_index = 0; search_next_diff(); function check_result(result) { if (library_namespace.is_thenable(result)) { result = result.then(check_result); if (session) session.check_and_run_next(run_next_status, result); // 直接跳出。之後會等 promise 出結果才繼續執行。 } else { found = result; if (found) finish_search_revision(); else search_next_diff(); } } function search_next_diff() { // console.trace(diff_index + '/' + diff_list.length); var result = undefined; if (diff_index === diff_list.length) { if (options.revision_post_processor) { result = options .revision_post_processor(newer_revision); } if (library_namespace.is_thenable(result)) { result = result.then(finish_search_revision); if (session) session.check_and_run_next(run_next_status, result); // 直接跳出。之後會等 promise 出結果才繼續執行。 } else { finish_search_revision(); } // console.trace(result); // var session = wiki_API.session_of_options(options); // console.trace(session); // console.trace(session && session.actions); return; } var diff = diff_list[diff_index++]; // console.trace(revision_index, diff_index, diff); if (options.search_diff) { result = to_search(diff, newer_revision, this_revision); } else { // var removed_text = diff[0], added_text = diff[1]; result = // 警告:在 line_mode,"A \n"→"A\n" 的情況下, // "A" 會同時出現在增加與刪除的項目中,此時必須自行檢測排除。 do_search(diff[options.search_deleted ? 0 : 1]) // && !do_search(diff[options.search_deleted ? 1 : 0]); } // console.trace(result); check_result(result); } function finish_search_revision(page_data, error) { delete newer_revision.lines; // console.trace([this_revision.revid,found,do_search(this_revision)]) if (found) { delete this_revision.lines; // console.log(diff_list); callback(newer_revision, page_data); return; } newer_revision = this_revision; if (revision_index === revisions.length) { delete this_revision.lines; } search_next_revision(); } } function finish_search() { revision_count += page_data.revisions; if (revision_count > options.limit) { // not found callback(null, page_data); return; } if (false) { // console.trace(page_data.response); var page_options_continue = page_data.response['continue']; // console.trace(page_options_continue); if (page_options_continue) { options.page_options_continue = page_options_continue; // console.trace(options); library_namespace.debug( 'tracking_revisions: search next ' + options.rvlimit + (options.limit > 0 ? '/' + options.limit : '') + ' revisions...', 2); get_pages(); return; } } else { var rvcontinue = page_data.response['continue']; if (rvcontinue) { options.rvcontinue = rvcontinue.rvcontinue; // console.trace(options); library_namespace.debug( 'tracking_revisions: search next ' + options.rvlimit + (options.limit > 0 ? '/' + options.limit : '') + ' revisions...', 2); get_pages(); return; } } // assert: 'batchcomplete' in page_data.response // if no response['continue'], append a null revision, // and do not search continued revisions. var result = !options.search_deleted && do_search(newer_revision); if (library_namespace.is_thenable(result)) { result = result.then(do_callback); if (session) session.check_and_run_next(run_next_status, result); // 直接跳出。之後會等 promise 出結果才繼續執行。 } else { do_callback(result); } function do_callback(result) { if (result) { callback(newer_revision, page_data); } else { // not found callback(null, page_data); } } } } function get_pages() { wiki_API.page(title, search_revisions, options); } get_pages(); } wiki_API.tracking_revisions = tracking_revisions; // ------------------------------------------------------------------------ // 強制更新快取/清除緩存並重新載入/重新整理/刷新頁面。 // @see https://www.mediawiki.org/w/api.php?action=help&modules=purge // 極端做法:[[WP:NULL|Null edit]], re-edit the same contents wiki_API.purge = function(title, callback, options) { var action = normalize_title_parameter(title, options); if (!action) { throw new Error('wiki_API.purge: ' // gettext_config:{"id":"invalid-title-$1"} + gettext('Invalid title: %1', wiki_API.title_link_of(title))); } // POST_parameters var post_data = action[1]; action[1] = { // forcelinkupdate : 1, // forcerecursivelinkupdate : 1, action : 'purge' }; wiki_API.query(action, typeof callback === 'function' // && function(data, error) { // copy from wiki_API.redirects_here() if (wiki_API.query.handle_error(data, error, callback)) { return; } // data: // {"batchcomplete":"","purge":[{"ns":0,"title":"Title","purged":""}]} if (!data || !data.purge) { library_namespace.warn([ 'wiki_API_purge: ', { // gettext_config:{"id":"unknown-api-response-$1"} T : [ 'Unknown API response: %1', (typeof data === 'object' // && typeof JSON !== 'undefined' // ? JSON.stringify(data) : data) ] } ]); if (library_namespace.is_debug() // .show_value() @ interact.DOM, application.debug && library_namespace.show_value) library_namespace.show_value(data); callback(undefined, data); return; } var page_data_list = data.purge; // page_data_list: e.g., [{ns:4,title:'Meta:Sandbox',purged:''}] if (page_data_list.length < 2 && (!options || !options.multi)) { // 沒有特別設定的時候,回傳與輸入的形式相同。輸入單頁則回傳單頁。 page_data_list = page_data_list[0]; } // callback(page_data) or callback({Array}page_data_list) callback(page_data_list); }, post_data, options); }; // ------------------------------------------------------------------------ /** * 取得頁面之重定向資料(重新導向至哪一頁)。 * * 注意: 重定向僅代表一種強烈的關聯性,而不表示從屬關係(對於定向到章節的情況)或者等價關係。 * 例如我們可能將[[有罪推定]]定向至[[無罪推定]],然而雙方是完全相反的關係。 * 只因為[[無罪推定]]是一種比較值得關注的特性,而[[有罪推定]]沒有特殊的性質(common)。因此我們只談[[無罪推定]],不會特別拿[[有罪推定]]出來談。 * * TODO: * https://www.mediawiki.org/w/api.php?action=help&modules=searchtranslations * * https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Renaming_or_moving_modules * * @example <code> CeL.wiki.redirect_to('史記', function(redirect_data, page_data) { CeL.show_value(redirect_data); }); </code> * * @param {String|Array}title * title or [ {String}API_URL, {String}title or {Object}page_data ] * @param {Function}[callback] * 回調函數。 callback({String}title that redirects to or {Object}with * redirects to what section, {Object}page_data, error) * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @see https://www.mediawiki.org/w/api.php?action=help&modules=query%2Brevisions */ wiki_API.redirect_to = function(title, callback, options) { wiki_API.page(title, function(page_data, error) { if (error || !wiki_API.content_of.page_exists(page_data)) { // error? 此頁面不存在/已刪除。 callback(undefined, page_data, error); return; } // e.g., [ { from: 'AA', to: 'A', tofragment: 'aa' } ] // e.g., [ { from: 'AA', to: 'A', tofragment: '.AA.BB.CC' } ] var redirect_data = page_data.response.query.redirects; if (redirect_data) { if (redirect_data.length !== 1) { // 可能是多重重定向? // e.g., A→B→C library_namespace.warn('wiki_API.redirect_to: ' + 'Get ' + redirect_data.length + ' redirects for [' // title.join(':') + title + ']!'); library_namespace.warn(redirect_data); } // 僅取用並回傳第一筆資料。 redirect_data = redirect_data