cejs
Version:
A JavaScript module framework that is simple to use.
1,625 lines (1,428 loc) • 63.9 kB
JavaScript
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): parse wikitext 解析維基語法
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
parser [[WP:維基化]] [[w:en:Wikipedia:AutoWikiBrowser/General fixes]] [[w:en:Wikipedia:WikiProject Check Wikipedia]]
https://www.mediawiki.org/wiki/API:Edit_-_Set_user_preferences
</code>
*
* @since 2022/10/28 13:28:55 拆分自 CeL.application.net.wiki.wikitext
*/
// 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.evaluate',
// for_each_subelement
require : 'application.net.wiki.parser.'
// strftime
+ '|data.date.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, for_each_subelement = wiki_API.parser.parser_prototype.each, strftime = library_namespace.date.Date_to_String.parser.strftime;
// --------------------------------------------------------------------------------------------
function is_invalid_page_title(page_title_string) {
// e.g., "{{#ifexist: A#C | exists | doesn't exist }}"
page_title_string = page_title_string.replace(/#.*$/, '').trim();
return !page_title_string
|| wiki_API.PATTERN_invalid_page_name_characters
.test(page_title_string);
}
// 演算/轉換 wikitext 中的所有 {{{parameter}}}。
function convert_parameter(wikitext, parameters, options) {
if (!parameters)
parameters = Object.create(null);
var parsed = wikitext;
if (typeof wikitext === 'string') {
parsed = wiki_API.parse(wikitext, options);
}
// console.trace([ wikitext, options ]);
// console.trace(parsed);
var have_template_parameters, has_complex_parameter_name;
for_each_subelement.call(parsed, 'parameter', function(token) {
have_template_parameters = true;
var value = token[0];
if (typeof value !== 'string') {
// e.g., `{{{{{{foo}}}}}}`, `{{{ {{{parameter_name}}} | ... }}}`
has_complex_parameter_name = true;
// Skip this parameter token
return;
}
// 取決於參數的定義性
// https://en.wikipedia.org/wiki/Help:Conditional_expressions
if (value in parameters) {
// 先將 parsed 充作 parent 使用。
var parsed = token;
// 預防循環參照。
// e.g., parameters[1] = `{{{1}}}`
while (parsed = parsed.parent) {
// console.trace(parsed);
if (parsed.parameter_NO === value)
return;
}
// 避免污染原 parameter。
parsed = wiki_API.parse(parameters[value].toString(), options);
if (Array.isArray(parsed)) {
// 預防循環參照。
parsed.parameter_NO = value;
for_each_subelement.call(parsed,
//
library_namespace.null_function, {
add_index : true
});
}
// console.trace(parsed);
return parsed;
}
if (token.length < 2) {
// e.g., `{{{1}}}` without parameter 1, return `{{{1}}}` itself.
return;
}
token = [ token[1] ];
convert_parameter(token, parameters, options);
return token[0];
}, true);
// console.trace([ has_complex_parameter_name, parsed.toString() ]);
if (has_complex_parameter_name) {
has_complex_parameter_name = parsed.toString();
if (!wikitext || wikitext !== has_complex_parameter_name) {
// Re-parse again
parsed = convert_parameter(has_complex_parameter_name,
parameters, options);
} else if (wikitext) {
// assert: wikitext === has_complex_parameter_name
parsed.has_complex_parameter_name = true;
}
}
if (have_template_parameters)
parsed.have_template_parameters = true;
// console.trace([ has_complex_parameter_name, parsed.toString() ]);
return parsed;
}
function set_shell(parsed) {
// assert: wiki_API.is_parsed_element(parsed)
if (parsed.type !== 'plain') {
parsed = [ parsed ];
parsed.has_shell = true;
}
return parsed;
}
// 演算 wikitext 中的所有 magic word。
function evaluate_parsed(parsed, options) {
if (!parsed)
return parsed === undefined ? '' : /* 0 || '' */parsed;
if (parsed.evaluate) {
return parsed.evaluate(options);
}
// Error.stackTraceLimit = Infinity;
// console.trace([ parsed.toString(), parsed ]);
// Error.stackTraceLimit = 10;
parsed = set_shell(parsed);
var promise = for_each_subelement.call(parsed, 'magic_word_function',
//
function(token) {
// console.trace(token);
return evaluate_parser_function_token.call(token, options);
}, true);
// Error.stackTraceLimit = Infinity;
// console.trace([ parsed.toString(), parsed, promise ]);
// Error.stackTraceLimit = 10;
return library_namespace.is_thenable(promise)
//
? promise.then(function return_parsed() {
return parsed;
}) : parsed;
}
function template_preprocessor(wikitext, options) {
if (!wikitext.match) {
console.trace(wikitext);
}
var matched = wikitext
// 優先權高低: <onlyinclude> → <nowiki> → <noinclude>, <includeonly>
// [[mw:Transclusion#Partial transclusion markup]]
.match(/<onlyinclude(\s[^<>]*)?>[\s\S]*?<\/onlyinclude>/g);
if (matched) {
// 只有被<onlyinclude>和</onlyinclude>包含的文字才出現在呼叫模板的頁面中,模板的其他內容不出現在呼叫模板的頁面中。
wikitext = matched.join('').replace(/<onlyinclude(\/|\s[^<>]*)?>/g,
'').replace(/<\/onlyinclude>/g, '');
}
var parsed = wiki_API.parser(wikitext, options).parse();
// console.trace(wikitext);
parsed.each('tag_single', function(token) {
if (token.tag === 'noinclude') {
// Allow `<noinclude />`
return '';
}
}, true);
parsed.each('tag', function(token) {
if (token.tag === 'noinclude')
return '';
if (token.tag === 'includeonly')
return token.join('');
}, true);
wikitext = parsed.toString();
// console.trace(wikitext);
return wikitext;
}
function generate_expand_template_function(transclusion_config) {
// 利用語境 context。
return function general_expand_template(options) {
// console.trace(transclusion_config);
transclusion_config.usage_times++;
var wikitext = transclusion_config.simplified_template_wikitext;
// console.trace(transclusion_config);
return transclusion_config.need_evaluate ? simplify_transclusion(
wikitext, this, options) : wikitext;
};
}
/**
* 演算/簡化要 transclusion 的模板 wikitext。
*
* @param {String}wikitext
* 模板 wikitext。
* @param {Array}template_token_called
* 正呼叫此模板的 template token。 The template token being called.
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項。
* @param {Number}[level]
* 迭代呼叫層數。
*
* @returns {Promise|Array} 簡化過且解析過的 wiki syntax。
*/
function simplify_transclusion(wikitext, template_token_called, options,
level) {
// console.trace(template_token_called);
// console.trace(template_token_called[0].toString(), level);
var parameters = template_token_called.parameters;
var page_data, transclusion_config;
if (wiki_API.is_page_data(wikitext)) {
page_data = wikitext;
wikitext = wiki_API.content_of(page_data);
}
wikitext = template_preprocessor(wikitext, options);
if (!wikitext)
return wikitext;
// Error.stackTraceLimit = Infinity;
// console.trace(wikitext);
// Error.stackTraceLimit = 10;
var parsed = convert_parameter(wikitext, parameters, options);
// console.trace(page_data);
if (page_data) {
// cache template code
var session = wiki_API.session_of_options(options);
// 只在第一次執行時(!!page_data=true)顯示訊息。
options = Object.assign({
show_NYI_message : true,
transclusion_from_page : page_data
}, options);
if (session) {
var template_name = session
.remove_namespace(page_data, options);
// delete page_data.revisions;
// console.trace(page_data);
transclusion_config = {
title : page_data.title,
// page_data : page_data,
need_evaluate : parsed.have_template_parameters,
// cache simplified wikitext
simplified_template_wikitext : wikitext,
// 引用次數
usage_times : 1,
fetch_date : Date.now()
};
// console.trace(page_data.title, wiki_API.template_functions);
if (wiki_API.template_functions) {
wiki_API.template_functions.set_proto_properties(
//
template_name, {
expand :
//
generate_expand_template_function(transclusion_config)
}, options);
} else {
library_namespace.debug(
//
'建議 include application.net.wiki.template_functions', 1,
'simplify_transclusion');
}
if (transclusion_config.need_evaluate)
transclusion_config = null;
}
}
wikitext = parsed.toString();
// console.trace([ wikitext, page_data ]);
if (!level)
level = 1;
else
level++;
// 避免不解析模板。
// e.g., "=={{USA}} USA=="
delete options.target_array;
parsed = wiki_API.parser(wikitext, options).parse();
if (parsed.length === 1 && typeof parsed[0] === 'string'
&& parsed[0].includes('{{')) {
library_namespace.warn('simplify_transclusion: Cannot parse '
+ JSON.stringify(wikitext));
// console.trace(wikitext);
// console.trace(options);
console.trace(Object.keys(options));
// console.trace(parsed);
// console.trace(wiki_API.parse(wikitext, options));
}
if (options.template_token_called !== template_token_called) {
options = Object.clone(options);
// 紀錄正呼叫的 template token。
options.template_token_called = template_token_called;
}
/**
* TODO:<code>
parse 嵌入section內文 [[mw:Extension:Labeled Section Transclusion#Transclude the introduction]]:
{{#lsth:page_title|section begin in wikitext|section end in wikitext}}, {{#section-h:page_title}} 語意上相當於 {{page_title#section}}。如果有多個相同名稱的section,僅轉換第一個。The matching is case insensitive
TODO: parse <section begin=chapter1 />, {{#lst:page_title|section begin|section end}}, {{#lstx:page_title|section|replacement_text}}
@see [[w:en:Template:Excerpt]]
</code>
*/
// [[mw:Help:Substitution]]
// {{subst:FULLPAGENAME}} {{safesubst:FULLPAGENAME}}
var promise = parsed.each('magic_word_function', function(token) {
if (token.name !== 'SAFESUBST' && token.name !== 'SUBST') {
if (transclusion_config && !transclusion_config.need_evaluate) {
transclusion_config.need_evaluate = true;
transclusion_config = null;
}
return;
}
// token = evaluate_parsed(token, options);
var wikitext = token.toString().replace(/^({{)[^:]+:/, '$1');
var parsed = wiki_API.parse(wikitext, options);
if (level > 3 || !parsed || parsed.type !== 'transclusion') {
// `page_data ?`: 為了維持cache與第一次執行的輸出相同。
// 例如在 `await CeL.wiki.expand_transclusion(
// '{{Namespace detect|main=Article text}}')`
return page_data ? token : parsed;
}
// expand template
parsed = expand_transclusion(parsed, options, level);
return parsed;
}, true);
function resolve_magic_word_function() {
var wikitext = parsed.toString();
// console.trace([ wikitext, page_data ]);
parsed = wiki_API.parser(wikitext, options).parse();
if (/{{/.test(parsed)) {
// console.trace(page_data && page_data.title || wikitext);
}
var promise = evaluate_parsed(parsed, options);
// console.trace([ promise ]);
return promise || parsed;
}
if (promise)
return promise.then(resolve_magic_word_function);
return resolve_magic_word_function();
}
// 循環展開模板節點。
// 可考慮是否採用 CeL.wiki.wikitext_to_plain_text()
function repeatedly_expand_template_token(token, options) {
while (token && token.type === 'transclusion') {
if (typeof token.expand !== 'function') {
if (wiki_API.template_functions) {
// console.trace(options);
wiki_API.template_functions.adapt_function(token, null,
null, options);
// console.trace([ token, token.expand, options ]);
} else {
library_namespace.debug(
//
'建議 include application.net.wiki.template_functions', 1,
'repeatedly_expand_template_token');
}
if (typeof token.expand !== 'function') {
break;
}
}
// console.trace(token);
// console.trace(options);
// expand template, .expand_template(), .to_wikitext()
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
var promise = token.expand(options);
if (library_namespace.is_thenable(promise)) {
// e.g., general_expand_template()
if (options && options.allow_promise) {
return promise.then(function(token) {
// console.log([ token, options ]);
// console.trace(token.toString());
return repeatedly_expand_template_token(
//
token, options);
});
}
library_namespace
.error('repeatedly_expand_template_token: '
+ 'Using async function + options.allow_promise to expand: '
+ token);
// Error.stackTraceLimit = Infinity;
console.trace(token);
// delete token.expand;
break;
}
if (token.expand.incomplete) {
// e.g., @
// CeL.application.net.wiki.template_functions.general_functions
// 生成未完全實作的註解。
promise = '<!-- Incomplete expansion of '
+ token.page_title
+ (typeof token.expand.incomplete === 'string' ? ': '
+ token.expand.incomplete : '') + ' -->'
+ promise;
// console.trace(promise);
}
// console.trace(promise);
// re-parse
token = wiki_API.parse(promise.toString(), options);
}
return token;
}
// 類似 wiki_API_expandtemplates()
// ** 僅能提供簡單的演算功能,但提供 cache,不必每次從伺服器重新取得嵌入的頁面。
// [[Special:ExpandTemplates]]
// 使用上注意: 應設定 options[KEY_on_page_title_option]
// 可考慮是否採用 CeL.wiki.wikitext_to_plain_text()
function expand_transclusion(wikitext, options, level) {
if (!wikitext)
return wikitext;
if (library_namespace.is_thenable(wikitext)) {
return wikitext.then(function(wikitext) {
return expand_transclusion(wikitext, options, level);
});
}
var parsed;
if (typeof options === 'string') {
// temp
parsed = options;
options = {
allow_promise : true,
set_not_evaluated : true
};
options[KEY_on_page_title_option] = parsed;
} else {
// .new_options(): 會設定 options.something_not_evaluated,避免污染。
options = Object.assign({
allow_promise : true,
set_not_evaluated : true
}, options);
}
if (Array.isArray(wikitext)) {
parsed = set_shell(wikitext);
// console.trace(parsed);
} else {
parsed = wiki_API.parser(wikitext, options).parse();
if (!options[KEY_on_page_title_option]
&& wiki_API.is_page_data(wikitext)) {
options[KEY_on_page_title_option] = wikitext;
}
}
// console.trace(parsed, options);
if (options.template_token_called) {
parsed = convert_parameter(parsed,
options.template_token_called.parameters, options);
}
var session = wiki_API.session_of_options(options);
// console.trace(parsed);
// Error.stackTraceLimit = Infinity;
// console.trace(parsed.toString());
// Error.stackTraceLimit = 10;
var promise = for_each_subelement.call(parsed, 'transclusion',
//
function(token) {
// Error.stackTraceLimit = Infinity;
// console.trace(token);
token = repeatedly_expand_template_token(token, options);
// console.trace(token);
// Error.stackTraceLimit = 10;
if (!token || token.type !== 'transclusion')
return token;
token = expand_template_name(token);
if (library_namespace.is_thenable(token)) {
return token.then(fetch_and_resolve_template);
}
// console.trace(token);
return fetch_and_resolve_template(token);
}, true);
function expand_template_name(token) {
// console.trace(token[0]);
var template_name = token[0].toString();
var promise = expand_transclusion(token[0], options, level);
if (!library_namespace.is_thenable(promise)) {
if (false) {
Error.stackTraceLimit = Infinity;
console.trace([ token, token[0], token[0].toString(),
template_name, promise ]);
Error.stackTraceLimit = 10;
}
if (template_name !== token[0].toString()) {
token[0] = promise;
// console.trace('re-parse ' + token[0]);
token = wiki_API.parse(token.toString(), options);
token = repeatedly_expand_template_token(token, options);
if (token.type === 'plain' && token.length === 1)
token = token[0];
// console.trace(token);
}
return token;
}
// e.g., `{{ {{t|a|b}}|b|d}}`
return promise.then(function(template_name) {
var _token = wiki_API.parse('{{' + template_name + '}}',
options);
// console.trace(_token);
token[0] = _token[0];
token.page_title = _token.page_title;
return expand_template_name(token);
});
}
function fetch_and_resolve_template(token) {
if (!token || token.type !== 'transclusion') {
// Error.stackTraceLimit = Infinity;
// console.trace(token);
// Error.stackTraceLimit = 10;
return token;
}
if (false) {
console.trace(token);
var some_sub_token_not_evaluated;
for_each_subelement.call(token, 'magic_word_function',
//
function(magic_word_function) {
if (magic_word_function.not_evaluated) {
some_sub_token_not_evaluated = true;
return for_each_subelement.exit;
}
});
console.trace(some_sub_token_not_evaluated);
}
var page_title = token.page_title.toString();
// @see PATTERN_page_name @ CeL.application.net.wiki.namespace
if (is_invalid_page_title(page_title)) {
library_namespace.warn('expand_transclusion: Cannot expand '
+ token);
// Error.stackTraceLimit = Infinity;
// console.trace(token);
// Error.stackTraceLimit = 10;
return token;
}
return new Promise(function(resolve, reject) {
function evaluate(page_data, error) {
if (error) {
// e.g. 頁面不存在,不做更改。
library_namespace.error('expand_transclusion: '
+ wiki_API.title_link_of(page_title) + ': '
+ error);
// reject(error);
resolve();
return;
}
resolve(simplify_transclusion(page_data, token, options,
level));
}
// var session = wiki_API.session_of_options(options);
var page_options = Object.assign({
redirects : 1
}, options);
if (!session) {
page_title = wiki_API.to_namespace(page_title, 'Template');
wiki_API.page(page_title, evaluate, page_options);
return;
}
page_title = session.to_namespace(page_title, 'Template');
// console.trace(page_title);
// 盡量避免網路操作。
if (!session.templates_now_fetching)
session.templates_now_fetching = Object.create(null);
if (page_title in session.templates_now_fetching) {
session.templates_now_fetching[page_title].push(evaluate);
if (false) {
console.trace([ session.templates_now_fetching
//
[page_title].length, page_title ]);
}
return;
}
session.templates_now_fetching[page_title] = [ evaluate ];
if (false) {
console.trace(page_title);
Error.stackTraceLimit = Infinity;
console.trace([ session.running, session.actions.length,
//
session.actions[
//
wiki_API.KEY_waiting_callback_result_relying_on_this] ]);
Error.stackTraceLimit = 10;
}
library_namespace.log_temporary('fetch_and_resolve_template: '
+ wiki_API.title_link_of(page_title));
// Error.stackTraceLimit = Infinity;
// console.trace(page_title);
session.register_redirects(page_title,
//
function(page_data, error) {
// console.trace([ page_data, page_title, error ]);
session.page(page_data || page_title, function(page_data,
error) {
// console.trace([ page_data, page_title, error ]);
var evaluate_list
//
= session.templates_now_fetching[page_title];
delete session.templates_now_fetching[page_title];
evaluate_list.forEach(function(evaluate) {
evaluate(page_data, error);
});
}, page_options);
}, {
// namespace : 'Template',
no_message : true
});
// Error.stackTraceLimit = 10;
});
}
function return_evaluated() {
// console.trace(parsed.toString());
if (parsed.has_shell)
parsed = parsed[0];
parsed = evaluate_parsed(parsed, options);
// console.trace(parsed.toString());
return parsed;
}
// Error.stackTraceLimit = Infinity;
// console.trace(promise);
// Error.stackTraceLimit = 10;
if (!library_namespace.is_thenable(promise))
return return_evaluated();
promise = promise.then(return_evaluated);
return promise;
}
// --------------------------------------------------------------------------------------------
var PATTERN_expr_number = /([+\-]?(?:[\d.]+)(?:e[+\-]\d+)?|\.|(?:pi|e|NAN)(?!\s*[+\-\d\w]|$))/i;
function generate_PATTERN_expr_operations(operations, operand_count) {
return new RegExp((operand_count === 1 ? /(operations)\s*number/
// assert: operand_count === 2
: /number\s*(operations)\s*number/).source.replace('operations',
operations).replace(/number/g, PATTERN_expr_number.source),
'ig');
}
var PATTERN_expr_e_notation = generate_PATTERN_expr_operations('[eE]', 2);
var PATTERN_expr_floor = generate_PATTERN_expr_operations('floor', 1);
var PATTERN_expr_power = generate_PATTERN_expr_operations(/\^/.source, 2);
// 乘 除 模除/餘數
var PATTERN_expr_乘除 = generate_PATTERN_expr_operations('[*/]|div|mod|fmod',
2);
var PATTERN_expr_加減 = generate_PATTERN_expr_operations('[+\-]', 2);
var PATTERN_expr_round = generate_PATTERN_expr_operations('round', 2);
var
// 其他的一元運算
PATTERN_expr_unary_operations = generate_PATTERN_expr_operations(
// [+\-]+|exp|ln|abs|sqrt|trunc|floor|ceil|sin|cos|tan|asin|acos|atan|not
/[+\-]+|exp|ln|abs|sqrt|trunc|ceil|sin|cos|tan|asin|acos|atan|not/.source,
1),
// 其他的二元運算
PATTERN_expr_binary_operations = generate_PATTERN_expr_operations(
// [+\-*/^<>=eE]|[<>!]=|<>|and|or|div|mod|fmod|round
/[<>=]|[<>!]=|<>/.source, 2),
// 用括號框起來起來的數字。
PATTERN_expr_bracketed_number = new RegExp(/\(\s*number\s*\)/.source
.replace(/number/g, PATTERN_expr_number.source), 'g');
var PATTERN_expr_and = generate_PATTERN_expr_operations('and', 2);
var PATTERN_expr_or = generate_PATTERN_expr_operations('or', 2);
// [[mw:Help:Extension:ParserFunctions##expr]], [[mw:Help:Help:Calculation]]
function eval_expr(expression) {
// console.trace(expression);
var TRUE = 1, FALSE = 0;
function to_Number(number) {
if (number === '.')
return 0;
number = number.toLowerCase();
if (number === 'pi')
return Math.PI;
if (number === 'e')
return Math.E;
if (number === 'nan')
return 'NAN';
// '123.456.789e33' → '123.456e33'
number = number.replace(/^([^.]*\.[^.]*)\.[\d.]*/, '$1');
return +number;
}
function _handle_binary_operations(all, _1, op, _2) {
op = op.toLowerCase();
if (op === 'e') {
var number = +all;
// console.trace([ all, number, _1, op, _2 ]);
if (!isNaN(number))
return number;
// e.g., '{{#expr:6e(5-2)e-2}}'
number = _2.match(/^([\d+\-.]+)(e[\d+\-]+)$/);
if (number)
return _1 * Math.pow(10, number[1]) + number[2];
return _1 * Math.pow(10, _2);
}
_1 = to_Number(_1);
_2 = to_Number(_2);
// console.trace([ _1, op, _2 ]);
switch (op) {
case '+':
return _1 + _2;
case '-':
return _1 - _2;
case '*':
return _1 * _2;
case '/':
case 'div':
return _1 / _2;
case 'mod':
// 將兩數截斷為整數後的除法餘數。
return Math.floor(_1) % Math.floor(_2);
case 'fmod':
return (_1 % _2).to_fixed();
case '^':
var power = Math.pow(_1, _2);
return isNaN(power) ? 'NAN' : power;
case 'round':
var power = Math.pow(10, Math.trunc(_2));
var is_negative = _1 < 0;
if (is_negative)
_1 = -_1;
// https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Math/round
// giving a different result in the case of negative numbers
// with a fractional part of exactly 0.5.
_1 = Math.round((_1 * power).to_fixed()) / power;
if (is_negative)
_1 = -_1;
return _1;
case '>':
return _1 > _2 ? TRUE : FALSE;
case '<':
return _1 < _2 ? TRUE : FALSE;
case '>=':
return _1 >= _2 ? TRUE : FALSE;
case '<=':
return _1 <= _2 ? TRUE : FALSE;
case '=':
return _1 === _2 ? TRUE : FALSE;
case '<>':
case '!=':
return _1 !== _2 ? TRUE : FALSE;
case 'and':
return _1 && _2 ? TRUE : FALSE;
case 'or':
return _1 || _2 ? TRUE : FALSE;
}
throw new Error('二元運算符未定義,這不該發生,請聯絡函式庫作者: '
+ [ all, _1, op, _2, expression ]);
}
function handle_binary_operations(all, _1, op, _2) {
var result = _handle_binary_operations(all, _1, op, _2);
// preserve plus sign
// e.g., '{{#expr:2+3*4}}'
return /^\s*\+/.test(_1)
&& (typeof result === 'number' ? result >= 0 : !/^\s*\+/
.test(result)) ? '+' + result : result;
}
function handle_unary_operations(all, op, number) {
op = op.toLowerCase();
number = to_Number(number);
// console.trace([ op, number ]);
if (/^[+\-]+$/.test(op)) {
return (op.length - op.replace(/-/g, '').length) % 2 === 0
// preserve plus sign
// e.g., '{{#expr:(abs-2)+3}}'
? '+' + number
// e.g., "{{#expr:+-+-++-5}}"
: -number;
}
switch (op) {
case 'exp':
return Math.exp(number);
case 'ln':
return Math.log(number);
case 'abs':
return Math.abs(number);
case 'sqrt':
return Math.sqrt(number);
case 'trunc':
return Math.trunc(number);
case 'floor':
return Math.floor(number);
case 'ceil':
return Math.ceil(number);
case 'sin':
return Math.sin(number);
case 'cos':
return Math.cos(number);
case 'tan':
return Math.tan(number);
case 'asin':
return Math.asin(number);
case 'acos':
return Math.acos(number);
case 'atan':
return Math.atan(number);
case 'not':
return number ? FALSE : TRUE;
}
throw new Error('一元運算符未定義,這不該發生,請聯絡函式庫作者: '
+ [ all, op, number, expression ]);
}
while (true) {
var new_expression = expression;
new_expression = new_expression.replace(
PATTERN_expr_bracketed_number, '$1');
new_expression = new_expression.replace(PATTERN_expr_e_notation,
handle_binary_operations);
new_expression = new_expression.replace(PATTERN_expr_floor,
handle_unary_operations);
// @see https://en.wikipedia.org/wiki/Order_of_operations
new_expression = new_expression.replace(PATTERN_expr_power,
handle_binary_operations);
new_expression = new_expression.replace(PATTERN_expr_乘除,
handle_binary_operations);
new_expression = new_expression.replace(PATTERN_expr_加減,
handle_binary_operations);
new_expression = new_expression.replace(PATTERN_expr_round,
handle_binary_operations);
new_expression = new_expression.replace(
PATTERN_expr_binary_operations, handle_binary_operations);
new_expression = new_expression.replace(PATTERN_expr_and,
handle_binary_operations);
new_expression = new_expression.replace(PATTERN_expr_or,
handle_binary_operations);
new_expression = new_expression.replace(
PATTERN_expr_unary_operations, handle_unary_operations);
if (new_expression === expression)
break;
// console.trace([ expression, new_expression ]);
expression = new_expression;
}
expression = expression.trim();
// console.trace([ expression, !isNaN(expression) ]);
if (!isNaN(expression))
return expression ? String(+expression) : '';
// e.g., for {{#expr:pi}}
var number = to_Number(expression);
return isNaN(number) ? expression : String(number);
}
// --------------------------------------------------------------------------------------------
function wiki_date_to_String(date_value, format, options) {
var session = wiki_API.session_of_options(options);
var locale = wiki_API.site_name(session);
var conversion = strftime.get_conversion(locale);
if (!conversion) {
locale = wiki_date_to_String.default_locale;
conversion = strftime.get_conversion(locale);
}
var search_pattern = wiki_date_to_String.search_pattern[locale];
if (!search_pattern) {
search_pattern = wiki_date_to_String.search_pattern[locale] = new RegExp(
'("[^"]*"|' + Object.keys(conversion).join('|') + ')', 'g');
}
// "Y" → "%Y"
format = format.replace(search_pattern, function(all, conversion) {
if (/^"/.test(conversion))
return conversion;
return '%' + conversion;
});
if (/\w/.test(format.replace(/%\w/g, ''))) {
// 警告不能完全模擬的功能。
// console.trace([ format, format.replace(/[-\s]/g, '') ]);
return new wiki_error('NYI');
}
options = Object.assign(Object.clone(options), {
locale : locale
});
// console.trace([ date_value, format, locale, options ]);
return strftime(date_value, format, locale, options);
}
wiki_date_to_String.default_locale = 'enwiki';
wiki_date_to_String.search_pattern = Object.create(null);
var gettext_date = library_namespace.gettext.date;
// https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time
// https://www.php.net/manual/en/datetimeimmutable.createfromformat.php
strftime.set_conversion({
Y : function(date_value) {
return date_value.getUTCFullYear();
},
y : function(date_value) {
return date_value.getUTCYear();
},
L : function(date_value) {
var year = date_value.getUTCFullYear();
return library_namespace.date.is_leap_year(year);
},
n : function(date_value, options) {
return 1 + date_value.getUTCMonth();
},
m : function(date_value, options) {
return (1 + date_value.getUTCMonth()).pad(2);
},
j : function(date_value, options) {
return date_value.getUTCDate();
},
d : function(date_value, options) {
return date_value.getUTCDate().pad(2);
},
F : function(date_value, options) {
return gettext_date.month(1 + date_value.getUTCMonth(), 'en');
}
// others: TODO
}, wiki_date_to_String.default_locale, {
no_gettext : true
});
// --------------------------------------------------------------------------------------------
// https://stackoverflow.com/questions/1382107/whats-a-good-way-to-extend-error-in-javascript
function wiki_error(message) {
// Error.call(this, message);
this.message = message;
// https://groups.google.com/g/mozilla.dev.tech.js-engine/c/rxlcWq-_yzI
this.stack = (new Error).stack;
}
wiki_error.prototype = Object.assign(Object.create(Error.prototype), {
name : 'wiki_error',
// is_wiki_error : true,
constructor : wiki_error
});
// --------------------------------------------------------------------------------------------
var buggy_toLocaleString = (1234).toLocaleString('en') !== '1,234';
if (false && buggy_toLocaleString) {
// e.g., node.js 0.10
library_namespace
.warn('Number.prototype.toLocaleString() 有缺陷,{{FORMATNUM:}} 可能產生錯誤結果!');
}
var KEY_on_page_title_option = 'on_page_title';
// 小數點, 千位分號
var THOUSANDS_SEPARATOR = {
de : '.',
fr : ' ',
en : ',',
zh : ',',
ISO : ' '
}, DECIMAL_SEPARATOR = {
de : ',',
fr : ',',
en : '.',
ja : '.',
zh : '.',
ISO : '.' || ','
};
// https://en.wikipedia.org/wiki/Help:Conditional_expressions
function evaluate_parser_function_token(options) {
var session = wiki_API.session_of_options(options);
var token = this, allow_promise = session && options
&& options.allow_promise;
function NYI(reason) {
// delete token.expand;
token.not_evaluated = reason || true;
if (!options) {
return token;
}
// 標記並直接回傳,避免 evaluate_parsed() 重複呼叫。
if (options.set_not_evaluated && !options.something_not_evaluated) {
options.something_not_evaluated = true;
}
if (options.show_NYI_message) {
var message_name = token.name;
if (message_name === '#invoke') {
message_name += ':' + token.module_name + '|'
+ token.function_name;
}
var transclusion_message = '';
var transclusion_from_page = options.transclusion_from_page;
if (wiki_API.is_page_data(transclusion_from_page)) {
transclusion_message = '(自 '
+ wiki_API.title_link_of(transclusion_from_page)
+ ' 嵌入)';
// 已顯示的訊息。
var showed_evaluate_messages = transclusion_from_page.showed_evaluate_messages;
if (!showed_evaluate_messages) {
showed_evaluate_messages = transclusion_from_page.showed_evaluate_messages = Object
.create(null);
}
// 避免重複顯示訊息。
if (showed_evaluate_messages[message_name]) {
message_name = null;
} else {
showed_evaluate_messages[message_name] = true;
}
}
if (message_name) {
library_namespace.warn('evaluate_parser_function_token: '
+ '尚未加入演算 {{' + message_name + '}} 的功能'
+ transclusion_message + ': '
+ (reason ? reason + ': ' : '') + token);
}
// Error.stackTraceLimit = Infinity;
// console.trace(options);
// console.trace(token);
// Error.stackTraceLimit = 10;
}
return token;
}
function get_parameter(NO) {
// var is_parser_function = token.name.startsWith('#');
var parameter =
// is_parser_function ? token.parameters[NO] :
// token.parameters[NO] === token[NO + 1]
token[NO];
// console.trace([ parameter, '' + token, token ]);
// if (parameter === 0) return '0';
return parameter || '';
}
function value_to_String(value, allow_thenable, as_key) {
function return_parameter(value) {
if (Array.isArray(value)) {
for_each_subelement.call(value, 'comment', function() {
return for_each_subelement.remove_token;
});
if (as_key) {
var extensiontag_hash = session
&& session.configurations.extensiontag_hash
|| wiki_API.wiki_extensiontags;
for_each_subelement.call(value, function(token) {
if ((token.type === 'tag'
//
|| token.type === 'tag_single')
//
&& (token.tag.toLowerCase() in extensiontag_hash)) {
value.has_extensiontag = true;
return for_each_subelement.exit;
}
});
if (value.has_extensiontag)
return value;
}
}
value = String(value || '');
// console.trace([ '' + token, token ]);
// var is_parser_function = token.name.startsWith('#');
// if (!is_parser_function)
value = value.trim();
return value;
}
// console.trace(value);
var _value =
// /[{}]/.test(value) &&
expand_transclusion(value, options);
if (!library_namespace.is_thenable(_value)) {
// console.trace(_value);
value = return_parameter(_value);
} else if (allow_thenable) {
value = _value.then(return_parameter);
} else {
value = return_parameter(value);
}
return value;
}
function get_parameter_String(NO, allow_thenable, as_key) {
return value_to_String(get_parameter(NO), allow_thenable, as_key);
}
function get_page_data() {
if (options
&& wiki_API.is_page_data(options[KEY_on_page_title_option])) {
return options[KEY_on_page_title_option];
}
}
function get_page_revision() {
var page_data = get_page_data();
if (!page_data)
return;
var revision = wiki_API.content_of.revision(page_data);
return revision;
}
function get_page_title(remove_namespace) {
var title = wiki_API.normalize_title(get_parameter_String(1)
// [[mw:Help:Magic words#Page names]]
|| options && options[KEY_on_page_title_option], options) || '';
return remove_namespace ? wiki_API.remove_namespace(title, options)
: title;
}
function get_interface_message(message_id) {
// console.trace(message_id);
if (!message_id || !(message_id = String(message_id).trim()))
return message_id;
if (!session.interface_messages)
session.interface_messages = new Map;
if (session.interface_messages.has(message_id))
return session.interface_messages.get(message_id);
return new Promise(function(resolve, reject) {
/**
* <code>
[[w:en:WP:MWN#Technical details]]
The difference between {{MediaWiki:}} and {{int:}} is that {{MediaWiki:}} transcludes using the default language of the Wiki (i.e. English), whereas {{int:}} transcludes using the language set by the user's preferences.
</code>
*/
session.page('MediaWiki:' + message_id, function(page_data,
error) {
if (false && error) {
reject(error);
return;
}
var content = library_namespace.HTML_to_Unicode(!error
&& wiki_API.content_of(page_data)
|| ('⧼' + page_data.title + '⧽'));
library_namespace.info(
//
'get_interface_message: Cache interface message: ['
+ message_id + '] = ' + JSON.stringify(content));
session.interface_messages.set(message_id, content);
resolve(content);
});
});
}
function reparse_token_name(name) {
// 避免汙染。
// console.trace(token.toString());
token = wiki_API.parse(token.toString(), options);
token[0] = name + ':';
// console.trace(token.toString());
token = wiki_API.parse(token.toString(), options);
if (Array.isArray(token.name)) {
token.name.evaluated = true;
}
// console.trace(token);
return evaluate_parser_function_token.call(token, options);
}
function check_token_key(attribute_name, setter) {
var token_key = token[attribute_name];
if (!Array.isArray(token_key) || token_key.evaluated) {
return;
}
function set_attribute(key) {
if (Array.isArray(key)) {
// 避免循環設定。
key.evaluated = true;
}
token[attribute_name] = key;
}
var promise = wiki_API.parse
.wiki_element_to_key(expand_transclusion(token_key, options));
if (library_namespace.is_thenable(promise)) {
if (setter)
return promise.then(setter);
return promise.then(set_attribute).then(
evaluate_parser_function_token.bind(token, options));
}
// console.trace(promise);
if (setter) {
return setter(promise);
}
set_attribute(promise);
}
function fullurl(is_localurl) {
var query = get_parameter_String(2);
var url = encodeURI(get_parameter_String(1));
url = query ? (session ? session.latest_site_configurations.general.script
: '/w/index.php')
+ '?title=' + url + '&' + query
: (session ? session.latest_site_configurations.general.articlepath
: '/wiki/$1').replace('$1', url);
if (!is_localurl) {
if (!session)
return NYI();
url = session.latest_site_configurations.general.server + url;
}
return url;
}
// TODO: https://www.mediawiki.org/wiki/Manual:PAGENAMEE_encoding
function PAGENAMEE_encoding(page_title) {
if (!token.name.endsWith('EE'))
return page_title;
return encodeURI(page_title.replace(/ /g, '_'));
}
// --------------------------------------------------------------------
if (Array.isArray(token.name) && !token.name.evaluated) {
return check_token_key('name', reparse_token_name);
}
// All (token.name)s MUST in default_magic_words_hash
// @ CeL.application.net.wiki.parser.wikitext
switch (token.name) {
// @see prefix_page_name(page_name) @ CeL.application.net.wiki.namespace
case '!':
// '{{!}}' → '|'
return '|';
case '=':
// '{{=}}' → '='
return '=';
// ----------------------------------------------------------------
case '#len':
// {{#len:string}}
// TODO: ags such as <nowiki> and other tag extensions will always
// have a length of zero, since their content is hidden from the
// parser.
return get_parameter_String(1).length;
case '#sub':
// {{#sub:string|start|length}}
return get_parameter_String(3) ? get_parameter_String(1).substring(
get_parameter_String(2), get_parameter_String(3))
: get_parameter_String(1).slice(get_parameter_String(2));
case 'LC':
return get_parameter_String(1).toLowerCase();
case 'UC':
return get_parameter_String(1).toUpperCase();
case 'LCFIRST':
return get_parameter_String(1).replace(/^./, function(fc) {
return fc.toLowerCase();
});
case 'UCFIRST':
return get_parameter_String(1).replace(/^./, function(fc) {
return fc.toUpperCase();
});
case 'PADLEFT':
return get_parameter_String(1).padStart(get_parameter_String(2),
get_parameter_String(3) || '0');
case 'PADRIGHT':
return get_parameter_String(1).padEnd(get_parameter_String(2),
get_parameter_String(3) || '0');
// ----------------------------------------------------------------
case 'FORMATNUM':
var number = get_parameter_String(1);
var type = get_parameter_String(2);
// TODO: 此為有缺陷的極精簡版。
if (type === 'R' || type === 'NOSEP')
return number.replace(/,/g, '');
number = number.match(/^([\s\S]+?)\.(.+)?$/);
var thousands_separator = THOUSANDS_SEPARATOR[wiki_API.language]
|| THOUSANDS_SEPARATOR.ISO;
var decimal_separator = DECIMAL_SEPARATOR[wiki_API.language]
|| DECIMAL_SEPARATOR.ISO;
if (false && !buggy_toLocaleString)
return (+number[1]).toLocaleString('en')
+ (number[2] ? decimal_separator + number[2] : '');
// digits
// number[1] maybe null
number[1] = (number[1] || '').chars();
for (var index = number[1].length, numbers = 0; index > 0; index--) {
if (!/^\d$/.test(number[1][index])) {
numbers = 0;
} else if (++numbers === 3) {
number[1].splice(index, 0, thousands_separator);
numbers = 0;
}
}
return number[1].join('')
+ (number[2] ? decimal_separator + number[2] : '');
// ----------------------------------------------------------------
// [[mw:Help:Magic words#Date and time]]
case 'CURRENTYEAR':
return (new Date).getUTCFullYear();
case 'CURRENTMONTH':
return ((new Date).getUTCMonth() + 1).toString().padStart(2, 0);
case 'CURRENTMONTH1':
return (new Date).getUTCMonth() + 1;
case 'CURRENTDAY':
return (new Date).getUTCDate();
case 'CURRENTDAY2':
return (new Date).getUTCDate().toString().padStart(2, 0);
case 'CURRENTDAY':
return (new Date).getUTCDate();
case 'CURRENTDOW':
return (new Date).getUTCDay();
case 'CURRENTHOUR':
return (new Date).getUTCHours().toString().padStart(2, 0);
case 'CURRENTTIME':
return (new Date).getUTCHours().toString().padStart(2, 0) + ':'
+ (new Date).getUTCMinutes().toString().padStart(2, 0);
case 'CURRENTTIMESTAMP':
return (new Date).toISOString().replace(/[\-:TZ]/g, '').replace(
/\.\d+$/, '');
case '#dateformat':
case '#formatdate':
var date = new Date(get_parameter_String(1) + ' UTC');
var type = get_parameter_String(2);
// console.trace([ get_parameter_String(1), date, type ]);
// TODO: 此為有缺陷的極精簡版。
switch (type) {
case 'ISO 8601':
return date.format('%Y-%2m-%2d', {
zone : 0
});
case 'ymd':
type = '%Y %B %d';
break;
case 'dmy':
type = '%d %B %Y';
break;
case 'mdy':
type = '%B %d, %Y';
break;
default:
// TODO: 未指定日期格式時,會自動判別,並且輸出格式化過的完整日期。
return get_parameter_String(1);
}
return date.format({
format : type,
zone : 0,
locale : 'en'
});
case '#time':
// https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time
// {{#time: format string | date/time object | language code | local
// }}
var argument_2 = get_parameter_String(2);
var date;
if (!argument_2 || argument_2 === 'now') {
date = new Date;
} else {
date = new Date(argument_2 + ' UTC');
if (!date) {
return new wiki_error('Error: Invalid time.');
}
date = new Date(date);
}
// console.trace([ argument_2, new Date(date) ]);
date = wiki_date_to_String(date, get_parameter_String(1), options);
if (date instanceof wiki_error)
return NYI();
return date;
// ----------------------------------------------------------------
case '#if':
// console.trace([ '#if%', token, get_parameter_String(1)
// ]);
token = token.parameters[get_parameter_String(1) ? 2 : 3] || '';
// console.trace(token);
break;
case '#ifeq':
token = token.parameters[get_parameter_String(1) === get_parameter_String(2)
|| +get_parameter_String(1) === +get_parameter_String(2) ? 3
: 4]
|| '';
// console.trace(token);
break;
case '#ifexist':
var page_title = get_parameter_String(1, true);
var return_parameter = function return_parameter(NO) {
var parameter_value = get_parameter_String(NO, true);
// e.g., `a|b {{#ifexist: a{{!}}b | exists | doesn't exist }}`
if (!library_namespace.is_thenable(parameter_value)) {
return parameter_value;
}
if (!allow_promise) {
return NYI();
}
return Promise.resolve(parameter_value);
};
var ifexist_return_by_page_data = function ifexist_return_by_page_data(
page_data) {
return return_parameter(!page_data
//
|| ('missing' in page_data)
//
|| ('invalid' in page_data) ? 3 : 2);
};
var check_page_existence_via_net = function check_page_existence_via_net(
page_title) {
return new Promise(function(resolve, reject) {
// console.trace([ page_title, token.toString() ]);
session.page(page_title, function(page_data, error) {
if (error) {
// console.trace(error);
reject(error);
return return_parameter(3);
}
// console.trace(page_data);
if (!page_data) {
console.error(token.toString());
}
resolve(ifexist_return_by_page_data(page_data));
}, /* ifexist_page_options */{
prop : ''
});
});
};
if (library_namespace.is_thenable(page_title)) {
if (!allow_promise) {
return NYI();
}
return Promise.resolve(page_title).then(function(page_title) {
if (is_invalid_page_title(page_title))
return get_parameter_String(3, true);
return check_page_existence_via_net(page_title);
});
}
if (is_invalid_page_title(page_title)) {
return return_parameter(3);
}
if (session && session.last_page
//
&& session.last_page.title === session.normalize_title(page_title)) {
return ifexist_return_by_page_data(session.last_page);
}
if (!allow_promise) {
return NYI();
}
return check_page_existence_via_net(page_title);
// ----------------------------------------------------------------
case '#switch':
var key = get_parameter_String(1, true, true), default_value = '';
if (library_namespace.is_thenable(key)) {
return NYI();
}
for (var index = 2, found; index < token.length; index++) {
// var parameter = get_parameter_String(index);
var parameter = token[index];
if ('value' in parameter) {
// assert: 'key=value'
if (typeof parameter.key !== 'number') {
parameter.key = value_to_String(parameter.key, true,
true);
}
parameter.value = value_to_String(parameter.value, true);
if (library_namespace.is_thenable(parameter.key)
|| library_namespace.is_thenable(parameter.value)) {
return NYI();
}
if (found || key === parameter.key)
return parameter.value;
if (parameter.key === '#default')
default_value = parameter.value;
} else {
// assert: 'value'
default_value = index;
parameter = get_parameter_String(index, true);
if (library_namespace.is_thenable(parameter)) {
return NYI();
}
if (key === parameter) {
found = true;
}
}
}
return typeof default_value === 'number' ? get_parameter_String(
default_value, true) : default_value;
// ----------------------------------------------------------------
case '#language':
if (typeof Intl !== 'object' || !Intl.DisplayNames) {
return NYI('No valid Intl.');
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#examples
// Language Display Names
var languageNames = new Intl.DisplayNames([ get_parameter_String(2)
|| get_parameter_String(1) ], {
type : 'language'
});
return languageNames.of(get_parameter_String(1));
// ----------------------------------------------------------------
// https://www.mediawiki.org/wiki/Help:Magic_words#URL_data
// https://www.mediawiki.org/wiki/Manual:PAGENAMEE_encoding#Encodings_compared
case 'URLENCODE':
// TODO: https://www.mediawiki.org/wiki/Manual:PAGENAMEE_encoding
return encodeURI(get_parameter_String(1));
case 'ANCHORENCODE':
return wiki_API.wikitext_to_plain_text(
// {{anchorencode:A[[B]]C/D}} === e