UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,573 lines (1,413 loc) 171 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): parse wikitext / * wikicode 解析維基語法 * * @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 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.wikitext', // for_each_subelement require : 'application.net.wiki.parser.', // 設定不匯出的子函式。 no_extend : 'this,*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring var wiki_API = library_namespace.application.net.wiki; // @inner var PATTERN_wikilink = wiki_API.PATTERN_wikilink, PATTERN_wikilink_global = wiki_API.PATTERN_wikilink_global, PATTERN_URL_WITH_PROTOCOL_GLOBAL = wiki_API.PATTERN_URL_WITH_PROTOCOL_GLOBAL, PATTERN_invalid_page_name_characters = wiki_API.PATTERN_invalid_page_name_characters; var /** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */ NOT_FOUND = ''.indexOf('_'); // -------------------------------------------------------------------------------------------- // CeL.wiki.HTML_to_wikitext(HTML) // Please use CeL.wiki.wikitext_to_plain_text() instead! // TODO: 應該 parse HTML。 // @see [[Module:Plain text]], // https://www.mediawiki.org/w/api.php?action=help&modules=flow-parsoid-utils // https://www.mediawiki.org/w/api.php?action=help&modules=parse // https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates function HTML_to_wikitext(HTML, options) { return HTML // .replace(/<\/i><i>/g, '').replace(/<\/b><b>/g, '').replace( /<\/strong><strong>/g, '') // .replace(/<i>([\s\S]+?)<\/i>/g, "''$1''").replace( /<b>([\s\S]+?)<\/b>/g, "'''$1'''").replace( /<strong>([\s\S]+?)<\/strong>/g, "'''$1'''") // .replace_till_stable(/<span(?: [^<>]*)?>([^<>]*?)<\/span>/g, "$1") // .replace(/<a ([^<>]+)>([\s\S]+?)<\/a>/g, // function(all, attributes, innerHTML) { var href = attributes.match(/href="([^"]+)"/); return '[' + (href ? href[1] : '#') + ' ' + innerHTML + ']'; }) // .replace(/\s*<br(?:[^\w<>][^<>]*)?>[\r\n]*/ig, '\n').replace( /<p ?\/>\n*/ig, '\n\n') // ignore style, remove <p style="...">...</p> // .replace(/<p[^<>]*>([^<>]*)<\/p>[\s\n]*/g, '$1\n\n') .replace(/<p>([\s\S]+?)<\/p>\n*/g, '$1\n\n') // .replace(/\r?\n/g, '\n').replace(/\n{3,}/g, '\n\n'); } // decode page title / section title // @see [[mw:Manual:PAGENAMEE encoding#Encodings compared]] function decode_title(title, original_title) { if (!/%[\dA-F]{2}/i.test(title)) { return original_title; } try { // @see HTML_to_Unicode() title = decodeURIComponent(title.replace_till_stable( // MediaWiki 採用了非標準的 decodeURIComponent(), // 可能是只 replace(/%([\s\S]{2}|$)/g)? // [[#1%]] 不會像 decodeURIComponent() 一般 throw。 /%([\s\S]{2}|$)/g, function(all, code) { return /^[\dA-F]{2}$/i.test(code) ? all : encodeURIComponent('%') + code; })); } catch (e) { // e.g., error after convert /\.([\dA-F]{2})/g return original_title; // 可能是非 UTF-8 編碼? // TODO: 無法解碼可能會被辨識為普通文字而非 wikilink。 // e.g., "[[Francisco_Hern%E1ndez_de_C%F3"... @ // [[w:en:Talk:Francisco_Hernández_de_Córdoba_(Yucatán_conquistador)]] } if (/[\x00-\x1F\x7F]/.test(title)) { // e.g. [[w:ja:エヴァンゲリオン (架空の兵器)#Mark.09]] return original_title; } return title; } // -------------------------------------------------------------------------------------------- /** * excluding the disambiguator, and remove diacritics of page_title * * @param {String}page_title * 頁面標題。 * @param {Boolean}to_lower_case * for case-insensitive compare * * @returns {String} sort key */ function page_title_to_sort_key(page_title, to_lower_case) { if (!page_title) return; if (page_title.title) { // input page_data page_title = page_title.title; } // excluding the disambiguator // e.g., [[Abdoul Karim Sylla (footballer, born 1981)]] // → "Abdoul Karim Sylla" var sort_key = page_title.toString().replace(/ \([^()]+\)$/, ''); if (sort_key.normalize) { // with diacritics removed. to Latin alphabet // https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript sort_key = sort_key.normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); } if (to_lower_case) sort_key = sort_key.toLowerCase(); return sort_key; } // TODO: check if the sort_key is the same as page title or DEFAULTSORT function set_sort_key_of_category(sort_key) { if (typeof sort_key === 'undefined' || sort_key === null) return; var category_token = this; // const var old_sort_key = category_token.sort_key && page_title_to_sort_key(category_token.sort_key); if (old_sort_key) { if (old_sort_key === sort_key) { // Nothing changed return; } if (old_sort_key.length > sort_key || !old_sort_key.startsWith(sort_key)) { // <syntaxhighlight lang="wikitext" inline>...</syntaxhighlight> library_namespace.debug('The sort key of <code><nowiki>' + category_token + '</nowiki></code> will be set to ' + JSON.stringify(sort_key) + '!', 1, 'set_sort_key_of_category'); } } category_token[2] = category_token.sort_key = sort_key; return true; } // -------------------------------------------------------------------------------------------- // parse wikitext. /** * 不包含可 parse 之要素,不包含 text 之 type。<br /> * 不應包含 section title,因可能有 "==[[]]==" 的情況。 * * @type {Object} */ var atom_type = { namespace : true, // https://phabricator.wikimedia.org/T173889 page_title : true, // external_link : true, url : true, style : true, tag_single : true, comment : true }; // tree level var KEY_DEPTH = 'depth'; /** * 設定 token 為指定 type。將 token 轉為指定 type。 * * @param {Array}token * parse_wikitext() 解析 wikitext 所得之,以 {Array} 組成之結構。 * @param {String}type * 欲指定之類型。 e.g., 'transclusion'. * * @returns {Array}token * * @see wiki_element_toString */ function set_wiki_type(token, type, parent) { // console.trace(token); if (typeof token === 'string') { token = [ token ]; } else if (!Array.isArray(token)) { library_namespace.warn('set_wiki_type: The token is not Array!'); } else if (token.type && token.type !== 'plain') { // 就算 token.type !== type,可能是 <span> 中嵌套 <span> 的形式, // 不該直接 `return token` 。 // 預防token本來就已經有設定類型。 token = [ token ]; } // assert: Array.isArray(token) token.type = type; if (type in atom_type) { token.is_atom = true; } // check if (false && !wiki_element_toString[type]) { throw new Error('.toString() not exists for type [' + type + ']!'); } token.toString = wiki_element_toString[type]; if (false) { Object.defineProperty(token, 'toString', wiki_element_toString[type]); } if (false) { var depth; if (parent >= 0) { // 當作直接輸入 parent depth。 depth = parent + 1; } else if (parent && parent[KEY_DEPTH] >= 0) { depth = parent[KEY_DEPTH] + 1; } // root 的 depth 為 (undefined|0)===0 token[KEY_DEPTH] = depth | 0; } return token; } // -------------------------------------------------------------------------------------------- /** * 將特殊標記解譯/還原成 {Array} 組成之結構。 * * @param {Array}queue * temporary queue. * @param {String}include_mark * 解析用之起始特殊標記。 * @param {String}end_mark * 結束之特殊標記。 * * @see parse_wikitext() */ function resolve_escaped(queue, include_mark, end_mark, options) { if (false) { library_namespace.debug('queue: ' + queue.join('\n--- '), 4, 'resolve_escaped'); console.log('resolve_escaped: ' + JSON.stringify(queue)); } var resolve_filter = options && options.resolve_filter; function resolving_item(item) { // result queue var result = []; item.split(include_mark).forEach(function(piece, index) { if (index === 0) { if (piece) { result.push(piece); } return; } index = piece.indexOf(end_mark); var token; if (index === NOT_FOUND || index === 0 // || !((token = +piece.slice(0, index)) in queue)) { result.push(include_mark + piece); return; } token = queue[token]; if (resolve_filter && !resolve_filter(token)) { result.push(include_mark + piece); return; } result.push(token); if (piece = piece.slice(index + end_mark.length)) result.push(piece); }); if (result.length > 1) { // console.log(result); set_wiki_type(result, 'plain'); } else { // console.trace(result); result = result[0]; } if (!resolve_filter && result.includes(include_mark)) { // Error.stackTraceLimit = Infinity; throw new Error('resolve_escaped: 仍有 include mark 殘留!'); } return result; } if (options && ('resolve_item' in options)) { return resolving_item(options.resolve_item); } var length = queue.length; for (var index = queue.last_resolved_length | 0; index < length; index++) { var item = queue[index]; if (false) library_namespace.debug([ 'item', index, item ], 4, 'resolve_escaped'); if (typeof item !== 'string') { // already resolved // assert: Array.isArray(item) continue; } var result = resolving_item(item); queue[index] = result; } if (!resolve_filter) queue.last_resolved_length = length; // console.log('resolve_escaped end: '+JSON.stringify(queue)); } // 經測試發現 {{...}} 名稱中不可有 [{}<>\[\]] // while(/{{{[^{}\[\]]+}}}/g.exec(wikitext)); // [|{}] or [|{}=] // 但允許 "{{\n name}}" // 模板名#後的內容會忽略。 /** {RegExp}模板的匹配模式。 */ // var PATTERN_transclusion = // /{{[\s\n]*([^\s\n#\|{}<>\[\]][^#\|{}<>\[\]]*)(?:#[^\|{}]*)?((?:(\||{{\s*!\s*}})[^<>\[\]]*)*?)}}/g; /** * {RegExp}wikilink內部連結的匹配模式。 * * @see PATTERN_wikilink */ // var PATTERN_link = // /\[\[[\s\n]*([^\s\n\|{}<>\[\]�][^\|{}<>\[\]�]*)((?:(\||{{\s*!\s*}})[^\|{}<>\[\]]*)*)\]\]/g; /** * Wikimedia projects 的 external link 匹配模式。 * * matched: [ all external link wikitext, URL, protocol with "://", * URL_others, delimiter, link name ] * * 2016/2/23: 經測試,若為結尾 /$/ 不會 parse 成 external link。<br /> * 2016/2/23: "[ http...]" 中間有空白不會被判別成 external link。 * * @type {RegExp} * * @see PATTERN_URL_GLOBAL, PATTERN_URL_WITH_PROTOCOL_GLOBAL, * PATTERN_URL_prefix, PATTERN_WIKI_URL, PATTERN_wiki_project_URL, * PATTERN_external_link_global * * @see https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=protocols&utf8&format=json */ var PATTERN_external_link_global = new RegExp( PATTERN_URL_WITH_PROTOCOL_GLOBAL.source // 允許 [//example.com/] .replace('):)', '):|\\/\\/)').replace(/^\([^()]+\)/, /\[/.source) + /(?:([^\S\r\n]+)([^\]]*))?\]/.source, 'ig'), // 若包含 br|hr| 會導致 "aa<br>\nbb</br>\ncc" 解析錯誤! /** {String}以"|"分開之 wiki tag name。 [[Help:Wiki markup]], HTML tags. 不包含 <a>! */ markup_tags = 'bdi|b|del|ins|i|u|font|big|small|sub|sup|h[1-6]|cite|code|em|strike|strong|s|tt|var|div|center|blockquote|[oud]l|table|caption|thead|tbody|tr|th|td|pre|ruby|r[tbp]|p|span|abbr|dfn|kbd|samp|data|time|mark' // [[Help:Parser tag]], [[Help:Extension tag]] + '|includeonly|noinclude|onlyinclude' // https://phabricator.wikimedia.org/T263082 // 會讀取目標語言的 MediaWiki 轉換表 // [[w:zh:Wikipedia:互助客栈/技术#新的语言转换语法已经启用]] // 使用 <langconvert> 的頁面,優先級順序大概是:-{}- 頁面語言切換 > <langconvert> > 轉換組? + '|langconvert' // [[Special:Version#mw-version-parser-extensiontags]] // <ce> is deprecated, using <chem> // Replace all usages of <ce> with <chem> on wiki // https://phabricator.wikimedia.org/T155125 + '|categorytree|ce|chem|charinsert|gallery|graph|hiero|imagemap|indicator|inputbox|nowiki|mapframe|maplink|math|poem|quiz|ref|references|score|section|source|syntaxhighlight|templatedata|templatestyles|timeline' // https://www.mediawiki.org/wiki/Extension:DynamicPageList_(Wikimedia) // + '|DynamicPageList' // [[w:en:Template:Term]] + '|li|dt|dd', // @see function get_PATTERN_full_tag() PATTERN_invalid_end_tag = new RegExp('<(/)(' + markup_tags + ')([\\s/][^<>]*)?>', 'ig'), // "<nowiki />", "<nowiki>...<nowiki>" are valid, // but "<nowiki> without end tag" is invalid. // 必須要寫成 <nowiki/> // Parser extension tags @ [[Special:Version]] // For {{#lst}}, {{#section:}} // [[w:en:Help:Labeled section transclusion]] // TODO: 標籤(tag)現在可以本地化 [[mw:Extension:Labeled_Section_Transclusion/zh]] // templatestyles: https://www.mediawiki.org/wiki/Extension:TemplateStyles // https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=extensiontags&utf8&format=json // 優先權高低: <onlyinclude> → <nowiki> → <noinclude>, <includeonly> // [[mw:Transclusion#Partial transclusion markup]] // <noinclude>, <includeonly> 在解析模板時優先權必須高於其他 tags。 wiki_extensiontags = 'includeonly|noinclude|onlyinclude|' // 在其內部的 wikitext 不會被 parse。允許內部採用 table 語法的 tags。例如 // [[mw:Manual:Extensions]] // configurations.extensiontags + 'pre|nowiki|gallery|indicator|langconvert|timeline|hiero|imagemap|source|syntaxhighlight|poem|quiz|score|templatestyles|templatedata|graph|maplink|mapframe|charinsert|ref|references|inputbox|categorytree|section|math|ce|chem', // <nowiki>不允許內部再解析,但這幾個的內部得再解析。 wiki_extensiontags_must_parse_inner = { onlyinclude : true, includeonly : true, noinclude : true }, /** * {RegExp}HTML tags 的匹配模式 of <nowiki>。這些 tag 就算中間置入 "<!--" 也不會被當作 * comments,必須在 "<!--" 之前解析。 PATTERN_WIKI_TAG_of_wiki_extensiontags */ PATTERN_wiki_extensiontags = wiki_API.get_PATTERN_full_tag( wiki_extensiontags, true), // MediaWiki 可接受的 HTML void elements 標籤. self-closed HTML tags // NO b|span|sub|sup|li|dt|dd|center|small // 包含可使用,亦可不使用 self-closing 的 tags。 // self-closing: void elements + foreign elements // https://www.w3.org/TR/html5/syntax.html#void-elements // @see [[phab:T134423]] // https://www.mediawiki.org/wiki/Manual:OutputPage.php // https://www.mediawiki.org/wiki/Help:Lint_errors/self-closed-tag self_closed_tags = 'area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr', /** {RegExp}HTML self closed tags 的匹配模式。 */ PATTERN_WIKI_TAG_VOID = new RegExp('<(\/)?(' + self_closed_tags // [[w:en:Help:Labeled section transclusion]] // Allow `<section begin=chapter1 />` + '|' + wiki_extensiontags // allow "<br/>" + ')(\/|\\s[^<>]*)?>', 'ig'); /** {RegExp}HTML tags 的匹配模式。 */ // var PATTERN_WIKI_TAG = wiki_API.get_PATTERN_full_tag(markup_tags); wiki_extensiontags = wiki_extensiontags.split('|').to_hash(); markup_tags = markup_tags.split('|'); /** {RegExp}HTML tags 的匹配模式 without <nowiki>。 */ var PATTERN_non_wiki_extensiontags = wiki_API .get_PATTERN_full_tag(markup_tags.filter(function(tag) { return !(tag in wiki_extensiontags); })); var PATTERN_HTML_tag = wiki_API .get_PATTERN_full_tag('[^<>\\s]+', null, 'i'); /** * Test if the section title is old dot-encoded. * * e.g.,<code> [[臺灣話#.E5.8F.97.E6.97.A5.E6.9C.AC.E8.AA.9E.E5.BD.B1.E9.9F.BF.E8.80.85|(其他參考資料)]] [[Kingdom of Italy#Fascist regime .281922.E2.80.931943.29|Fascist Italy]] </code> * * @see [[w:en:Help:Link#Section linking (anchors)]], [[w:en:WP:ANCHOR]] * https://en.wikipedia.org/wiki/Percent-encoding#Types_of_URI_characters */ var PATTERN_is_dot_encoded = /^([\w\s\-~!*'();:@&=+$,/?#\[\]]|\.[\dA-F]{2})+$/; // [[MediaWiki:Converter-manual-rule-error]]: 在手动语言转换规则中检测到错误 var VALUE_converter_rule_error = 'converter-manual-rule-error'; /** * .toString() of wiki elements: wiki_element_toString[token.type]<br /> * parse_wikitext() 將把 wikitext 解析為各 {Array} 組成之結構。當以 .toString() 結合時,將呼叫 * .join() 組合各次元素。此處即為各 .toString() 之定義。<br /> * 所有的 key (type) 皆為小寫。 * * CeL.wiki.parse.wiki_element_toString.* * * @type {Object} * * @see parse_wikitext() */ var wiki_element_toString = { // internal/interwiki link : language links : category links, file, // subst 替換引用, ... : title // e.g., [[m:en:Help:Parser function]], [[m:Help:Interwiki linking]], // [[:File:image.png]], [[wikt:en:Wiktionary:A]], // [[:en:Template:Editnotices/Group/Wikipedia:Miscellany for deletion]] // [[:en:Marvel vs. Capcom 3: Fate of Two Worlds]] // [[w:en:Help:Link#Http: and https:]] // // 應當使用 [[w:zh:維基百科:編輯提示|編輯提示]] 而非 [[:zh:w:維基百科:編輯提示|編輯提示]], // 見 [[User:Cewbot/Stop]]。 // // @see [[Wikipedia:Namespace]] // https://www.mediawiki.org/wiki/Markup_spec#Namespaces // [[ m : abc ]] is OK, as "m : abc". // [[: en : abc ]] is OK, as "en : abc". // [[ :en:abc]] is NOT OK. namespaced_title : function() { return this.join(this.oddly ? '' : ':'); }, // page title, template name page_title : function() { return this.join(':'); }, // link 的變體。但可採用 .name 取得 file name。 file : function() { var wikitext = '[[' + this[0] // anchor 網頁錨點 + this[1]; if (this.length > 2) { var pipe = this.pipe; for (var index = 2; index < this.length; index++) { // `pipe &&`: for .file.call([]) wikitext += (pipe && pipe[index - 2] || '|') + this[index]; } } return wikitext + ']]'; }, // link 的變體。但可採用 .name 取得 category name。 category : function() { return '[[' + this[0] // anchor 網頁錨點 + this[1] // + (this.length > 2 ? (this.pipe || '|') // + this[2] : '') + ']]'; }, // 內部連結 (wikilink / internal link) + interwiki link // TODO: [ start_quote (e.g., [<!----><!---->[), page_title, #anchor, // pipe (e.g., {{!}}), display_text, end_quote (e.g., ]<!---->]) ] // TODO: .normalize(): [[A|A]] → [[A]] link : function() { // 警告: for_each_subelement() 會忽略 .pipe, start_quote, end_quote 中的元素 // ({{!}}, <!---->)! return '[[' + this[0] // this: [ page_title, #anchor, display_text ] + (this[1] || '') + (this.length > 2 // && this[2] !== undefined && this[2] !== null ? (this.pipe || '|') // + (this[2] || '') + this[2] : '') + ']]'; }, // 外部連結 external link, external web link external_link : function() { // [ url, delimiter, display_text ] // assert: this.length === 1 or 3 // assert: this.length === 3 // && this[1].trim() === '' && this[2] === this[2].trimStart() return '[' + this.join('') + ']'; }, url : function() { return this.join(''); }, // template parameter parameter : function() { return '{{{' + this.join('|') + '}}}'; }, // template_parameter parameter_unit : function() { // [ parameter_name, " = ", parameter_value // , [optional tail spaces] ] return this.join(''); // assert: this[1] && this[1].includes('=') // assert: this.parent && this.parent.type === 'transclusion' }, // e.g., template transclusion : function() { return '{{' + this.join('|') + '}}'; }, magic_word_function : function() { return '{{' + this[0] + this.slice(1).join('|') + '}}'; }, // [[Help:Table]] table : function() { // this: [ table style, row, row, ... ] return (this.indentation || '') + '{|' + this.join('') + ('ending' in this ? this.ending : '\n|}'); }, // table attributes / styles, old name before 2021/1/24: table_style table_attributes : function() { return this.join('') + (this.suffix || ''); }, // table caption caption : function() { // this: [ main caption, invalid caption, ... ] return (this.delimiter || '') + this.join(''); }, table_row : function() { // this: [ row style, cell, cell, ... ] return (this.delimiter || '') + this.join(''); }, table_cell : function() { // this: [ contents ] // this.delimiter: // /\n[!|]|!!|\|\|/ or undefined (在 style/第一區間就已當作 cell) return (this.delimiter || '') + this.join(''); }, // 手工字詞轉換 language conversion -{}- convert : function(language, lang_fallbacks, force_show) { if (!language) { return '-{' // + ('flag' in this ? (this._flag || this.flag) + '|' : '') + this.join(';') + '}-'; } if (language === 'rule') { // gets the rule of conversion only return this.join(';'); } if (language === 'normalized rule') { // @see function item_to_conversion(item) @ // CeL.application.net.wiki var rule = parse_wikitext('-{A|' // .toString('rule') + this.join(';') + '}-', { normalize : true, }) // .toString('rule') .join(';'); if (lang_fallbacks) { // as 通用的轉換式 // function normalized_general_rule(token) // const rule = token.toString('normalized rule', true); // 通用的轉換式不該為連結。 rule = rule.replace(/:\[\[([^\[\]]+)\]\]($|;)/g, ':$1$2') .replace(/^\[\[([^\[\]]+)\]\]$/g, '$1'); } return rule; } var flag = this.flag; if (!force_show && (flag in { // hidden: add rule for convert code // (but no display in placed code) H : true, // title T : true, // delete 去掉此 rule '-' : true })) { return ''; } if (flag in { // raw content R : true, // description D : true }) { return this.join(';'); } language = language.trim().toLowerCase(); if (Array.isArray(flag)) { if (!flag.includes(language)) { // 單純顯示不繁簡轉換的文字。 return this.join(';'); } // TODO: 顯示繁簡轉換後的文字。 return this.join(';'); } // TODO: 後援語種 fallback language variant。 // https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=general%7Cnamespaces%7Cnamespacealiases%7Cstatistics // language fallbacks: [[mw:Localisation statistics]] // (zh-tw, zh-hk, zh-mo) → zh-hant (→ zh?) // (zh-cn, zh-sg, zh-my) → zh-hans (→ zh?) // [[Wikipedia_talk:地区词处理#zh-my|馬來西亞簡體華語]] // [[MediaWiki:Variantname-zh-tw]] if (!this.conversion[language]) { if (/^zh-(?:tw|hk|mo)/.test(language)) { language = 'zh-hant'; } else if (/^zh/.test(language)) { language = 'zh-hans'; } } var convert_to = this.conversion[language]; if (Array.isArray(convert_to)) { // e.g., -{H|zh-tw:a-{b}-c}- var not_all_string; convert_to = convert_to.map(function(token) { if (typeof token === 'string') return token; if (token.type === 'convert' && typeof token.converted === 'string') return token.converted; not_all_string = true; }); if (!not_all_string) convert_to = convert_to.join(''); else convert_to = this.conversion[language]; } if (convert_to) return convert_to; if (typeof this.converted === 'string' && this.converted) return this.converted; // e.g., "-{zh-cn:下划线; zh-tw:底線}-" .toString('zh') // console.trace([ this, arguments ]); return VALUE_converter_rule_error; }, // Behavior switches 'switch' : function() { // assert: this.length === 1 return '__' + this[0] + '__'; }, // italic type italic : function() { // this: [ start_mark(''), content, end_mark('') ] // @see KEY_apostrophe_element_content // Warning: (0 in this), (2 in this) 可能為 false! // Warning: this[0], this[2] 可能為 {Array}! // assert: this.length === 3 || this.length === 2 return this.join(''); }, // emphasis bold : function() { // this: [ start_mark('''), content, end_mark(''') ] // @see KEY_apostrophe_element_content // Warning: (0 in this), (2 in this) 可能為 false! // Warning: this[0], this[2] 可能為 {Array}! // assert: this.length === 3 || this.length === 2 return this.join(''); }, // section title / section name // show all section titles: // parser=CeL.wiki.parser(page_data);parser.each('section_title',function(token,index){console.log('['+index+']'+token.title);},false,1); // @see for_each_subelement() // parser.each('plain',function(token){},{slice:[1,2]}); section_title : function(get_inner) { // this.join(''): 必須與 wikitext 相同。見 parse_wikitext.title。 var inner = this.join(''); if (get_inner) { // section_title.toString(true): get inner // Must .trim() yourself. return inner; } var level = '='.repeat(this.level); return level + inner + level // this.postfix maybe undefined, string, {Array} + (this.postfix || ''); }, // [[Help:Wiki markup]], HTML tags tag : function() { // this: [ {String}attributes, {Array}inner nodes ].tag // 欲取得 .tagName,請用 this.tag.toLowerCase(); // 欲取得 .inner nodes,請用 this[1]; // 欲取得 .innerHTML,請用 this[1].toString(); return '<' + this.tag + (this[0] || '') + '>' + this[1] + ('ending' in this ? this.ending : '</' + this.tag + '>'); }, tag_attributes : function() { return this.join(''); }, tag_inner : function() { // console.trace(this); return this.join(''); }, tag_single : function() { // this: [ {String}attributes ].tag // 欲取得 .tagName,請用 this.tag.toLowerCase(); // 有 .slash 代表 invalid tag。 return '<' + (this.slash || '') + this.tag + this.join('') + '>'; }, // comments: <!-- ... --> comment : function() { // "<\": for Eclipse JSDoc. return '<\!--' + this.join('') + (this.no_end ? '' : '-->'); }, line : function() { // https://www.mediawiki.org/wiki/Markup_spec/BNF/Article // NewLine = ? carriage return and line feed ? ; return this.join('\n'); }, list : function() { return this.join(''); }, list_item : function() { return (this.list_prefix || '') + this.join(''); }, pre : function() { return ' ' + this.join('\n '); }, hr : function() { return this[0]; }, paragraph : function() { return this.join('\n') + (this.separator || ''); }, // plain text 或尚未 parse 的 wikitext. plain : function() { return this.join(''); } }; // const , for <dl> var DEFINITION_LIST = 'd'; // !!default_magic_words_hash[magic_word] === 必須指定數值,採用 {{#MW:value}} // else 可單用 {{MW}} var default_magic_words_hash = Object.create(null); // https://www.mediawiki.org/wiki/Help:Magic_words // https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=functionhooks&utf8&format=json ('DISPLAYTITLE|DEFAULTSORT|DEFAULTSORTKEY|DEFAULTCATEGORYSORT|デフォルトソート' + '|ns|nse|lc|lcfirst|uc|ucfirst' + '|padleft|padright|bidi' + '|formatnum' + '|urlencode|anchorencode' + '|localurl|fullurl|filepath' // https://www.mediawiki.org/wiki/Help:Magic_words#Transclusion_modifiers // https://en.wikipedia.org/wiki/Help:Transclusion#Transclusion_modifiers // https://www.mediawiki.org/wiki/Help:Magic_words#Localization + '|PLURAL|GRAMMAR|GENDER|int|msg|raw|msgnw|subst|safesubst' // 這些需要指定數值。 e.g., {{NS:1}}: OK, {{NS}} will get " ", {{NS:}} will get "" ).split('|').forEach(function name(magic_word) { default_magic_words_hash[magic_word.toUpperCase()] = true; }); // https://zh.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=variables&utf8&format=json ('!|=' + '|CURRENTYEAR|CURRENTMONTH|CURRENTDAY|CURRENTTIME|CURRENTHOUR|CURRENTWEEK|CURRENTTIMESTAMP' + '|NAMESPACE|NAMESPACENUMBER' + '|FULLPAGENAME|PAGENAME|BASEPAGENAME|SUBPAGENAME|SUBJECTPAGENAME|ARTICLEPAGENAME|TALKPAGENAME|ROOTPAGENAME' + '|FULLPAGENAMEE|PAGENAMEE|BASEPAGENAMEE|SUBPAGENAMEE|SUBJECTPAGENAMEE|ARTICLEPAGENAMEE|TALKPAGENAMEE|ROOTPAGENAMEE' // 這些不用指定數值。 ).split('|').forEach(function name(magic_word) { default_magic_words_hash[magic_word.toUpperCase()] = false; }); // matched: [ all, inner parameters ] var PATTERN_language_conversion = /-{(|[^\n{].*?)}-/g; // parse 手動轉換語法的轉換標籤的語法 // 經測試,":"前面與後面不可皆有空白。 // (\s{2,}): 最後的單一/\s/會被轉換為"&#160;" // matched: [ all, 指定轉換字串, 指定轉換詞, spaces, // this language code, colon, this language token, last spaces ] var PATTERN_conversion_slice = /^(([\s\S]+?)=>)?(\s*)(zh(?:-(?:cn|tw|hk|mo|sg|my|hant|hans))?)(\s*:|:\s*)(\S.*?)(\s{2,})?$/; // 狀態開關: [[mw:Help:Magic words#Behavior switches]] var PATTERN_BEHAVIOR_SWITCH = /__([A-Z]+(?:_[A-Z]+)*)__/g; PATTERN_BEHAVIOR_SWITCH = /__(NOTOC|FORCETOC|TOC|NOEDITSECTION|NEWSECTIONLINK|NONEWSECTIONLINK|NOGALLERY|HIDDENCAT|NOCONTENTCONVERT|NOCC|NOTITLECONVERT|NOTC|INDEX|NOINDEX|STATICREDIRECT|NOGLOBAL)__/g; // [[w:en:Wikipedia:Extended image syntax]] // [[mw:Help:Images]] var file_options = { // Type, display format, 表示形式 thumb : 'format', thumbnail : 'format', frame : 'format', framed : 'format', frameless : 'format', // Border, 外枠, 縁取る, 境界 border : 'border', // Location, Horizontal alignment option, 配置位置 right : 'location', left : 'location', // 居中, 不浮動 center : 'location', // 不浮動 none : 'location', // Vertical alignment option, 垂直方向の位置 baseline : 'alignment', middle : 'alignment', sub : 'alignment', 'super' : 'alignment', 'text-top' : 'alignment', 'text-bottom' : 'alignment', top : 'alignment', bottom : 'alignment', // Link option // link : 'link', // alt : 'alt', // lang : 'language', // https://en.wikipedia.org/wiki/Wikipedia:Creation_and_usage_of_media_files#Setting_a_video_thumbnail_image // thumbtime : 'video_thumbtime', // start : 'video_start', // end : 'video_end', // page : 'book_page', // 'class' : 'CSS_class', // Size, Resizing option // 放大倍數 upright : 'size' }; function join_string_of_array(array) { for (var index = 1; index < array.length;) { if (typeof array[index] !== 'string') { index++; continue; } if (array[index] === '') { array.splice(index, 1); continue; } if (typeof array[index - 1] === 'string') { array[index - 1] += array[index]; array.splice(index, 1); } else { index++; } } return array; } // is_wiki_token(), is_token() function is_parsed_element(value) { return Array.isArray(value) && (value.type in wiki_element_toString); } /** token為無意義的token。 */ function is_meaningless_element(token) { if (typeof token === 'string') return !token.trim(); if (Array.isArray(token)) { return token.type in { comment : true }; } } // 可算 function preprocess_section_link_token(token, options) 的簡化版。 // 可能保留 "\n"、不可見字符 必須自己 .trim()。 function wiki_element_to_key(token) { if (!Array.isArray(token)) return token; var has_no_string; // key token must accept '\n'. e.g., "key_ \n _key = value" var _token = token.filter(function(t) { if (t.type === 'plain') t = wiki_element_to_key(t); if (!Array.isArray(t)) return t; // 去除註解 comments。 if (t.type === 'comment') { return; } has_no_string = true; return true; }); // console.trace([ has_no_string, _token ]); if (token.type === 'plain' && !has_no_string) return _token.join(''); return set_wiki_type(_token, token.type); } // for_each_subelement() 的超簡化版 // @inner function for_each_non_plain_subtoken(token, for_each_token, index, parent) { if (!Array.isArray(token) || token.type !== 'plain') { return for_each_token(token, index, parent); } return token.some(function(suntoken, index, parent) { return for_each_non_plain_subtoken(suntoken, for_each_token, index, parent); }); } /** * 將層層疊疊的 plain: [ plain[], plain[], ... ]<br /> * 轉成 [ non-plain, non-plain, non-plain, … ] * * @param {Array}token * root token * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * @returns */ function flat_plain_element(token, options) { var element_list = set_wiki_type([], 'plain'); function add_subtoken(plain_token) { for (var index = 0, length = plain_token.length; index < length; index++) { var subtoken = plain_token[index]; if (subtoken.type === 'plain') { add_subtoken(subtoken); continue; } // element_list[element_list.length - 1]; var previous_subtoken = element_list.at(-1); if (typeof previous_subtoken === 'string' // e.g., "</s>" only && (subtoken.type === 'tag_single' && subtoken.slash // Keep new line. || typeof subtoken === 'string' && !subtoken.startsWith('\n') // && !previous_subtoken.endsWith('\n'))) { // assert: element_list.length > 0 // Warning: may need re-parse! element_list[element_list.length - 1] += subtoken; } else { element_list.push(subtoken); } } } add_subtoken(token.type === 'plain' ? token : [ token ]); return element_list; } function generate_token_pattern(pattern_template, options) { if (library_namespace.is_RegExp(pattern_template)) pattern_template = pattern_template.source; // assert: typeof pattern_template === 'string' options = library_namespace.setup_options(options); var include_mark = library_namespace .to_RegExp_pattern(options.include_mark || default_include_mark), // end_mark = library_namespace.to_RegExp_pattern(options.end_mark || default_end_mark), // /\d+/ queue_index = /\d{1,5}/.source, // pattern = new RegExp(pattern_template.replace( /token_mark/g, include_mark + (options.with_queue_index ? '(' + queue_index + ')' : queue_index) + end_mark).replace( /include_mark/g, include_mark).replace(/end_mark/g, end_mark), options.flags || 'g'); return pattern; } var KEY_apostrophe_element_start_quote = 0, KEY_apostrophe_element_content = 1, KEY_apostrophe_element_end_quote = 2, // default_include_mark = '\x00', default_end_mark = '\x01', // \n, $ 都會截斷 italic, bold // <tag> 不會截斷 italic, bold template_PATTERN_text_apostrophe_unit = /([^\n]*?)('(?:(?:token_mark)*')+|\n|$)/.source, // [ all, text, apostrophes(''+) ] default_PATTERN_text_apostrophe_unit = generate_token_pattern(template_PATTERN_text_apostrophe_unit), // is_dt PATTERN_prefix_is_description_term = /;\s*$/; /** * parse The MediaWiki markup language (wikitext / wikicode). 解析維基語法。 * 維基語法解析器 * * TODO:<code> <p<!-- -->re>...</pre> parse: {{Template:Single chart}} </code> * * 此功能之工作機制/原理:<br /> * 找出完整的最小單元,並將之 push 入 queue,並把原 string 中之單元 token 替換成:<br /> * {String}include_mark + ({ℕ⁰:Natural+0}index of queue) + end_mark<br /> * e.g.,<br /> * "a[[p]]b{{t}}" →<br /> * "a[[p]]b\00;", queue = [ ["t"].type='transclusion' ] →<br /> * "a\01;b\00;", queue = [ ["t"].type='transclusion', ["p"].type='link' ]<br /> * 最後再依 queue 與剩下的 wikitext,以 resolve_escaped() 作 resolve。 * * @param {String}wikitext * wikitext to parse * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * @param {Array}[queue] * temporary queue. 基本上僅供內部使用。 * * @returns {Array}parsed data * * @see https://blog.wikimedia.org/2013/03/04/parsoid-how-wikipedia-catches-up-with-the-web/ * https://phabricator.wikimedia.org/diffusion/GPAR/ * * @see [[w:en:Help:Wikitext]], [[Wiki標記式語言]] * https://www.mediawiki.org/wiki/Markup_spec/BNF/Article * https://www.mediawiki.org/wiki/Markup_spec/BNF/Inline_text * https://www.mediawiki.org/wiki/Markup_spec * https://www.mediawiki.org/wiki/Wikitext * https://doc.wikimedia.org/mediawiki-core/master/php/html/Parser_8php.html * Parser.php: PHP parser that converts wiki markup to HTML. * https://mwparserfromhell.readthedocs.io/ */ function parse_wikitext(wikitext, options, queue) { if (!wikitext) { return wikitext; } function _set_wiki_type(token, type) { // 這可能性已經在下面個別處理程序中偵測並去除。 if (false && typeof token === 'string' && token.includes(include_mark)) { queue.push(token); resolve_escaped(queue, include_mark, end_mark); token = [ queue.pop() ]; } return set_wiki_type(token, type, wikitext); // 因為parse_wikitext()採用的是從leaf到root的解析法,因此無法在解析leaf時就知道depth。 // 故以下廢棄。 var node = set_wiki_type(token, type); library_namespace.debug('set depth ' + depth_of_children + ' to children [' + node + ']', 3, '_set_wiki_type'); node[KEY_DEPTH] = depth_of_children; return node; } // 正規化並提供可隨意改變的同內容參數,以避免修改或覆蓋附加參數。 // 每個parse_wikitext()都需要新的options,需要全新的。 // options = Object.assign({}, options); options = library_namespace.setup_options(options); var session = wiki_API.session_of_options(options); if (false) { // assert: false>=0, (undefined>=0) // assert: (NaN | 0) === 0 var depth_of_children = ((options[KEY_DEPTH]) | 0) + 1; // assert: depth_of_children >= 1 library_namespace.debug('[' + wikitext + ']: depth_of_children: ' + depth_of_children, 3, 'parse_wikitext'); options[KEY_DEPTH] = depth_of_children; } var /** * 解析用之起始特殊標記。<br /> * 需找出一個文件中不可包含,亦不會被解析的字串,作為解析用之起始特殊標記。<br /> * e.g., '\u0000' === '\x00' === '\0'.<br /> * include_mark + ({ℕ⁰:Natural+0}index of queue) + end_mark * * assert: /\s/.test(include_mark) === false * * @see [[mw:Strip marker]] * * @type {String} */ include_mark = options.include_mark || default_include_mark, /** * {String}結束之特殊標記。 end of include_mark. 不可為數字 (\d) 或 * include_mark,不包含會被解析的字元如 /;/。應為 wikitext 所不容許之字元。<br /> * e.g., '\x01' === '\u0001'.<br /> */ end_mark = options.end_mark || default_end_mark, // PATTERN_text_apostrophe_unit = include_mark === default_include_mark && end_mark === default_end_mark ? default_PATTERN_text_apostrophe_unit : generate_token_pattern(template_PATTERN_text_apostrophe_unit, options), /** {Boolean}是否順便作正規化。預設不會規範頁面內容。 */ normalize = options.normalize, /** {Array}是否需要初始化。 [ {String}prefix added, {String}postfix added ] */ initialized_fix = !queue && [ '', '' ], // 這項設定不應被繼承。 no_resolve = options.no_resolve; if (no_resolve) { delete options.no_resolve; } if (/\d/.test(end_mark) || include_mark.includes(end_mark)) throw new Error('Error end of include_mark!'); if (initialized_fix) { // 初始化。 if (!wikitext.replace) { if (is_parsed_element(wikitext)) { library_namespace.debug('Treat [' + wikitext + '] as parsed token and return directly!', 3, 'parse_wikitext'); return wikitext; } console.trace(wikitext); } wikitext = wikitext // 注意: 2004年5月早期的中文維基百科換行採用 "\r\n",因此必須保留 "\r"。 // .replace(/\r\n/g, '\n') .replace( // 先 escape 掉會造成問題之 characters。 new RegExp(include_mark.replace(/([\s\S])/g, '\\$1'), 'g'), include_mark + end_mark); if (!wikitext.startsWith('\n') && // /\n([*#:;]+|[= ]|{\|)/: // https://www.mediawiki.org/wiki/Markup_spec/BNF/Article#Wiki-page // https://www.mediawiki.org/wiki/Markup_spec#Start_of_line_only /^(?:[*#;:=\s]|{\|)/.test(wikitext)) wikitext = (initialized_fix[0] = '\n') + wikitext; if (!wikitext.endsWith('\n')) wikitext += (initialized_fix[1] = '\n'); // setup temporary queue queue = []; } var section_title_hierarchy = queue.section_title_hierarchy || (queue.section_title_hierarchy = []); if (!section_title_hierarchy[0]) { // As root section_title_hierarchy[0] = options.target_array || Object.create(null); section_title_hierarchy[0].child_section_titles = []; } if (!queue.conversion_table) { // [[MediaWiki:Conversiontable/zh-hant]] queue.conversion_table = Object.create(null); } if (typeof options.prefix === 'function') { wikitext = options.prefix(wikitext, queue, include_mark, end_mark) || wikitext; } // 預防有特殊 elements 置入其中。此時將之當作普通 element 看待。 // cf. deep resolve_escaped() function shallow_resolve_escaped(text) { if (text.includes(include_mark)) { // 經過改變,需再進一步處理。 text = parse_wikitext(text, options, queue); } return text; } // console.trace(wikitext); // ------------------------------------------------------------------------ // parse functions function parse_language_conversion(all, parameters) { // -{...}- 自 end_mark 向前回溯。 var index = parameters.lastIndexOf('-{'), // 在先的,在前的,前面的; preceding // (previous 反義詞 following, preceding 反義詞 exceeds) previous; if (index > 0) { previous = '-{' + parameters.slice(0, index); parameters = parameters.slice(index + '}-'.length); } else { previous = ''; } library_namespace.debug(previous + ' + ' + parameters, 4, 'parse_wikitext.convert'); // console.log(parameters); var conversion = Object.create(null), // conversion_list = [], latest_language; var _flag = parameters.match(/^([a-zA-Z\-;\s]*)\|(.*)$/), flag; if (_flag) { parameters = _flag[2]; var flag_hash = Object.create(null); _flag = _flag[1]; flag = _flag.split(';').map(function(f) { f = f.trim(); if (f) flag_hash[f] = true; return f; }).filter(function(f) { return !!f; }); if (flag.length === 0) { flag = ''; } else { // https://doc.wikimedia.org/mediawiki-core/master/php/ConverterRule_8php_source.html // 僅取首先符合者。 [ 'R', 'N', '-', 'T', 'H', 'A', 'D' ].some(function(f) { if (flag_hash[f]) { flag = f; return true; } }); } } var conversion_table = flag && (flag in { // '+' add rules for alltext // '+' : true, // these flags above are reserved for program // remove convert (not implement) // '-' : true, // add rule for convert code (but no display in placed code) H : true, // add rule for convert code (all text convert) A : true }) && queue.conversion_table; // console.log('parameters: ' + JSON.stringify(parameters)); parameters = parameters.split(';'); parameters.forEach(function(converted, index) { if (normalize) { // remove tail spaces converted = converted.trim(); } if (PATTERN_conversion_slice.test(converted) // e.g., "-{ a; zh-tw: tw }-" 之 " a" || conversion_list.length === 0 // 最後一個是空白。 || !converted.trim() && index + 1 === parameters.length) { conversion_list.push(converted); } else { conversion_list[conversion_list.length - 1] // e.g., "-{zh-tw: tw ; tw : tw2}-" += ';' + converted; } }); // console.log(conversion_list); var convert_from_hash = conversion_table && Object.create(null); var unidirectional = []; /** * [[Help:高级字词转换语法#基本语法]] * * <code> -{zh-cn:cn; zh-tw:tw;}- → conversion_table['cn'] = conversion_table['tw'] = {'zh-cn':'cn','zh-tw':'tw'} -{txt=>zh-cn:cn; txt=>zh-tw:tw;}- → conversion_table['txt'] = {'zh-cn':'cn','zh-tw':'tw'} -{txt=>zh-cn:cn; zh-cn:cn; zh-tw:tw;}- → conversion_table['txt'] = {'zh-cn':'cn'} ∪ conversion_table['cn'] = conversion_table['tw'] = {'zh-cn':'cn','zh-tw':'tw'} </code> */ // TODO: 剖析不出任何對應規則的話,則為 R 旗標轉換,即是停用字詞轉換,顯示原文(R stands for raw)。 conversion_list = conversion_list.map(function(token) { var matched = token.match(PATTERN_conversion_slice); // console.log(matched); if (!matched // e.g., -{A|=>zh-tw:tw}- || matched[1] && !(matched[2] = matched[2].trim())) { // 經過改變,需再進一步處理。 return parse_wikitext(token, options, queue); } // matched.shift(); matched = matched.slice(1); // matched: [ 指定轉換字串, 指定轉換詞, spaces, // this language code, colon, this language token, last spaces ] if (!matched[6]) matched.pop(); // 語言代碼 language variant 用字模式 var language_code = matched[3].trim(), convert_to // 經過改變,需再進一步處理。 = matched[5] = parse_wikitext(matched[5], options, queue); if (!convert_to) { // 'converter-manual-rule-error' return parse_wikitext(token, options, queue); } conversion[language_code] = convert_to; if (!matched[2]) { matched.splice(2, 1); } // 指定僅轉換某些特殊詞彙。 // unidirectional_convert_from var uniconvert_from; if (matched[0]) { uniconvert_from = matched[1].trim(); if (!uniconvert_from) { // if $from is empty, strtr() could return a wrong // result. } matched.splice(1, 1); } else { matched.splice(0, 2); } token = _set_wiki_type(matched, 'plain'); token.is_conversion = true; if (!conversion_table) { ; } else if (uniconvert_from) { // 單向轉換 unidirectional convert unidirectional.push(uniconvert_from); if (!conversion_table[uniconvert_from]) { conversion_table[uniconvert_from] // Initialization = Object.create(null); } else if (conversion_table[uniconvert_from].conversion) { conversion_table[uniconvert_from] = Object.clone( // assert: // conversion_table[uniconvert_from].type==='convert' conversion_table[uniconvert_from].conversion); } if (false && options.conflict_conversion // overwrite && conversion_table[uniconvert_from][language_code]) { options.conflict_conversion.call(conversion_table, uniconvert_from, language_code, conversion_table[uniconvert_from] // [language_code], convert_to); } conversion_table[uniconvert_from][language_code] // settle = convert_to; } else if (typeof convert_to === 'string') { // 後面的設定會覆蓋先前的設定。 convert_from_hash[language_code] = convert_to; } else if (convert_to && convert_to.type === 'plain') { // 雙向轉換 bidirectional convert // -{H|zh-cn:俄-{匊}-斯;zh-tw:俄-{匊}-斯;zh-hk:俄-{匊}-斯;}- // 當作 "俄匊斯" var not_all_string; convert_to = convert_to.map(function(token) { if (typeof token === 'string') return token; if (token.type === 'convert' && typeof token.converted === 'string') return token.converted; not_all_string = true; }); if (!not_all_string) { // 後面的設定會覆蓋先前的設定。 convert_from_hash[language_code] = convert_to.join(''); } } // console.log(JSON.stringify(token)); return token; }); if (normalize) { // 正規化後可以不保留 -{...;}- 最後的 ';' conversion_list = conversion_list.filter(function(token) { return !!token; }); conversion_list.sort(function(_1, _2) { // assert: {Array} _1, _2 return _1[0] < _2[0] ? -1 : _1[0] > _2[0] ? 1 : 0; }); } // console.log(conversion_list); parameters = _set_wiki_type(conversion_list, 'convert'); parameters.conversion = conversion; if (unidirectional.length > 0) parameters.unidirectional = unidirectional.unique(); if (typeof _flag === 'string') { if (_flag !== flag) parameters._flag = _flag; parameters.flag = flag; if (flag === 'T') options.conversion_title = parameters; } // console.log(convert_from_hash); convert_from_hash && Object.values(convert_from_hash) // .forEach(function(convert_from_string) { // console.log(convert_from_string); conversion_table[convert_from_string] = parameters; }); // console.log(JSON.stringify(wikitext)); // console.log(conversion_table); if (queue.switches && (queue.switches.__NOCC__ // 使用魔術字 __NOCC__ 或 __NOCONTENTCONVERT__ 可避免轉換。 || queue.switches.__NOCONTENTCONVERT__)) { parameters.no_convert = true; } else if (Object.keys(conversion).length === 0) { // assert: parameters.length === 1 // e.g., "-{ t {{T}} }-" // NOT "-{ zh-tw: tw {{T}} }-" parameters.converted = parameters[0]; } else if (options.language) { // TODO: 先檢測當前使用的語言,然後轉成在當前環境下轉換過、會顯示出的結果。 var converted = parameters.toString(options.language); if (converted !== VALUE_converter_rule_error) parameters.converted = converted; } queue.push(parameters); return previous + include_mark + (queue.length - 1) + end_mark; } var PATTERN_token_unit; function setup_PATTERN_token_unit() { if (!PATTERN_token_unit) { PATTERN_token_unit = generate_token_pattern( // matched: [ , string, token_mark, queue_index ] /([\s\S]*?)(token_mark|$)/g, Object.assign(Object .clone(options), { with_queue_index : true })); } else { // reset PATTERN index PATTERN_token_unit.lastIndex = 0; } } function has_invalid_token(original_value, invalid_token_filter) { // assert: original_value.includes(include_mark) setup_PATTERN_token_unit(); var invalid_token_data = [/* valid_value */'' ]; while (PATTERN_token_unit.lastIndex < original_value.length) { var matched = PATTERN_token_unit.exec(original_value); // matched: [ , string, token_ma