cejs
Version:
A JavaScript module framework that is simple to use.
1,668 lines (1,496 loc) • 163 kB
JavaScript
/**
* @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