cejs
Version:
A JavaScript module framework that is simple to use.
1,690 lines (1,499 loc) • 77.2 kB
JavaScript
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): parse sections and
* anchors
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2021/12/15 6:7:47 拆分自 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.section',
require : 'application.net.wiki.parser.'
//
+ '|application.net.wiki.parser.wikitext',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// @inner
var PATTERN_BOT_NAME = wiki_API.PATTERN_BOT_NAME;
var for_each_subelement = wiki_API.parser.parser_prototype.each;
var
/** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */
NOT_FOUND = ''.indexOf('_');
// --------------------------------------------------------------------------------------------
// 這些 <tag> 都不能簡單解析出來。
// @see wiki_extensiontags
var untextify_tags = {
ref : true,
// e.g., <references group="gg"/>
references : true,
math : true
};
// Is {String} and will not used in normal wikitext or parse_wikitext().
var element_placeholder = '__element_placeholder__',
//
PATTERN_element_placeholder = new RegExp(element_placeholder, 'g');
// @inner
function preprocess_section_link_token(token, options) {
// console.trace(token);
if (!token) {
// e.g., CeL.wiki.parse('=={{lang|en}}==')
return token;
}
// console.trace(token);
// 前置作業: 處理模板之類特殊節點。
if (typeof options.preprocess_section_link_token === 'function') {
token = options.preprocess_section_link_token(token, options);
}
// console.trace(token);
token = wiki_API.repeatedly_expand_template_token(token, options);
// console.trace(token);
if (token.has_shell && !token.type && token.length === 1) {
token = token[0];
// console.trace(token);
}
// ------------------------
if (token.type === 'pre') {
var new_token = [];
// @see wiki_element_toString.pre
token.forEach(function(sub_token) {
new_token.push('\n ', sub_token);
});
new_token[0] = ' ';
token = wiki_API.parse.set_wiki_type(new_token, 'plain');
// free
new_token = null;
}
if (token.type in {
tag_inner : true,
parameter_unit : true,
plain : true
}) {
for_each_subelement.call(token, function(sub_token, index, parent) {
// console.trace(sub_token);
sub_token = preprocess_section_link_token(sub_token, options);
// console.trace(sub_token);
return sub_token;
}, options);
return token;
}
// 去除註解。 Remove comments. "<!-- comment -->"
if (token.type === 'comment') {
return '';
}
if (token.type === 'hr') {
return token.toString();
}
if (token.type === 'table') {
return wiki_API.table_to_array(token, Object.assign({
cell_processor : function(cell) {
if (!cell)
return '';
cell = wiki_API.parse.set_wiki_type(cell.slice(), 'plain');
cell = wiki_API.wikitext_to_plain_text(cell,
//
options).trim();
return cell;
}
}, options)).map(function(row) {
return row.join('\t');
}).join('\n');
}
// console.log(token);
if (token.type === 'tag'/* || token.type === 'tag_single' */) {
// token: [ tag_attributes, tag_inner ]
if (token.tag === 'nowiki') {
// escape characters inside <nowiki>
return preprocess_section_link_token(token[1] ? token[1]
.toString() : '', options);
}
// 容許一些特定標籤能夠顯示格式。以繼承原標題的粗體斜體和顏色等等格式。
// @see markup_tags
if (token.tag in {
// 展現格式用的 tags
b : true,
i : true,
q : true,
s : true,
u : true,
big : true,
small : true,
sub : true,
sup : true,
em : true,
ins : true,
del : true,
strike : true,
strong : true,
mark : true,
font : true,
code : true,
ruby : true,
rb : true,
rt : true,
center : true,
// container
span : true,
div : true,
// nowiki : true,
langconvert : true
}) {
// reduce HTML tags. e.g., <b>, <sub>, <sup>, <span>
token.tag_attributes = token.shift();
token.original_type = token.type;
token.type = 'plain';
token.toString = wiki_API.parse.wiki_element_toString[token.type];
return token;
}
// console.trace(token);
// 其他 HTML tag 大多無法準確轉換。
options.root_token_list.imprecise_tokens.push(token);
if (token.tag in untextify_tags) {
// trick: 不再遍歷子節點。避免被進一步的處理。
token.is_atom = true;
token.unconvertible = true;
return token;
}
// TODO: <a>
// token that may be handlable 請檢查是否可處理此標題。
options.root_token_list.tokens_maybe_handlable.push(token);
// reduce HTML tags. e.g., <ref>
var new_token = preprocess_section_link_tokens(token[1] || '',
options);
new_token.tag = token.tag;
return new_token;
}
if (token.type === 'tag_single') {
if (token.tag in {
templatestyles : true,
// For {{#lst}}, {{#section:}}
// [[w:en:Help:Labeled section transclusion]]
// e.g., @ [[w:en:Island Line, Isle of Wight]]
section : true,
// hr : true,
// e.g., <br />
br : true,
nowiki : true
}) {
return '';
}
options.root_token_list.imprecise_tokens.push(token);
// 從上方 `token.type === 'tag'` 複製過來的。
if (token.tag in untextify_tags) {
// trick: 不再遍歷子節點。避免被進一步的處理。
token.is_atom = true;
token.unconvertible = true;
return token;
}
// token that may be handlable 請檢查是否可處理此標題。
options.root_token_list.tokens_maybe_handlable.push(token);
return token;
}
if (false && token.type === 'convert') {
// TODO: e.g., '==-{[[:三宝颜共和国]]}-=='
token = token.converted;
// 接下來交給 `token.type === 'link'` 處理。
}
if ((token.type === 'file' || token.type === 'category')
&& !token.is_link) {
// 顯示時,TOC 中的圖片、分類會被消掉,圖片在內文中才會顯現。
return options.use_element_placeholder ? element_placeholder : '';
}
// TODO: interlanguage links will be treated as normal link!
if (token.type === 'link' || token.type === 'category'
// e.g., [[:File:file name.jpg]]
|| token.type === 'file') {
// escape wikilink
// return display_text
if (token.length > 2) {
token = token.slice(2);
token.type = 'plain';
// @see wiki_API.parse.wiki_element_toString.file, for
// token.length > 2
token.toString = function() {
return this.join('|')
};
token = preprocess_section_link_tokens(token, options);
} else {
// 去掉最前頭的 ":"。 @see wiki_API.parse.wiki_element_toString
token = token[0].toString().replace(/^ *:?/, '') + token[1];
}
// console.log(token);
return token;
}
// 這邊僅處理常用模板。需要先保證這些模板存在,並且具有預期的功能。
// 其他常用 template 可加在 wiki.template_functions[site_name] 中。
//
// 模板這個部分除了解析模板之外沒有好的方法。
// 正式應該採用 parse 或 expandtemplates 解析出實際的 title,之後 callback。
// https://www.mediawiki.org/w/api.php?action=help&modules=parse
if (token.type === 'transclusion') {
// 各語言 wiki 常用 template-linking templates:
// {{Tl}}, {{Tlg}}, {{Tlx}}, {{Tls}}, {{T1}}, ...
if (/^(?:T[l1n][a-z]{0,3}[23]?)$/.test(token.name)) {
// TODO: should expand as
// "{{[[Template:{{{1}}}|{{{1}}}]]}}"
token.shift();
return token;
}
if ((token.name in {
// {{lang|語言標籤|內文}}
Lang : true
}) && token.parameters[2]) {
return preprocess_section_link_token(token.parameters[2],
options);
}
// moved to CeL.application.net.wiki.template_functions.zhmoegirl
if (false
&& token.name === 'Lj'
&& wiki_API.site_name(wiki_API.session_of_options(options)) === 'zhmoegirl') {
return preprocess_section_link_token(wiki_API.parse('-{'
+ token.parameters[1] + '}-'), options);
}
// TODO: [[Template:User link]], [[Template:U]]
// TODO: [[Template:疑問]], [[Template:Block]]
// console.trace(token);
// 警告: 在遇到標題包含模板時,因為不能解析連模板最後產出的結果,會產生錯誤結果。
options.root_token_list.imprecise_tokens.push(token);
// trick: 不再遍歷子節點。避免被進一步的處理。
token.is_atom = true;
token.unconvertible = true;
return token;
}
if (token.type === 'external_link') {
// escape external link
// console.log('>> ' + token);
// console.log(token[2]);
// console.log(preprocess_section_link_tokens(token[2], options));
if (token[2]) {
return preprocess_section_link_tokens(token[2], options);
}
// TODO: error: 用在[URL]無標題連結會失效。需要計算外部連結的序號。
options.root_token_list.imprecise_tokens.push(token);
// trick: 不再遍歷子節點。避免被進一步的處理。
token.is_atom = true;
token.unconvertible = true;
return token;
}
if (token.type in {
'switch' : true,
parameter : true
}) {
options.root_token_list.imprecise_tokens.push(token);
return '';
}
if (token.type in {
italic : true,
bold : true
}) {
// 去除粗體與斜體。
token.original_type = token.type;
// assert: token.length === 2 || token.length === 3
if (token[2])
token.end_mark = token.pop();
token.start_mark = token.shift();
token.type = 'plain';
token.toString = wiki_API.parse.wiki_element_toString[token.type];
return token;
}
if (typeof token === 'string') {
// console.log('>> ' + token);
// console.log('>> [' + index + '] ' + token);
// console.log(parent);
// decode '"', '%00', ...
token = library_namespace.HTML_to_Unicode(token);
if (/\S/.test(token)) {
// trick: 不再遍歷子節點。避免被進一步的處理,例如"&amp;"。
token = [ token ];
token.is_atom = true;
token.unconvertible = true;
token.is_plain = true;
}
// console.trace(token);
return token;
}
if (token.type in {
convert : true,
url : true
}) {
// 其他可處理的節點。
return token;
}
// console.trace(token);
if (token.type === 'magic_word_function') {
// e.g., {{!}} {{=}}
token = wiki_API.evaluate_parser_function_token
.call(token, options);
if (typeof token !== 'object')
return token;
token.unconvertible = true;
}
/**
* TODO: check all <code>
[[mw:Help:Advanced editing#Reformatting and/or disabling wikitext interpretation|<nowiki>character</nowiki>]]
</code>
*/
if (token.type === 'parameter') {
// TODO: return token.evaluate()
token.unconvertible = true;
}
// console.trace(token);
// token that may be handlable 請檢查是否可處理此標題。
if (!token.unconvertible)
options.root_token_list.tokens_maybe_handlable.push(token);
if (!token.is_plain) {
// `token.is_plain`: 由 {String} 轉換而成。
options.root_token_list.imprecise_tokens.push(token);
}
return token;
}
// @inner
function preprocess_section_link_tokens(tokens, options) {
if (tokens.type !== 'plain') {
tokens = wiki_API.parse.set_wiki_type([ tokens ], 'plain');
}
if (false) {
library_namespace.info('preprocess_section_link_tokens: tokens:');
console.log(tokens);
}
// console.trace(tokens);
if (!tokens.imprecise_tokens) {
// options.root_token_list.imprecise_tokens
tokens.imprecise_tokens = [];
tokens.tokens_maybe_handlable = [];
}
if (!options.root_token_list)
options.root_token_list = tokens;
options.modify = true;
// console.trace(options);
// console.trace(tokens);
if (options.try_to_expand_templates) {
// 警告: 必須自行 wiki_API.expand_transclusion().
// example: @see get_all_anchors()
}
return preprocess_section_link_token(tokens, options);
}
// TODO: The method now is NOT a good way!
// extract_plain_text_of_wikitext(), get_plain_display_text()
// @see [[w:en:Module:Delink]]
// 可考慮是否採用 CeL.wiki.expand_transclusion()
function wikitext_to_plain_text(wikitext, options) {
options = library_namespace.new_options(options);
wikitext = wiki_API.parse(String(wikitext), options);
// console.trace(wikitext);
wikitext = preprocess_section_link_tokens(wikitext, options);
// console.trace(wikitext);
return wikitext.toString();
}
// --------------------------------
// 用在 summary 必須設定 is_URI !
function section_link_escape(text, is_URI) {
// escape wikitext control characters,
// including language conversion -{}-
if (true) {
text = text.replace(
// 盡可能減少字元的使用量,因此僅處理開頭,不處理結尾。
// @see [[w:en:Help:Wikitext#External links]]
// @see PATTERN_page_name
is_URI ? /[\|{}<>\[\]%]/g
// 為了容許一些特定標籤能夠顯示格式,"<>"已經在preprocess_section_link_token(),section_link()裡面處理過了。
// display_text 在 "[[", "]]" 中,不可允許 "[]"
: /[\|{}<>]/g && /[\|{}\[\]]/g,
// 經測試 anchor 亦不可包含[\[\]{}\t\n�]。
function(character) {
if (is_URI) {
return '%' + character.charCodeAt(0)
// 會比 '&#' 短一點。
.toString(16).toUpperCase();
}
return '&#' + character.charCodeAt(0) + ';';
}).replace(/[\s\n]+/g, ' ');
} else {
// 只處理特殊字元而不是採用encodeURIComponent(),這樣能夠保存中文字,使其不被編碼。
text = encodeURIComponent(text);
}
return text;
}
// @inner
// return [[維基連結]]
// TODO: using external link to display "�"
function section_link_toString(page_title, style, underlined_anchor) {
var anchor = (this[1] || '').replace(/�/g, '?'),
// 目前 MediaWiki 之 link anchor, display_text 尚無法接受
// REPLACEMENT CHARACTER U+FFFD "�" 這個字元。
display_text = (this[2] || '').replace(/�/g, '?');
if (underlined_anchor) {
// 2023/1/26 在頁面被 transclusion 的時候,空白不會被自動轉為 "_"。
// assert: 這裡的 \s 應該只剩下空白字元 " "。
anchor = anchor.replace(/\s/g, '_');
}
display_text = display_text ?
//
style ? '<span style="' + style + '">' + display_text + '</span>'
: display_text : '';
return wiki_API.title_link_of((page_title || this[0] || '') + '#'
+ anchor, display_text);
return '[[' + (page_title || this[0] || '') + '#' + anchor + '|'
+ display_text + ']]';
}
// 用來保留 display_text 中的 language conversion -{}-,
// 必須是標題裡面不會存在的字串,並且也不會被section_link_escape()轉換。
var section_link_START_CONVERT = '\x00\x01', section_link_END_CONVERT = '\x00\x02',
//
section_link_START_CONVERT_reg = new RegExp(library_namespace
.to_RegExp_pattern(section_link_START_CONVERT), 'g'),
//
section_link_END_CONVERT_reg = new RegExp(library_namespace
.to_RegExp_pattern(section_link_END_CONVERT), 'g');
// wiki_API.section_link.pre_parse_section_title()
function pre_parse_section_title(parameters, options, queue) {
parameters = parameters.toString()
// 先把前頭的空白字元提取出來,避免被當作 <pre>。
// 先把前頭的列表字元提取出來,避免被當作 list。
// 這些會被當作普通文字。
.match(/^([*#;:=\s]*)([\s\S]*)$/);
// console.trace(parameters);
var prefix = parameters[1];
// 經過改變,需再進一步處理。
parameters = wiki_API.parse(parameters[2], options, queue);
// console.trace(parameters);
if (parameters.type !== 'plain') {
parameters = wiki_API.parse.set_wiki_type([ parameters ], 'plain');
}
if (prefix) {
if (typeof parameters[0] === 'string')
parameters[0] = prefix + parameters[0];
else
parameters.unshift(prefix);
}
return parameters;
}
section_link.pre_parse_section_title = pre_parse_section_title;
/**
* 從話題/議題/章節標題產生連結到章節標題的wikilink。
*
* @example <code>
// for '== section_title ==',
CeL.wiki.section_link('section_title')
</code>
*
* @param {String}section_title
* section title in wikitext inside "==" (without '=='s). 章節標題。
* 節のタイトル。
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*
* @returns {Array}link object (see below)
*
* @see [[phabricator:T18691]] 未來章節標題可能會有分享連結,這將更容易連結到此章節。
* @see [[H:MW]], {{anchorencode:章節標題}}, [[Template:井戸端から誘導の使用]], escapeId()
* @see https://phabricator.wikimedia.org/T152540
* https://lists.wikimedia.org/pipermail/wikitech-l/2017-August/088559.html
*/
function section_link(section_title, options) {
if (typeof options === 'string') {
options = {
page_title : options
};
} else if (typeof options === 'function') {
options = {
// TODO
callback : options
};
} else {
options = library_namespace.new_options(options);
options.use_element_placeholder = true;
}
// console.trace(wiki_API.parse(section_title, null, []));
// TODO: "==''==text==''==\n"
var parsed_title = pre_parse_section_title(section_title, options);
// console.trace([ section_title, parsed_title, options ]);
// pass session.
parsed_title = preprocess_section_link_tokens(parsed_title, options);
// console.trace([ section_title, parsed_title ]);
// 注意: 當這空白字字出現在功能性token中時,可能會出錯。
var id = parsed_title.toString().trim().replace(
PATTERN_element_placeholder, '')
// 去頭去尾僅針對 "\x20",不包括 ===   (\xa0),所以不能用 .trim()。
.replace(/^ +/, '').replace(/ +$/, '')
// 多個空白字元轉為單一空白字元。
.replace(/[\s\n]+/g, ' '),
// anchor 網頁錨點: 可以直接拿來做 wikilink anchor 的章節標題。
// 有多個完全相同的 anchor 時,後面的會加上"_2", "_3",...。
// 這個部分的處理請見 function for_each_section()
anchor = section_link_escape(id
// anchor 中所有的空白字元都會被轉成 "_"。沒採用空白字元 " " 是因為 " " 會被轉為 "%20"。
// 處理連續多個空白字元。<s>長度相同的情況下,盡可能保留原貌。</s>
.replace(/[ _]+/g, '_').replace(/&/g, '&'), true)
// recover space: anchor 最好還是以空白字元呈現。
.replace(/_/g, ' ');
// var session = wiki_API.session_of_options(options);
// TODO: for zhwiki, the anchor should NOT includes "-{", "}-"
// console.log(parsed_title);
for_each_subelement.call(parsed_title, function(token, index, parent) {
if (token.type === 'convert') {
// @see wiki_API.parse.wiki_element_toString.convert
// return token.join(';');
token.toString = function convert_for_recursion() {
var converted = this.converted;
if (converted === undefined) {
// e.g., get display_text of
// '==「-{XX-{zh-hans:纳; zh-hant:納}-克}-→-{XX-{奈}-克}-」=='
return section_link_START_CONVERT
// @see wiki_API.parse.wiki_element_toString.convert
+ this.join(';') + section_link_END_CONVERT;
}
if (Array.isArray(converted)) {
// e.g., '==-{[[:三宝颜共和国]]}-=='
converted = converted.toString()
// e.g.,
// '==「-{XX-{zh-hans:纳; zh-hant:納}-克}-→-{XX-{奈}-克}-」=='
// recover language conversion -{}-
.replace(section_link_START_CONVERT_reg, '-{').replace(
section_link_END_CONVERT_reg, '}-');
converted = section_link(converted, Object.assign(
//
Object.clone(options), {
// recursion, self-calling, 遞迴呼叫
is_recursive : true
}))[2];
}
return section_link_START_CONVERT
// + this.join(';')
+ converted + section_link_END_CONVERT;
};
} else if (token.original_type) {
// revert type
token.type = token.original_type;
token.toString
//
= wiki_API.parse.wiki_element_toString[token.type];
// 保留 display_text 中的 ''', '', <b>, <i>, <span> 屬性。
if (token.type === 'tag') {
// 容許一些特定標籤能夠顯示格式: 會到這裡的應該都是一些被允許顯示格式的特定標籤。
token.unshift(token.tag_attributes);
} else {
if (token.start_mark)
token.unshift(token.start_mark);
if (token.end_mark)
token.push(token.end_mark);
}
} else if (token.type === 'tag' || token.type === 'tag_single') {
parent[index] = token.toString().replace(/</g, '<').replace(
/>/g, '>');
} else if (token.is_plain) {
if (false) {
// use library_namespace.DOM.Unicode_to_HTML()
token[0] = library_namespace.Unicode_to_HTML(token[0])
// reduce size
.replace(/>/g, '>');
}
// 僅作必要的轉換
token[0] = token[0].replace(/&/g, '&')
// 這邊也必須 escape "<>"。這邊可用 "%3C", "%3E"。
.replace(/</g, '<').replace(/>/g, '>').replace(/"/g,
'"').replace(/'/g, "'");
}
}, Object.assign(Object.clone(options), {
modify : true
}));
// console.log(parsed_title);
// console.trace(parsed_title.toString().trim());
// display_text 應該是對已經正規化的 section_title 再作的變化。
var display_text = parsed_title.toString().replace(
PATTERN_element_placeholder, '').trim();
display_text = section_link_escape(display_text);
if (!options.is_recursive) {
// recover language conversion -{}-
display_text = display_text.replace(section_link_START_CONVERT_reg,
'-{').replace(section_link_END_CONVERT_reg, '}-');
}
// link = [ page title 頁面標題, anchor 網頁錨點 / section title 章節標題,
// display_text / label 要顯示的連結文字 default: section_title ]
var link = [ options && options.page_title,
// Warning: anchor, display_text are with "&",
// id is not with "&".
// Warning: 這裡的網頁錨點沒包括 "#",和 wiki_element_toString.link 不同。
anchor, display_text ];
// console.log(link);
// console.trace(parsed_title);
if (parsed_title.imprecise_tokens
// section_title_token.link.imprecise_tokens
&& parsed_title.imprecise_tokens.length > 0) {
link.imprecise_tokens = parsed_title.imprecise_tokens;
// section_title_token.link.tokens_maybe_handlable
if (parsed_title.tokens_maybe_handlable
&& parsed_title.tokens_maybe_handlable.length > 0) {
link.tokens_maybe_handlable = parsed_title.tokens_maybe_handlable
.unique();
link.tokens_maybe_handlable.forEach(function(parsed) {
for_each_subelement.call(parsed, function(token, index,
parent) {
if (token.type === 'convert') {
token.toString
// recover .toString of token.type === 'convert'
// @see convert_for_recursion()
= wiki_API.parse.wiki_element_toString[token.type];
}
});
});
}
}
Object.assign(link, {
// link.id = {String}id
// section title, NOT including "<!-- -->" and "&"
id : id,
// original section title, including "<!-- -->",
// not including "&".
title : section_title,
// only for debug
// parsed_title : parsed_title,
// anchor : anchor.toString().trimEnd(),
// display_text : display_text,
// section.section_title.link.toString()
toString : section_link_toString
});
// 用以獲得實際有效的 anchor 網頁錨點。 effect anchor: parsed.each_section()
// and then section_title_token.link.id
return link;
}
// --------------------------------------------------------------------------------------------
/**
* 快速取得第一個標題 lead section / first section / introduction 序言 導入文 文字用。
*
* TODO: expandtemplates for cascading protection
*
* @example <code>
CeL.wiki.lead_text(content);
</code>
*
* @param {String}wikitext
* wikitext to parse
*
* @returns {String}lead section wikitext 文字
*
* @see function simplify_transclusion() @ CeL.application.net.wiki.parser.evaluate
*
* @see 文章的開頭部分[[WP:LEAD|導言章節]] (lead section, introduction),
* [[en:Wikipedia:Hatnote]] 頂註
* https://zh.wikipedia.org/api/rest_v1/#/Page%20content/get_page_summary__title_
*/
function lead_text(wikitext) {
var page_data;
if (wiki_API.is_page_data(wikitext)) {
page_data = wikitext;
wikitext = wiki_API.content_of(page_data);
}
if (!wikitext || typeof wikitext !== 'string') {
return wikitext;
}
var matched = wikitext.indexOf('\n=');
if (matched >= 0) {
wikitext = wikitext.slice(0, matched);
}
// match/去除一開始的維護模板/通知模板。
// <del>[[File:file|[[link]]...]] 因為不容易除盡,放棄處理。</del>
while (matched = wikitext.match(/^[\s\n]*({{|\[\[)/)) {
// 注意: 此處的 {{ / [[ 可能為中間的 token,而非最前面的一個。但若是沒有中間的 token,則一定是第一個。
matched = matched[1];
// may use wiki_API.title_link_of()
var index_end = wikitext.indexOf(matched === '{{' ? '}}' : ']]');
if (index_end === NOT_FOUND) {
library_namespace.debug('有問題的 wikitext,例如有首 "' + matched
+ '" 無尾? [' + wikitext + ']', 2, 'lead_text');
break;
}
// 須預防 -{}- 之類 language conversion。
var index_start = wikitext.lastIndexOf(matched, index_end);
wikitext = wikitext.slice(0, index_start)
// +2: '}}'.length, ']]'.length
+ wikitext.slice(index_end + 2);
}
if (page_data) {
page_data.lead_text = lead_text;
}
return wikitext.trim();
}
// ------------------------------------------
/**
* 擷取出頁面簡介。例如使用在首頁優良條目簡介。
*
* @example <code>
CeL.wiki.extract_introduction(page_data).toString();
</code>
*
* @param {Array|Object}first_section
* first section or page data
* @param {String}[title]
* page title.
*
* @returns {Undefined|Array} introduction object
*
* @since 2019/4/10
*/
function extract_introduction(first_section, title) {
var parsed;
if (wiki_API.is_page_data(first_section)) {
if (!title)
title = wiki_API.title_of(first_section);
parsed = wiki_API.parser(first_section).parse();
parsed.each_section(function(section, index) {
if (!section.section_title) {
first_section = section;
}
});
}
if (!first_section)
return;
// --------------------------------------
var introduction_section = [], representative_image;
if (parsed) {
introduction_section.page = parsed.page;
introduction_section.title = title;
// Release memory. 釋放被占用的記憶體。
parsed = null;
}
introduction_section.toString = first_section.toString;
// --------------------------------------
var index = 0;
for (; index < first_section.length; index++) {
var token = first_section[index];
// console.log(token);
if (token.type === 'file') {
// {String}代表圖像。
if (!representative_image) {
representative_image = token;
}
continue;
}
if (token.type === 'transclusion') {
if (token.name === 'NoteTA') {
// preserve 轉換用詞
// TODO:
// 因為該頁會嵌入首頁,所以請不要使用{{noteTA}}進行繁簡轉換;請用-{zh-hans:簡體字;zh-hant:繁體字}-進行單個詞彙轉換。
// [[繁體字]] → [[繁體字|-{zh-hans:簡體字;zh-hant:繁體字}-]]
introduction_section.push(token);
continue;
}
if (token.name in {
Cfn : true,
Sfn : true,
Sfnp : true,
Efn : true,
NoteTag : true,
R : true,
Clear : true
}) {
// Skip references
continue;
}
// 抽取出代表圖像。
if (!representative_image) {
representative_image = token.parameters.image
|| token.parameters.file
// ||token.parameters['Image location']
;
}
if (!representative_image) {
token = token.toString();
// console.log(token);
var matched = token
.match(/\|[^=]+=([^\|{}]+\.(?:jpg|png|svg|gif|bmp))[\s\n]*[\|}]/i);
if (matched) {
representative_image = matched[1];
}
}
continue;
}
if ((token.type === 'tag' || token.type === 'tag_single')
&& token.tag === 'ref') {
// 去掉所有參考資料。
continue;
}
if (token.type === 'table'
// e.g., __TOC__
|| token.type === 'switch') {
// 去掉所有參考資料。
continue;
}
if (!token.toString().trim()) {
continue;
}
if (token.type === 'bold' || token.type === 'plain'
&& token.toString().includes(title)) {
// title_piece
introduction_section.title_token = token;
}
if (token.type === 'link') {
if (!token[0] && token[1]) {
// 將[[#章節|文字]]的章節連結改為[[條目名#章節|文字]]的形式。
token[0] = title;
}
}
// console.log('Add token:');
// console.log(token);
introduction_section.push(token);
if (introduction_section.title_token)
break;
}
// ------------------
// 已經跳過導航模板。把首段餘下的其他內容全部納入簡介中。
while (++index < first_section.length) {
token = first_section[index];
// remove {{Notetag}}, <ref>
if ((token.type === 'tag' || token.type === 'tag_single')
&& token.tag === 'ref' || token.type === 'transclusion'
&& token.name === 'Notetag')
continue;
introduction_section.push(token);
}
index = introduction_section.length;
// trimEnd() 去頭去尾
while (--index > 0) {
if (introduction_section[index].toString().trim())
break;
introduction_section.pop();
}
// --------------------------------------
// 首個段落不包含代表圖像。檢查其他段落以抽取出代表圖像。
if (!representative_image) {
parsed.each('file', function(token) {
representative_image = token;
return for_each_subelement.exit;
});
}
// --------------------------------------
if (typeof representative_image === 'string') {
// assert: {String}representative_image
// remove [[File:...]]
representative_image = representative_image.replace(/^\[\[[^:]+:/i,
'').replace(/\|[\s\S]*/, '').replace(/\]\]$/, '');
representative_image = wiki_API.parse('[[File:'
+ wiki_API.title_of(representative_image) + ']]');
}
introduction_section.representative_image = representative_image;
return introduction_section;
}
// ------------------------------------------
/**
* <code>
CeL.wiki.sections(page_data);
page_data.sections.forEach(for_sections, page_data.sections);
CeL.wiki.sections(page_data)
//
.forEach(for_sections, page_data.sections);
</code>
*/
// 將 wikitext 拆解為各 section list
// get {Array}section list
//
// @deprecated: 無法處理 '<pre class="c">\n==t==\nw\n</pre>'
// use for_each_section() instead.
function deprecated_get_sections(wikitext) {
var page_data;
if (wiki_API.is_page_data(wikitext)) {
page_data = wikitext;
wikitext = wiki_API.content_of(page_data);
}
if (!wikitext || typeof wikitext !== 'string') {
return;
}
var section_list = [], index = 0, last_index = 0,
// 章節標題。
section_title,
// [ all title, "=", section title ]
PATTERN_section = /\n(={1,2})([^=\n]+)\1\s*?\n/g;
section_list.toString = function() {
return this.join('');
};
// 章節標題list。
section_list.title = [];
// index hash
section_list.index = Object.create(null);
while (true) {
var matched = PATTERN_section.exec(wikitext),
// +1 === '\n'.length: skip '\n'
// 使每個 section_text 以 "=" 開頭。
next_index = matched && matched.index + 1,
//
section_text = matched ? wikitext.slice(last_index, next_index)
: wikitext.slice(last_index);
if (false) {
// 去掉章節標題。
section_text.replace(/^==[^=\n]+==\n+/, '');
}
library_namespace.debug('next_index: ' + next_index + '/'
+ wikitext.length, 3, 'get_sections');
// console.log(matched);
// console.log(PATTERN_section);
if (section_title) {
// section_list.title[{Number}index] = {String}section title
section_list.title[index] = section_title;
if (section_title in section_list) {
// 重複標題。
library_namespace.debug('重複 section title ['
+ section_title + '] 將僅取首個 section text。', 2,
'get_sections');
} else {
if (!(section_title >= 0)) {
// section_list[{String}section title] =
// {String}wikitext
section_list[section_title] = section_text;
}
// 不採用 section_list.length,預防 section_title 可能是 number。
// section_list.index[{String}section title] = {Number}index
section_list.index[section_title] = index;
}
}
// 不採用 section_list.push(section_text);,預防 section_title 可能是 number。
// section_list[{Number}index] = {String}wikitext
section_list[index++] = section_text;
if (matched) {
// 紀錄下一段會用到的資料。
last_index = next_index;
section_title = matched[2].trim();
// section_title = wiki_API.section_link(section_title).id;
} else {
break;
}
}
if (page_data) {
page_data.sections = section_list;
// page_data.lead_text = lead_text(section_list[0]);
}
// 檢核。
if (false && wikitext !== section_list.toString()) {
// debug 用. check parser, test if parser working properly.
throw new Error('get_sections: Parser error'
//
+ (page_data ? ': ' + wiki_API.title_link_of(page_data) : ''));
}
return section_list;
}
/**
* 為每一個章節(討論串)執行特定作業 for_section(section)
*
* If you want to get **every** sections, please using
* `parsed..each('section_title', ...)` or traversals hierarchy of
* `parsed.child_section_titles` instead of enumerating `parsed.sections`.
* `parsed.sections` do NOT include titles like this:
* {{Columns-list|\n==title==\n...}}
*
* CeL.wiki.parser.parser_prototype.each_section
*
* TODO: 這會漏算沒有日期標示的簽名
*
* @example <code>
// TODO: includeing `<h2>...</h2>`, `==<h2>...</h2>==`
parsed = CeL.wiki.parser(page_data);
parsed.each_section(function(section, section_index) {
if (!section.section_title) {
// first_section = section;
// Skip lead section / first section / introduction.
return;
}
console.log('#' + section.section_title);
console.log([ section.users, section.dates ]);
console.log([section_index, section.toString()]);
section.each('link', function(token) {
console.log(token.toString());
}, {
// for section.users, section.dates
get_users : true,
// 採用 parsed 的 index,而非 section 的 index。
// 警告: 會從 section_title 開始遍歷 traverse!
use_global_index : true
});
return parsed.each.exit;
}, {
level_filter : [ 2, 3 ],
get_users : true
});
parsed.each_section();
parsed.sections.forEach(...);
</code>
*/
function for_each_section(for_section, options) {
options = library_namespace.new_options(options);
// this.options is from function page_parser(wikitext, options)
if (!options[KEY_SESSION] && this.options && this.options[KEY_SESSION]) {
// set options[KEY_SESSION] for
// `var date = wiki_API.parse.date(token, options);`
options[KEY_SESSION] = this.options[KEY_SESSION];
}
// this: parsed
var _this = this, page_title = this.page && this.page.title,
// parsed.sections[0]: 常常是設定與公告區,或者放置維護模板/通知模板。
all_root_section_list = this.sections = [];
/**
* 2021/11/3 18:23:24: .parent_section 歸於 .parent_section_title,
* .subsections 歸於 .child_section_titles。
*/
// var section_hierarchy = [ this.subsections = [] ];
//
/** `section link anchor` in section_title_hash: had this title */
var section_title_hash = Object.create(null);
// this.section_title_hash = section_title_hash;
// to test: 沒有章節標題的文章, 以章節標題開頭的文章, 以章節標題結尾的文章, 章節標題+章節標題。
// 加入 **上一個** section, "this_section"
function add_root_section(next_section_title_index) {
// assert: _this.type === 'plain'
// section_title === parsed[section.range[0] - 1]
var this_section_title_index = all_root_section_list.length > 0 ? all_root_section_list
.at(-1).range[1]
: undefined,
// range: 本 section inner 在 root parsed 中的 index.
// parsed[range[0]] to parsed[range[1] - 1]
range = [ this_section_title_index >= 0
// +1: 這個範圍不包括 section_title。
? this_section_title_index + 1 : 0, next_section_title_index ],
//
section = _this.slice(range[0], range[1]);
if (this_section_title_index >= 0) {
// page_data.parsed[section.range[0]-1]===section.section_title
section.section_title = _this[this_section_title_index];
}
// 添加常用屬性與方法。
// TODO: using Object.defineProperties(section, {})
Object.assign(section, {
type : 'section',
// section = parsed.slice(range[0], range[1]);
// assert: parsed[range[0]] === '\n',
// is the tail '\n' of "==title== "
range : range,
each : for_each_subelement,
replace_by : replace_section_by,
toString : _this.toString
});
section[wiki_API.KEY_page_data] = _this.page;
all_root_section_list.push(section);
}
// max_section_level
var level_filter
// 要篩選的章節標題層級 e.g., {level_filter:[1,2]}
= Array.isArray(options.level_filter)
// assert: 必須皆為 {Number}
? (Math.max.apply(null, options.level_filter) | 0) || 2
// e.g., { level_filter : 3 }
: 1 <= options.level_filter && (options.level_filter | 0)
// default: level 2. 僅處理階級2的章節標題。
|| 2;
// get topics / section title / stanza title using for_each_subelement()
// 讀取每一個章節的資料: 標題,內容
// TODO: 不必然是章節,也可以有其它不同的分割方法。
// TODO: 可以讀取含入的子頁面
this.each('section_title', function(section_title_token,
// section 的 index of parsed。
section_title_index, parent_token) {
var section_title_link = section_title_token.link;
if (page_title) {
// [0]: page title
section_title_link[0] = page_title;
}
var id = section_title_link.id;
if (id in section_title_hash) {
// The index of 2nd title starts from 2
var duplicate_NO = 2, base_anchor = id;
// 有多個完全相同的 anchor 時,後面的會加上 "_2", "_3", ...。
// [[w:en:Help:Section#Section linking]]
while ((id = base_anchor + ' ' + duplicate_NO)
// 測試是否有重複的標題 duplicated section title。
in section_title_hash) {
duplicate_NO++;
}
if (!section_title_link.duplicate_NO) {
section_title_link.duplicate_NO = duplicate_NO;
// hack for [[w:en:WP:DUPSECTNAME|Duplicate section names]]
if (Array.isArray(section_title_link[1]))
section_title_link[1].push('_' + duplicate_NO);
else
section_title_link[1] += '_' + duplicate_NO;
// 用以獲得實際有效的 anchor 網頁錨點。 effect anchor
section_title_link.id = id;
// console.trace(section_title_token);
}
}
// 登記已有之 anchor。
section_title_hash[id] = null;
var level = section_title_token.level;
// console.trace([ level, level_filter, id ]);
if (parent_token === _this
// ↑ root sections only. Do not include
// {{Columns-list|\n==title==\n...}}
// level_filter: max_section_level
&& level <= level_filter) {
// console.log(section_title_token);
add_root_section(section_title_index);
} else {
// library_namespace.warn('Ignore ' + section_title_token);
// console.log([ parent_token === _this, level ]);
}
// ----------------------------------
if (false) {
// 此段已搬到 parse_section() 中。
if (section_hierarchy.length > level) {
// 去尾。
section_hierarchy.length = level;
}
section_hierarchy[level] = section_title_token;
// console.log(section_hierarchy);
while (--level >= 0) {
// 注意: level 1 的 subsections 可能包含 level 3!
var parent_section = section_hierarchy[level];
if (parent_section) {
if (parent_section.subsections) {
if (false) {
library_namespace.log(parent_section + ' → '
+ section_title_token);
}
parent_section.subsections
.push(section_title_token);
section_title_token
//
.parent_section = parent_section;
} else {
// assert: is root section list, parent_section ===
// this.subsections === section_hierarchy[0]
parent_section.push(section_title_token);
}
break;
}
}
section_title_token.subsections = [];
}
}, Object.assign({
// 不可只檢查第一層之章節標題。就算在 template 中的 section title 也會被記入 TOC。
// e.g.,
// [[w:en:Wikipedia:Vital_articles/Level/5/Everyday_life/Sports,_games_and_recreation]]
// max_depth : 1,
modify : false
},
// options.for_each_subelement_options
options));
// add the last section
add_root_section(this.length);
if (all_root_section_list[0].range[1] === 0) {
// 第一個章節為空。 e.g., 以章節標題開頭的文章。
// 警告:此時應該以是否有 section.section_title 來判斷是否為 lead_section,
// 而非以 section_index === 0 判定!
all_root_section_list.shift();
}
// ----------------------------
// 讀取每一個章節的資料: 參與討論者,討論發言的時間
// 統計各討論串中簽名的次數和發言時間。
// TODO: 無法判別先日期,再使用者名稱的情況。 e.g., [[w:zh:Special:Diff/54030530]]
if (options.get_users) {
all_root_section_list.forEach(function(section) {
// console.log(section);
// console.log('section: ' + section.toString());
// [[WP:TALK]] conversations, dialogues, discussions, messages
// section.discussions = [];
// 發言用戶名順序
section.users = [];
// 發言時間日期
section.dates = [];
for (var section_index = 0,
// list buffer
buffer = [], this_user, token;
// Only check the first level. 只檢查第一層。
// TODO: parse [[Wikipedia:削除依頼/暫定2車線]]: <div>...</div>
// check <b>[[User:|]]</b>
section_index < section.length || buffer.length > 0;) {
token = buffer.length > 0 ? buffer.shift()
: section[section_index++];
while (/* token && */token.type === 'list') {
var _buffer = [];
token.forEach(function(list_item) {
// 因為使用習慣問題,每個列表必須各別計算使用者留言次數。
_buffer.append(list_item);
});
token = _buffer.shift();
Array.prototype.unshift.apply(buffer, _buffer);
}
if (typeof token === 'string') {
// assert: {String}token
if (!token.trim() && token.includes('\n\n')) {
// 預設簽名必須與日期在同一行。不可分段。
this_user = null;
continue;
}
} else {
// assert: {Array}token
token = token.toString();
// assert: wikiprojects 計畫的簽名("~~~~~")必須要先從名稱再有日期。
// 因此等到出現日期的時候再來處理。
// 取得依照順序出現的使用者序列。
var user_list = wiki_API.parse.user.all(token, true);
if (false && section.section_title
&& section.section_title.title.includes('')) {
console.log('token: ' + token);
console.log('user_list: ' + user_list);
}
// 判別一行內有多個使用者名稱的情況。
// 當一行內有多個使用者名稱的情況,會取最後一個簽名。
if (user_list.length > 0) {
this_user = user_list.at(-1);
// ↑ 這個使用者名稱可能為 bot。
if (options.ignore_bot
&& PATTERN_BOT_NAME.test(this_user)) {
this_user = null;
}
}
// --------------------------------
if (false) {
// 以下為取得多個使用者名稱的情況下,欲判別出簽名的程式碼。由於現在僅簡單取用最後一個簽名,已經被廢棄。
if (user_list.length > 1
// assert: 前面的都只是指向機器人頁面的連結。
&& /^1+0$/.test(user_list.map(function(user) {
return PATTERN_BOT_NAME.test(user) ? 1 : 0;
}).join(''))) {
user_list = user_list.slice(-1);
}
// 因為現在有個性化簽名,需要因應之。應該包含像[[w:zh:Special:Diff/48714597]]的簽名。
if (user_list.length === 1) {
this_user = user_list[0];
} else {
// 同一個token卻沒有找到,或找到兩個以上簽名,因此沒有辦法準確判別到底哪一個才是真正的留言者。
// console.log(token);
// console.log(token.length);
// console.log(this_user);
if (user_list.length >= 2
// 若是有其他非字串的token介於名稱與日期中間,代表這個名稱可能並不是發言者,那麼就重設名稱。
// 簽名長度不應超過255位元組。
|| token.length > 255 - '[[U:n]]'.length) {
// 一行內有多個使用者名稱的情況,取最後一個?
// 例如簽名中插入自己的舊名稱或者其他人的情況
this_user = null;
}
if (!this_user) {
continue;
}
}
}
// 繼續解析日期,預防有類似 "<b>[[User:]] date</b>" 的情況。
}
if (!this_user) {
continue;
}
var date = wiki_API.parse.date(token, options);
// console.log([ this_user, date ]);
if (!date
// 預設不允許未來時間。
|| !options.allow_future && !(Date.now() - date > 0)) {
continue;
}
// 同時添加使用者與日期。
section.dates.push(date);
section.users.push(this_user);
// reset
this_user = null;
}
if (section.dates.length === 0) {
section.dates = wiki_API.parse.date(section.toString(),
// 一些通知只能取得日期,文中未指定用戶名。
Object.assign({
get_date_list : true
}, options));
section.dates.need_to_clean = true;
}
var min_timevalue, max_timevalue;
// console.trace(section.dates);
section.dates.forEach(function(date) {
if (!date || isNaN(date = +date)) {
return;
}
if (!(min_timevalue <= date))
min_timevalue = date;
else if (!(max_timevalue >= date))
max_timevalue = date;
});
if (section.dates.need_to_clean)
section.dates = [];
// console.trace([ min_timevalue, max_timevalue ]);
if (min_timevalue) {
section.dates.min_timevalue = min_timevalue;
section.dates.max_timevalue = max_timevalue
|| min_timevalue;
}
if (false) {
section.dates.max_timevalue = Math.max.apply(null,
section.dates.map(function(date) {
return date.getTime();
}));
}
if (false) {
parsed.each_section();
// scan / traversal section templates:
parsed.each.call(parsed.sections[section_index],
'template', function(token) {
;
});
}
if (false) {
// 首位發言者, 發起人 index
section.initiator_index = parsed.each_section.index_filter(
section, true, 'first');
}
// 最後發言日期 index
var last_update_index = for_each_section.index_filter(section,
true, 'last');
// section.users[section.last_update_index] = {String}最後更新發言者
// section.dates[section.last_update_index] = {Date}最後更新日期
if (last_update_index >= 0) {
section.last_update_index = last_update_index;
}
// 回應數量
section.replies
// 要先有不同的人發言,才能算作有回應。
= section.users.unique().length >= 2 ? section.users.length - 1
: 0;
// console.log('users: ' + section.users);
// console.log('replies: ' + section.replies);
});
}
// console.trace(for_section);
if (typeof for_section !== 'function') {
return this;
}
level_filter
// 要篩選的章節標題層級 e.g., {level_filter:[1,2]}
= Array.isArray(options.level_filter) ? options.level_filter
// e.g., { level_filter : 3 }
: 1 <= options.level_filter && (options.level_filter | 0)
// default: level 2. 僅處理階級2的章節標題。
|| 2;
var section_filter = function(section) {
var section_title = section.section_title;
if (!section_title)
return true;
if (Array.isArray(level_filter))
return level_filter.includes(section_title.level);
return level_filter === section_title.level;
};
// TODO: return (result === for_each_subelement.remove_token)
// TODO: move section to another page
if (!library_namespace.is_async_function(for_section)
|| all_root_section_list.length === 0) {
// for_section(section, section_index)
all_root_section_list.some(function(section) {
// return parsed.each.exit;
return section_filter(section) && (for_each_subelement.exit
// exit if the result calls exit
=== for_section.apply(this, arguments));
}, this);
return this;
}
// console.log(all_root_section_list);
if (options.allow_parallel_processing) {
// Promise.allSettled() 不會 throw。
return Promise.all(all_root_section_list.map(function(section,
section_index) {
return section_filter(section)
&& for_section.apply(this, arguments);
}, this));
// @deprecated
all_root_section_list.forEach(function(section, section_index) {
if (false) {
console.log('Process: ' + section.section_title
// section_title.toString(true): get inner
&& section.section_title.toString(true));
}
if (!section_filter(section))
return;
return eval('(async function() {'
//
+ ' try { return await for_section(section, section_index); }'
+ ' catch(e) { library_namespace.error(e); }'
+ ' })();');
});
}
// 預設為依序 resolve。
var promise;
all_root_section_list.forEach(function(section, section_index) {
if (!section_filter(section))
return;
if (!promise) {
promise = for_section.apply(_this, arguments);
return;
}
var _arguments = arguments;
promise = promise.then(function() {
return for_section.apply(_this, _arguments);
});
});
return promise;
}
function replace_section_by(wikitext, options) {
options = library_namespace.setup_options(options);
var parsed = this[wiki_API.KEY_page_data].parsed;
// assert: parsed[range[0]] === '\n',
// is the tail '\n' of "==title== "
var index = this.range[0];
if (typeof wikitext === 'string')
wikitext = wikitext.trim();
if (options.preserve_section_title === undefined
// 未設定 options.preserve_section_title,則預設若有 wikitext,則保留 section title。
? !wikitext : !options.preserve_section_title) {
// - 1: point to section_title
index--;
}
if (wikitext) {
parsed[index] += wikitext + '\n\n';
} else {
parsed[index] = '';
}
while (++index < this.range[1]) {
// 清空到本章節末尾。
parsed[index] = '';
}
}
// var section_index_filter =
// CeL.wiki.parser.parser_prototype.each_section.index_filter;
for_each_section.index_filter = function filter_users_of_section(section,
filter, type) {
// filter: user_name_filter
var _filter;
if (typeof filter === 'function') {
_filter = filter;
} else if (Array.isArray(filter)) {
_filter = function(user_name) {
// TODO: filter.some()
return filter.includes(user_name);
};
} else if (library_namespace.is_Object(filter)) {
_filter = function(user_name) {
return user_name in filter;
};
} else if (library_namespace.is_RegExp(filter)) {
_filter = function(user_name) {
return filter.test(user_name);
};
} else if (typeof filter === 'string') {
_filter = function(user_name) {
return filter === user_name;
};
} else if (filter === true) {
_filter = function() {
return true;
};
} else {
throw 'for_each_section.index_filter: Invalid filter: ' + filter;
}
// ----------------------------
if (!type) {
var user_and_da