UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,654 lines (1,494 loc) 212 kB
/** * @name CeL function for MediaWiki (Wikipedia / 維基百科): wikidata * * @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。 * * TODO:<code> https://www.wikidata.org/wiki/Help:QuickStatements </code> * * @since 2019/10/11 拆分自 CeL.application.net.wiki * * @see https://github.com/maxlath/wikibase-sdk * https://github.com/OpenRefine/OpenRefine/wiki/Reconciliation */ // 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.data', require : 'data.native.|data.date.' + '|application.net.wiki.' // load MediaWiki module basic functions + '|application.net.wiki.namespace.' // + '|application.net.wiki.query.|application.net.wiki.page.' // wiki_API.edit.check_data() + '|application.net.wiki.edit.' // wiki_API.parse.redirect() + '|application.net.wiki.parser.' // + '|application.net.Ajax.get_URL', // 設定不匯出的子函式。 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, KEY_HOST_SESSION = wiki_API.KEY_HOST_SESSION; // @inner var API_URL_of_options = wiki_API.API_URL_of_options, is_api_and_title = wiki_API.is_api_and_title, is_wikidata_site_nomenclature = wiki_API.is_wikidata_site_nomenclature, language_code_to_site_alias = wiki_API.language_code_to_site_alias; var KEY_CORRESPOND_PAGE = wiki_API.KEY_CORRESPOND_PAGE, PATTERN_PROJECT_CODE_i = wiki_API.PATTERN_PROJECT_CODE_i; var get_URL = this.r('get_URL'); var /** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */ NOT_FOUND = ''.indexOf('_'); var gettext = library_namespace.cache_gettext(function(_) { gettext = _; }); // ------------------------------------------------------------------------ // 用來取得 entity value 之屬性名。 函數 : wikidata_entity_value // 為了方便使用,不採用 Symbol()。 var KEY_get_entity_value = 'value'; // ------------------------------------------------------------------------ // 客製化的設定。 // wikidata_site_alias[site code] = Wikidata site code // @see https://www.wikidata.org/w/api.php?action=help&modules=wbeditentity // for sites var wikidata_site_alias = { // 為粵文維基百科特別處理。 yuewiki : 'zh_yuewiki', // 為日文特別修正: 'jp' is wrong! jpwiki : 'jawiki' }; function get_data_API_URL(options, default_API_URL) { // library_namespace.debug('options:', 0, 'get_data_API_URL'); // console.trace(options); var API_URL = options && options.data_API_URL, session; if (API_URL) { } else if (wiki_API.is_wiki_API( // session = wiki_API.session_of_options(options))) { if (session.data_session) { API_URL = session.data_session.API_URL; } if (!API_URL && session[KEY_HOST_SESSION]) { // is data session. e.g., https://test.wikidata.org/w/api.php API_URL = session.API_URL; } if (!API_URL) { // e.g., lingualibre API_URL = session.data_API_URL; } } else { API_URL = API_URL_of_options(options); } if (!API_URL) { API_URL = default_API_URL || wikidata_API_URL; } library_namespace.debug('API_URL: ' + API_URL, 3, 'get_data_API_URL'); return API_URL; } // -------------------------------------------------------------------------------------------- // Wikidata 操作函數 // https://www.wikidata.org/wiki/Wikidata:Data_access /** * @see <code> // https://meta.wikimedia.org/wiki/Wikidata/Notes/Inclusion_syntax {{label}}, {{Q}}, [[d:Q1]] http://wdq.wmflabs.org/api_documentation.html https://github.com/maxlath/wikidata-sdk </code> * * @since */ /** * 測試 value 是否為實體項目 wikidata entity / wikibase-item. * * is_wikidata_page() * * @param value * value to test. 要測試的值。 * @param {Boolean}[strict] * 嚴格檢測。 * * @returns {Boolean}value 為實體項目。 */ function is_entity(value, strict) { return library_namespace.is_Object(value) // {String}id: Q\d+ 或 P\d+。 && (strict ? /^[PQ]\d{1,10}$/.test(value.id) : value.id) // && library_namespace.is_Object(value.labels); } /** * API URL of wikidata.<br /> * e.g., 'https://www.wikidata.org/w/api.php', * 'https://test.wikidata.org/w/api.php' * * @type {String} */ var wikidata_API_URL = wiki_API.api_URL('wikidata'); /** * Combine ((session)) with Wikidata. 立即性(asynchronous)設定 this.data_session。 * * @param {wiki_API}session * 正作業中之 wiki_API instance。 * @param {Function}[callback] * 回調函數。 callback({Array}entity list or {Object}entity or * @param {String}[API_URL] * language code or API URL of Wikidata * @param {String}[password] * user password * @param {Boolean}[force] * 無論如何重新設定 this.data_session。 * * @inner */ function setup_data_session(session, callback, API_URL, password, force) { if (force === undefined) { if (typeof password === 'boolean') { // shift arguments. force = password; password = null; } else if (typeof API_URL === 'boolean' && password === undefined) { // shift arguments. force = API_URL; API_URL = null; } } if (session.data_session && API_URL & !force) { return; } if (session.data_session) { library_namespace.debug('直接清空佇列。', 2, 'setup_data_session'); // TODO: 強制中斷所有正在執行之任務。 session.data_session.actions.clear(); } if (!API_URL // https://test.wikipedia.org/w/api.php // https://test2.wikipedia.org/w/api.php && /test\d?\.wikipedia/.test(session.API_URL)) { API_URL = 'test.wikidata'; } else if (typeof API_URL === 'string' && !/wikidata/i.test(API_URL) && !PATTERN_PROJECT_CODE_i.test(API_URL)) { // e.g., 'test' → 'test.wikidata' API_URL += '.wikidata'; } // data_configuration: set Wikidata session var data_login_options = { user_name : session.token.lgname, // wiki.set_data(host session, password) password : password || session.token.lgpassword, // API_URL: host session API_URL : typeof API_URL === 'string' && wiki_API.api_URL(API_URL) || get_data_API_URL(session), preserve_password : session.preserve_password }; // console.trace([ data_login_options, session.API_URL ]); if (false && data_login_options.API_URL === session.API_URL) { // TODO: test // e.g., lingualibre library_namespace.debug('設定 session 的 data_session 即為本身。', 2, 'setup_data_session'); session.data_session = session; } else if (data_login_options.user_name && data_login_options.password) { session.data_session = wiki_API.login(data_login_options); } else { // 警告: 可能需要設定 options.is_running session.data_session = new wiki_API(data_login_options); } library_namespace.debug('Setup 宿主 host session.', 2, 'setup_data_session'); session.data_session[KEY_HOST_SESSION] = session; library_namespace.debug('run callback: ' + callback, 2, 'setup_data_session'); session.data_session.run(callback); } // ------------------------------------------------------------------------ function normalize_wikidata_key(key) { if (typeof key !== 'string') { library_namespace.error('normalize_wikidata_key: key: ' + JSON.stringify(key)); // console.trace(key); throw new Error('normalize_wikidata_key: key should be string!'); } return key.replace(/_/g, ' ').trim(); } /** * 搜索標籤包含特定關鍵字(label=key)的項目。 * * 此搜索有極大問題:不能自動偵測與轉換中文繁簡體。 或須轉成英語再行搜尋。 * * @example<code> CeL.wiki.data.search('宇宙', function(entity) {result=entity;console.log(entity[0]==='Q1');}, {get_id:true}); CeL.wiki.data.search('宇宙', function(entity) {result=entity;console.log(entity==='Q1');}, {get_id:true, limit:1}); CeL.wiki.data.search('形狀', function(entity) {result=entity;console.log(entity==='P1419');}, {get_id:true, type:'property'}); </code> * * @param {String}key * 要搜尋的關鍵字。item/property title. * @param {Function}[callback] * 回調函數。 callback({Array}entity list or {Object}entity or * {String}entity id, error) * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 */ function wikidata_search(key, callback, options) { if (!key) { callback(undefined, 'wikidata_search: No key assigned.'); return; } if (typeof options === 'function') options = { filter : options }; else if (typeof options === 'string') { options = { language : options }; } else { // 正規化並提供可隨意改變的同內容參數,以避免修改或覆蓋附加參數。 options = library_namespace.new_options(options); } var language = options.language; var type = options.type; // console.trace([ key, is_api_and_title(key, 'language') ]); if (is_api_and_title(key, 'language')) { if (is_wikidata_site_nomenclature(key[0])) { wikidata_entity(key, function(entity, error) { // console.log(entity); var id = !error && entity && entity.id; // 預設找不到 sitelink 會作搜尋。 if (!id && !options.no_search) { key = key.clone(); if (key[0] = key[0].replace(/wiki.*$/, '')) { wikidata_search(key, callback, options); return; } } callback(id, error); }, { props : '' }); return; } // for [ {String}language, {String}key ].type if (key.type) type = key.type; language = key[0]; key = key[1]; } // console.log('key: ' + key); key = normalize_wikidata_key(key); language = language || wiki_API.site_name(options, { get_all_properties : true }).language; var action = { action : 'wbsearchentities', // search. e.g., // https://www.wikidata.org/w/api.php?action=wbsearchentities&search=abc&language=en&utf8=1 search : key, // https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities language : language }; if (options.limit || wikidata_search.default_limit) { action.limit = options.limit || wikidata_search.default_limit; } if (type) { // item|property // 預設值:item action.type = type; } if (options['continue'] > 0) action['continue'] = options['continue']; action = [ API_URL_of_options(options) || wikidata_API_URL, action ]; wiki_API.query(action, function handle_result(data, error) { error = wiki_API.query.handle_error(data, error); // 檢查伺服器回應是否有錯誤資訊。 if (error) { library_namespace.error('wikidata_search: ' + error); callback(data, error); return; } /** * e.g., <code> {"searchinfo":{"search":"Universe"},"search":[{"id":"Q1","title":"Q1","pageid":129,"repository":"wikidata","url":"//www.wikidata.org/wiki/Q1","concepturi":"http://www.wikidata.org/entity/Q1","label":"universe","description":"totality consisting of space, time, matter and energy","match":{"type":"label","language":"en","text":"universe"}}],"search-continue":1,"success":1} </code> */ // console.trace(data); // console.trace(data.search); var list; if (!Array.isArray(data.search)) { list = []; } else if (!('filter' in options) || typeof options.filter === 'function') { list = data.search.filter(options.filter || // default filter function(item) { // 自此結果能得到的資訊有限。 // label: 'Universe' // match: { type: 'label', language: 'zh', text: '宇宙' } if (item.match && key.toLowerCase() // .trim() === item.match.text.toLowerCase() // 通常不會希望取得維基百科消歧義頁。 // @see 'Wikimedia disambiguation page' @ // [[d:MediaWiki:Gadget-autoEdit.js]] && !/disambiguation|消歧義|消歧義|曖昧さ回避/.test(item.description)) { return true; } }); } if (Array.isArray(options.list)) { options.list.push(list); } else { options.list = [ list ]; } list = options.list; if (!options.limit && data['search-continue'] > 0) { options['continue'] = data['search-continue']; wikidata_search(key, callback, options); return; } if (Array.isArray(list.length) && list.length > 1) { // clone list list = list.clone(); } else { list = list[0]; } if (options.get_id) { list = list.map(function(item) { return item.id; }); } // multiple pages if (!options.multi && ( // options.limit <= 1 list.length <= 1)) { list = list[0]; } // console.trace(options); callback(list); }, null, options); } // wikidata_search_cache[{String}"zh:隸屬於"] = {String}"P31"; var wikidata_search_cache = { // 載於, 出典, source of claim // 'en:stated in' : 'P248', // 導入自, source // 'en:imported from Wikimedia project' : 'P143', // 來源網址, website // 'en:reference URL' : 'P854', // 檢索日期 // 'en:retrieved' : 'P813' }, // entity (Q\d+) 用。 // 可考量加入 .type (item|property) 為 key 的一部分, // 或改成 wikidata_search_cache={item:{},property:{}}。 wikidata_search_cache_entity = Object.create(null); // wikidata_search.default_limit = 'max'; // TODO: add more types: form, item, lexeme, property, sense, sense // https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities wikidata_search.add_cache = function add_cache(key, id, language, is_entity) { var cached_hash = is_entity ? wikidata_search_cache_entity : wikidata_search_cache; language = wiki_API.site_name(language, { get_all_properties : true }).language; cached_hash[language + ':' + key] = id; }; // wrapper function of wikidata_search(). wikidata_search.use_cache = function use_cache(key, callback, options) { if (!options && library_namespace.is_Object(callback)) { // shift arguments. options = callback; callback = undefined; } // console.trace(options); if (options && options.must_callback && !callback) { library_namespace.warn('設定 options.must_callback,卻無 callback!'); } var language_and_key, // 須與 wikidata_search() 相同! // TODO: 可以 guess_language(key) 猜測語言。 language = options && options.language || wiki_API.site_name(options, { get_all_properties : true }).language, // https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities cached_hash = options && options.type && options.type !== // default_options.type: 'property' wikidata_search.use_cache.default_options.type ? wikidata_search_cache_entity : wikidata_search_cache; // console.trace([ key, language, options ]); key = normalize_value_of_properties(key, language); var entity_type = key && key.type; if (typeof key === 'string') { key = normalize_wikidata_key(key); language_and_key = language + ':' + key; } else if (Array.isArray(key)) { // console.trace(key); if (is_api_and_title(key, 'language')) { // key.join(':') language_and_key = key[0] + ':' // + normalize_wikidata_key(key[1]); } else { // 處理取得多 keys 之 id 的情況。 var index = 0, // cache_next_key = function() { library_namespace.debug(index + '/' + key.length, 3, 'use_cache.cache_next_key'); if (index === key.length) { // done. callback(id_list) var id_list = key.map(function(k) { if (is_api_and_title(k, 'language')) { return cached_hash[k[0] + ':' // + normalize_wikidata_key(k[1])]; } k = normalize_wikidata_key(k); return cached_hash[language + ':' + k]; }); // console.trace(id_list); callback(id_list); return; } // console.trace(options); wikidata_search.use_cache(key[index++], cache_next_key, // Object.assign({ API_URL : get_data_API_URL(options) }, wikidata_search.use_cache.default_options, { // 警告: 若是設定 must_callback=false,會造成程序不 callback 而中途跳出! must_callback : true }, options)); }; cache_next_key(); return; } } else { // 避免若是未match is_api_and_title(key, 'language'), // 可能導致 infinite loop! key = 'wikidata_search.use_cache: Invalid key: [' + key + ']'; // console.warn(key); callback(undefined, key); return; } library_namespace.debug('search ' + (language_and_key || JSON.stringify(key)) + ' (' + is_api_and_title(key, 'language') + ')', 4, 'wikidata_search.use_cache'); if ((!options || !options.force) // TODO: key 可能是 [ language code, labels|aliases ] 之類。 // &&language_and_key && (language_and_key in cached_hash)) { library_namespace.debug('has cache: [' + language_and_key + '] → ' + cached_hash[language_and_key], 4, 'wikidata_search.use_cache'); key = cached_hash[language_and_key]; if (/^[PQ]\d{1,10}$/.test(key)) { } if (options && options.must_callback) { callback(key); return; } else { // 只在有 cache 時才即刻回傳。 return key; } } if (!options || library_namespace.is_empty_object(options)) { options = Object.clone(wikidata_search.use_cache.default_options); } else if (!options.get_id) { if (!options.must_callback) { // 在僅設定 .must_callback 時,不顯示警告而自動補上應有的設定。 library_namespace.warn('wikidata_search.use_cache: 當把實體名稱 [' + language_and_key + '] 轉換成 id 時,應設定 options.get_id。 options: ' + JSON.stringify(options)); } options = Object.assign({ get_id : true }, options); } else if (entity_type) { options = Object.clone(options); } if (entity_type) options.type = entity_type; // console.log(arguments); wikidata_search(key, function(id, error) { // console.log(language_and_key + ': ' + id); // console.trace(options.search_without_cache); if (!id) { library_namespace .error('wikidata_search.use_cache: Nothing found: [' + language_and_key + ']'); // console.log(options); // console.trace('wikidata_search.use_cache: Nothing found'); } else if (!options.search_without_cache && typeof id === 'string' && /^[PQ]\d{1,10}$/.test(id)) { library_namespace.info('wikidata_search.use_cache: cache ' // 搜尋此類型的實體。 預設值:item + (options && options.type || 'item') // + ' [' + language_and_key + '] → ' + id); } if (!options.search_without_cache) { // 即使有錯誤,依然做 cache 紀錄,避免重複偵測操作。 cached_hash[language_and_key] = id; } // console.trace([ language_and_key, id ]); // console.trace('' + callback); if (callback) callback(id, error); }, options); }; // default options passed to wikidata_search() wikidata_search.use_cache.default_options = { // 若有必要用上 options.API_URL,應在個別操作內設定。 // 通常 property 才值得使用 cache。 // entity 可採用 'item' // https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities type : 'property', // limit : 1, get_id : true }; // ------------------------------------------------------------------------ /** * {Array}時間精度(精密度)單位。 * * 注意:須配合 index_precision @ CeL.data.date! * * @see https://doc.wikimedia.org/Wikibase/master/php/md_docs_topics_json.html#json_datavalues_time */ var time_unit = 'gigayear,100 megayear,10 megayear,megayear,100 kiloyear,10 kiloyear,millennium,century,decade,year,month,day,hour,minute,second,microsecond' .split(','), // 精密度至日: 11。 INDEX_OF_PRECISION = time_unit.to_hash(); // 千紀: 一千年, https://en.wikipedia.org/wiki/Kyr time_unit.zh = '十億年,億年,千萬年,百萬年,十萬年,萬年,千紀,世紀,年代,年,月,日,時,分,秒,毫秒,微秒,納秒' .split(','); /** * 將時間轉為字串。 * * @inner */ function time_toString() { var unit = this.unit; if (this.power) { unit = Math.abs(this[0]) + unit[0]; return this.power > 1e4 ? unit + (this[0] < 0 ? '前' : '後') // : (this[0] < 0 ? '前' : '') + unit; } return this.map(function(value, index) { return value + unit[index]; }).join(''); } /** * 將經緯度座標轉為字串。 * * @inner */ function coordinate_toString(type) { // 經緯度座標 coordinates [ latitude 緯度, longitude 經度 ] return Marh.abs(this[0]) + ' ' + (this[0] < 0 ? 'S' : 'N') // + ', ' + Marh.abs(this[1]) + ' ' + (this[1] < 0 ? 'W' : 'E'); } // https://www.wikidata.org/wiki/Help:Statements // https://www.mediawiki.org/wiki/Wikibase/DataModel#Statements // statement = claim + rank + references // claim = snak + qualifiers // snak: data type + value /** * 將特定的屬性值轉為 JavaScript 的物件。 * * @param {Object}data * 從Wikidata所得到的屬性值。 * @param {Function}[callback] * 回調函數。 callback(轉成JavaScript的值) * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns 轉成JavaScript的值。 * * @see https://www.mediawiki.org/wiki/Wikibase/API#wbformatvalue * https://www.mediawiki.org/wiki/Wikibase/DataModel/JSON#Claims_and_Statements * https://www.mediawiki.org/wiki/Wikibase/API * https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Value_representation * https://www.wikidata.org/wiki/Special:ListDatatypes */ function wikidata_datavalue(data, callback, options) { // console.log(data); // console.log(JSON.stringify(data)); if (library_namespace.is_Object(callback) && !options) { // shift arguments. options = callback; callback = undefined; } // 正規化並提供可隨意改變的同內容參數,以避免修改或覆蓋附加參數。 options = library_namespace.new_options(options); callback = typeof callback === 'function' && callback; var value = options.multi && !Array.isArray(data) ? [ data ] : data; if (Array.isArray(value)) { if (!options.single) { if (options.multi) { delete options.multi; } // TODO: array + ('numeric-id' in value) // TODO: using Promise.allSettled([]) if (callback) { // console.log(value); value.run_parallel(function(run_next, item, index) { // console.log([ index, item ]); wikidata_datavalue(item, function(v, error) { // console.log([ index, v ]); value[index] = v; run_next(); }, options); }, function() { callback(value); }); } return value.map(function(v) { return wikidata_datavalue(v, undefined, options); }); } // 選擇推薦值/最佳等級。 var first; if (value.every(function(v) { if (!v) { return true; } if (v.rank !== 'preferred') { if (!first) { first = v; } return true; } // TODO: check v.mainsnak.datavalue.value.language value = v; // return false; })) { // 沒有推薦值,選擇首個非空的值。 value = first; } } if (is_entity(value)) { // get label of entity value = value.labels; var language = wiki_API.site_name(options, { get_all_properties : true }).language; language = language && value[language] || value[wiki_API.language] // 最起碼選個國際通用的。 || value.en; if (!language) { // 隨便挑一個語言的 label。 for (language in value) { value = value[language]; break; } } return value.value; } if (!value || typeof value !== 'object') { callback && callback(value); return value; } // TODO: value.qualifiers, value['qualifiers-order'] // TODO: value.references value = value.mainsnak || value; if (value) { // console.log(value); // console.log(JSON.stringify(value)); // 與 normalize_wikidata_value() 須同步! if (value.snaktype === 'novalue') { value = null; callback && callback(value); return value; } if (value.snaktype === 'somevalue') { // e.g., [[Q1]], Property:P1419 形狀 // Property:P805 主條目 if (callback && data.qualifiers && Array.isArray(value = data.qualifiers.P805)) { if (value.length === 1) { value = value[0]; } delete options[library_namespace.new_options.new_key]; // console.log(value); wikidata_datavalue(value, callback, options); return; } value = wikidata_edit.somevalue; callback && callback(value); return value; } } // assert: value.snaktype === 'value' value = value.datavalue || value; var type = value.type; // TODO: type 可能為 undefined! if ('value' in value) { if (type === 'literal' // e.g., SPARQL: get ?linkcount of: // ?item wikibase:sitelinks ?linkcount && value.datatype === 'http://www.w3.org/2001/XMLSchema#integer') { // assert: typeof value.value === 'string' // Math.floor() value = +value.value; } else { value = value.value; } } if (typeof value !== 'object') { // e.g., typeof value === 'string' callback && callback(value); return value; } if ('text' in value) { // e.g., { text: 'Ὅμηρος', language: 'grc' } value = value.text; callback && callback(value); return value; } if ('amount' in value) { // qualifiers 純量數值 value = +value.amount; callback && callback(value); return value; } if ('latitude' in value) { // 經緯度座標 coordinates [ latitude 緯度, longitude 經度 ] var coordinate = [ value.latitude, value.longitude ]; if (false) { // geodetic reference system, 大地座標系/坐標系統測量基準 var system = value.globe.match(/[^\\\/]+$/); system = system && system[0]; switch (system) { case 'Q2': coordinate.system = 'Earth'; break; case 'Q11902211': coordinate.system = 'WGS84'; break; case 'Q215848': coordinate.system = 'WGS'; break; case 'Q1378064': coordinate.system = 'ED50'; break; default: if (system) coordinate.system = system; else // invalid data? ; } } // TODO: precision coordinate.precision = value.precision; coordinate.toString = coordinate_toString; value = coordinate; callback && callback(value); return value; } if ('time' in value) { // date & time. 時間日期 var matched, year, precision = value.precision; // console.trace([ value, precision ]); if (precision <= INDEX_OF_PRECISION.year) { // 時間尺度為1年以上 matched = value.time.match(/^[+\-]\d+/); year = +matched[0]; var power = Math.pow(10, INDEX_OF_PRECISION.year - precision); matched = [ year / power | 0 ]; matched.unit = [ time_unit.zh[precision] ]; matched.power = power; } else { // 時間尺度為不到一年 matched = value.time.match( // [ all, Y, m, d, H, M, S ] /^([+\-]\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z$/); matched = matched.slice(1, precision - // +1: is length, not index // +1: year starts from 1. INDEX_OF_PRECISION.year + 1 + 1).map(function(value) { return +value; }); year = matched[0]; matched.unit = time_unit.zh.slice(INDEX_OF_PRECISION.year, precision + 1); } // proleptic Gregorian calendar: // http://www.wikidata.org/entity/Q1985727 // proleptic Julian calendar: // http://www.wikidata.org/entity/Q1985786 var type = value.calendarmodel.match(/[^\\\/]+$/); if (type && type[0] === 'Q1985786') { matched.Julian = true; // matched.type = 'Julian'; } else if (type && type === 'Q1985727') { // matched.type = 'Gregorian'; } else { // matched.type = type || value.calendarmodel; } var Julian_day; if (year >= -4716 // && (Julian_day = library_namespace.Julian_day)) { // start JDN matched.JD = Julian_day.from_YMD(year, matched[1], matched[2], !matched.Julian); } matched.toString = time_toString; // console.trace([ matched, value, precision ]); callback && callback(matched); return matched; } if ('numeric-id' in value) { // wikidata entity. 實體 value = 'Q' + value['numeric-id']; if (callback) { library_namespace.debug('Trying to get entity ' + value, 1, 'wikidata_datavalue'); // console.log(value); // console.log(wiki_API.site_name(options,{get_all_properties:true}).language); wikidata_entity(value, options.get_object ? callback // default: get label 標籤標題 : function(entity, error) { // console.log([ entity, error ]); if (error) { library_namespace.debug( 'Failed to get entity ' + value, 0, 'wikidata_datavalue'); callback && callback(undefined, error); return; } entity = entity.labels || entity; entity = entity[wiki_API.site_name(options, { get_all_properties : true }).language] || entity; callback && callback('value' in entity ? entity.value : entity); }, { languages : wiki_API.site_name(options, { get_all_properties : true }).language }); } return value; } library_namespace.warn('wikidata_datavalue: 尚無法處理此屬性: [' + type + '],請修改本函數。'); callback && callback(value); return value; } // 取得value在property_list中的index。相當於 property_list.indexOf(value) // type=-1: list.lastIndexOf(value), type=1: list.includes(value), // other type: list.indexOf(value) wikidata_datavalue.get_index = function(property_list, value, type) { function to_comparable(value) { if (Array.isArray(value) && value.JD) { // e.g., new Date('2000-1-1 UTC+0') var date = new Date(value.join('-') + ' UTC+0'); if (isNaN(date.getTime())) { library_namespace .error('wikidata_datavalue.get_index: Invalid Date: ' + value); } value = date; } // e.g., library_namespace.is_Date(value) return typeof value === 'object' ? JSON.stringify(value) : value; } property_list = wikidata_datavalue(property_list, undefined, { // multiple multi : true }).map(to_comparable); value = to_comparable(value && value.datavalue ? wikidata_datavalue(value) : value); if (!isNaN(value) && property_list.every(function(v) { return typeof v === 'number'; })) { value = +value; } // console.log([ value, property_list ]); if (type === 0) { return [ property_list, value ]; } if (type === 1) { return property_list.includes(value); } if (type === -1) { return property_list.lastIndexOf(value); } return property_list.indexOf(value); }; // ------------------------------------------------------------------------ /** * get label of entity. 取得指定實體的標籤。 * * CeL.wiki.data.label_of() * * @param {Object}entity * 指定實體。 * @param {String}[language] * 指定取得此語言之資料。 * @param {Boolean}[use_title] * 當沒有標籤的時候,使用各語言連結標題。 * @param {Boolean}[get_labels] * 取得所有標籤。 * * @returns {String|Array}標籤。 */ function get_entity_label(entity, language, use_title, get_labels) { if (get_labels) { if (use_title) { use_title = get_entity_link(entity, language); if (!Array.isArray(use_title)) use_title = use_title ? [ use_title ] : []; } return entity_labels_and_aliases(entity, language, use_title); } var labels = entity && entity.labels; if (labels) { var label = labels[language || wiki_API.language]; if (label) return label.value; if (!language) return labels; } if (use_title) { return get_entity_link(entity, language); } } /** * get site link of entity. 取得指定實體的語言連結標題。 * * CeL.wiki.data.title_of(entity, language) * * @param {Object}entity * 指定實體。 * @param {String}[language] * 指定取得此語言之資料。 * * @returns {String}語言標題。 */ function get_entity_link(entity, language) { var sitelinks = entity && entity.sitelinks; if (sitelinks) { var link = sitelinks[wiki_API.site_name(language)]; if (link) { return link.title; } if (!language) { link = []; for (language in sitelinks) { link.push(sitelinks[language].title); } return link; } } } // https://www.wikidata.org/w/api.php?action=help&modules=wbgetentities // Maximum number of values is 50 var MAX_ENTITIES_TO_GET = 50; var PATTERN_entity_id = /^Q(\d{1,10})$/i; var PATTERN_property_id = /^P(\d{1,5})$/i; /** * 取得特定實體的特定屬性值。 * * @example<code> CeL.wiki.data('Q1', function(entity) {result=entity;}); CeL.wiki.data('Q2', function(entity) {result=entity;console.log(JSON.stringify(entity).slice(0,400));}); CeL.wiki.data('Q1', function(entity) {console.log(entity.id==='Q1'&&JSON.stringify(entity.labels)==='{"zh":{"language":"zh","value":"宇宙"}}');}, {languages:'zh'}); CeL.wiki.data('Q1', function(entity) {console.log(entity.labels['en'].value+': '+entity.labels['zh'].value==='universe: 宇宙');}); // Get the property of wikidata entity. // 取得Wikidata中指定實體項目的指定屬性/陳述。 CeL.wiki.data('Q1', function(entity) {console.log(entity['en'].value+': '+entity['zh'].value==='universe: 宇宙');}, 'labels'); // { id: 'P1', missing: '' } CeL.wiki.data('Q1|P1', function(entity) {console.log(JSON.stringify(entity[1])==='{"id":"P1","missing":""}');}); CeL.wiki.data(['Q1','P1'], function(entity) {console.log(entity);}); CeL.wiki.data('Q11188', function(entity) {result=entity;console.log(JSON.stringify(entity.labels.zh)==='{"language":"zh","value":"世界人口"}');}); CeL.wiki.data('P6', function(entity) {result=entity;console.log(JSON.stringify(entity.labels.zh)==='{"language":"zh","value":"政府首长"');}); CeL.wiki.data('宇宙', '形狀', function(entity) {result=entity;console.log(entity==='宇宙的形狀');}) CeL.wiki.data('荷马', '出生日期', function(entity) {result=entity;console.log(''+entity==='前8世紀');}) CeL.wiki.data('荷马', function(entity) {result=entity;console.log(CeL.wiki.entity.value_of(entity.claims.P1477)==='Ὅμηρος');}) CeL.wiki.data('艾薩克·牛頓', '出生日期', function(entity) {result=entity;console.log(''+entity==='1643年1月4日,1642年12月25日');}) // 實體項目值的鏈接數據界面 (無法篩選所要資料,傳輸量較大。) // 非即時資料! CeL.get_URL('https://www.wikidata.org/wiki/Special:EntityData/Q1.json',function(r){r=JSON.parse(r.responseText);console.log(r.entities.Q1.labels.zh.value)}) // ------------------------------------------------------------------------ wiki = CeL.wiki.login(user_name, pw, 'wikidata'); wiki.data(id, function(entity){}, {is_key:true}).edit_data(function(entity){}); wiki.page('title').data(function(entity){}, options).edit_data().edit() wiki = Wiki(true) wiki.page('宇宙').data(function(entity){result=entity;console.log(entity);}) wiki = Wiki(true, 'wikidata'); wiki.data('宇宙', function(entity){result=entity;console.log(entity.labels['en'].value==='universe');}) wiki.data('宇宙', '形狀', function(entity){result=entity;console.log(entity==='宇宙的形狀');}) wiki.query('CLAIM[31:14827288] AND CLAIM[31:593744]', function(entity) {result=entity;console.log(entity.labels['zh-tw'].value==='維基資料');}) </code> * * @param {String|Array}key * entity id. 欲取得之特定實體 id。 e.g., 'Q1', 'P6' * @param {String}[property] * 取得特定屬性值。 * @param {Function}[callback] * 回調函數。 callback(轉成JavaScript的值, error) * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @see https://www.mediawiki.org/wiki/Wikibase/DataModel/JSON * @see https://www.wikidata.org/w/api.php?action=help&modules=wbgetentities */ function wikidata_entity(key, property, callback, options) { if (typeof property === 'function' && !options) { // shift arguments. options = callback; callback = property; property = null; } if (typeof options === 'string') { options = { props : options }; } else if (typeof options === 'function') { options = { filter : options }; } else { // 正規化並提供可隨意改變的同內容參數,以避免修改或覆蓋附加參數。 options = library_namespace.new_options(options); } var API_URL = get_data_API_URL(options); // ---------------------------- // convert property: title to id if (typeof property === 'string' && !PATTERN_property_id.test(property)) { if (library_namespace.is_debug(2) && /^(?:(?:info|sitelinks|sitelinks\/urls|aliases|labels|descriptions|claims|datatype)\|)+$/ .test(property + '|')) library_namespace.warn( // 'wikidata_entity: 您或許該採用 options.props = ' + property); /** {String}setup language of key and property name. 僅在需要 search 時使用。 */ property = [ wiki_API.site_name(options, { get_all_properties : true }).language, property ]; } // console.log('property: ' + property); if (is_api_and_title(property, 'language')) { // TODO: property 可能是 [ language code, 'labels|aliases' ] 之類。 property = wikidata_search.use_cache(property, function(id, error) { wikidata_entity(key, id, callback, options); }, options); if (!property) { // assert: property === undefined // Waiting for conversion. return; } } // ---------------------------- // convert key: title to id if (typeof key === 'number') { key = [ key ]; } else if (typeof key === 'string' && !/^[PQ]\d{1,10}(\|[PQ]\d{1,10})*$/.test(key)) { /** {String}setup language of key and property name. 僅在需要 search 時使用。 */ key = [ wiki_API.site_name(options, { get_all_properties : true }).language, key ]; } if (Array.isArray(key)) { if (is_api_and_title(key)) { if (is_wikidata_site_nomenclature(key[0])) { key = { site : key[0], title : key[1] }; } else { wikidata_search(key, function(id) { // console.trace(id); if (id) { library_namespace.debug( // 'entity ' + id + ' ← [[:' + key.join(':') + ']]', 1, 'wikidata_entity'); wikidata_entity(id, property, callback, options); return; } // 可能為重定向頁面? // 例如要求 "A of B" 而無此項, // 但 [[en:A of B]]→[[en:A]] 且存在 "A",則會回傳本"A"項。 wiki_API.page(key.clone(), function(page_data) { var content = wiki_API.content_of(page_data), // 測試檢查是否為重定向頁面。 redirect = wiki_API.parse.redirect(content); if (redirect) { library_namespace.info( // 'wikidata_entity: 處理重定向頁面: [[:' + key.join(':') + ']] → [[:' + key[0] + ':' + redirect + ']]。'); wikidata_entity([ key[0], // wiki_API.normalize_title(): // 此 API 無法自動轉換首字大小寫之類!因此需要自行正規化。 wiki_API.normalize_title(redirect) ], property, callback, options); return; } library_namespace.error( // 'wikidata_entity: Wikidata 不存在/已刪除 [[:' + key.join(':') + ']] 之數據,' + (content ? '但' : '且無法取得/不') + '存在此 Wikipedia 頁面。無法處理此 Wikidata 數據要求。'); callback(undefined, 'no_key'); }); }, Object.assign({ API_URL : API_URL, get_id : true, limit : 1 }, options)); // Waiting for conversion. return; } } else if (key.length > MAX_ENTITIES_TO_GET) { if (!key.not_original) { key = key.clone(); key.not_original = true; } var result, _error; var get_next_slice = function get_next_slice() { library_namespace.info('wikidata_entity: ' + key.length + ' items left...'); wikidata_entity(key.splice(0, MAX_ENTITIES_TO_GET), // property, function(entities, error) { // console.log(Object.keys(entities)); if (result) Object.assign(result, entities); else result = entities; _error = error || _error; if (key.length > 0) { get_next_slice(); } else { callback(result, _error); } }, options); } get_next_slice(); return; } else { key = key.map(function(id) { if (PATTERN_entity_id.test(id) // || PATTERN_property_id.test(id)) return id; if (library_namespace.is_digits(id)) return 'Q' + id; library_namespace.warn( // 'wikidata_entity: Invalid id: ' + id); return ''; }).join('|'); } } // ---------------------------- if (!key || library_namespace.is_empty_object(key)) { library_namespace.error('wikidata_entity: 未設定欲取得之特定實體 id。'); console.trace(key); callback(undefined, 'no_key'); return; } // 實體項目 entity // https://www.wikidata.org/w/api.php?action=wbgetentities&ids=Q1&props=labels&utf8=1 // TODO: claim/聲明/屬性/分類/陳述/statement // https://www.wikidata.org/w/api.php?action=wbgetclaims&ids=P1&props=claims&utf8=1 // TODO: 維基百科 sitelinks // https://www.wikidata.org/w/api.php?action=wbgetentities&ids=Q1&props=sitelinks&utf8=1 var action; // 不採用 wiki_API.is_page_data(key) // 以允許自行設定 {title:title,language:language}。 // console.trace(key); if (key.title) { if (false) { console.trace([ key.site, wiki_API.site_name(key.language, options), key ]); } action = 'sites=' + (key.site || // 在 options 包含之 wiki session 中之 key.language。 // e.g., "cz:" 在 zhwiki 將轉為 cs.wikipedia.org wiki_API.site_name(key.language, options)) + '&titles=' + encodeURIComponent(key.title); } else { if (typeof key === 'object') { console.trace(key); callback(undefined, 'wikidata_entity: Input object instead of string'); return; } action = 'ids=' + key; } library_namespace.debug('action: [' + action + ']', 2, 'wikidata_entity'); // https://www.wikidata.org/w/api.php?action=help&modules=wbgetentities action = [ API_URL, 'action=wbgetentities&' + action ]; if (property && !('props' in options)) { options.props = 'claims'; } var props = options.props; if (Array.isArray(props)) { props = props.join('|'); } if (wiki_API.is_page_data(key) && typeof props === 'string') { // for data.lastrevid if (!props) { props = 'info'; } else if (!/(?:^|\|)info(?:$|\|)/.test(props)) { props += '|info'; } } // 可接受 "props=" (空 props)。 if (props || props === '') { // retrieve properties. 僅擷取這些屬性。 action[1] += '&props=' + props; if (props.includes('|')) { // 對於多種屬性,不特別取之。 props = null; } } if (options.languages) { // retrieve languages, language to callback. 僅擷取這些語言。 action[1] += '&languages=' + options.languages; } // console.log(options); // console.log(action); // console.trace([ key, arguments, action ]); // console.log(wiki_API.session_of_options(options)); // library_namespace.log('wikidata_entity: API_URL: ' + API_URL); // library_namespace.log('wikidata_entity: action: ' + action); var _arguments = arguments; // TODO: wiki_API.query(action, function handle_result(data, error) { error = wiki_API.query.handle_error(data, error); // 檢查伺服器回應是否有錯誤資訊。 if (error) { if (error.code === 'param-missing') { library_namespace.error( /** * 可能是錯把 "category" 之類當作 sites name?? * * wikidata_entity: [param-missing] A parameter that is * required was missing. (Either provide the item "ids" or * pairs of "sites" and "titles" for corresponding pages) */ 'wikidata_entity: 未設定欲取得之特定實體 id。請確定您的要求,尤其是 sites 存在: ' + decodeURI(action[0])); } else { library_namespace.error('wikidata_entity: ' + error); } callback(data, error); return; } // assert: library_namespace.is_Object(data): // {entities:{Q1:{pageid:129,lastrevid:0,id:'P1',labels:{},claims:{},...},P1:{id:'P1',missing:''}},success:1} // @see https://www.mediawiki.org/wiki/Wikibase/DataModel/JSON // @see https://www.wikidata.org/wiki/Special:ListDatatypes if (data && data.entities) { data = data.entities; var list = []; for ( var id in data) { list.push(data[id]); } data = list; if (data.length === 1) { data = data[0]; if (props && (props in data)) { data = data[props]; } else { if (wiki_API.is_page_data(key)) { library_namespace.debug('id - ' + data.id + ' 對應頁面: ' + wiki_API.title_link_of(key), 1, 'wikidata_entity'); data[KEY_CORRESPOND_PAGE] = key; if (false && !data.lastrevid) { library_namespace .log('wikidata_entity: action: ' + action); console.trace(_arguments); } } // assert: KEY_get_entity_value, KEY_SESSION // is NOT in data Object.defineProperty(data, KEY_get_entity_value, { value : wikidata_entity_value }); if (options && options[KEY_SESSION]) { // for .resolve_item data[KEY_SESSION] = options[KEY_SESSION]; } } } } if (property && data) { property = (data.claims // session.structured_data() // [[commons:Commons:Structured data]] || data.statements || data)[property]; } if (property) { wikidata_datavalue(property, callback, options); } else { callback(data); } }, null, options); } /** * 取得特定屬性值。 * * @param {String}[property] * 取得特定屬性值。 * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * @param {Function}[callback] * 回調函數。 callback(轉成JavaScript的值) * * @returns 屬性的值 * * @inner */ function wikidata_entity_value(property, options, callback) { // console.trace(property); if (Array.isArray(property)) { // e.g., entity.value(['property','property']) var property_list = property; property = Object.create(null); property_list.forEach(function(key) { property[key] = null; }); } if (library_namespace.is_Object(property)) { // e.g., entity.value({'property':'language'}) if (callback) { ; } // console.trace(property); // TODO: for callback for ( var key in property) { var _options = property[key]; if (typeof _options === 'string' && PATTERN_PROJECT_CODE_i.test(_options)) { _options = Object.assign({ language : _options.toLowerCase() }, options); } else { _options = options; } property[key] = wikidata_entity_value.call(this, key, _options); } return property; } var value, language = wiki_API.site_name(options, { get_all_properties : true }).language, matched = typeof property === 'string' && property.match(PATTERN_property_id); if (matched) { property = +matched[1]; } if (property === 'label') { value = this.labels && this.labels[language]; } else if (property === 'alias') { value = this.aliases && this.aliases[language]; } else if (property === 'sitelink') { value = this.sitelinks && this.sitelinks[language]; } else if (typeof property === 'number') { if (!this.claims) { library_namespace .warn('wikidata_entity_value: 未取得 entity.claims!'); value = null; } else { value = this.claims['P' + property]; } } else if (value = wikidata_search.use_cache(property, Object.assign({ type : 'property' }, options))) { // 一般 property if (!this.claims) { library_namespace .warn('wikidata_entity_value: 未取得 entity.claims!'); value = null; } else if (Array.isArray(value)) { var property_list = value; for (var index = 0; index < property_list.length; index++) { var property_name = property_list[index]; if (property_name in this.claims) { value = this.claims[property_name]; library_namespace.log('wikidata_entity_value: 自多個 "' + property + '" 同名屬性中 (' + property_list.join(', ') + '),選擇第一個有屬性值的 ' + property_name + '。'); break; } } } else { value = this.claims[value]; } } else { library_namespace .error('wikidata_entity_value: Cannot deal with property [' + property + ']'); return; } if (options && options.resolve_item) { // console.trace([ property, value ]); value = wikidata_datavalue(value); if (Array.isArray(value)) { // 有的時候因為操作錯誤,所以會有相同的屬性值。但是這一種情況應該要更正原資料。 // value = value.unique(); } this[KEY_SESSION][KEY_HOST_SESSION].data(value, callback); return value; } return wikidata_datavalue(value, callback, options); } // ------------------------------------------------------------------------ // test if is Q4167410: Wikimedia disambiguation page 維基媒體消歧義頁 // [[Special:链接到消歧义页的页面]]: 頁面內容含有 __DISAMBIG__ (或別名) 標籤會被作為消歧義頁面。 // CeL.wiki.data.is_DAB(entity) function is_DAB(entity, callback) { var property = entity && entity.claims && entity.claims.P31; var entity_is_DAB = property // wikidata 的 item 或 Q4167410 需要手動加入,非自動連結。 // 因此不能光靠 Q4167410 準確判定是否為消歧義頁。其他屬性相同。 // 準確判定得自行檢查原維基之資訊,例如檢查 action=query&prop=info。 ? wikidata_datavalue(property) === 'Q4167410' // : entity && /\((?:disambiguation|消歧義|消歧義|曖昧さ回避)\)$/ // 檢查標題是否有 "(消歧義)" 之類。 .test(typeof entity === 'string' ? entity : entity.title); // 基本上只有 Q(entity, 可連結 wikipedia page) 與 P(entity 的屬性) 之分。 // 再把各 wikipedia page 手動加入 entity 之 sitelink。 // TODO: expand 之後檢查 __DISAMBIG__ page property // TODO: 檢查 [[Category:All disambiguation pages]] // TODO: 檢查 // https://en.wikipedia.org/w/api.php?action=query&titles=title&prop=pageprops // 看看是否 ('disambiguation' in page_data.pageprops); // 這方法即使在 wikipedia 沒 entity 時依然有效。 if (callback) { callback(entity_is_DAB, entity); } return entity_is_DAB; } // ------------------------------------------------------------------------ // TODO: 自 root 開始