cejs
Version:
A JavaScript module framework that is simple to use.
1,664 lines (1,442 loc) • 94.2 kB
JavaScript
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): wikitext parser 維基語法解析器
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
parser 所有子頁面加入白名單 white-list
parser for_each_subelement() 所有node當前之level層級
parser for_each_subelement() 提供 .previousSibling, .nextSibling, .parentNode 將文件結構串起來。
</code>
*
* @since 2019/10/10 拆分自 CeL.application.net.wiki
* @since 2021/12/14 18:53:43 拆分至 CeL.application.net.wiki.parser.wikitext,
* CeL.application.net.wiki.parser.section,
* CeL.application.net.wiki.parser.misc
*
* @see https://github.com/earwig/mwparserfromhell
*/
// 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.parser',
require : 'application.net.wiki.'
// load MediaWiki module basic functions
+ '|application.net.wiki.namespace.'
// for PATTERN_BOT_NAME
+ '|application.net.wiki.task.'
// CeL.DOM.HTML_to_Unicode(), CeL.DOM.Unicode_to_HTML()
+ '|interact.DOM.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
var
/** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */
NOT_FOUND = ''.indexOf('_');
// --------------------------------------------------------------------------------------------
// page parser setup.
/*
* should use: class Wiki_page extends Array { }
* http://www.2ality.com/2015/02/es6-classes-final.html
*/
/**
* constructor (建構子) of {wiki page parser}. wikitext 語法分析程式, wikitext 語法分析器.
*
* TODO:<code>
should use:
parsetree of https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
or
https://www.mediawiki.org/w/api.php?action=help&modules=parse
class Wiki_page extends Array { }
http://www.2ality.com/2015/02/es6-classes-final.html
</code>
*
* @param {String|Object}wikitext
* wikitext / page data to parse
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*
* @returns {wiki page parser}
*/
function page_parser(wikitext, options) {
options = library_namespace.setup_options(options);
// console.log(wikitext);
// console.log(wiki_API.is_page_data(wikitext));
if (typeof wikitext === 'string' || wikitext === 0) {
wikitext = [ String(wikitext) ];
} else if (wiki_API.is_page_data(wikitext)) {
// 可以用 "CeL.wiki.parser(page_data).parse();" 來設置 parser。
var page_data = wikitext;
if (!page_data.parsed || options.wikitext
|| typeof options.revision_index === 'number'
// re-parse
|| options.reparse) {
wikitext = options
&& options.wikitext
|| wiki_API.content_of(page_data,
options.revision_index || 0);
// prevent wikitext === undefined (missing: '')
wikitext = wikitext
// usinf this[0] @ parse_page(options)
? [ wikitext ] : [];
page_data.parsed = wikitext;
wikitext.page = page_data;
} else {
return page_data.parsed;
}
} else if (!wikitext) {
if (!wiki_API.is_valid_parameters_value(wikitext)) {
library_namespace.warn('page_parser: ' + 'Null wikitext: '
+ wikitext);
// console.trace(wikitext);
}
wikitext = [];
} else if (Array.isArray(wikitext) && wikitext.type === 'plain') {
// assert: already parsed
if (wikitext.options)
return wikitext;
} else {
// console.trace(wikitext);
throw new Error('page_parser: ' + 'Invalid wikitext type: {'
+ typeof wikitext + '} ' + JSON.stringify(wikitext) + '.');
}
if (typeof options === 'string') {
options = library_namespace.setup_options(options);
}
if (library_namespace.is_Object(options)) {
wikitext.options = options;
}
// copy prototype methods
Object.assign(wikitext, page_parser.parser_prototype);
wiki_API.parse.set_wiki_type(wikitext, 'plain');
var session = wiki_API.session_of_options(options);
if (session) {
wiki_API.add_session_to_options(session, wikitext);
}
// console.trace(wikitext);
return wikitext;
}
// CeL.wiki.parser.parser_prototype, wiki_API.parser.parser_prototype
/** {Object}prototype of {wiki page parser} */
page_parser.parser_prototype = {
// traversal_tokens(), parsed.each()
// CeL.wiki.parser.parser_prototype.each.call(token_list,'Template:',(token,index,parent)=>{});
// 在執行 .each() 之前,應該先執行 .parse()。
each : for_each_subelement,
parse : parse_page,
parse_references : parse_references,
get_categories : get_categories,
append_category : register_and_append_category,
analysis_layout_indices : analysis_layout_indices,
insert_layout_element : insert_layout_element,
insert_before : insert_before,
// has_template
find_template : find_template
};
/**
* {Object}alias name of type. The target MUST be one of
* wiki_API.parse.wiki_element_toString
*/
page_parser.type_alias = {
wikilink : 'link',
weblink : 'external_link',
table_caption : 'caption',
row : 'table_row',
tr : 'table_row',
// 注意: table_cell 包含 th + td,須自行判別!
th : 'table_cell',
td : 'table_cell',
template : 'transclusion',
// wikitext, 'text': plain text
text : 'plain',
'' : 'plain'
};
// CeL.wiki.parser.footer_order()
page_parser.footer_order = footer_order;
// ------------------------------------------
// CeL.wiki.parser.remove_heading_spaces(parent, index, max_length)
// remove heading spaces from parent_token[index]
function remove_heading_spaces(parent_token, index, max_length,
do_not_preserve_tail_spaces) {
if (index >= parent_token.length)
return;
max_length = typeof max_length === 'number' && max_length >= 0 ? Math
.min(max_length, parent_token.length) : parent_token.length;
var _i = index;
var combined_tail;
for (; index < max_length; index++) {
var token = parent_token[index];
// assert: 以 "\n" 開頭的,都應該 `typeof token === 'string'`。
if (typeof token !== 'string') {
if (!combined_tail)
return;
index--;
break;
}
if (!token) {
continue;
}
if (combined_tail)
combined_tail += token;
else
combined_tail = token;
if (/[^\s\n]/.test(token)) {
break;
}
parent_token[index] = '';
}
// console.trace(JSON.stringify(combined_tail));
// 在全是 "" 的 element 中刪除 children,
// 此時 index 可能等於 parent_token.length,combined_tail === undefined。
if (combined_tail !== undefined) {
if (!/^\s/.test(combined_tail)) {
// No need to change
// 注意: /\s/.test('\n') === true
} else if (/^\s*?\n/.test(combined_tail)) {
var preserve_heading_new_line;
while (_i > 0) {
var token = parent_token[--_i];
if (token) {
// 前文以 new line 作結,或者要 trim 的 token 是第一個 token,
// 則不保留末尾的 preserve_heading_new_line。
preserve_heading_new_line =
// typeof token !== 'string' ||
!/\n\s*?$/.test(token);
break;
}
// assert: token === ''
}
combined_tail = combined_tail
// 去除後方的空白 + 僅一個換行。 去除前方的空白或許較不合適?
// e.g., "* list\n\n{{t1}}\n{{t2}}",
// remove "{{t1}}\n" → "* list\n\n{{t2}}"
.replace(/^\s*?\n/, preserve_heading_new_line ? '\n' : '');
} else {
combined_tail = combined_tail
// 去除後方太多空白,僅留下最後一個空白。
.replace(/^(\s)*/, do_not_preserve_tail_spaces ? '' : '$1');
}
parent_token[index] = combined_tail;
}
return index;
}
page_parser.remove_heading_spaces = remove_heading_spaces;
// CeL.wiki.parser.remove_token(parent, index, max_length)
function remove_token_from_parent(parent_token, index, max_length,
do_not_preserve_tail_spaces) {
if (index === undefined && parent_token.parent
&& parent_token.index >= 0) {
// remove parent_token itself
// CeL.wiki.parser.remove_token(token)
index = parent_token.index;
parent_token = parent_token.parent;
}
var token = parent_token[index];
// 直接改成空字串而非 `parent_token.splice(index, 1)`,避免index跑掉。
parent_token[index] = '';
var next_index = remove_heading_spaces(parent_token, index + 1,
max_length, do_not_preserve_tail_spaces);
if (index > 0 && /\n$/.test(parent_token[index - 1])
&& /^\n/.test(parent_token[next_index])) {
// e.g., "\n{{to del}}\n==t==\n" → "\n\n==t==\n"
// → "\n==t==\n"
parent_token[next_index] = parent_token[next_index].replace(/^\n/,
'');
} else if (index > 0 && index + 1 === parent_token.length
&& typeof parent_token[index - 1] === 'string'
&& /\n$/.test(parent_token[index - 1])) {
// e.g., "{{t|TTT\n{{to del}}}}" → "{{t|TTT\n}}"
// → "{{t|TTT}}"
parent_token[index - 1] = parent_token[index - 1]
.replace(/\n$/, '');
} else if ((index === 0 || /\n$/.test(parent_token[index - 1]))
&& /^\s/.test(parent_token[next_index])) {
// e.g., "\n{{to del}} [[L]]" → "[[L]]"
if (index > 0) {
parent_token[index - 1] = parent_token[index - 1].replace(
/\n$/, '');
}
parent_token[next_index] = parent_token[next_index].replace(/^\s+/,
'');
}
// free
// next_index = undefined;
var list_token = parent_token.parent;
// assert: list_token.type === 'list'
if (parent_token.type === 'list_item' && list_token
// remove all empty / blank list_item
&& parent_token.every(function(token) {
// token maybe undefined
if (!token)
return token !== 0;
if (typeof token === 'string')
return /^[\s\n]*$/.test(token);
if (token.type === 'transclusion') {
// e.g., {{zh-tw}}
return /^Zh(-[a-z]+)?$/.test(token.name);
}
return token.type === 'comment';
})) {
// TODO: fix removing "*{{T|1}}\n*{{T|2}}\n" in one operation,
// see [[w:zh:Special:Diff/65133690/65133727|香港巴士迷文化]]
parent_token.index = list_token.indexOf(parent_token);
if (parent_token.index + 1 < list_token.length) {
var next_list_item = list_token[parent_token.index + 1];
// assert: next_list_item.type === 'list_item'
var new_lines = parent_token.list_prefix.match(/^\n*/)[0];
// shift new_lines
next_list_item.list_prefix = next_list_item.list_prefix
.replace(/^\n*/, new_lines);
}
list_token.splice(parent_token.index, 1);
} else if (parent_token.type === 'list_item') {
// console.trace(parent_token);
// console.trace(list_token);
// throw new Error();
// e.g., ",見{{evchk}}。"
library_namespace
.debug(
'清除 token (如模板)時,還遺留具意涵的元素,未能完全清除掉此 token 所在的列表項目。可能需要手動修飾語句。',
1, 'remove_token_from_parent');
}
// console.log(parent_token.slice(index - 2, i + 2));
return token;
}
page_parser.remove_token = remove_token_from_parent;
// ------------------------------------------------------------------------
(function() {
wikitext = 'a\n[[File:f.jpg|thumb|d]]\nb';
parsed = CeL.wiki.parser(wikitext).parse();
parsed.each('namespaced_title', function(token, index, parent) {
console.log([ index, token, parent ]);
}, true);
// @see 20210414.翻訳用出典テンプレートのsubst展開.js
parsed.each('template:cite', function(token, index, parent) {
if (CeL.wiki.parse.token_is_children_of(token, function(parent) {
// [[w:en:Help:Pipe trick#Where it doesn't work]]
return parent.tag === 'ref' || parent.tag === 'gallery'
// e.g., @ [[w:ja:Template:Round corners]]
|| parent.tag === 'includeonly';
})) {
console.log([ index, token, parent ]);
}
}, {
add_index : 'all'
});
parsed.toString();
});
// 注意: 必須配合 `parsed.each(, {add_index : 'all'})` 使用。
function token_is_children_of(token, parent_filter) {
var parent;
while (token && (parent = token.parent)) {
if (parent_filter(parent))
return true;
token = parent;
}
}
// CeL.wiki.parser.token_is_children_of()
page_parser.token_is_children_of = token_is_children_of;
/**
* 對所有指定類型 type 的元素(tokens),皆執行特定作業 processor。
*
* TODO: 可中途跳出。
*
* @param {String}[type]
* 欲搜尋之類型。 e.g., 'template'. see
* ((wiki_API.parse.wiki_element_toString)).<br />
* 未指定: 處理所有節點。
* @param {Function}processor
* 執行特定作業: processor({Array|String|undefined}inside token list,
* {ℕ⁰:Natural+0}index of token, {Array}parent of token,
* {ℕ⁰:Natural+0}depth) {<br />
* return {String}wikitext or {Object}element;}
* @param {Boolean}[modify_by_return]
* 若 processor 的回傳值為{String}wikitext,則將指定類型節點替換/replace作此回傳值。
* 注意:即使設定為 false,回傳 .remove_token 依然會刪除當前 token!
* @param {Natural}[max_depth]
* 最大深度。1: 僅到第1層(底層)。2: 僅到第2層(開始遍歷子節點)。 0||NaN: 遍歷所有子節點。
*
* @returns {Promise|Undefine}
*
* @see page_parser.type_alias
*/
function for_each_subelement(type, processor, modify_by_return, max_depth) {
if (!Array.isArray(this)) {
// console.trace(this);
return this;
}
if (typeof type === 'function' && max_depth === undefined) {
// for_each_subelement(processor, modify_by_return, max_depth)
// shift arguments.
max_depth = modify_by_return;
modify_by_return = processor;
processor = type;
type = undefined;
}
var options;
// for_each_subelement(type, processor, options)
if (max_depth === undefined && typeof modify_by_return === 'object') {
options = modify_by_return;
modify_by_return = options.modify;
max_depth = options.max_depth;
} else {
options = Object.create(null);
}
// console.log(options);
if (typeof modify_by_return === 'number' && modify_by_return > 0
&& max_depth === undefined) {
// for_each_subelement(type, processor, max_depth)
// shift arguments.
max_depth = modify_by_return;
modify_by_return = undefined;
}
// console.log('max_depth: ' + max_depth);
var session = wiki_API.session_of_options(options);
if (!session
&& (session = wiki_API.session_of_options(this)
|| wiki_API.session_of_options(this.options))) {
// for wiki_API.template_functions.adapt_function()
wiki_API.add_session_to_options(session, options);
}
var token_name;
if (type || type === '') {
if (typeof type !== 'string') {
library_namespace.warn('for_each_subelement: Invalid type ['
+ type + ']');
return;
}
token_name = type.match(/^(Template):(.+)$/i);
if (token_name) {
if (session) {
token_name = session.redirect_target_of(type);
token_name = session.remove_namespace(token_name);
} else {
// type = token_name[0];
token_name = wiki_API.normalize_title(token_name[2]);
}
type = 'transclusion';
}
// normalize type
// assert: typeof type === 'string'
type = type.toLowerCase().replace(/\s/g, '_');
if (type in page_parser.type_alias) {
type = page_parser.type_alias[type];
}
if (!(type in wiki_API.parse.wiki_element_toString)) {
library_namespace.warn('for_each_subelement: Unknown type ['
+ type + ']');
}
}
// options.slice: range index: {Number}start index
// || {Array}[ {Number}start index, {Number}end index ]
var slice = options.slice, exit;
// console.log(slice);
if (slice >= 0) {
// 第一層 start from ((slice))
slice = [ slice ];
} else if (slice && (!Array.isArray(slice) || slice.length > 2)) {
library_namespace.warn('for_each_subelement: Invalid slice: '
+ JSON.stringify(slice));
slice = undefined;
}
if (!this.parsed && typeof this.parse === 'function') {
// 因為本函數為 CeL.wiki.parser(content) 最常使用者,
// 因此放在這少一道 .parse() 工序。
this.parse();
}
// ----------------------------------------------------------
var ref_list_to_remove = [], promise;
function set_promise(operator) {
promise = promise.then(operator);
// promise.operator = operator;
}
function check_if_result_is_thenable(result) {
if (library_namespace.is_thenable(result)) {
// console.trace(result);
promise = promise ? promise.then(function() {
return result;
}) : result;
// promise._result = result;
return true;
}
}
// 遍歷 tokens。
function traversal_tokens(parent_token, depth, resolve) {
// depth: depth of parent_token
var index, length;
if (slice && depth === 0) {
// 若有 slice,則以更快的方法遍歷 tokens。
// TODO: 可以設定多個範圍,而不是只有一個 range。
index = slice[0] | 0;
length = slice[1] >= 0 ? Math.min(slice[1] | 0,
parent_token.length) : parent_token.length;
} else {
// console.log(parent_token);
index = 0;
length = parent_token.length;
// parent_token.some(for_token);
}
var use_parent_token_length = length === parent_token.length;
function traversal_next_sibling() {
if (promise) {
// console.trace([ index + '/' + length, depth, exit ]);
}
if (exit || !(index < length)) {
// 已遍歷所有本階層節點,或已設定 exit 跳出。
if (promise) {
set_promise(resolve);
// console.trace([ promise, resolve ]);
}
return;
}
var token = parent_token[index];
if (false) {
console.log('token depth ' + depth
+ (max_depth ? '/' + max_depth : '')
+ (exit ? ' (exit)' : '') + ':');
console.trace([ type, token ]);
}
if ((!type
// 'plain': 對所有 plain text 或尚未 parse 的 wikitext.,皆執行特定作業。
|| type === (Array.isArray(token) ? token.type : 'plain'))
&& (!token_name || (session ? token.type === 'transclusion'
&& session.is_template(token_name, token)
: token.name === token_name))) {
// options.set_index
if (options.add_index && token && typeof token === 'object') {
// 假如需要自動設定 .parent, .index 則必須特別指定。
// token.parent[token.index] === token
// .index_of_parent
token.index = index;
token.parent = parent_token;
}
// 警告: 應該在 processor()) 中使用
// token = repeatedly_expand_template_token(token, options);
if (wiki_API.template_functions) {
// console.trace(options);
wiki_API.template_functions.adapt_function(token,
index, parent_token, options);
}
// get result. 須注意: 此 token 可能為 Array, string, undefined!
// for_each_subelement(token, token_index, parent_of_token,
// depth)
var result = processor(token, index, parent_token, depth);
if (use_parent_token_length
&& length !== parent_token.length) {
library_namespace.debug('parent_token 長度改變: ' + length
+ '→' + parent_token.length + '。', 1,
'for_each_subelement');
length = parent_token.length;
}
// console.log(modify_by_return);
// console.trace(result);
if (false && token.toString().includes('Internetquelle')) {
console.trace([ index + '/' + length + ' ' + token,
result, promise ]);
}
if (check_if_result_is_thenable(result) || promise) {
set_promise(function _check_result(
result_after_promise_resolved) {
// console.trace(result_after_promise_resolved);
if (false && token.toString().includes(
'Internetquelle'))
console.trace([
//
index + '/' + length + ' ' + token,
//
parent_token.toString(),
//
result_after_promise_resolved,
//
promise, depth, exit ]);
check_result(
token,
library_namespace.is_thenable(result) ? result_after_promise_resolved
: result);
});
} else {
// console.trace(result);
// assert: !promise || (promise is resolved)
// if (promise) console.trace(promise);
check_result(token, result);
}
return;
}
if (options.add_index === 'all' && token
&& typeof token === 'object') {
token.index = index;
token.parent = parent_token;
}
if (promise) {
// NG:
// set_promise(traversal_children(null, token, null));
}
return traversal_children(token);
}
function check_result(token, result) {
// assert: !promise || (promise is resolved)
if (result === for_each_subelement.exit) {
library_namespace.debug('Abort the operation', 3,
'for_each_subelement');
// exit: 直接跳出。
exit = true;
return traversal_children();
}
// `return parsed.each.remove_token;`
if (result === for_each_subelement.remove_token) {
// 重新確認 index,預防中途做過了插入或者刪除操作。
var _index;
if (parent_token[index] !== token) {
_index = scan_token_index(token, index, parent_token);
if (_index !== NOT_FOUND)
index = _index;
}
if (_index === NOT_FOUND) {
library_namespace
.warn('token 已不存在 parent_token 中,無法刪除! '
+ token);
} else if (parent_token.type === 'list') {
// for <ol>, <ul>: 直接消掉整個 item token。
// index--: 刪除完後,本 index 必須再遍歷一次。
parent_token.splice(index--, 1);
length--;
} else {
if (token.type === 'tag' && token.tag === 'ref'
&& token.attributes && token.attributes.name) {
// @see wikibot/20190913.move_link.js
library_namespace.debug(
'將刪除可能被引用的 <ref>,並嘗試自動刪除所有引用。您仍須自行刪除非{{r|name}}型態的模板參考引用: '
+ token.toString(), 1,
'for_each_subelement');
ref_list_to_remove.push(token.attributes.name);
}
remove_token_from_parent(parent_token, index, length);
token = '';
}
} else if (modify_by_return) {
// console.trace([ index, result, parent_token ]);
// 換掉整個 parent[index] token 的情況。
// `return undefined;` 不會替換,應該 return
// .each.remove_token; 以清空。
// 小技巧: 可以用 return [ inner ].is_atom = true 來避免進一步的
// parse 或者處理。
if (typeof result === 'string') {
// {String}wikitext to ( {Object}element or '' )
result = wiki_API.parse(result, options, []);
}
// console.trace([ result && result.toString(), index,
// parent_token.toString() ]);
if (typeof result === 'string'
//
|| typeof result === 'number'
//
|| Array.isArray(result)) {
// 將指定類型節點替換作此回傳值。
parent_token[index] = token = result;
// console.trace([ result.toString(),
// parent_token.toString() ]);
} else if (result) {
library_namespace.debug('Invalid result to replace: '
+ result, 1, 'for_each_subelement');
}
}
return traversal_children(token, result);
}
function traversal_children(token, result) {
// assert: !promise || (promise is resolved)
// depth-first search (DFS) 向下層巡覽,再進一步處理。
// 這樣最符合token在文本中的出現順序。
// Skip inner tokens, skip children.
if (result !== for_each_subelement.skip_inner
// is_atom: 不包含可 parse 之要素,不包含 text。
&& Array.isArray(token) && !token.is_atom
// 最起碼必須執行一次 `traversal_next_sibling()`。
&& token.length > 0 && !exit
// comment 可以放在任何地方,因此能滲透至任一層。
// 但這可能性已經在 wiki_API.parse() 中偵測並去除。
// && type !== 'comment'
&& (!max_depth || depth + 1 < max_depth)) {
traversal_tokens(token, depth + 1, _traversal_next_sibling);
} else if (promise) {
_traversal_next_sibling();
}
if (false && promise) {
console.trace([ index + '/' + length, depth, promise,
modify_by_return ]);
promise.then(function(r) {
console
.trace([ r, index + '/' + length, depth,
promise ]);
});
}
}
function _traversal_next_sibling() {
index++;
if (false)
console.trace([ index + '/' + length, depth, promise,
modify_by_return ]);
if (true) {
traversal_next_sibling();
} else {
// also work:
set_promise(traversal_next_sibling);
}
}
// 一旦 processor() 回傳 is_thenable,那麼就直接跳出迴圈,自此由 promise 接手。
// 否則就可以持續迴圈,以降低呼叫層數。
while (index < length && !exit) {
// console.trace([index, length, depth]);
// 最起碼必須執行一次 `traversal_next_sibling()`
traversal_next_sibling();
if (promise) {
// Waiting for promise resolved.
break;
}
index++;
}
}
// ----------------------------------------------------------
function check_ref_list_to_remove() {
// if (promise) console.trace(promise);
if (ref_list_to_remove.length === 0) {
return;
}
var result;
result = for_each_subelement.call(this, 'tag_single', function(
token, index, parent) {
if (token.tag === 'ref' && token.attributes
// 嘗試自動刪除所有引用。
&& ref_list_to_remove.includes(token.attributes.name)) {
library_namespace.debug('Also remove: ' + token.toString(),
3, 'for_each_subelement');
return for_each_subelement.remove_token;
}
});
check_if_result_is_thenable(result);
result = for_each_subelement.call(this, 'transclusion',
// also remove {{r|name}}
function(token, index, parent) {
if (for_each_subelement.ref_name_templates.includes(token.name)
// 嘗試自動刪除所有引用。
&& ref_list_to_remove.includes(token.parameters['1'])) {
if (token.parameters['2']) {
library_namespace
.warn('for_each_subelement: Cannot remove: '
+ token.toString());
} else {
library_namespace.debug('Also remove: '
+ token.toString(), 3, 'for_each_subelement');
return for_each_subelement.remove_token;
}
}
});
check_if_result_is_thenable(result);
}
var overall_resolve, overall_reject;
function finish_up() {
// console.trace([ 'finish_up()', promise ]);
promise = promise.then(check_ref_list_to_remove).then(
overall_resolve, overall_reject);
if (false) {
promise.then(function() {
console.trace([ '** finish_up()', promise ]);
});
}
}
if (options.use_global_index) {
if (!slice && this[wiki_API.KEY_page_data]
&& this[wiki_API.KEY_page_data].parsed) {
slice = [ this.range[0], this.range[1] ];
if (slice[0] > 0) {
// 加入 .section_title。
slice[0]--;
}
} else {
delete options.use_global_index;
}
}
// console.trace([ this, type ]);
// var parsed = this;
traversal_tokens(
options.use_global_index ? this[wiki_API.KEY_page_data]
&& this[wiki_API.KEY_page_data].parsed : this, 0,
finish_up);
if (!promise) {
return check_ref_list_to_remove();
}
// console.trace(promise);
return new Promise(function(resolve, reject) {
overall_resolve = resolve;
overall_reject = reject;
});
}
Object.assign(for_each_subelement, {
// CeL.wiki.parser.parser_prototype.each.exit
// for_each_subelement.exit: 直接跳出。
exit : typeof Symbol === 'function'
//
? Symbol('EXIT_for_each_subelement')
: [ 'for_each_subelement.exit: abort the operation' ],
// CeL.wiki.parser.parser_prototype.each.skip_inner
// for_each_subelement.skip_inner: Skip inner tokens, skip children.
skip_inner : typeof Symbol === 'function' ? Symbol('SKIP_CHILDREN')
: [ 'for_each_subelement.skip_inner: skip children' ],
// CeL.wiki.parser.parser_prototype.each.remove_token
// for_each_subelement.remove_token: remove current children token
remove_token : typeof Symbol === 'function' ? Symbol('REMOVE_TOKEN')
: [ 'for_each_subelement.skip_inner: remove current token' ],
ref_name_templates : [ 'R' ]
});
// 在 parent_token 中搜索 token 的 index。
// 注意: 必須配合 `parsed.each(, {add_index : 'all'})` 使用。
function scan_token_index(token, index, parent_token) {
if (!parent_token) {
if (Array.isArray(index)) {
parent_token = index;
index = undefined;
} else
parent_token = token.parent;
if (!parent_token) {
library_namespace.error('scan_token_index: '
+ 'No parent_token specified!');
return NOT_FOUND;
}
}
if (typeof index !== 'number')
index = token.index;
if (typeof index !== 'number' || !(index >= 0))
index = 0;
if (parent_token[index] !== token) {
for (index = 0; index < parent_token.length; index++) {
if (parent_token[index] === token) {
break;
}
}
if (index === parent_token.length)
return NOT_FOUND;
}
token.index = index;
token.parent = parent_token;
return index;
}
if (false) {
// re-generate token:
// Set token.index, token.parent first, and then
new_token = CeL.wiki.replace_element(token, token.toString(), options);
}
// 注意: 必須配合 `parsed.each(, {add_index : 'all'})` 使用。
function replace_element(replace_from_token, replace_to, options) {
var index = replace_from_token.index;
var parent_token = replace_from_token.parent;
index = scan_token_index(replace_from_token, index, parent_token);
if (index === NOT_FOUND) {
library_namespace.error('replace_element: ' + 'Skip replace: '
+ replace_from_token + '→' + replace_to);
return;
}
if (!Array.isArray(replace_to)) {
replace_to = wiki_API.parse(replace_to, options);
// Create properties of token.
wiki_API.template_functions.adapt_function(replace_to, index,
parent_token, options);
}
replace_to.index = index;
replace_to.parent = parent_token;
parent_token[index] = replace_to;
return replace_to;
}
// 注意: 必須配合 `parsed.each(, {add_index : 'all'})` 使用。
// 兩 token 都必須先有 .index, .parent!
// token.parent[token.index] === token
// @see options.add_index @ function for_each_subelement()
// 注意: 這個交換純粹只操作於 page_data.parsed 上面,
// 不會改變其他參照,例如 page_data.parsed.reference_list!
// 通常一個頁面只能夠交換一次,交換兩次以上可能就會出現問題!
function swap_elements(token_1, token_2) {
// console.log([ token_1, token_2 ]);
token_1.parent[token_1.index] = token_2;
token_2.parent[token_2.index] = token_1;
var index_1 = token_1.index;
token_1.index = token_2.index;
token_2.index = index_1;
var parent_1 = token_1.parent;
token_1.parent = token_2.parent;
token_2.parent = parent_1;
}
// ------------------------------------------------------------------------
function next_meaningful_element(parent_element, start_index, options) {
var options;
if (library_namespace.is_Object(start_index)) {
options = start_index;
start_index = options.start_index;
} else {
options = Object.create(null);
}
if (!(start_index >= 1)) {
start_index = 0;
}
for (; start_index < parent_element.length; start_index++) {
var this_element = parent_element[start_index];
if (!this_element)
continue;
if (typeof this_element === 'string') {
if (this_element.trim()) {
return options.get_index ? start_index : this_element;
}
continue;
}
if (this_element.type === 'comment')
continue;
return options.get_index ? start_index : this_element;
}
// 向上追溯。
if (options.trace_upwards && parent_element.parent
&& parent_element.index >= 0) {
return next_meaningful_element(parent_element.parent,
parent_element.index + 1, options);
}
}
// ------------------------------------------------------------------------
/**
* 設定好,並執行解析頁面的作業。
*
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*
* @returns {wiki page parser}
*
* @see wiki_API.parse()
*/
function parse_page(options) {
options = library_namespace.setup_options(options);
if (!this.parsed || options.wikitext
|| typeof options.revision_index === 'number'
// re-parse, force: true
|| options.reparse) {
// cf. CeL.wiki.inplace_reparse_element(template_token)
if (options.reparse
&& (typeof this[0] !== 'string' || this.length > 1)) {
this[0] = this.toString();
this.truncate(1);
}
// assert: this = [ {String} ]
// @see function page_parser(wikitext, options)
var parsed = options.wikitext
// 指定要採用的版本。
|| typeof options.revision_index === 'number'
&& wiki_API.content_of(this.page, options) || this[0];
parsed = wiki_API.parse(parsed, Object.assign({
target_array : this
}, this.options, options));
// library_namespace.log(parsed);
// console.trace(parsed);
if (parsed
// for parsed === undefined (missing: '')
&& (!Array.isArray(parsed) || parsed.type !== 'plain')) {
// this.truncate();
this[0] = parsed;
}
this.parsed = true;
}
return this;
}
// ------------------------------------------------------------------------
// search_template
// TODO: templates
function find_template(template_name, options) {
var template_token;
// console.trace(this);
var session = wiki_API.session_of_options(options)
|| wiki_API.session_of_options(this);
// console.trace(session);
if (session) {
template_name = session.remove_namespace(template_name);
} else {
template_name = template_name.replace(/^Template:/i, '');
}
template_name = 'Template:' + template_name;
// console.trace(template_name);
this.each(template_name, function(token, index, parent) {
// console.trace(template_token);
template_token = token;
template_token.index = index;
template_token.parent = parent;
// Find the first matched.
return for_each_subelement.exit;
}, options);
return template_token;
}
// ------------------------------------------------------------------------
// @inner
function do_append_category(category_token) {
// this: parser
if (!/\n$/.test(this.at(-1))) {
this.push('\n');
}
this.push(category_token, '\n');
}
// parsed.append_category()
function register_and_append_category(category_token, options) {
// console.trace(category_token.name);
// console.trace(category_token);
options = library_namespace.setup_options(options);
if (typeof category_token === 'string') {
category_token = category_token.trim();
if (!category_token.startsWith('[[')) {
// `Category:name` or `Category:name|sort_key`
var matched = category_token.match(/^([^|]+)(\|.*)$/);
var _options = library_namespace.new_options(options);
_options.namespace = 'category';
if (matched) {
matched[1] = wiki_API.to_namespace(matched[1], _options);
category_token = matched[1] + matched[2];
// Release memory. 釋放被占用的記憶體。
matched = null;
} else {
category_token = wiki_API.to_namespace(category_token,
_options);
}
// console.trace(category_token);
category_token = '[[' + category_token + ']]';
}
category_token = wiki_API.parse(category_token, options);
}
// console.assert(category_token.type === 'category');
// const 例如可設定成繁簡轉換後的 key
// @see 20211119.維基詞典展開語言標題模板.js
var category_name = options.category_name
|| typeof options.get_key === 'function'
&& options.get_key(category_token, options)
|| category_token.name;
// this: parser
if (!this.category_Map) {
this.get_categories(options);
}
if (!this.category_Map.has(category_name)) {
this.category_Map.set(category_name, category_token);
if (!options.is_existed)
do_append_category.call(this, category_token);
return;
}
// console.trace(category_token);
if (!category_token.sort_key) {
// 保留 old_category_token,跳過沒有新資訊的。
return;
}
// const
var old_category_token = this.category_Map.get(category_name);
// console.trace(old_category_token);
if (old_category_token.sort_key) {
library_namespace.warn('register_and_append_category: '
+ library_namespace.wiki.title_link_of(this.page)
+ ': Multiple sort key: ' + old_category_token + ', '
+ category_token);
if (options.do_not_overwrite_sort_key) {
if (!options.is_existed) {
// Will overwrite the sort key
do_append_category.call(this, category_token);
}
return;
}
// default: Will overwrite the sort key.
}
if (false && !old_category_token.set_sort_key) {
console.trace(old_category_token);
}
// reuse old category_token
old_category_token.set_sort_key(category_token.sort_key);
if (options.is_existed) {
// 移除重複的/同時存在繁體簡體的 category_token。
return this.each.remove_token;
}
}
// parsed.get_categories()
function get_categories(options) {
if (!this.category_Map) {
this.category_Map = new Map;
options = library_namespace.new_options(options);
options.is_existed = true;
delete options.category_name;
var parsed = this;
// 先從頭登記一次現有的 Category。
this.each('Category', function(category_token, index, parent) {
// for remove
// category_token.index = index;
// category_token.parent = parent;
return parsed.append_category(category_token, options);
}, {
modify : options.remove_existed_duplicated
});
}
// 警告: 重複的 category 只會取得首個出現的。
return Array.from(this.category_Map.values());
}
// ------------------------------------------------------------------------
// parse <ref> of page
// TODO: <ref group="">
// TODO: <ref> in template
function parse_references(options) {
if (this.reference_list)
return this.reference_list;
if (typeof options === 'function') {
options = {
processor : options
};
}
/** {Array}參考文獻列表, starts from No. 1 */
var reference_list = new Array(1);
this.each(function(token) {
if (!token.tag || token.tag.toLowerCase() !== 'ref')
return;
if (typeof options.processor === 'function') {
options.processor.apply(null, arguments);
}
if (token.attributes && ('name' in token.attributes)) {
var attribute_name = token.attributes.name,
// <ref>: name 屬性不能使用數字,請使用可描述內容的標題
list = reference_list[attribute_name];
if (list) {
// index with the same name
token.reference_index = list.length;
list.push(token);
// 已存在相同的名稱,不添加到 reference_list 以增加 NO。
} else {
token.reference_index = 0;
list = [ token ];
reference_list[attribute_name] = list;
reference_list.push(list);
}
if (!list.main && token.type === 'tag'
// 會採用第一個有內容的。
&& token[1].toString().trim()) {
list.main = token;
}
} else {
reference_list.push(token);
}
}, false, Infinity);
this.reference_list = reference_list;
return reference_list;
}
// ------------------------------------------------------------------------
// {{*Navigation templates}} (footer navboxes)
// {{Coord}} or {{Coord missing}}
// {{Authority control}}
// {{featured list}}, {{featured article}}, {{good article}}
// {{Persondata}}
// {{DEFAULTSORT:}}
// [[Category:]]
// {{Stub}}
/** {Array}default footer order */
var default_footer_order = 'transclusion|Coord,Coord Missing|Authority Control|Featured List,Featured Article,Good Article|Persondata|DEFAULTSORT,デフォルトソート|category|Stub'
//
.split('|').map(function(name) {
if (name.includes(','))
return name.split(',');
return name;
});
// return
// {ℕ⁰:Natural+0}: nodes listed in order_list
// undefined: comments / <nowiki> or text may ignored ('\n') or other texts
// NOT_FOUND < 0: unknown node
// @deprecates: use parsed.insert_layout_element() instead
function footer_order(node_to_test, order_list) {
if (false && typeof node_to_test === 'string') {
// skip text. e.g., '\n\n'
return;
}
var type = node_to_test.type;
if (!order_list) {
order_list = default_footer_order;
}
if (type === 'category') {
var order = order_list.lastIndexOf('category');
if (order >= 0) {
return order;
}
}
if (type === 'transclusion') {
var order = order_list.length, name = node_to_test.name;
while (--order > 0) {
var transclusion_name = order_list[order];
if (Array.isArray(transclusion_name) ? transclusion_name
.includes(name) : transclusion_name === name) {
return order;
}
}
if (order_list[0] === 'transclusion') {
// skip [0]
return 0;
}
if (false) {
// other methods 1
// assert: NOT_FOUND + 1 === 0
return order_list.indexOf(node_to_test.name) + 1;
// other methods 2
if (order === NOT_FOUND) {
// 當作 Navigation templates。
return 0;
library_namespace.debug('skip error/unknown transclusion: '
+ node_to_test);
}
return order;
}
}
if (type === 'comment' || node_to_test.tag === 'nowiki') {
// skip comment. e.g., <!-- -->, <nowiki />
return;
}
if (type) {
library_namespace.debug('skip error/unknown node: ' + node_to_test);
return NOT_FOUND;
}
// 其他都不管了。
}
// @deprecates: use parsed.insert_layout_element() instead
function insert_before(before_node, to_insert, options) {
var order_needed = wiki_API.parse(before_node, options, []), order_list = this.order_list;
if (order_needed) {
order_needed = footer_order(order_needed, order_list);
}
if (!(order_needed >= 0)) {
library_namespace.warn('insert_before: skip error/unknown node: '
+ node_to_test);
return this;
}
var index = this.length;
// 從後面開始搜尋。
while (index-- > 0) {
// find the node/place to insert before
if (typeof this[index] === 'string') {
// skip text. e.g., '\n\n'
continue;
}
var order = footer_order(this[index], order_list);
if (order >= 0) {
if (order === order_needed) {
// insert before node_to_test
this.splice(index, 0, to_insert);
break;
}
if (order < order_needed) {
// 已經過頭。
// insert AFTER node_to_test
this.splice(index + 1, 0, to_insert);
break;
}
}
}
return this;
}
// ------------------------------------------------------------------------
// @inner
// get_layout_templates('short description', 'Template:Short description',
// callback, session)
function get_layout_templates(layout, layout_to_fetch, callback, options) {
wiki_API.redirects_here(layout_to_fetch, function(root_page_data,
redirect_list, error) {
var session = wiki_API.session_of_options(options);
var layout_index = session.configuration.layout_index;
if (!layout_index[layout])
layout_index[layout] = Object.create(null);
if (false) {
console.assert(!redirect_list
|| redirect_list === root_page_data.redirect_list);
console.log([ root_page_data, redirect_list ]);
}
redirect_list.forEach(function(page_data) {
layout_index[layout][page_data.title] = null;
});
callback();
}, Object.assign({
// Making .redirect_list[0] the redirect target.
include_root : true
}, options));
}
// @inner
function get_layout_categories(layout, layout_to_fetch, callback, options) {
wiki_API.redirects_root(layout_to_fetch, function(title, page_data) {
wiki_API.list(title, function(list/* , target, options */) {
// assert: Array.isArray(list)
if (list.error) {
library_namespace.error(list.error);
callback();
return;
}
var session = wiki_API.session_of_options(options);
var layout_index = session.configuration.layout_index;
if (!layout_index[layout])
layout_index[layout] = Object.create(null);
list.forEach(function(page_data) {
layout_index[layout][page_data.title] = layout_to_fetch;
});
callback();
}, Object.assign({
type : 'categorymembers'
}, options));
}, options);
}
// @inner
function get_layout_elements(callback, options) {
var layout_list = options.layout_list;
var layout = layout_list.shift();
if (!layout) {
callback();
return;
}
var layout_to_fetch = layout[1];
if (Array.isArray(layout_to_fetch)) {
if (layout_to_fetch.length === 0) {
// Skip null layout_to_fetch
get_layout_elements(callback, options);
return;
}
layout_to_fetch = layout_to_fetch.shift();
layout_list.unshift(layout);
}
layout = layout[0];
if (/^Template:/i.test(layout_to_fetch)) {
get_layout_templates(layout, layout_to_fetch, function() {
get_layout_elements(callback, options);
}, options);
return;
}
if (/^Category:/i.test(layout_to_fetch)) {
get_layout_categories(layout, layout_to_fetch, function() {
get_layout_elements(callback, options);
}, options);
return;
}
throw new TypeError('Invalid layout to fetch: [' + layout + '] '
+ layout_to_fetch);
}
// 取得定位各布局項目所需元素。
// TODO: Not yet tested
function setup_layout_elements(callback, options) {
var session = wiki_API.session_of_options(options);
if (!session.configuration)
session.configuration = Object.create(null);
var layout_index = session.configuration.layout_index;
if (layout_index) {
callback();
return;
}
layout_index = session.configuration.layout_index = Object.create(null);
var layout_list = [];
for ( var layout in layout_configuration) {
var layout_to_fetch = layout_configuration[layout];
layout_list.push([ layout, layout_to_fetch ]);
}
// console.log(layout_list);
options.layout_list = layout_list;
library_namespace.info('setup_layout_elements: Get all elements...');
get_layout_elements(callback, options);
}
var layout_configuration = {
};
// ------------------------------------------------------------------------
var default_layout_order = [
// header
'page_begin',
'deletion_templates',
'redirect', 'redirect_end',
// lead_section_locations
'article_lead_section', 'talk_page_lead', 'hatnote_templates',
// [[Category:Wikipedia maintenance templates]]
'maintenance_templates', 'wikiproject_banners', 'infobox_templates',
'lead_templates_end', 'lead_section_end',
'content', 'content_end',
'appendices',
//
'end_matter', 'navigation_templates', 'DEFAULTSORT', 'categories',
// 小作品模板 stub_templates, zhwiki:featured_template
'page_footer',
//
'page_end' ];
// 整個頁面只能有單一個這種 location。
var single_layout_location_types = [ 'redirect', 'DEFAULTSORT' ];
var KEY_map_template_to_location = typeof Symbol === 'function' ? Symbol('template_to_location')
: '\0template_to_location';
var lead_section_locations = [ 'article_lead_section', 'hatnote_templates',
'talk_page_lead' ];
// template_order_of_layout[site name][layout type] = [ template name ]
// assert: default_layout_order.includes(layout type)
setup_layout_elements.template_order_of_layout = {
enwiki : {
// Deletion / protection tags
// @see [[Category:Deletion templates]]
deletion_templates : [ 'Category:Deletion tags',
'Category:Speedy deletion templates' ],
// [[Wikipedia:Manual of Style/Lead section]]
// [[Wikipedia:Manual of Style/Layout#Order of article elements]]
article_lead_section : [ 'Soft redirect', 'Short description',
//
'DISPLAYTITLE', 'Lowercase title', 'Italic title',
// Hatnotes / hatnote templates
'Category:Hatnote templates',
'Featured list', 'Featured article', 'Good article',
// deletion templates [[Category:刪除模板]]
'Proposed deletion', 'Category:Protection templates',
// 'Category:Proposed deletion-related templates',
'Article for deletion', 'Copy edit',
// Maintenance / dispute tags
'Category:Cleanup templates', 'Category:Dispute templates',
// English variety and date format
'Category:Use English templates',
// @see [[Category:Time and date maintenance templates]]
'Use mdy dates', 'Use dmy dates',
// Infoboxes / infobox templates
'Infobox',
// e.g., {{Contains special characters}}
'Category:Language maintenance templates',
// zhwiki: foreign character warning box
// 'Category:Foreign character warning boxes'
// Images
// Navigation header templates (sidebar templates)
// / Navigational boxes (header navboxes)
// introduction
],
// [[Wikipedia:Talk page layout#Lead (bannerspace)]]
// @ [[w:en:Wikipedia:Talk page layout#Talk page layout]]
// @see
// "Other talk page template redirect fixes; Cleanup redirects"
// @ https://en.wikipedia.org/wiki/User:Magioladitis/WikiProjects
talk_page_lead : [
// Active nominations, when applicable
'GA nominee', 'Featured article candidates', 'Peer review',
// Skip templates
'Skip to talk', 'Skip to bottom', 'Skip to section',
//
'Skip to top and bottom',
// On redirect talk pages
'Talk page of redirect', 'Soft redirect',
// should only be used where it is needed
'Talk header',
// High-importance attention templates
'Notice', 'Contentious topics/talk notice', 'Gs/talk notice',
//
'BLP others',
// TalkWarningTemplates @
// https://sourceforge.net/p/autowikibrowser/code/HEAD/tree/AWB/WikiFunctions/TalkPageFixes.cs#l237
// @see [[Category:Notice and warning templates]]
"COI editnotice", "Warning", "Austrian economics sanctions",
// Specific talk page guideline banners
'Calm', 'Censor', 'Controversial', 'Not a forum', 'FAQ',
//
'Round in circles',
// Language-related talk page guideline banners
'Category:Varieties of English templates',
// Any "article history"
// @see [[Category:Talk message boxes]]
'GA', 'FailedGA', 'Old XfD multi', 'Old prod', 'Old peer review',
// TalkHistoryBT