UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,594 lines (1,445 loc) 144 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): basic 工具函數, namespace, * site configuration * * @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。 * * TODO:<code> </code> * * @since 2020/5/24 6:21:13 拆分自 CeL.application.net.wiki */ // 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.namespace', require : 'data.native.' // for library_namespace.get_URL // + '|application.net.Ajax.' // for library_namespace.URI() + '|application.net.' // CeL.DOM.HTML_to_Unicode(), CeL.DOM.Unicode_to_HTML() + '|interact.DOM.' // setup module namespace + '|application.net.wiki.', // 設定不匯出的子函式。 no_extend : '*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { // requiring var wiki_API = library_namespace.application.net.wiki; var gettext = library_namespace.cache_gettext(function(_) { gettext = _; }); // -------------------------------------------------------------------------------------------- wiki_API.general_parameters = { format : 'json', // https://www.mediawiki.org/w/api.php?action=help&modules=json // 加上 "&utf8", "&utf8=1" 可能會導致把某些 link 中 URL 編碼也給 unescape 的情況! utf8 : 1 }; // 設定可被匯入 general_parameters 的屬性。 wiki_API.general_parameters_normalizer = { // for cross-domain AJAX request (CORS) origin : function(value) { if (value === true) value = '*'; return value; }, format : 'string', utf8 : 'boolean|number|string' }; // CeL.wiki.KEY_SESSION /** {String}KEY_wiki_session old key: 'wiki' */ var KEY_SESSION = 'session', KEY_HOST_SESSION = 'host'; // @inner TODO: MUST re-design function get_wikimedia_project_name(session) { return wiki_API.is_wiki_API(session) // e.g., commons, wikidata && session.family === 'wikimedia' // https://meta.wikimedia.org/wiki/Special:SiteMatrix // TODO: using session.project_name or something others // using get_first_domain_name_of_session() && session.language; } // https://meta.wikimedia.org/wiki/Help:Page_name#Special_characters // @see $wgLegalTitleChars var PATTERN_invalid_page_name_characters = /[{}\[\]\|<>\t\n#�]/, // https://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(technical_restrictions)#Forbidden_characters PATTERN_page_name = /((?:&#(?:\d{1,8}|x[\da-fA-F]{1,8});|[^{}\[\]\|<>\t\n#�])+)/, /** * {RegExp}wikilink內部連結的匹配模式v2 [ all_link, page_and_anchor, page_name, * anchor / section_title, pipe_separator, displayed_text ] * * 頁面標題不可包含無效的字元:PATTERN_invalid_page_name_characters,<br /> * 經測試 anchor 亦不可包含[\n\[\]{}],但 display text 表達文字可以包含 [\n] * * @see PATTERN_link */ PATTERN_wikilink = /\[\[(((?:&#(?:\d{1,8}|x[\da-fA-F]{1,8});|[^{}\[\]\|<>\t\n#�])+)(#(?:-{[^\[\]{}\t\n\|]+}-|[^\[\]{}\t\n\|]+)?)?|#[^\[\]{}\t\n\|]*)(?:(\||{{\s*!\s*}})([\s\S]+?))?\]\]/, // PATTERN_wikilink_global = new RegExp(PATTERN_wikilink.source, 'g'); var /** * 匹配URL網址。 * * [http://...]<br /> * {{|url=http://...}} * * matched: [ URL ] * * TODO: "https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/", * "{{t0|urn:0{{t1|urn:1}}}}" * * @deprecated Use `PATTERN_URL_WITH_PROTOCOL_GLOBAL` instead. * * @see https://github.com/5j9/wikitextparser/blob/master/tests/wikitext/test_external_links.py * https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers * * @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 */ // PATTERN_URL_GLOBAL = /(?:https?:)?\/\/(?:[^\s\|<>\[\]{}]+|{[^{}]*})+/ig, /** * 匹配URL網址,僅用於 parse_wikitext()。 * * matched: [ all, previous, URL, protocol with "://", URL_others ] * * @type {RegExp} * * @see https://en.wikipedia.org/wiki/Help:URL#Fixing_links_with_unsupported_characters * https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers * * @see PATTERN_URL_GLOBAL, PATTERN_URL_WITH_PROTOCOL_GLOBAL, * PATTERN_URL_prefix, PATTERN_WIKI_URL, PATTERN_wiki_project_URL, * PATTERN_external_link_global */ PATTERN_URL_WITH_PROTOCOL_GLOBAL // 警告: PATTERN_external_link_global 會用到 '):)' = /(^|[^a-z\d_])(((?:https?|ssh|telnet|ftps?|sftp|gopher|ircs?|news|nntp|worldwind|svn|git|mms):?\/\/|(?:mailto|urn):)((?:\[[a-f\d:]+\]|[^\s\|<>\[\]\/])[^\s\|<>\[\]]*))/ig; /** * 匹配以URL網址起始。 * * matched: [ prefix ] * * @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 */ var PATTERN_URL_prefix = new RegExp(PATTERN_URL_WITH_PROTOCOL_GLOBAL.source .replace(/^\([^()]+\)/, '(^)'), 'i'); // 嘗試從 options 取得 API_URL。 function API_URL_of_options(options) { // library_namespace.debug('options:', 0, 'API_URL_of_options'); // console.log(options); var session = session_of_options(options); if (session) { return session.API_URL; } } /** * 測試看看指定值是否為API語言以及頁面標題或者頁面。 * * @param value * value to test. 要測試的值。 * @param {Boolean|String}[type] * test type: true('simple'), 'language', 'URL' * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns {Boolean}value 為 [ {String}API_URL/language, {String}title or * {Object}page_data ] */ function is_api_and_title(value, type, options) { // console.trace(value); if (!Array.isArray(value) || value.length !== 2 // || get_page_content.is_page_data(value[0])) { // 若有必要設定,應使用 wiki_API.normalize_title_parameter(title, options)。 // 此時不能改變傳入之 value 本身,亦不能僅測試是否有 API_URL。 return false; } var API_URL = value[0]; /** {Boolean|String} ignore API test, 'set': set API */ var ignore_API_test; if (typeof options === 'object') { ignore_API_test = options.ignore_API_test; } else { ignore_API_test = options; options = null; } if (type === true) { // type === true: simple test, do not test more. return !API_URL || typeof API_URL === 'string'; } var title = value[1]; // test title: {String}title or {Object}page_data or {Array}titles if (!title || typeof title !== 'string' // value[1] 為 titles (page list)。 && !Array.isArray(title) // 為了預防輸入的是問題頁面。 && !get_page_content.is_page_data(title) // 處理 is_id。 && (!(title > 0) // 注意:這情況下即使是{Natural}page_id 也會pass! || !options || !options.is_id)) { library_namespace.debug('輸入的是問題頁面 title: ' + title, 2, 'is_api_and_title'); return false; } // test API_URL: {String}API_URL/language if (!API_URL) { if (options) { library_namespace.debug('嘗試從 options[KEY_SESSION] 取得 API_URL。', 2, 'is_api_and_title'); // console.log(options); // console.log(API_URL_of_options(options)); API_URL = API_URL_of_options(options); if (API_URL) { value[0] = API_URL; } // 接下來繼續檢查 API_URL。 } else { return !!ignore_API_test; } } if (typeof API_URL !== 'string') { // 若是未設定 action[0],則將在 wiki_API.query() 補設定。 // 因此若為 undefined || null,此處先不回傳錯誤。 return !API_URL; } // for property = [ {String}language, {String}title or {Array}titles ] if (type === 'language') { var metched = PATTERN_PROJECT_CODE_i.test(API_URL); if (options && options.multi === false) { // 明確指定 `title` 為單一標題,{Array} 只能解釋為 [ language, title ]。 if (!metched) { library_namespace.warn('is_api_and_title: 強制將 "' + API_URL + '" 視為語言代碼 [' + value + ']'); } return true; } return metched; } // 處理 [ {String}API_URL/language, {String}title or {Object}page_data ] var metched = PATTERN_URL_prefix.test(API_URL); if (type === 'URL') { return metched; } // for key = [ {String}language, {String}title or {Array}titles ] // for id = [ {String}language/site, {String}title ] return metched || PATTERN_PROJECT_CODE_i.test(API_URL); } /** * 規範化 title_parameter * * setup [ {String}API_URL, title ] * * @param {String|Array}title * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns {Array}action = [ {String}API_URL, {Search_parameters}parameters ] * * @see api_URL */ function normalize_title_parameter(title, options) { options = library_namespace.setup_options(options); var session = wiki_API.session_of_options(options); if (false && library_namespace.is_Set(title)) { title = Array.from(title); } var action = options.multi && Array.isArray(title) && title.length === 2 // 即便設定 options.multi,也不該有 /^https?:\/\/.+\.php/i 的標題。 && !/^https?:\/\/.+\.php$/.test(title[0]) || !is_api_and_title(title, true) ? [ session && session.API_URL || undefined, title ] // title.clone(): 不改變原 title。 : Array.isArray(title) ? title.clone() : []; if (false && library_namespace.is_Set(action[1])) { action[1] = Array.from(action[1]); } if (options.slice_size >= 1) { // console.trace(action); if (Array.isArray(action[1])) { if (action[1].length > options.slice_size) { var titles_left = action[1].splice(options.slice_size, action[1].length); if (Array.isArray(options.titles_left)) { Array.prototype.unshift.apply(options.titles_left, titles_left); } else { if (options.titles_left) { throw new Error( // Warning: 'normalize_title_parameter: Invalid usage: options.titles_left is not {Array}!'); titles_left.push(options.titles_left); } options.titles_left = titles_left; library_namespace .warn('normalize_title_parameter: 將 title list 切分成 slice: ' + action[1].length + ' + ' + options.titles_left.length + '。'); } } } else if (!action[1] && Array.isArray(options.titles_left)) { action[1] = options.titles_left.splice(0, options.slice_size); library_namespace .log('normalize_title_parameter: 接續取得 title list slice: ' + action[1].length + ' + ' + options.titles_left.length + '。'); } } if (Array.isArray(options.titles_left) && options.titles_left.length === 0) { delete options.titles_left; } // console.trace([ title, action ]); if (!is_api_and_title(action, false, options)) { // console.trace('normalize_title_parameter: Invalid title!'); library_namespace.warn([ 'normalize_title_parameter: ', { // gettext_config:{"id":"invalid-title-$1"} T : [ 'Invalid title: %1', // wiki_API.title_link_of(title) // || '(title: ' + JSON.stringify(title) + ')' ] } ]); // console.trace(JSON.stringify(title)); return; } // 處理 [ {String}API_URL, title ] action[1] = wiki_API.query.title_param(action[1], // 'multi_param' in options ? options.multi_param // 'multi' in options : options.multi !== undefined ? options.multi : true, options.is_id); if (options.redirects) { // 舊版毋須 '&redirects=1','&redirects' 即可。 action[1].redirects = 1; } // console.trace(action); return action; } /** * set / append additional parameters of MediaWiki API. * * @param {Array}action * @param {Object}options * 附加參數/設定選擇性/特殊功能與選項 * @inner */ function set_parameters(action, options) { if (!options.parameters) { return; } // Should use // `action = wiki_API.extract_parameters(options, action, true);` if (typeof action[1] === 'string' && !/^[a-z]+=/.test(action[1])) { library_namespace .warn('set_parameters: Did not set action! Will auto add "action=".'); console.trace(action); action[1] = 'action=' + action[1]; } // action[1] = // wiki_API.extract_parameters(options.parameters, action[1], true); action[1] = library_namespace.Search_parameters(action[1]); action[1].set_parameters(options.parameters); } // -------------------------------------------------------------------------------------------- // 工具函數。 // https://phabricator.wikimedia.org/rOPUP558bcc29adc3dd7dfebbc66c1bf88a54a8b09535#3ce6dc61 // server: // (wikipedia|wikibooks|wikinews|wikiquote|wikisource|wikiversity|wikivoyage|wikidata|wikimediafoundation|wiktionary|mediawiki) // e.g., [[s:]], [[zh-classical:]], [[zh-min-nan:]], [[test2:]], // [[metawikipedia:]], [[betawikiversity:]] // @see [[m:Help:Interwiki linking#Project titles and shortcuts]], // [[:zh:Help:跨语言链接#出現在正文中的連結]] // https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities // 警告: 應配合 get_namespace.pattern 排除 'Talk', 'User', 'Help', 'File', ... var PATTERN_PROJECT_CODE = /^[a-z][a-z\d\-]{0,14}$/, // 須亦能匹配 site key: // https://www.wikidata.org/w/api.php?action=help&modules=wbgetentities PATTERN_PROJECT_CODE_i = new RegExp(PATTERN_PROJECT_CODE.source, 'i'); // 可用來拆分 language, family。以防 incase wikt, wikisource // testwikidatawiki → [,testwikidata,wiki] // https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities // e.g., 'zh_min_nanwikibooks' // MariaDB [zhwiki_p]> SHOW DATABASES; // e.g., "wikimania2018wiki_p" // 2020/5 left: // ["centralauth_p","heartbeat_p","information_schema","information_schema_p","meta_p"] // [ all, language code, family ] var PATTERN_SITE = /^([a-z\d\_]{2,13})(wiki|wikibooks|wiktionary|wikiquote|wikisource|wikinews|wikiversity|wikivoyage|wikimedia)$/; /** * Wikimedia projects 的 URL match pattern 匹配模式。 * * matched: [ 0: protocol + host name, 1: protocol, 2: host name,<br /> * 3: 第一 domain name (e.g., language code / project),<br /> * 4: 第二 domain name (e.g., family: 'wikipedia') ] * * @deprecated using wiki_API.hostname_of_API_URL() or wiki_API.site_name() * * @type {RegExp} * * @see PATTERN_PROJECT_CODE * @see PATTERN_URL_GLOBAL, PATTERN_URL_WITH_PROTOCOL_GLOBAL, * PATTERN_URL_prefix, PATTERN_WIKI_URL, PATTERN_wiki_project_URL, * PATTERN_external_link_global */ var PATTERN_wiki_project_URL = /^(https?:)?(?:\/\/)?(([a-z][a-z\d\-]{0,14})\.([a-z]+)+(?:\.[a-z]+)+)/i; // @see wiki_API.api_URL() function hostname_of_API_URL(API_URL) { if (!/\/api\.php$/.test(API_URL)) return; var url = new library_namespace.URI(API_URL); return url && url.hostname; } /** * Get the API URL of specified project. * * project = language_code.family * * @param {String}project * wiki project, domain or language. 指定維基百科語言/姊妹計劃<br /> * e.g., 'en', 'en.wikisource'. * * @returns {String}API URL * * @see https://en.wikipedia.org/wiki/Wikipedia:Wikimedia_sister_projects * TODO: * https://zh.wikipedia.org/wiki/Wikipedia:%E5%A7%8A%E5%A6%B9%E8%AE%A1%E5%88%92#.E9.93.BE.E6.8E.A5.E5.9E.8B */ function api_URL(project, options) { if (!project) { var session = wiki_API.session_of_options(options); return session && session.API_URL || wiki_API.API_URL; } project = String(project); var lower_case = project.toLowerCase(); if (lower_case in api_URL.alias) { project = api_URL.alias[lower_case]; } library_namespace.debug('project: ' + project, 3, 'api_URL'); // PATTERN_PROJECT_CODE_i.test(undefined) === true if (PATTERN_PROJECT_CODE_i.test(project)) { if (lower_case in api_URL.wikimedia) { project += '.wikimedia'; } else if (lower_case in api_URL.family) { // (wiki_API.language || 'www') + '.' + project project = wiki_API.language + '.' + project; } else if (/wik[it]/i.test(project)) { // e.g., 'mediawiki' → 'www.mediawiki' // e.g., 'wikidata' → 'www.wikidata' project = 'www.' + project; } else { // e.g., 'en' → 'en.wikipedia' ({{SERVERNAME}}) // e.g., 'zh-yue' → 'zh-yue.wikipedia', 'zh-classical' // e.g., 'test2' → 'test2.wikipedia' ({{SERVERNAME}}) project += '.wikipedia'; } } // @see PATTERN_PROJECT_CODE if (/^[a-z][a-z\d\-]{0,14}\.[a-z]+$/i.test(project)) { // e.g., 'en.wikisource', 'en.wiktionary' project += '.org'; } // console.trace([ wiki_API.API_URL, project ]); var url = new library_namespace.URI(project, // /:\/\//.test(wiki_API.API_URL) && wiki_API.API_URL); if (url && url.hostname) { // 先測試是否為自訂 API。 return /\/api\.php$/.test(project) ? project // e.g., '//zh.wikipedia.org/' // e.g., 'https://www.mediawiki.org/w/api.php' // e.g., 'https://www.mediawiki.org/wiki/' : (url.protocol || api_URL.default_protocol || 'https:') + '//' + url.hostname + '/w/api.php'; } library_namespace.error('api_URL: Unknown project: [' + project + ']! Using default API URL.'); return wiki_API.API_URL; } // the key MUST in lower case! // @see https://www.wikimedia.org/ // @see [[Special:Interwiki]] 跨維基資料 跨 wiki 字首 api_URL.wikimedia = { meta : true, commons : true, species : true, incubator : true, // mul : true, phabricator : true, wikitech : true, // https://quarry.wmflabs.org/ quarry : true, releases : true } // shortcut, namespace aliases. // the key MUST in lower case! // @see [[m:Help:Interwiki linking#Project titles and shortcuts]], // [[mw:Manual:InitialiseSettings.php]] // https://noc.wikimedia.org/conf/highlight.php?file=InitialiseSettings.php // [[:zh:Help:跨语言链接#出現在正文中的連結]] // @see [[Special:Interwiki]] 跨維基資料 跨 wiki 字首 // @see two-letter project code shortcuts // [[m:Requests_for_comment/Set_short_project_namespace_aliases_by_default_globally]] api_URL.alias = { // project with language prefix // project: language.*.org w : 'wikipedia', n : 'wikinews', // 維基教科書 b : 'wikibooks', q : 'wikiquote', s : 'wikisource', // 維基學院 v : 'wikiversity', voy : 'wikivoyage', wikt : 'wiktionary', // project: *.wikimedia.org m : 'meta', // 這一項會自動判別語言。 metawikipedia : 'meta', c : 'commons', wikispecies : 'species', phab : 'phabricator', download : 'releases', // project: www.*.org d : 'wikidata', mw : 'mediawiki', wmf : 'wikimedia', betawikiversity : 'beta.wikiversity' }; // families must with language prefix // the key MUST in lower case! api_URL.family = 'wikipedia|wikibooks|wikinews|wikiquote|wikisource|wikiversity|wikivoyage|wiktionary' .split('|').to_hash(); // api_URL.shortcut_of_project[project] = alias api_URL.shortcut_of_project = Object.create(null); Object.keys(api_URL.alias).forEach(function(shortcut) { api_URL.shortcut_of_project[api_URL.alias[shortcut]] = shortcut; }); /** * setup API URL. * * @param {wiki_API}session * 正作業中之 wiki_API instance。 * @param {String}[API_URL] * language code or API URL of MediaWiki project * * @inner */ function setup_API_URL(session, API_URL) { library_namespace.debug('API_URL: ' + API_URL + ', default language: ' + wiki_API.language, 3, 'setup_API_URL'); // console.log(session); // console.trace(wiki_API.language); if (API_URL === true) { // force to login. API_URL = session.API_URL || wiki_API.API_URL; } if (API_URL && typeof API_URL === 'string' // && wiki_API.is_wiki_API(session) ) { session.API_URL = api_URL(API_URL); // remove cache delete session.last_page; delete session[KEY_HOST_SESSION]; // force to login again: see wiki_API.login // 據測試,不同 projects 間之 token 不能通用。 delete session.token.csrftoken; delete session.token.lgtoken; // library_namespace.set_debug(6); if (library_namespace.platform.nodejs) { // 初始化 agent。 // create and keep a new agent. 維持一個獨立的 agent。 // 以不同 agent 應對不同 host。 var agent = library_namespace.application.net // .Ajax.setup_node_net(session.API_URL); session.get_URL_options = { // start_time : Date.now(), // API_URL : session.API_URL, agent : agent, headers : Object.create(null) }; if (false) { // set User-Agent to use: // Special:ApiFeatureUsage&wpagent=CeJS script_name wiki.get_URL_options.headers['User-Agent'] = library_namespace.get_URL.default_user_agent; } } else { // e.g., 老舊版本 or using XMLHttpRequest @ WWW session.get_URL_options = {}; } } // TODO: 這只是簡陋的判別方法。 var matched = wiki_API.site_name(session, { get_all_properties : true }); // console.trace(matched); if (matched && (matched.family in api_URL.family)) { // e.g., "wikipedia" session.family = matched.family; } } // @see set_default_language(), language_to_site_name() function setup_API_language(session, language_code) { if (!language_code || typeof language_code !== 'string') return; language_code = language_code.toLowerCase(); var site_name = wiki_API.site_name(language_code, add_session_to_options(session, { get_all_properties : true })); if (site_name && site_name.language && site_name.language !== 'multilingual') { // e.g., API_URL=zh.wiktionary language_code = site_name.language; } if (PATTERN_PROJECT_CODE_i.test(language_code) // 不包括 test2.wikipedia.org 之類。 && !/^test|wik[it]/i.test(language_code) // 排除 'Talk', 'User', 'Help', 'File', ... && !(session.configurations // ↑ session === wiki_API? && session.configurations.namespace_pattern || get_namespace.pattern) .test(language_code)) { if (language_code === 'simple') { session.first_damain_name = language_code; // [[w:en:Basic English]] // language_code = 'en-basiceng'; language_code = 'en'; } else if (language_code in wiki_API.language_code_to_site_alias) { // e.g., 'cmn' language_code = wiki_API.language_code_to_site_alias[language_code]; } // [[m:List of Wikipedias]] session.language // e.g., 'zh-classical', 'zh-yue', 'zh-min-nan' = language_code; site_name = wiki_API.site_name(session, { get_all_properties : true }); // console.trace([ language_code, site_name ]); if (site_name.language === 'multilingual' // e.g., language_code === 'commons' && language_code === site_name.project) { // default: English session.language = 'en'; } site_name = site_name.site; var time_interval_config = wiki_API.query.edit_time_interval; // apply local lag interval rule. if (!(session.edit_time_interval >= 0) && ((site_name in time_interval_config) || (language_code in time_interval_config))) { session.edit_time_interval = time_interval_config[site_name] || time_interval_config[language_code]; library_namespace.debug('Use interval ' + session.edit_time_interval + ' for language ' + language_code, 1, 'setup_API_language'); } } } // -------------------------------------------------------------------------------------------- // @inner function get_first_domain_name_of_session(session) { var first_damain_name; if (session) { first_damain_name = // e.g., 'simple' session.first_damain_name // assert: typeof session.API_URL === 'string' // 注意:在取得 page 後,中途更改過 API_URL 的話,session.language 會取得錯誤的資訊! || session.language // 應該採用來自宿主 host session 的 language. @see setup_data_session() || get_first_domain_name_of_session(session[KEY_HOST_SESSION]); } return first_damain_name; } // [[en:Help:Interwikimedia_links]] [[Special:Interwiki]] // https://zh.wikipedia.org/wiki/Special:GoToInterwiki/testwiki: // TODO: link prefix: e.g., 'zh:n:' for zh.wikinews // [[:phab:T102533]] // [[:gerrit:gitweb?p=mediawiki/core.git;a=blob;f=RELEASE-NOTES-1.23]] /** * language code → Wikidata site code / Wikidata site name / Wikimedia * project name. get_project()<br /> * 將語言代碼轉為 Wikidata API 可使用之 site name。 [[yue:]] → zh-yue → zh_yuewiki。 亦可自 * options 取得 wikidata API 所須之 site parameter。 * * @example<code> // e.g., 'enwiki', 'zhwiki', 'enwikinews' CeL.wiki.site_name(wiki) </code> * * @param {String|wiki_API}language * 語言代碼。 language / family / project code / session. default * language: wiki_API.language e.g., 'en', 'zh-classical', 'ja', * ... * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項<br /> * options.get_all_properties: return * {language,family,site,API_URL} * * @returns {String}wiki_project, Wikidata API 可使用之 site name parameter。 * * @see mediaWiki.config.get('wgWikiID') * https://www.mediawiki.org/wiki/ResourceLoader/Core_modules#mediaWiki.config * @see set_default_language() * @see [[:en:Help:Interwiki linking#Project titles and shortcuts]], * [[:zh:Help:跨语言链接#出現在正文中的連結]] * @see [[meta:List of Wikimedia projects by size]] * @see [[m:List of Wikipedias]] IETF language tag language code for * gettext() * @see https://www.wikidata.org/w/api.php?action=help&modules=wbgetentities * * @since 2017/9/4 20:57:8 整合原先的 language_to_project(), * language_to_site_name() */ function language_to_site_name(language, options) { // console.trace(language); var session; // 擷取出維基姊妹項目各種 type: 先要能擷取出 language code + family // types: 'API', 'db', 'site', 'link', 'dump', ... // 不能保證 wiki_API.is_wiki_API(language) → is_Object(language), // 因此使用 typeof。 if (language && (typeof language === 'object' // || language === wiki_API )) { // treat language as options with session. session = wiki_API.session_of_options(language); // options.language 較 session 的設定優先。 // language.language language = get_first_domain_name_of_session(language) // wikidata 沒有 session.language,會用 // session[KEY_HOST_SESSION].language。 || get_first_domain_name_of_session(session) // || language ; if (false && typeof language === 'object') console.trace(language); } else if (typeof language === 'function') { throw new Error('Invalid type of language: ' + typeof language); } // console.log(session); // console.trace(session.family); /** * Wikimedia project / family. e.g., wikipedia, wikinews, wiktionary. * assert: family && /^wik[it][a-z]{0,9}$/.test(family) * * @type {String} */ var family; /** {wiki_API}in what wiki session */ var in_session; if (typeof options === 'string') { // shift arguments family = options; options = null; } else { in_session = wiki_API.session_of_options(options); family = options && options.family; } // console.trace(language); var page_name; var matched = typeof language === 'string' && !language.includes('://') && language.match(/^[:\s]*(\w+):(?:(\w+):)?(.*)/); if (matched) { matched.family = api_URL.alias[matched[1]]; page_name = matched[3]; if (matched.family) { if (matched[2]) { // e.g., "n:zh:title" language = matched[2]; } else { // e.g., "n:", "n:zh", "n:title" language = matched[3]; } } else if (matched.family = api_URL.alias[matched[2]]) { // e.g., "zh:n:title" language = matched[1]; } else { // e.g., "zh:title" language = matched[1]; } family = family || matched.family; } // console.trace(language); matched = wiki_API.namespace(language, options); // console.trace([ matched, language ]); if (matched && !isNaN(matched) // && (matched !== wiki_API.namespace.hash.project // e.g., 'wikidata' || language.trim().toLowerCase() === 'project')) { // e.g., input "language" of [[Category:title]] // 光是只有 "Category",代表還是在本 wiki 中,不算外語言。 language = null; } // console.trace(language); // console.trace(in_session); // 正規化。 language = String(language // || in_session && in_session.language || get_first_domain_name_of_session(in_session) // else use default language // 警告: 若是沒有輸入,則會直接回傳預設的語言。因此您或許需要先檢測是不是設定了 language。 || wiki_API.language).trim().toLowerCase(); // zh_yue → zh-yue language = language.replace(/[_ ]/g, '-'); // console.trace(language); var API_URL; var interwiki_pattern = in_session && in_session.configurations && in_session.configurations.interwiki_pattern; var interwikimap = library_namespace.is_RegExp(interwiki_pattern) && in_session.latest_site_configurations && in_session.latest_site_configurations.interwikimap; // console.trace([ interwiki_pattern, interwikimap ]); if (Array.isArray(interwikimap)) { matched = language.match(interwiki_pattern); if (matched && interwikimap.some(function(map) { if (map.prefix === matched[1]) { // console.log(map); // API_URL = map.url; return matched = map // .url.replace(/\/wiki\/\$1/, '/w/api.php') // .replace(/\$1/, ''); } })) { language = matched; } } else if (language in language_code_to_site_alias) { // e.g., 'lzh' → 'zh-classical' language = language_code_to_site_alias[language]; } else if (!family && session && !session.family && !session[KEY_HOST_SESSION] && session.API_URL) { // e.g., API_URL: 'https://zh.moegirl.org.cn/api.php' // console.trace([ language, family ]); language = session.API_URL; } var site, project, // 是為猜測的語言。 is_guessing_language; matched = language // e.g., 'zh-min-nan' → 'zh_min_nan' .replace(/-/g, '_') // 'zhwikinews' → zh.wikinews .match(PATTERN_SITE); if (matched) { language = matched[1]; family = family || matched[2]; } else if (matched = language.match(/^[a-z\d\-]{2,13}$/)) { // e.g., 'zh-classical', 'zh-min-nan' language = matched[0]; if (language === 'wikidata') { family = language; language = 'www'; } // console.trace([ language, family ]); } else if (matched = wiki_API.hostname_of_API_URL(language)) { // treat language as API_URL. API_URL = language; // console.trace(matched); // console.trace(session); library_namespace.debug(language, 4, 'language_to_site_name'); if (library_namespace.is_IP(matched)) { // We cannot get information from IP. matched = [ matched.replace(/\./g, '_') ]; } else { matched = matched.split('.'); if (matched.length === 2) { // e.g., "lingualibre.org" matched.unshift(''); } } /** * 去掉 '.org' 之類。 language-code.wikipedia.org e.g., * zh-classical.wikipedia.org */ family = family || matched[1]; // incase 'https://test.wikidata.org/w/api.php' language = !/^test|wik[it]/i.test(matched[0]) && matched[0]; if (!language) { is_guessing_language = true; language = wiki_API.language; } } else if (matched = language.match(/^([a-z\d\-_]+)\.([a-z\d\-_]+)/)) { language = matched[1]; family = family || matched[2]; } else { library_namespace.error('language_to_site_name: Invalid language: ' + language); if (false) { console.trace([ language, wiki_API.hostname_of_API_URL(language), session, in_session ]); } } // console.trace(family); family = family || session && session.family || in_session && in_session.family; // console.trace(family); if (!family || family === 'wiki') family = 'wikipedia'; if (false) { console.trace([ API_URL, session && session.API_URL, language, family ]); } API_URL = API_URL || session && session.API_URL || api_URL(language + '.' + family); // console.trace(API_URL); if (family === 'wikidata') { // wikidatawiki_p site = family + 'wiki'; } else if (family === 'wikimedia' && language === 'en' // || (site = API_URL.match(/\/\/([\w]+)\./)) // e.g., API_URL === 'https://test.wikipedia.org/w/api.php' && /^test/i.test(site = site[1])) { // e.g., @ console @ https://commons.wikimedia.org/ project = site; // assert: (project in wiki_API.api_URL.wikimedia) // 'commonswiki' site += 'wiki'; } else { site = is_guessing_language ? '' : language.toLowerCase().replace( /-/g, '_'); // e.g., 'zh' + 'wikinews' → 'zhwikinews' site += (family === 'wikipedia' // using "commonswiki" instead of "commonswikimedia" || (language in wiki_API.api_URL.wikimedia) ? 'wiki' : family); } // console.trace(site); library_namespace.debug(site, 3, 'language_to_site_name'); project = project || language === 'www' ? family : language in wiki_API.api_URL.wikimedia ? language : null; if (project) { // e.g., get from API_URL // wikidata, commons: multilingual language = 'multilingual'; } else { project = is_guessing_language ? family : language + '.' + family; } if (is_guessing_language && session && session.language) { language = session.language; is_guessing_language = false; } // throw site; if (options && options.get_all_properties) { var family_prefix = wiki_API.api_URL.shortcut_of_project[family]; // for API_URL==="https://lingualibre.org/api.php", // is_guessing_language=true && family_prefix===undefined site = { // en, zh language : language, is_guessing_language : is_guessing_language, // family: 'wikipedia' (default), 'wikimedia', // wikibooks|wiktionary|wikiquote|wikisource|wikinews|wikiversity|wikivoyage family : family, family_prefix : family_prefix, // interwikimap prefix. 在像是 https://lingualibre.org/ 的情況下不設定 interwiki_prefix : (is_guessing_language ? undefined : (family_prefix || family) + ':' + language + ':'), // Wikimedia project name: wikidata, commons, zh.wikipedia project : project, // wikidata API 所須之 site name parameter。 wikiID // site_namewiki for Wikidata API. e.g., zh-classical → // zh_classicalwiki // for database: e.g., zh-classical → zh_classicalwiki_p // e.g., 'zhwiki'. `.wikiid` @ siteinfo // Also for dump: e.g., 'zhwikinews' // https://dumps.wikimedia.org/backup-index.html // @see wikidatawiki_p.wb_items_per_site.ips_site_id // wikidatawiki, commonswiki, zhwiki site : site, // API URL (default): e.g., // https://en.wikipedia.org/w/api.php // https://www.wikidata.org/w/api.php API_URL : API_URL }; if (session // session === wiki_API? && session.configurations // e.g., for Fandom sites && session.configurations.sitename) { site.sitename = session.configurations.sitename; } if (session && session.site_name) { site.site_name = session.site_name; } var project = session && session.latest_site_configurations && session.latest_site_configurations.general.wikiid; if (project) { site.wikiid = project; } if (page_name) { site.page_name = page_name; } } // assert: {String}site return session && session.site_name || site; } // -------------------------------------------------------------------------------------------- /** * get NO of namespace * * 注意: [[d:Q1]], [[en:T]] 之 namespace 亦為 0。須採 session.is_article() 測試。 * * @param {String|Integer}namespace * namespace or page title * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns {Integer|String|Undefined}namespace NO. */ function get_namespace(namespace, options) { options = library_namespace.setup_options(options); var is_page_title = options.is_page_title; if (wiki_API.is_page_data(namespace)) { namespace = namespace.title; is_page_title = true; } if (!is_page_title && (namespace == Math.floor(namespace))) { if (options.get_name) { // e.g., `wiki.namespace(NS_Category, {get_name: true});` namespace = String(namespace); } else { // {Integer}namespace return namespace; } } var session = session_of_options(options); var namespace_hash = options.namespace_hash || session // session === wiki_API? && session.configurations && session.configurations.namespace_hash || get_namespace.hash; if (Array.isArray(namespace)) { namespace = namespace.join('|'); } // console.log(namespace); if (typeof namespace === 'string') { var list = []; // e.g., 'main{{!}}template' → 'main|template' namespace = prefix_page_name(namespace) // e.g., 'User_talk' → 'User talk' .replace(/[\s_]+/g, ' '); (is_page_title ? [ namespace.toLowerCase() ] // : namespace.toLowerCase() // for ',Template,Category', ';Template;Category', // 'main|file|module|template|category|help|portal|プロジェクト' // https://www.mediawiki.org/w/api.php?action=help&modules=main#main.2Fdatatypes .split(/(?:[,;|\u001F]|%7C|%1F)/)).forEach(function(n) { if (is_page_title && n.startsWith(':')) { // e.g., [[:title]] n = n.slice(1); } if (false && n.startsWith(':')) { // Invalid page title / namespace list.push(undefined); } // get namespace `_n` only. // e.g., 'wikipedia:sandbox' → 'wikipedia' var _n = n.includes(':') ? n.replace(/:.*$/, '').trim() // e.g., get_namespace('Wikipedia', {...}) : is_page_title ? 0 : n; if (!_n) { // _n === '' list.push(0); return; } if (!is_page_title && (!isNaN(_n) // 要指定所有值,請使用*。 To specify all values, use *. || _n === '*')) { // {Integer}_n list.push(_n); return; } if (_n in namespace_hash) { list.push(namespace_hash[_n]); return; } if (is_page_title) { list.push(0); return; } if (namespace_hash === get_namespace.hash) { // console.trace(namespace); library_namespace.debug('Invalid namespace: [' // + n + '] @ namespace list ' + namespace, // 2, 'get_namespace'); // console.trace(arguments); } else { list.push(is_page_title === false // main article space // is_page_title === false 亦即 // options.is_namespace === true && _n !== 'main' ? undefined : 0); } }); if (list.length === 0) { return; } // list.sort().unique_sorted().join('|'); list = list.unique(); if (options.get_name) { var name_of_NO = options.name_of_NO || session // session === wiki_API? && session.configurations && session.configurations.name_of_NO || get_namespace.name_of_NO; list = list.map(function(namespace_NO) { return namespace_NO in name_of_NO // ? name_of_NO[namespace_NO] : namespace_NO; }); } return list.length === 1 ? list[0] : list.join('|'); } if (namespace) { library_namespace.warn('get_namespace: Invalid namespace: [' + namespace + ']'); // console.trace(arguments); } return; } /** * The namespace number of the page. 列舉型別 (enumeration) * * assert: 正規名稱必須擺在最後一個,供 function namespace_text_of_NO() 使用。 * * CeL.wiki.namespace.hash * * {{NAMESPACENUMBER:{{FULLPAGENAME}}}} * * @type {Object} * * @see https://en.wikipedia.org/wiki/Wikipedia:Namespace */ get_namespace.hash = { // Virtual namespaces media : -2, special : -1, // 0: (Main/Article) main namespace 主要(條目內容/內文)命名空間/識別領域 // 條目 条目 entry 文章 article: ns = 0, 頁面 page: ns = any. 章節/段落 section main : 0, '' : 0, // 討論對話頁面 talk : 1, // 使用者頁面 user : 2, 'user talk' : 3, // ---------------------------- // NS_PROJECT // the project namespace for matters about the project // Varies between wikis project : 4, // https://meta.wikimedia.org/wiki/Requests_for_comment/Set_short_project_namespace_aliases_by_default_globally // [[w:ja:Wikipedia:バグの報告#WPショートカットが機能しない]] // [[phab:rOMWCa30603ab09d162fd30ff4081f85054df81a0ae49]] // https://noc.wikimedia.org/conf/highlight.php?file=InitialiseSettings.php wp : 4, wb : 4, wv : 4, ws : 4, wn : 4, wq : 4, wt : 4, // WD : 4, wikidata : 4, // [[commons:title]] @ enwiki 會造成混亂 // commons : 4, // COM : 4, // https://en.wikinews.org/wiki/Help:Namespace // WN : 4, wikinews : 4, // WP : 4, // 正規名稱必須擺在最後一個,供 function namespace_text_of_NO() 使用。 wikipedia : 4, // ---------------------------- // Varies between wikis 'project talk' : 5, // 正規名稱必須擺在最後一個,供 function namespace_text_of_NO() 使用。 'wikipedia talk' : 5, // image file : 6, 'file talk' : 7, // [[MediaWiki:title]] mediawiki : 8, 'mediawiki talk' : 9, 模板 : 10, テンプレート : 10, plantilla : 10, 틀 : 10, template : 10, 'template talk' : 11, // [[Help:title]], [[使用說明:title]] // H : 12, // 正規名稱必須擺在最後一個,供 function namespace_text_of_NO() 使用。 help : 12, 'help talk' : 13, // https://commons.wikimedia.org/wiki/Commons:Administrators%27_noticeboard#Cleaning_up_after_creation_of_CAT:_namespace_redirect // CAT : 14, // 正規名稱必須擺在最後一個,供 function namespace_text_of_NO() 使用。 category : 14, 'category talk' : 15, // 主題/主題首頁 portal : 100, // 主題討論 'portal talk' : 101, book : 108, 'book talk' : 109, draft : 118, 'draft talk' : 119, // Education Program 'education program' : 446, // Education Program talk 'education program talk' : 447, // TimedText timedtext : 710, // TimedText talk 'timedtext talk' : 711, // 模块 模塊 模組 module : 828, 'module talk' : 829, // Gadget gadget : 2300, 'gadget talk' : 2301, // Gadget definition 'gadget definition' : 2302, 'gadget definition talk' : 2303, // 話題 The Flow namespace (prefix Topic:) topic : 2600 }; // Should use `CeL.wiki.namespace.name_of(NS, session)` // NOT `wiki_API.namespace.name_of_NO[NS]` get_namespace.name_of_NO = []; /** * build session.configurations.namespace_pattern || get_namespace.pattern * * @inner */ function generate_namespace_pattern(namespace_hash, name_of_NO) { var source = []; for ( var namespace in namespace_hash) { name_of_NO[namespace_hash[namespace]] = upper_case_initial( namespace) // [[Mediawiki talk:]] → [[MediaWiki talk:]] .replace(/^Mediawiki/, 'MediaWiki'); if (namespace) source.push(namespace); } // namespace_pattern matched: [ , namespace, title ] return new RegExp('^(' + source.join('|').replace(/ /g, '[ _]') + '):(.+)$', 'i'); } get_namespace.pattern = generate_namespace_pattern(get_namespace.hash, get_namespace.name_of_NO); // console.log(get_namespace.pattern); function namespace_text_of_NO(NS, options) { if (!NS) return ''; var session = session_of_options(options); if (NS === session // session === wiki_API? && session.configurations && session.configurations.namespace_hash ? session.configurations.namespace_hash.wikipedia : get_namespace.hash.wikipedia) { if (session && session.family) { return wiki_API.upper_case_initial( // e.g., commons, wikidata get_wikimedia_project_name(session) || session.family); } // e.g., testwiki: return 'Wikipedia'; } var name_of_NO = session // session === wiki_API? && session.configurations && session.configurations.name_of_NO || wiki_API.namespace.name_of_NO; return wiki_API.upper_case_initial(name_of_NO[NS]); } // CeL.wiki.namespace.name_of(NS, session) get_namespace.name_of = namespace_text_of_NO; /** * remove namespace part of the title. 剝離 namespace。 * * wiki.remove_namespace(), wiki_API.remove_namespace() * * TODO: wiki.remove_namespace(page, only_remove_this_namespace) * * @param {String}page_title * page title 頁面標題。 * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns {String}title without namespace */ function remove_page_title_namespace(page_title, options) { page_title = wiki_API.normalize_title(page_title, options); // console.trace(page_title); if (Array.isArray(page_title)) { return page_title.map(function(_page_title) { return remove_page_title_namespace(_page_title, options); }); } if (typeof page_title !== 'string') { library_namespace.debug(page_title, 5, 'remove_page_title_namespace'); return page_title; } var session = session_of_options(options); var namespace_pattern = session // session === wiki_API? && session.configurations && session.configurations.namespace_pattern || get_namespace.pattern; if (page_title.endsWith(':')) { // e.g., input "Template:" page_title += ' '; } var matched = page_title.match(namespace_pattern); library_namespace.debug('Test ' + wiki_API.title_link_of(page_title) + ', get [' + matched + '] using pattern ' + namespace_pattern, 4, 'remove_page_title_namespace'); if (matched) { // namespace_pattern matched: [ , namespace, title ] return (matched ? matched[2] : page_title).trim(); // do not normalize page title. } // Leave untouched return page_title; } function namespace_of_options(options) { var namespace = Array.isArray(options) ? options // 必須預防 {Object}options。 : !options ? 0 : typeof options === 'number' ? options : typeof options === 'string' ? library_namespace .is_digits(options) ? +options : options : options.namespace; return namespace; } // TODO: is_namespace(page_title, 'Wikipedia|User') function page_title_is_namespace(page_title, options) { var namespace = namespace_of_options(options); var page_ns; // console.trace(namespace, wiki_API.is_page_data(page_title)); if (wiki_API.is_page_data(page_title)) { page_ns = page_title.ns; } else { page_title = wiki_API.normalize_title(page_title, options); page_ns = get_namespace(page_title, Object.assign({ // for wiki_API.namespace() is_page_title : true }, options)); } function check_namespace(namespace) { // 預防 jawiki.namespace('Draft') === undefined 這情況下被當作 true。 var namespace_to_test = get_namespace(namespace, Object.assign({ is_page_title : false }, options)); if (namespace_to_test === undefined) namespace_to_test = namespace; return page_ns === namespace_to_test; } if (Array.isArray(namespace)) { // e.g., `CeL.wiki.is_namespace('User:user', ['Wikipedia', 'User'])` return namespace.some(check_namespace); } return check_namespace(namespace); } function convert_page_title_to_namespace(page_title, options) { var namespace = namespace_of_options(options); namespace = get_namespace(namespace, Object.assign({ get_name : true }, options)) + ':'; page_title = wiki_API.normalize_title(page_title, options); // console.trace(page_title); function to_namespace(page_title) { return page_title || page_title === 0 ? namespace + remove_page_title_namespace(page_title, options) : page_title; } if (Array.isArray(page_title)) { return page_title.map(to_namespace); } return to_namespace(page_title); } // ------------------------------------------ function is_talk_namespace(namespace, options) { options = Object.assign({ // for wiki_API.namespace() is_page_title : true }, options); // wiki_API.is_page_data(namespace, options) || // wiki_API.is_Page(namespace) if (typeof namespace === 'object') { namespace = namespace.ns >= 0 ? namespace.ns : namespace.title; } if (typeof namespace === 'string') { namespace = wiki_API.normalize_title(namespace, options) .toLowerCase(); var session = session_of_options(options); var name_of_NO = session // session === wiki_API? && session.configurations && session.configurations.name_of_NO || wiki_API.namespace.name_of_NO; if (session) { // assert: {Number|Undefined}namespace namespace = wiki_API.namespace(namespace, options); } else { // treat ((namespace)) as page title // get namespace only. e.g., 'wikipedia:sandbox' → 'wikipedia' if (namespace.includes(':')) { // get namespace only, remove page title var _namespace = namespace.replace(/:.*$/, ''); if (_namespace in name_of_NO) namespace = name_of_NO[_namespace]; else return PATTERN_talk_prefix.test(namespace) || PATTERN_talk_namespace_prefix .test(namespace); } namespace = +namespace; } } if (typeof namespace === 'number') { // 單數: talk page return namespace > 0 && namespace % 2 === 1; } } // 討論頁面不應包含 [[Special talk:*]]。 function to_talk_page(page_title, options) { options = Object.assign({ // for wiki_API.namespace() is_page_title : true }, options); // console.trace([ page_title, options ]); var session = session_of_options(options), namespace; if (wiki_API.is_page_data(page_title)) { // assert: {Number}namespace namespace = page_title.ns; page_title = wiki_API.title_of(page_title); } else { page_title = wiki_API.normalize_title(page_title, options); // console.trace([ page_title ]); if (!session) {