UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,581 lines (1,417 loc) 85.8 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): parse misc 歸屬於 * wiki_API.parse === CeL.application.net.wiki.parser.wikitext * * @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。 * * TODO:<code> </code> * * @since 2021/12/14 18:53:43 拆分自 CeL.application.net.wiki.parser */ // 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.misc', require : 'application.net.wiki.parser.wikitext.' // to_JS_value() + '|data.code.', // 設定不匯出的子函式。 no_extend : 'this,*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring var wiki_API = library_namespace.application.net.wiki; // @inner var PATTERN_URL_prefix = wiki_API.PATTERN_URL_prefix; // -------------------------------------------------------------------------------------------- // @see Nullish coalescing operator (??) // exclude NaN, null, undefined function is_valid_parameters_value(value) { return value // e.g., .text === '' // String(value) === '' || value === '' || value === 0; } wiki_API.is_valid_parameters_value = is_valid_parameters_value; // 僅添加有效的 parameters。基本上等同於 Object.assign(),只是不添加無效值。 function set_template_object_parameters(template_object, parameters, value_mapping) { if (!template_object) template_object = Object.create(null); for ( var key in parameters) { var value = parameters[key]; if (value_mapping) value = value_mapping[value]; // 不添加無效值。 if (is_valid_parameters_value(value)) { template_object[key] = value; } } return template_object; } /** * 將 parameters 形式的 object 轉成 wikitext。 * * @example<code> CeL.wiki.parse.template_object_to_wikitext('t', { 1 : 'v1', 2 : 'v2', p1 : 'vp1', p2 : 'vp2' }) === '{{t|v1|v2|p1=vp1|p2=vp2}}'; CeL.wiki.parse.template_object_to_wikitext('t', { 1 : 'v1', 2 : 'v2', 4 : 'v4', p1 : 'vp1' }) === '{{t|v1|v2|4=v4|p1=vp1}}'; CeL.wiki.parse.template_object_to_wikitext('t', { 1 : 'v1', 2 : 'v2', p1 : 'vp1', q2 : 'vq2' }, function(text_array) { return text_array.filter(function(text, index) { return !/^q/.test(text); }); }) === '{{t|v1|v2|p1=vp1}}'; </code> * * @param {String}template_name * template name * @param {Object}template_object * parameters 形式的 object。<br /> * e.g., { '1': value, '2': value, parameter1 : value1 } * @param {Object}[post_processor] * post-processor for text_array */ function template_object_to_wikitext(template_name, template_object, post_processor) { var text_array = [ '{{' + template_name ], index = 1; if (!template_object) template_object = Object.create(null); // 先置放數字 parameters。 while (true) { var value = template_object[index]; if (!is_valid_parameters_value(value)) { break; } if (false && typeof value !== 'string') { value = typeof value.toString === 'function' ? value.toString() : String(value); } value = String(value); if (value.includes('=')) value = index + '=' + value; // text_array.push(value); index++; text_array[index++] = value; } for ( var key in template_object) { if (key in text_array) { // 已處理過。 continue; } var value = template_object[key]; if (is_valid_parameters_value(value)) { value = String(value); if (value.includes('\n') && !text_array.at(-1).endsWith('\n')) { text_array[text_array.length - 1] += '\n'; } text_array.push(key + '=' + value); } } if (post_processor) { // text_array = [ '{{template_name', 'parameters', ... ] // 不包含 '}}' ! text_array = post_processor(text_array); } return text_array.join('|') + '}}'; } // ------------------------------------------ var KEY_remove_parameter = replace_parameter.KEY_remove_parameter = { remove_parameter : true }, // 必須為可列舉的、不可能為模板名稱的數值 Must be an enumerable value KEY_template_name = replace_parameter.KEY_template_name = '|template name'; function to_parameter_name_only(parameter_name_pairs) { var config = Object.create(null); Object.keys(parameter_name_pairs) // .forEach(function(from_parameter_name) { var to_parameter_name = parameter_name_pairs[from_parameter_name]; if (typeof to_parameter_name === 'string' // || typeof to_parameter_name === 'number') { config[from_parameter_name] = function(value) { var config = Object.create(null); config[to_parameter_name] = value; return config; }; } else if (to_parameter_name === KEY_remove_parameter) { config[from_parameter_name] = to_parameter_name; } else { library_namespace.error( // 'to_parameter_name_only: Replace to invalid parameter name: ' // + to_parameter_name); } }); return config; } // @inner function set_original_parameter_index(template_token, index) { if (template_token[index].original_parameter_index >= 0) return; var parameter_token = template_token[index] = [ [], template_token[index], [] ]; parameter_token.original_parameter_index = 1; template_token.need_reparse = true; wiki_API.parse.set_wiki_type(parameter_token[0], 'plain'); wiki_API.parse.set_wiki_type(parameter_token[2], 'plain'); wiki_API.parse.set_wiki_type(parameter_token, 'plain'); } // @inner function mode_space_of_parameters(template_token, index) { if (false) { template_token.forEach(function(parameter, index) { if (index === 0) return; // TODO: 分析模板參數的空白模式典型。 // return |$0parameter$1=$2value$3| }); } if (typeof index !== 'number' || !(index >= 0)) { // treat index as parameter_name index = template_token.index_of[index]; } if (!(index >= 0)) { // 不存在此 parameter name 可 replace。 return; } var this_parameter = template_token[index]; if (this_parameter.original_parameter_index >= 0) this_parameter = this_parameter[this_parameter.original_parameter_index]; // this_parameter = [ key, " = ", value ] || [ "", "", value ] // 判斷上下文使用的 spaces。 var spaces = this_parameter[0]; if (Array.isArray(spaces)) { spaces = spaces[0]; } spaces = typeof spaces === 'string' ? spaces.match(/^\s*/)[0] : ''; /** * 保留屬性質結尾的排版:多行保留行先頭的空白,但不包括末尾的空白。單行的則留最後一個空白。 preserve spaces for: * <code> {{T | 1 = 1 | 2 = 2 | 3 = 3 }} </code> */ // var spaces = template_token[index].toString().match(/(\n *| ?)$/); // // parameter: spaces[0] + key + spaces[1] + value + spaces[2] spaces = [ spaces,/* " = " */this_parameter[1], /* tail spaces */ this_parameter[3] || '' ]; return spaces; } function trim_parameter(parameter) { parameter = parameter.toString(); return parameter.trim().replace(/^([^={}]+)\s+=\s+/, '$1='); } // 可以省略數字參數名 numbered parameter / numeric parameter name。 // TODO: template 本身假如會產出 "a=b" 這樣的字串,恐怕會造成問題。 // https://www.mediawiki.org/wiki/Help:Templates#Numbered_parameters function may_omit_numbered_parameter(parameter_value, options) { if (!Array.isArray(parameter_value)) { if (typeof parameter_value !== 'string') return true; parameter_value = wiki_API.parse(parameter_value); } if (!parameter_value) return true; // 包含模板、註解外的 "=",或前面的空白很重要,就不能省略數字指定 prefix。 if (Array.isArray(parameter_value)) { if (parameter_value.type !== 'plain') parameter_value = [ parameter_value ]; parameter_value = parameter_value.filter(function(token) { // if (options.omit_numbered_parameters === 'lenient') // 採用比較寬鬆的標準。 return !(token.type in { parameter : true, // e.g., {{!}} {{=}} magic_word_function : true, transclusion : true, comment : true }); }).join(''); } else { // assert: typeof parameter_value === 'string' } // console.trace(parameter_value); return !parameter_value.includes('='); } /** * 將 wiki_API.parse === parse_wikitext() 獲得之 template_token 中的指定 parameter * 換成 replace_to。 replace_template_parameter(), set_parameter(), * modify_template() * * WARNING: 本函數只保證 template_token.toString() 這個正確。若之後還要利用 * template_token,應先執行 `CeL.wiki.inplace_reparse_element(template_token)`。 * * WARNING: 若不改變 parameter name,只變更 value,<br /> * 則應該使用 { value_only: true },<br /> * 或使用 'parameter name = value' 而非僅 'value'。 * * @example<code> // remove parameter CeL.wiki.parse.replace_parameter(template_token, parameter_name, CeL.wiki.parse.replace_parameter.KEY_remove_parameter); // replace value only token = CeL.wiki.parse('{{t|parameter_name=12|parameter_name_2=32}}'); changed_count = CeL.wiki.parse.replace_parameter(token, { parameter_name : 'replace to value', parameter_name_2 : 'replace to value_2', }, 'value_only'); token.toString(); // replace value and parameter name token = CeL.wiki.parse('{{t|parameter_name=12|parameter_name_2=32}}'); changed_count = CeL.wiki.parse.replace_parameter(token, { parameter_name : 'parameter name 3=replace to value', parameter_name_2 : 'parameter name 4=replace to value_2', }); token.toString(); // force_add token = CeL.wiki.parse('{{t}}'); CeL.wiki.parse.replace_parameter(token, { parameter_name : 'replace_to_value', parameter_name_2 : 'replace_to_value_2', }, { value_only : true, force_add : true, append_key_value : true }); token.toString(); // replace value only: old style 舊格式 CeL.wiki.parse.replace_parameter(token, parameter_name, { parameter_name : replace_to_value } ); // {{T|p=v|n=v}} → {{T|V|n=v}} CeL.wiki.parse.replace_parameter(token, 'p', 'V'); // replace `replace_from_parameter_name = *` to "replace to wikitext" CeL.wiki.parse.replace_parameter(token, replace_from_parameter_name, "replace to wikitext" ); // replace parameter name only CeL.wiki.parse.replace_parameter(token, replace_from_parameter_name, value => { return { replace_to_parameter_name : value }; } ); CeL.wiki.parse.replace_parameter(token, { parameter_1 : replace_to_parameter_1, parameter_2 : replace_to_parameter_2, }, 'parameter_name_only'); // replace parameter name: 不在乎 spaces 的版本。 CeL.wiki.parse.replace_parameter(token, replace_from_parameter_name, value => replace_from_parameter_name + '=' + value ); // replace 1 parameter to 2 parameters CeL.wiki.parse.replace_parameter(token, replace_from_parameter_name, original_value => { parameter_1 : value_1, parameter_2 : original_value, } ); // multi-replacement CeL.wiki.parse.replace_parameter(token, { replace_from_1 : replace_to_config_1, replace_from_2 : replace_to_config_2, }); </code> * * @see 20190912.fix_Infobox_company.js, 20190913.move_link.js * * @param {Array}template_token * 由 wiki_API.parse === parse_wikitext() 獲得之 template_token * @param {String}parameter_name * 指定屬性名稱 parameter name * @param {String|Number|Array|Object|Function}replace_to * 要換成的屬性名稱加上賦值。 e.g., "parameter name = value" ||<br /> * {parameter_1 = value, parameter_2 = value} ||<br /> * function replace_to(value, parameter_name, template_token) * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns {ℕ⁰:Natural+0} count of templates successful replaced */ function replace_parameter(template_token, parameter_name, replace_to, options) { function convert_replace_to(parameter_name) { if (typeof replace_to === 'function') { // function replace_to(value, parameter_name, template_token) { // return 'replace to value'; } replace_to = replace_to( template_token.parameters[parameter_name], parameter_name, template_token); } return replace_to; } if (library_namespace.is_Object(parameter_name)) { if (options) { library_namespace.error('replace_parameter: Invalid usage!'); } // treat `replace_to` as options options = library_namespace.setup_options(replace_to); // Replace parameter name only, preserve value. if (options.parameter_name_only) { parameter_name = to_parameter_name_only(parameter_name); } var operated_template_count = 0, latest_OK_key, key_of_spaces, spaces, next_insert_index; for ( var replace_from in parameter_name) { replace_to = parameter_name[replace_from]; if (typeof replace_from === 'string') replace_from = replace_from.trim(); if (convert_replace_to(replace_from) === undefined) { continue; } var index = replace_from === KEY_template_name ? 0 : template_token.index_of[replace_from]; if (!(index >= 0)) { // 不存在此 parameter name 可 replace。 if (replace_to !== KEY_remove_parameter && options.value_only && options.force_add) { // options.preserve_spacing if (!options.no_value_space // && (!key_of_spaces || key_of_spaces !== latest_OK_key) // && (key_of_spaces = options.append_key_value // && latest_OK_key // mode_parameter || Object.keys(template_token.parameters).pop())) { spaces = mode_space_of_parameters(template_token, // assert: typeof key_of_spaces === 'string' key_of_spaces); // console.log(spaces); } // console.trace([ replace_from, replace_to ]); if (spaces && spaces[1]) { replace_to = spaces[0] + replace_from + spaces[1] + replace_to + spaces[2]; } else if (library_namespace.is_digits(replace_from) && may_omit_numbered_parameter(replace_to, options)) { library_namespace.debug( 'Auto remove numbered parameter: ' + replace_from, 1, 'replace_parameter'); } else { replace_to = replace_from + '=' + replace_to; } if (options.before_parameter && template_token.index_of[options.before_parameter]) { // insert before parameter next_insert_index = template_token.index_of[options.before_parameter]; set_original_parameter_index(template_token, next_insert_index); // assert: // template_token[next_insert_index].type === // 'plain' template_token[next_insert_index][0].push( replace_to, '|'); } else if (options.append_key_value && next_insert_index >= 1) { // 警告: 這會使 template_token[next_insert_index] // 不合正規格式!但能插入在最接近前一個插入點之後, // 並維持 template_token.index_of 的可用性。 set_original_parameter_index(template_token, next_insert_index); template_token[next_insert_index][2].push('|' + replace_to); } else { template_token.index_of[replace_from] = template_token.length; if (false) { console.trace('index_of[' + replace_from + ']=' + template_token.length, replace_to .trim()); template_token.parameters[replace_from] = replace_to .trim(); } template_token.push(replace_to); } operated_template_count++; } continue; } var skip_replacement = undefined; if (options.value_only // 預防有 KEY_remove_parameter 之類。 && (typeof replace_to === 'string' || typeof replace_to === 'number' // 允許輸入 token。 || wiki_API.is_parsed_element(replace_to))) { var this_parameter = template_token[index]; if (index === 0) { // console.trace([ this_parameter, replace_to ]); if (options.override_same_value ? this_parameter .toString() !== replace_to.toString() : this_parameter.toString().trim() !== replace_to .toString().trim()) { template_token[index] = replace_to; operated_template_count++; } continue; } if (!Array.isArray(this_parameter) || this_parameter.length < 2) { // 可能是變造過的 parameter。 library_namespace.warn('replace_parameter: ' + 'Skip replace [' + replace_from + ']→[replace_to]'); console.trace(this_parameter); console.trace([ index, template_token ]); continue; } var parameters = template_token.parameters; // using this_parameter[2] to keep spaces and parameter // name. // e.g., "| key<!---->=1 |" → "| key<!---->=2 |" // NOT: "| key<!---->=1 |" → "| key=2 |" if (parameters && parameters[replace_from]) { this_parameter[2] = this_parameter[2].toString() // 留下註解之類。 .replace(parameters[replace_from], function(all) { if (options.override_same_value // ? all !== replace_to.toString() // : all.trim() !== replace_to.toString().trim()) { // console.trace([ all, replace_to ]); skip_replacement = 1; return replace_to; } // console.trace([ all, replace_to ]); skip_replacement = 0; return all; }); } if (!(skip_replacement >= 0)) { this_parameter[2] = replace_to; skip_replacement = 1; } if (!may_omit_numbered_parameter(replace_to, options) && !this_parameter[1]) { this_parameter[0] = replace_from; this_parameter[1] = '='; } if (parameters) { // Also update parameters parameters[replace_from] = replace_to; } if (skip_replacement > 0 && options.no_value_space) { if (this_parameter[3]) { this_parameter[3] = this_parameter[3].toString() .trimStart(); if (!this_parameter[3]) this_parameter.splice(3, 1); } this_parameter[1] = this_parameter[1].toString() .trimEnd(); // 避免 // https://en.wikipedia.org/w/index.php?title=Talk:Sasha_Allen_(The_Voice_21)&diff=prev&oldid=1198449816 if (!this_parameter.toString().includes('\n')) { this_parameter.forEach(function(piece, index) { if (typeof piece === 'string') { this_parameter[index] = piece.replace( /^\s{2,}/, ' ').replace(/\s{2,}$/, ' '); } }); } } // @deprecated: // replace_to = { [_replace_from] : replace_to }; // replace_to = Object.create(null); // replace_to[replace_from] = replace_to; } latest_OK_key = replace_from; next_insert_index = index; // console.trace([ replace_from, replace_to ]); if (skip_replacement >= 0) { operated_template_count += skip_replacement; continue; } operated_template_count += replace_parameter(template_token, replace_from, replace_to); } return operated_template_count; } // -------------------------------------- options = library_namespace.setup_options(options); var index = parameter_name === KEY_template_name ? 0 : template_token.index_of[parameter_name]; if (!(index >= 0)) { // 不存在此 parameter name 可 replace。 return 0; } if (convert_replace_to(parameter_name) === undefined) { return 0; } // console.trace(index, replace_to); if (replace_to === KEY_remove_parameter) { if (library_namespace.is_digits(parameter_name)) { // For numeral parameter_name, just replace to empty value. template_token[index] = ''; // Warning: this will NOT change .index_of , .parameters ! while (!template_token.at(-1)) template_token.pop(); } else { // remove the parameter template_token.splice(index, 1); replace_to = wiki_API.parse(template_token.toString()); if (!replace_to || replace_to.type !== 'transclusion') { throw new Error('replace_parameter: Parse error for ' + template_token); } Object.clone(replace_to, false, template_token); } return 1; } // -------------------------------------- // 判斷上下文使用的 spaces。 var spaces = mode_space_of_parameters(template_token, index); // console.trace(spaces); // console.trace(replace_to); // -------------------------------------- // 正規化 replace_to。 if (library_namespace.is_Object(replace_to)) { // console.trace(replace_to); replace_to = Object.keys(replace_to).map(function(key) { var value = replace_to[key]; if (!key) { library_namespace.warn('Including empty key: ' // TODO: allow {{|=...}}, e.g., [[w:zh:Template:Policy]] + JSON.stringify(replace_to)); key = parameter_name; } // TODO: using is_valid_parameters_value(value) return spaces[1] ? spaces[0] + key + spaces[1] + value // + spaces[2] : key + '=' + value; }); } if (Array.isArray(replace_to) && !replace_to.type) { replace_to = replace_to.join('|'); } else { replace_to = replace_to.toString(); } // assert: {String}replace_to // console.trace(replace_to); // 注意: 假如 numbered parameter 本來就沒有添上 1= 之類,那麼就算 .includes('=') // 也不會再主動添加。 if (!spaces[1] && library_namespace.is_digits(parameter_name)) { var matched = replace_to.match(/^\s*(\d+)\s*=\s*([\s\S]*)$/); if (matched && matched[1] == parameter_name && may_omit_numbered_parameter(matched[2], options)) { // e.g., replace [2] to non-named 'value' in {{t|1|2}} library_namespace.debug('Auto remove numbered parameter: ' + parameter_name, 1, 'replace_parameter'); // console.trace([ replace_to, matched ]); replace_to = matched[2]; } } if (spaces[2].includes('\n') && !/\n\s*?$/.test(replace_to)) { // Append new-line without tail "|" replace_to += spaces[2]; } if (template_token[index] && template_token[index].toString() === replace_to) { // 不處理沒有變更的情況。 return 0; } if (!options.override_same_value && template_token[index] && trim_parameter(template_token[index]) === trim_parameter(replace_to)) { // 不處理僅添加空白字元的情況。 return 0; } // -------------------------------------- // a little check: parameter 的數字順序不應受影響。 var PATERN_parameter_name = /(?:^|\|)[\s\n]*([^={}\s\n][\s\S]*?)=/; if (index === 0 || index + 1 < template_token.length) { // index === 0: template name 無影響。 // index + 1 < template_token.length: 最末尾沒有 parameter 了,影響較小。 } else if (!library_namespace.is_digits(parameter_name)) { // TODO: NG: {{t|a=a|1}} → {{t|a|1}} if (!PATERN_parameter_name.test(replace_to)) { library_namespace.warn('replace_parameter: ' + 'Insert named parameter [' + index + '] ' + JSON.stringify(parameter_name) + ' ' + JSON.stringify(replace_to) + ' and disrupt the order of parameters? ' + template_token); } } else { // NG: {{t|a|b}} → {{t|a=1|b}} var matched = replace_to.match(PATERN_parameter_name); if (!matched) { if (parameter_name !== KEY_template_name && index != parameter_name) { library_namespace.warn('replace_parameter: ' + 'Insert non-named parameter to [' + parameter_name + '] and disrupt the order of parameters? ' + template_token); } } else if (matched[1].trim() != parameter_name) { library_namespace .warn('replace_parameter: ' + 'Insert numbered parameter and disrupt the order of parameters? ' + template_token); } } // -------------------------------------- library_namespace.debug(parameter_name + ': "' + template_token[index] + '"→"' + replace_to + '"', 2, 'replace_parameter'); template_token[index] = replace_to; return 1; } // ------------------------------------------------------------------------ // Merge the parameters of from_template_token to to_template_token. // target_template_token, source_template_token function merge_template_parameters(to_template_token, from_template_token, options) { if (!Array.isArray(to_template_token) || to_template_token.type !== 'transclusion' || !Array.isArray(from_template_token) || from_template_token.type !== 'transclusion') { throw new Error( 'merge_template_parameters: Invalid template token!'); return; } // assert: 不動到 from_template_token。 var normalize_parameter = options && options.normalize_parameter; // 紀錄有衝突的 parameter_name var conflict_parameters = [], parameters_to_copy = []; for ( var parameter_name in from_template_token.index_of) { if (!(parameter_name in to_template_token.parameters) // 複製 to_template_token 為空的 parameter 值。 || !to_template_token.parameters[parameter_name] && (from_template_token.parameters[parameter_name] // 警告: 對於數字 parameter 尚有bug。 || library_namespace.is_digits(parameter_name))) { parameters_to_copy.push(parameter_name); continue; } var from_value = from_template_token.parameters[parameter_name]; var to_value = to_template_token.parameters[parameter_name]; if (normalize_parameter) { from_value = normalize_parameter(from_value, parameter_name); to_value = normalize_parameter(to_value, parameter_name); } if (from_value.toString() === to_value.toString()) { // Skip the same values. continue; } conflict_parameters.push(parameter_name); } // console.trace(conflict_parameters, parameters_to_copy); if (conflict_parameters.length > 0) return conflict_parameters; // assert: 在這之前都不動到 to_template_token。 parameters_to_copy.forEach(function(parameter_name) { var from_index = from_template_token.index_of[parameter_name]; var to_index = to_template_token.index_of[parameter_name]; if (!to_index) { to_template_token.index_of[parameter_name] // append. TODO: 依照兩個模板排出最適合的插入點 = to_index = to_template_token.length; } // else: 假如原先存在就採用原先的位置。 // 警告: 對於 object,這種複製方法,改變其一,兩者會一同改變。 to_template_token[to_index] = from_template_token[from_index]; to_template_token.parameters[parameter_name] // 警告: 對於 object,這種複製方法,改變其一,兩者會一同改變。 = from_template_token.parameters[parameter_name]; }); // console.trace(to_template_token, to_template_token.length); } // ------------------------------------------------------------------------ // 模板名#後的內容會忽略。 // matched: [ , Template name ] var TEMPLATE_NAME_PATTERN = /{{[\s\n]*([^\s\n#\|{}<>\[\]][^#\|{}<>\[\]]*)[|}]/, // TEMPLATE_START_PATTERN = new RegExp(TEMPLATE_NAME_PATTERN.source.replace( /\[[^\[]+$/, ''), 'g'); /** {RegExp}內部連結 PATTERN */ // var LINK_NAME_PATTERN = // /\[\[[\s\n]*([^\s\n\|{}<>\[\]�][^\|{}<>\[\]]*)(\||\]\])/; /** * parse template token. 取得完整的模板 token。<br /> * CeL.wiki.parse.template(); * * TODO:<br /> * {{link-en|{{convert|198|cuin|L|abbr=on}} ''斜置-6'' 198|Chrysler Slant 6 * engine#198}} * * @param {String}wikitext * 模板前後之 content。<br /> * assert: wikitext 為良好結構 (well-constructed)。 * @param {String|Array}[template_name] * 擷取模板名 template name。 * @param {Number}[parse_type] * 1: [ {String}模板名, parameters ]<br /> * true: 不解析 parameters。<br /> * false: 解析 parameters。 * * @returns {Undefine}wikitext 不包含此模板。 * @returns {Array}token = [ {String}完整的模板 wikitext token, {String}模板名, * {Array}parameters ];<br /> * token.count = count('{{') - count('}}'),正常情況下應為 0。<br /> * token.index, token.lastIndex: index.<br /> * parameters[0] is {{{1}}}, parameters[1] is {{{2}}}, ...<br /> * parameters[p] is {{{p}}} */ function parse_template(wikitext, template_name, parse_type) { template_name = wiki_API.normalize_title_pattern(template_name, true, true); var matched = template_name // 模板起始。 ? new RegExp(/{{[\s\n]*/.source + template_name + '\\s*[|}]', 'ig') : new RegExp(TEMPLATE_NAME_PATTERN.source, 'g'); library_namespace.debug('Use pattern: ' + matched, 3, 'parse_template'); // template_name : start token template_name = matched.exec(wikitext); if (!template_name) { // not found. return; } var pattern = new RegExp('}}|' // 不用 TEMPLATE_NAME_PATTERN,預防把模板結尾一起吃掉了。 + TEMPLATE_START_PATTERN.source, 'g'), count = 1; // lastIndex - 1 : the last char is [|}] template_name.lastIndex = pattern.lastIndex = matched.lastIndex - 1; while (count > 0 && (matched = pattern.exec(wikitext))) { // 遇到模板結尾 '}}' 則減1,否則增1。 if (matched[0] === '}}') count--; else count++; } wikitext = pattern.lastIndex > 0 ? wikitext.slice(template_name.index, pattern.lastIndex) : wikitext.slice(template_name.index); var result = [ // [0]: {String}完整的模板token wikitext, // [1]: {String}模板名 template_name[1].trim(), // [2] {String}parameters // 接下來要作用在已經裁切擷取過的 wikitext 上,需要設定好 index。 // assert: 其他餘下 parameters 的部分以 [|}] 起始。 // -2: 模板結尾 '}}'.length wikitext.slice(template_name.lastIndex - template_name.index, -2) ]; Object.assign(result, { count : count, index : template_name.index, lastIndex : pattern.lastIndex }); if (!parse_type || parse_type === 1) { // {{t|p=p|1|q=q|2}} → [ , 1, 2; p:'p', q:'q' ] var index = 1, /** {Array}parameters */ parameters = []; // 警告: 這邊只是單純的以 '|' 分割,但照理來說應該再 call parser 來處理。 // 最起碼應該除掉所有可能包含 '|' 的語法,例如內部連結 [[~|~]], 模板 {{~|~}}。 result[2].split(/[\s\n]*\|[\s\n]*/) // 不處理 template name。 .slice(1) // .forEach(function(token) { var matched = token.match(/^([^=]+)=(.*)$/); if (matched) { var key = matched[1].trim(), // value = matched[2].trim(); if (false) { if (key in parameters) { // 參數名重複: @see [[Category:調用重複模板參數的頁面]] // 如果一個模板中的一個參數使用了多於一個值,則只有最後一個值會在顯示對應模板時顯示。 // parser 調用超過一個Template中參數的值,只有最後提供的值會被使用。 if (Array.isArray(parameters[key])) parameters[key].push(value); else parameters[key] = [ parameters[key], value ]; } else { parameters[key] = value; } } parameters[key] = value; } else { parameters[index++] = token; } }); if (parse_type === 1) { parameters[0] = result[1]; result = parameters; // result[0] is template name. // result[p] is {{{p}}} // result[1] is {{{1}}} // result[2] is {{{2}}} } else { // .shift(): parameters 以 '|' 起始,因此需去掉最前面一個。 // 2016/5/14 18:1:51 採用 [index] 的方法加入,因此無須此動作。 // parameters.shift(); result[2] = parameters; } } return result; } // ---------------------------------------------------- // [[w:ks:وِکیٖپیٖڈیا:وَقٕت تہٕ تأریٖخ]] var ks_month_name = ',جَنؤری,فَرؤری,مارٕچ,اَپریل,مٔیی,جوٗن,جُلَے,اَگَست,سَتَمبَر,اَکتوٗبَر,نَوَمبَر,دَسَمبَر' .split(','); // 因應不同的 mediawiki projects 來處理日期。機器人只識別標準時間格式,預防誤判。 // date_parser_config[language] // = [ {RegExp}PATTERN, {Function}parser({Array}matched) : return {String}, // {Function}to_String({Date}date) : return {String} ] // // 可使用 parse API 來做測試。 // https://www.mediawiki.org/w/api.php?action=help&modules=parse // // 須注意當使用者特別設定時,在各維基計劃上可能採用不同語系的日期格式。 // // to_String: 日期的模式, should match "~~~~~". // // @see // https://www.mediawiki.org/wiki/Manual:$wgDefaultUserOptions#Available_preferences // $wgDefaultUserOptions['date'] var date_parser_config = { en : [ // e.g., "01:20, 9 September 2017 (UTC)" // [, time(hh:mm), d, m, Y, timezone ] /([0-2]?\d:[0-6]?\d)[, ]+([0-3]?\d) ([a-z]{3,9}) ([12]\d{3})(?: \(([A-Z]{3})\))?/ig, function(matched, options) { return matched[2] + ' ' + matched[3] + ' ' + matched[4] + ' ' + matched[1] + ' ' + (matched[6] || 'UTC'); }, { format : '%2H:%2M, %d %B %Y (UTC)', // use UTC zone : 0, locale : 'en-US' } ], ks : [ // [, time(hh:mm), d, m, Y, timezone ] /([0-2]?\d:[0-6]?\d)[, ]+([0-3]?\d) ([\u0624-\u06d2]{4,9}) ([12]\d{3})(?: \(([A-Z]{3})\))?/ig, function(matched, options) { matched[3] = ks_month_name.indexOf(matched[3]); return matched[3] > 0 && matched[4] + '-' + matched[3].pad(2) + '-' + +matched[2].pad(2) + ' ' + matched[1] + ' ' + (matched[6] || 'UTC'); }, { format : '%2H:%2M, %d %B %Y (UTC)', // use UTC zone : 0, locale : 'ks-IN' } ], ja : [ // e.g., "2017年9月5日 (火) 09:29 (UTC)" // [, Y, m, d, week, time(hh:mm), timezone ] /([12]\d{3})年([[01]?\d)月([0-3]?\d)日 \(([日月火水木金土])\)( [0-2]?\d:[0-6]?\d)(?: \(([A-Z]{3})\))?/g, function(matched) { return matched[1] + '/' + matched[2] + '/' + matched[3] + matched[5] + ' ' + (matched[6] || 'UTC+9'); }, { format : '%Y年%m月%d日 (%a) %2H:%2M (UTC)', // use UTC zone : 0, locale : 'ja-JP' } ], 'zh-classical' : [ // Warning: need CeL.data.numeral /([一二][〇一二三四五六七八九]{3})年([[〇一]?[〇一二三四五六七八九])月([〇一二三]?[〇一二三四五六七八九])日 (([日一二三四五六]))( [〇一二三四五六七八九]{1,2}時[〇一二三四五六七八九]{1,2})分(?: \(([A-Z]{3})\))?/g, function(matched, options) { return library_namespace .from_positional_Chinese_numeral(matched[1] + '/' + matched[2] + '/' + matched[3] + matched[5].replace('時', ':')) + ' ' + (matched[6] || 'UTC+8'); }, function(date, options) { return library_namespace.to_positional_Chinese_numeral(date .format({ format : '%Y年%m月%d日 (%a) %2H時%2M分 (UTC)', // use UTC zone : 0, locale : 'cmn-Hant-TW' })); } ], zh : [ // $dateFormats, 'Y年n月j日 (D) H:i' // https://github.com/wikimedia/mediawiki/blob/master/languages/messages/MessagesZh_hans.php // e.g., "2016年8月1日 (一) 00:00 (UTC)", // "2016年8月1日 (一) 00:00 (CST)" // [, Y, m, d, week, time(hh:mm), timezone ] /([12]\d{3})年([[01]?\d)月([0-3]?\d)日 \(([日一二三四五六])\)( [0-2]?\d:[0-6]?\d)(?: \(([A-Z]{3})\))?/g, function(matched, options) { return matched[1] + '/' + matched[2] + '/' + matched[3] // + matched[5] + ' ' // 'CST' in zh should be China Standard Time. // But `new Date('2017/12/1 0:0 CST')` using // Central Standard Time (North America) // === new Date('2017/12/1 0:0 UTC-6') // !== new Date('2017/12/1 0:0 UTC+8') + (!matched[6] || matched[6] === 'CST' ? 'UTC+8' // : matched[6]); }, { format : '%Y年%m月%d日 (%a) %2H:%2M (UTC)', // use UTC zone : 0, locale : 'cmn-Hant-TW' } ] }; // all wikimedia using English in default. // e.g., wikidata, commons date_parser_config.multilingual = date_parser_config.en; // warning: number_to_signed(-0) === "+0" function number_to_signed(number) { return number < 0 ? number : '+' + number; } // @inner function normalize_parse_date_options(options) { var session = wiki_API.session_of_options(options); if (options === true) { options = { get_timevalue : true }; } else if (typeof options === 'string' && (options in date_parser_config)) { options = { language : options }; } else { options = library_namespace.new_options(options); } var language = wiki_API.get_first_domain_name_of_session(options); if (session) { if (!language) { language = wiki_API.site_name(session, { get_all_properties : true }); language = language && language.language; } if (!date_parser_config[language]) { // e.g., https://simple.wikipedia.org/ → // wiki_API.get_first_domain_name_of_session(session) === // 'simple' && session.language === 'en' language = session.language; } if (!isNaN(options.timeoffset)) { options.zone = options.timeoffset / 60; } else if (!('timeoffset' in options)) { // e.g., 480 : UTC+8 options.zone = session.configurations.timeoffset / 60; } else { library_namespace .warn('normalize_parse_date_options: Invalid timeoffset: ' + options.timeoffset); } } options.zone |= 0; options.date_parser_config = date_parser_config[language]; if (!options.date_parser_config) { if (language) { library_namespace.error( // 'normalize_parse_date_options: Invalid language: ' + language); } // console.log(session); // console.trace([ language, wiki_API.language ]); options.date_parser_config = date_parser_config[wiki_API.language]; } return options; } /** * parse date string / 時間戳記 to {Date} * * @example <code> date_list = CeL.wiki.parse.date(CeL.wiki.content_of(page_data), { //language : 'en', session : session, get_timevalue : true, get_date_list : true }); </code> * * 技術細節警告:不同語系wiki有相異的日期辨識模式,採用和當前wiki不同語言的日期格式,可能無法辨識。 * * 經查本對話串中沒有一般型式的一般格式的日期,造成無法辨識。下次遇到這樣的問題,可以在最後由任何一個人加上本討論串已終結、準備存檔的字樣,簽名並且'''加上一般日期格式'''即可。 * * @param {String}wikitext * date text to parse. * @param {Object}options * 附加參數/設定選擇性/特殊功能與選項 * * @returns {Date|Array}date of the date string * * @see [[en:Wikipedia:Signatures]], "~~~~~", * [[en:Help:Sorting#Specifying_a_sort_key_for_a_cell]] */ function parse_date(wikitext, options) { options = normalize_parse_date_options(options); var date_list; if (options.get_date_list) { // get all dates. 若設定 options.get_date_list,須保證回傳 {Array}。 date_list = []; } if (!wikitext) { return date_list; } // <del>去掉</del>skip年分前之雜項。 // <del>去掉</del>skip星期與其後之雜項。 var date_parser = options.date_parser_config[1]; var PATTERN_date = options.date_parser_config[0], matched; // console.log('Using PATTERN_date: ' + PATTERN_date); var min_timevalue, max_timevalue; // reset PATTERN index PATTERN_date.lastIndex = 0; while (matched = PATTERN_date.exec(wikitext)) { // console.log(matched); // Warning: // String_to_Date()只在有載入CeL.data.date時才能用。但String_to_Date()比parse_date()功能大多了。 var date = date_parser(matched, options); // console.log(date); // Date.parse('2019/11/6 16:11 JST') === NaN date = date.replace(/ (JST)/, function(all, zone) { zone = library_namespace.String_to_Date // Warning: // String_to_Date()只在有載入CeL.data.date時才能用。但String_to_Date()功能大多了。 && (zone in library_namespace.String_to_Date.zone) // ? library_namespace.String_to_Date.zone[zone] : 9; return ' UTC' + number_to_signed(zone); }); date = Date.parse(date); if (isNaN(date)) { continue; } if (!(min_timevalue < date)) { min_timevalue = date; } else if (!(date < max_timevalue)) { max_timevalue = date; } if (!options.get_timevalue) { date = new Date(date); } if (!options.get_date_list) { return date; } date_list.push(date); } // Warning: 不一定總有 date_list.min_timevalue, date_list.max_timevalue if (min_timevalue) { date_list.min_timevalue = min_timevalue; date_list.max_timevalue = max_timevalue || min_timevalue; } return date_list; } /** * 產生時間戳記。日期格式跟標準簽名一樣,讓時間轉換的小工具起效用。 * * assert: the same as "~~~~~". * * @example <code> CeL.wiki.parse.date.to_String(new Date, session); </code> */ function to_wiki_date(date, options) { options = normalize_parse_date_options(options); // console.log(language || wiki_API.language); var to_String = options.date_parser_config[2]; if (typeof to_String === 'function') { date = to_String(date, options); } else { // treat `to_String` as date format // assert: library_namespace.is_Object(to_String) var zone = options.zone; if (!isNaN(zone) && to_String.zone !== zone) { // 不污染原型。 to_String = Object.clone(to_String); to_String.zone = zone; to_String.format = to_String.format // 顯示的時間跟隨 session.configurations.timeoffset。 .replace(/\(UTC(?:[+\-]\d)?\)/, '(UTC' + (zone < 0 ? zone : zone ? '+' + zone : '') + ')'); } // console.trace([ date, date.format(to_String), to_String ]); date = date.format(to_String); } return date; } parse_date.to_String = to_wiki_date; // ------------------------------------------ // 由使用者名稱來檢測匿名使用者/未註冊用戶 [[WP:IP]] / 匿名IP用戶 is_anonymous_user // [[m:Special:MyLanguage/Tech/News/2021/05]] // 在diffs中,IPv6位址被寫成了小寫字母。這導致了死連結,因為Special:使用者貢獻只接受大寫的IP。這個問題已經被修正。 // https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/FAQ#What_does_a_temporary_username_look_like? function parse_temporary_username(username) { // 從 testwiki 可發現 "~2024-2133" 也是正規臨時帳戶名稱。 var matched = username.trim().match(/^~(\d{4})-(\d+)/); if (!matched) return; return { // 臨時編號 temporary_NO : +matched[1], year : +matched[1] }; } /** * 使用者/用戶對話頁面所符合的匹配模式。 * * matched: [ all, " user name " ] * * user_name = matched[1].trim() * * match: [[:language_code:user_talk:user_name]] * * TODO: using PATTERN_page_name * * @type {RegExp} * * @see 使用者簽名將不能再有Lint錯誤和包含一些無效的HTML,嵌套替換引用也不允許,必須包含到使用者頁面、使用者討論頁或使用者貢獻頁之一的連結。 * https://www.mediawiki.org/wiki/New_requirements_for_user_signatures#Outcome * @see https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics&utf8 * https://github.com/wikimedia/mediawiki/blob/master/languages/messages/MessagesZh_hant.php */ var PATTERN_user_link = // user name do not allow "\/": e.g., [[user talk:user_name/Flow]] // 大小寫無差,但NG: "\n\t" // // https://zh.wikipedia.org/wiki/Wikipedia:互助客栈/其他#增设空间“U:”、“UT:”作为“User:”、“User_talk:”的Alias // https://phabricator.wikimedia.org/T183711 // Doesn't conflict with any language code or other interwiki link. // https://gerrit.wikimedia.org/r/#/c/400267/4/wmf-config/InitialiseSettings.php /\[\[ *:?(?:[a-z\d\-]{1,14}:?)?(?:user(?:[ _]talk)?|使用者(?:討論)?|用戶(?:討論|對話)?|用户(?:讨论|对话)?|利用者(?:‐会話)?|사용자(?:토론)?|UT?) *: *((?:&#(?:\d{1,8}|x[\da-fA-F]{1,8});|[^{}\[\]\|<>\t\n#�\/])+)/i, // [[特殊:功績]]: zh-classical, [[特別:投稿記録]]: ja // matched: [ all, " user name " ] PATTERN_user_contributions_link = /\[\[(?:Special|特別|特殊|特別) *: *(?:Contributions|Contribs|使用者貢獻|用戶貢獻|(?:用户)?贡献|投稿記録|功績)\/((?:&#(?:\d{1,8}|x[\da-fA-F]{1,8});|[^{}\[\]\|<>\t\n#�\/])+)/i, // PATTERN_user_link_all = new RegExp(PATTERN_user_link.source, 'ig'), PATTERN_user_contributions_link_all = new RegExp( PATTERN_user_contributions_link.source, 'ig'); /** * parse user name. 解析使用者/用戶對話頁面資訊。找出用戶頁、用戶討論頁、用戶貢獻頁的連結。 * * @example <code> if (CeL.wiki.parse.user(CeL.wiki.title_link_of(title), user)) {} </code> * * 採用模板來顯示簽名連結的方法,會影響到許多判斷簽名的程式,不只是簽名偵測。您可使用 * <code><nowiki>[[User:Example|<span style="color: #007FFF;">'''我的簽名'''</span>]]</nowiki></code> * 的方法來添加顏色,或者參考[[zhwiki:Wikipedia:簽名]]的其他範例。 * * TODO: 應該按照不同的維基項目來做篩選。 * * @param {String}wikitext * wikitext to parse * @param {String}[user_name] * 測試是否為此 user name。 注意:這只會檢查第一個符合的連結。若一行中有多個連結,應該採用 * CeL.wiki.parse.user.all() ! * @param {Boolean}[to_full_link] * get a full link * * @returns {String}user name / full link * @returns {Boolean}has the user name * @returns {Undefined}Not a user link. */ function parse_user(wikitext, user_name, to_full_link) { if (!wikitext) { return; } var matched = wikitext.match(PATTERN_user_link), via_contributions; if (!matched) { matched = wikitext.match(PATTERN_user_contributions_link); if (!matched) { return; } via_contributions = true; } if (typeof user_name === 'boolean') { to_full_link = user_name; user_name = undefined; } // 正規化連結中的使用者名稱。 var name_from_link = wiki_API.normalize_title(matched[1]); if (user_name) { // 用戶名正規化。 user_name = wiki_API.normalize_title(user_name); if (user_name !== name_from_link) { return false; } if (!to_full_link) { return true; } } // may use wiki_API.title_link_of() return to_full_link ? via_contributions ? '[[User:' + name_from_link + ']]' : matched[0].trimEnd() + ']]' : name_from_link; } /** * parse all user name. 解析所有使用者/用戶對話頁面資訊。 CeL.wiki.parse.user.all() * * @example <code> // 取得各使用者的簽名數量hash。 var user_hash = CeL.wiki.parse.user.all(wikitext), user_list = Object.keys(user_hash); // 取得依照第一次出現處排序、不重複的使用者序列。 var user_list = Object.keys(CeL.wiki.parse.user.all(wikitext)); // 取得依照順序出現的使用者序列。 var user_serial_list = CeL.wiki.parse.user.all(wikitext, true); </code> * * @param {String}wikitext * wikitext to parse/check * @param {String}[user_name] * 測試是否有此 user name,return {Integer}此 user name 之連結數量。 * 若輸入true表示取得依照順序出現的使用者序列。 * * @returns {Integer}link count of the user name * @returns {Object}normalized user name hash: hash[name] = {Integer}count */ function parse_all_user_links(wikitext, user_name) { function check_pattern(PATTERN_all) { // reset PATTERN index PATTERN_all.lastIndex = 0; var matched; library_namespace.debug(PATTERN_all, 3, 'parse_all_user_links'); while (matched = PATTERN_all.exec(wikitext)) { // 用戶名正規化。 var name = wiki_API.normalize_title(matched[1]); if (!user_name || user_name === name) { // console.log(name); if (user_list) { user_list.push(name); } else if (name in user_hash) { user_hash[name]++; } else { user_hash[name] = 1; } } } } var user_hash, user_list; if (user_name === true) { user_list = []; user_name = null; } else if (user_name) { // user_name should be {String}user name user_name = wiki_API.normalize_title(user_name); } else { user_hash = Object.create(null); } if (!wikitext) { return user_name ? 0 : user_list || user_hash; } library_namespace.debug(wikitext, 3, 'parse_all_user_links'); library_namespace.debug('user name: ' + user_name, 3, 'parse_all_user_links'); check_pattern(PATTERN_user_link_all); check_pattern(PATTERN_user_contributions_link_all); if (user_list) { return user_list; } if (user_name) { return user_name in user_hash[user_name] ? user_hash[user_name] : 0; } return user_hash; } // CeL.wiki.parse.user.all === wiki_API.parse.user.all parse_user.all = parse_all_user_links; // CeL.wiki.parse.user.parse_temporary_username() parse_user.parse_temporary_username = parse_temporary_username; /** * redirect/重定向頁所符合的匹配模式。 Note that the redirect link must be explicit – it * cannot contain magic words, templates, etc. * * matched: [ all, "title#section" ] * * zh-classical: 重新導向 * * @type {RegExp} * * @see function p.getTargetFromText(text) @ https://en.wikipedia.org/wiki/Module:Redirect * https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics&utf8 * https://github.com/wikimedia/mediawiki/blob/master/languages/messages/MessagesZh_hant.php * https://en.wikipedia.org/wiki/Help:Redirect * https://phabricator.wikimedia.org/T68974 */ var PATTERN_redirect_general = /^[\s\n]*#(?:REDIRECT|重定向|重新導向|転送|リダイレクト|넘겨주기)\s*(?::\s*)?\[\[([^{}\[\]\|<>\t\n�]+)(?:\|[^\[\]{}]+?)?\]\]/i; /** * parse redirect page. 解析重定向資訊,或判斷頁面是否為重定向頁面。<br /> * 若 wikitext 重定向到其他頁面,則回傳其{String}頁面名: "title#section"。 * * 應採用如下方法,可以取得 `('redirect' in page_data) && page_data.redirect === ''` 。 * * @example <code> wiki.page(title, function(page_data) { var redirect_to = CeL.wiki.parse.redirect(page_data); // `true` or {String}redirect_to or `undefined` console.log(redirect_to); }); wiki.page(title, function(page_data) { var is_protected = CeL.wiki.is_protected(page_data); // `true` or `undefined` console.log(is_protected); }, { prop : 'info' }); </code> * * @param {String}page_data * page data or wikitext to parse * @param {Object}options * 附加參數/設定選擇性/特殊功能與選項 * * @returns {String}title#section * @returns {Undefined}Not a redirect page. * * @see all_revision_SQL: page_is_redirect */ function parse_redirect(page_data, options) { var wikitext, is_page_data = wiki_API.is_page_data(page_data); if (is_page_data) { wikitext = wiki_API.content_of(page_data); } else { // treat page_data as wikitext. wikitext = page_data; } if (false) { if (Array.isArray(wikitext)) { throw '您可能取得了多個版本'; // 應該用: // content = CeL.wiki.content_of(page_data, 0); // 但是卻用成了: // content = CeL.wiki.content_of(page_data); } if (!wikitext || typeof wikitext !== 'string') { throw typeof wikitext; return; } } var session = wiki_API.session_of_options(options); var PATTERN_redirect; if (session && !(PATTERN_redirect = session.PATTERN_redirect) && session.latest_site_configurations) { session.latest_site_configurations.magicwords .some(function(magicword) { if (magicword.name !== 'redirect') return; PATTERN_redirect = new RegExp( // PATTERN_redirect_template /^[\s\n]*(?:re