UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,664 lines (1,442 loc) 94.2 kB
/** * @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