cejs
Version:
A JavaScript module framework that is simple to use.
1,654 lines (1,494 loc) • 212 kB
JavaScript
/**
* @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 開始