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