wikiapi.js

  1. /**
  2. * @name Wikiapi.js
  3. *
  4. * @fileoverview Main codes of module wikiapi (class Wikiapi)
  5. */
  6. 'use strict';
  7. /**
  8. * @description CeJS controller
  9. *
  10. * @type Function
  11. *
  12. * @ignore
  13. * @inner
  14. *
  15. * @see https://github.com/kanasimi/CeJS
  16. */
  17. let CeL;
  18. try {
  19. // Load CeJS library.
  20. CeL = require('cejs');
  21. if (typeof CeL.then === 'function' && typeof window === "object" && window.CeL) {
  22. // assert: @Snowpack
  23. CeL = window.CeL;
  24. }
  25. } catch (e) /* istanbul ignore next: Only for debugging locally */ {
  26. // https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
  27. // const Wikiapi = require('./wikiapi.js');
  28. require('./_CeL.loader.nodejs.js');
  29. CeL = globalThis.CeL;
  30. }
  31. // assert: typeof CeL === 'function'
  32. // Load modules.
  33. // @see `wiki loader.js`:
  34. // https://github.com/kanasimi/wikibot/blob/master/wiki%20loader.js
  35. CeL.run(['interact.DOM', 'application.debug',
  36. // 載入不同地區語言的功能 for wiki.work()。
  37. 'application.locale',
  38. // 載入操作維基百科的主要功能。
  39. 'application.net.wiki',
  40. // Optional 可選功能
  41. 'application.net.wiki.data', 'application.net.wiki.admin',
  42. // Add color to console messages. 添加主控端報告的顏色。
  43. 'interact.console',
  44. // for 'application.platform.nodejs': CeL.env.arg_hash, wiki_API.cache(),
  45. // CeL.fs_mkdir(), wiki_API.read_dump()
  46. 'application.storage']);
  47. // --------------------------------------------------------
  48. /**
  49. * @description syntactic sugar for CeJS MediaWiki module. CeL.net.wiki === CeL.wiki
  50. *
  51. * @ignore
  52. * @inner
  53. */
  54. const wiki_API = CeL.net.wiki;
  55. /**
  56. * key to get {@link wiki_API} operator when using {@link wiki_API}.
  57. *
  58. * @type Symbol
  59. *
  60. * @ignore
  61. * @inner
  62. */
  63. const KEY_SESSION = wiki_API.KEY_SESSION;
  64. // Set default language. 改變預設之語言。
  65. wiki_API.set_language('en');
  66. /**
  67. * @description key to get {@link wiki_API} operator inside {@link Wikiapi}.
  68. * <code>this[KEY_wiki_session]</code> inside module code will get {@link wiki_API} operator.
  69. *
  70. * @type Symbol
  71. *
  72. * @ignore
  73. * @inner
  74. */
  75. const KEY_wiki_session = Symbol('wiki_API session');
  76. // for debug
  77. // Wikiapi.KEY_wiki_session = KEY_wiki_session;
  78. /**
  79. * @description main Wikiapi operator 操作子.
  80. *
  81. * @param {String|Object} [API_URL] - language code or service endpoint of MediaWiki project.<br />
  82. * Input {Object} will be treat as options.
  83. *
  84. * @class
  85. */
  86. function Wikiapi(API_URL) {
  87. const wiki_session = new wiki_API(null, null, API_URL);
  88. // this[KEY_wiki_session] = new wiki_API(null, null, API_URL);
  89. setup_wiki_session.call(this, wiki_session);
  90. }
  91. // --------------------------------------------------------
  92. /**
  93. * @description Bind {@link wiki_API} instance to {@link Wikiapi} instance
  94. *
  95. * @param {wiki_API} wiki_session - wiki_API session
  96. *
  97. * @ignore
  98. * @inner
  99. */
  100. function setup_wiki_session(wiki_session) {
  101. Object.defineProperty(this, KEY_wiki_session, {
  102. value: wiki_session,
  103. writable: true,
  104. });
  105. }
  106. /**
  107. * @alias login
  108. * @description login into the target MediaWiki API using the provided username and password.
  109. * For bots, see [[Special:BotPasswords]] on your wiki.
  110. *
  111. * @param {String} user_name - Account username.
  112. * @param {String} password - Account's password.
  113. * @param {String} [API_URL] - API URL of target wiki site.
  114. *
  115. * @returns {Promise} Promise object represents {String} login_name
  116. *
  117. * @example <caption><span id="example__Login to wiki site 1">Login to wiki site method 1 (recommend).</span></caption>
  118. // <code>
  119. const wiki = new Wikiapi;
  120. const login_options = {
  121. user_name: '', password: '', API_URL: 'en',
  122. // e.g., lingualibre. @see https://github.com/kanasimi/wikibot/blob/master/wiki%20configuration.sample.js
  123. //data_API_URL: 'https://lingualibre.org/api.php',
  124. //SPARQL_API_URL: 'https://lingualibre.org/bigdata/namespace/wdq/sparql',
  125. // Calling in another domain
  126. origin: '*'
  127. };
  128. await wiki.login(login_options);
  129. // </code>
  130. *
  131. * @example <caption><span id="example__Login to wiki site 2">Login to wiki site method 2.</span></caption>
  132. // <code>
  133. const wiki = new Wikiapi;
  134. await wiki.login('user_name', 'password', 'en');
  135. // </code>
  136. *
  137. * @memberof Wikiapi.prototype
  138. */
  139. function Wikiapi_login(user_name, password, API_URL) {
  140. let options;
  141. if (!password && !API_URL && CeL.is_Object(user_name)) {
  142. options = user_name;
  143. } else if (CeL.is_Object(API_URL)) {
  144. options = { ...API_URL, user_name, password };
  145. } else {
  146. options = { user_name, password, API_URL };
  147. }
  148. function Wikiapi_login_executor(resolve, reject) {
  149. const wiki_session = wiki_API.login({
  150. preserve_password: true,
  151. ...options,
  152. API_URL: options.API_URL || this[KEY_wiki_session].API_URL,
  153. callback(login_name, error) {
  154. if (error) {
  155. reject(error);
  156. } else {
  157. resolve(login_name);
  158. }
  159. },
  160. // task_configuration_page: 'page title',
  161. });
  162. setup_wiki_session.call(this, wiki_session);
  163. }
  164. return new Promise(Wikiapi_login_executor.bind(this));
  165. }
  166. // --------------------------------------------------------
  167. /**
  168. * @description attributes of {Object} page_data, will setup by {@link set_page_data_attributes}.
  169. *
  170. * @type Object
  171. *
  172. * @ignore
  173. * @inner
  174. */
  175. const page_data_attributes = {
  176. /**
  177. * @description get {String}page content, maybe undefined.
  178. * 條目/頁面內容 = wiki_API.revision_content(revision)
  179. *
  180. * @type String
  181. */
  182. wikitext: {
  183. get() {
  184. // console.trace(this);
  185. // console.log(wiki_API.content_of(this, 0));
  186. return wiki_API.content_of(this, 0);
  187. }
  188. },
  189. /**
  190. * @description get {Object}revisions
  191. *
  192. * @type Object
  193. */
  194. revision: {
  195. value: function revision(revision_NO) {
  196. return wiki_API.content_of(this, revision_NO);
  197. }
  198. },
  199. /**
  200. * @description get {Attay} parsed data of page_data
  201. *
  202. * @type Array
  203. */
  204. parse: {
  205. value: function parse(options) {
  206. // this === page_data
  207. // options = { ...options, [KEY_SESSION]: this[KEY_wiki_session] };
  208. options = Wikiapi.prototype.append_session_to_options.call(this, options);
  209. // using function parse_page(options) @ wiki_API
  210. return wiki_API.parser(this, options).parse();
  211. // return {Array}parsed
  212. }
  213. },
  214. };
  215. /**
  216. * @description Bind {@link page_data_attributes} to <code>page_data</code>
  217. *
  218. * @param {Object} page_data - page data
  219. * @param {wiki_API} wiki - wiki_API session
  220. *
  221. * @returns {Promise} Promise object represents {Object} page's data
  222. *
  223. * @ignore
  224. * @inner
  225. */
  226. function set_page_data_attributes(page_data, wiki) {
  227. // `page_data` maybe non-object when error occurres.
  228. if (page_data) {
  229. page_data[KEY_wiki_session] = wiki;
  230. Object.defineProperties(page_data, page_data_attributes);
  231. }
  232. return page_data;
  233. }
  234. /**
  235. * @alias page
  236. * @description Given a title, returns the page's data.
  237. *
  238. * @param {String} title - page title
  239. * @param {Object} [options] - options to run this function
  240. *
  241. * @returns {Promise} Promise object represents {Object} page's data
  242. *
  243. * @example <caption>load page</caption>
  244. // <code>
  245. // on Wikipedia...
  246. const wiki = new Wikiapi('en');
  247. // ...or other MediaWiki websites
  248. //const wiki = new Wikiapi('https://awoiaf.westeros.org/api.php');
  249. let page_data = await wiki.page('Universe', {
  250. // You may also set rvprop.
  251. //rvprop: 'ids|content|timestamp|user',
  252. });
  253. console.log(page_data.wikitext);
  254. // </code>
  255. *
  256. * @example <caption>Get multi revisions</caption>
  257. // <code>
  258. const wiki = new Wikiapi;
  259. let page_data = await wiki.page('Universe', {
  260. // Get multi revisions
  261. revisions: 2
  262. });
  263. console.log(page_data.wikitext);
  264. // </code>
  265. *
  266. * @example <caption>parse wiki page (The parser is more powerful than the example. Please refer to link of wikitext parser examples showing in "Features" section of README.md.)</caption>
  267. // <code>
  268. // Usage with other language
  269. const zhwiki = new Wikiapi('zh');
  270. await zhwiki.login('user', 'password');
  271. let page_data = await zhwiki.page('Universe');
  272. // `page_data.parse(options)` will startup the parser process, create page_data.parsed. After .parse(), we can use parsed.each().
  273. const parsed = page_data.parse();
  274. // See all type in wiki_toString @ https://github.com/kanasimi/CeJS/tree/master/application/net/wiki/parser/wikitext.js
  275. // List all template name.
  276. parsed.each('template', token => console.log(token.name));
  277. // List all [[Template:Tl]] token.
  278. parsed.each('Template:Tl', token => console.log(token));
  279. // </code>
  280. *
  281. * @example <caption>Get information from Infobox template</caption>
  282. // <code>
  283. const wiki = new Wikiapi('en');
  284. const page_data = await wiki.page('JavaScript');
  285. const parsed = page_data.parse();
  286. let infobox;
  287. // Read [[w:en:MOS:INFOBOX|Infobox templates]], convert to JSON.
  288. parsed.each('template', template_token => {
  289. if (template_token.name.startsWith('Infobox')) {
  290. infobox = template_token.parameters;
  291. return parsed.each.exit;
  292. }
  293. });
  294. for (const [key, value] of Object.entries(infobox))
  295. infobox[key] = value.toString();
  296. // print json of the infobox
  297. console.log(infobox);
  298. // </code>
  299. *
  300. * @memberof Wikiapi.prototype
  301. */
  302. function Wikiapi_page(title, options) {
  303. function Wikiapi_page_executor(resolve, reject) {
  304. const wiki = this[KEY_wiki_session];
  305. wiki.page(title, (page_data, error) => {
  306. if (error) {
  307. reject(error);
  308. } else {
  309. resolve(set_page_data_attributes(page_data, wiki));
  310. }
  311. }, {
  312. // node.js v12.22.7: Cannot use "?."
  313. rvlimit: options && options.revisions,
  314. ...options
  315. });
  316. }
  317. return new Promise(Wikiapi_page_executor.bind(this));
  318. }
  319. // --------------------------------------------------------
  320. /**
  321. * @alias tracking_revisions
  322. * @description tracking revisions to lookup what revision had added / removed <code>to_search</code>.
  323. *
  324. * @param {String} title - page title
  325. * @param {String} to_search - filter / text to search. to_search(diff, revision, old_revision): `diff` 為從舊的版本 `old_revision` 改成 `revision` 時的差異。
  326. * @param {Object} [options] - options to run this function
  327. *
  328. * @returns {Promise} Promise object represents {Object} newer_revision,
  329. * newer_revision.page: page_data
  330. *
  331. * @memberof Wikiapi.prototype
  332. */
  333. function Wikiapi_tracking_revisions(title, to_search, options) {
  334. function Wikiapi_tracking_revisions_executor(resolve, reject) {
  335. const wiki = this[KEY_wiki_session];
  336. wiki.tracking_revisions(title, to_search, (revision, page_data, error) => {
  337. if (error) {
  338. reject(error);
  339. } else {
  340. if (!revision)
  341. revision = Object.create(null);
  342. revision.page = page_data;
  343. resolve(revision);
  344. }
  345. }, options);
  346. }
  347. return new Promise(Wikiapi_tracking_revisions_executor.bind(this));
  348. }
  349. // --------------------------------------------------------
  350. /**
  351. * @description Handle the result of MediaWiki API when executing edit operation.
  352. *
  353. * @param {Function} reject - reject function
  354. * @param {any} error - error object / message
  355. * @param {any} [result] - result of MediaWiki API
  356. *
  357. * @returns {Boolean} Return <code>true</code> if the edit operation failed.
  358. *
  359. * @ignore
  360. * @inner
  361. */
  362. function reject_edit_error(reject, error, result) {
  363. // skip_edit is not error
  364. if (!error
  365. // e.g., set options.skip_nochange
  366. // @see function do_batch_work_summary @ CeL.application.net.wiki.task
  367. || error === 'nochange' || error === 'skip'
  368. // @see wiki_API_edit.check_data
  369. || Array.isArray(error) && error[0] === Wikiapi.skip_edit[0]) {
  370. return;
  371. }
  372. if (Array.isArray(error) && typeof error[1] === 'string') {
  373. // console.log('' + reject);
  374. // console.trace(error);
  375. error = error[1];
  376. const error_object = new Error(error);
  377. error_object.from_string = error;
  378. error = error_object
  379. // console.log(error);
  380. }
  381. if (result && error && typeof error === 'object')
  382. error.result = result;
  383. reject(error);
  384. return true;
  385. }
  386. /**
  387. * @alias edit_page
  388. * @description edits content of target page.<br />
  389. * Note: for multiple pages, you should use {@link Wikiapi#for_each_page}.<br />
  390. * Note: The function will check sections of [[User talk:user name/Stop]] if somebody tells us needed to stop edit. See <a href="https://en.wikipedia.org/wiki/User:Cewbot/Stop">mechanism to stop operations</a>.
  391. *
  392. * @param {String} title - page title
  393. * @param {String|Function} content - 'wikitext page content' || page_data => 'wikitext'
  394. * @param {Object} [options] - options to run this function. e.g., { summary: '', bot: 1, nocreate: 1, minor: 1 }
  395. *
  396. * @returns {Promise} Promise object represents {Object} result of MediaWiki API
  397. *
  398. * @example <caption>edit page: method 1: basic operation</caption>
  399. // <code>
  400. const enwiki = new Wikiapi;
  401. await enwiki.login('bot name', 'password', 'en');
  402. const SB_page_data = await enwiki.page('Wikipedia:Sandbox');
  403. // You may do some operations on SB_page_data
  404. const parsed = SB_page_data.parse();
  405. parsed.each('template', template_token => {
  406. // modify template token
  407. });
  408. // and then edit it. ** You MUST call enwiki.page() before enwiki.edit()! **
  409. await enwiki.edit(parsed.toString(), { bot: 1, minor: 1, nocreate: 1 });
  410. // exmaple 2: append text in the tail of page content
  411. await enwiki.edit(page_data => {
  412. return page_data.wikitext
  413. + '\nTest edit using {{GitHub|kanasimi/wikiapi}}.';
  414. }, { bot: 1 });
  415. // exmaple 3: replace page content
  416. await enwiki.edit('Just replace by this wikitext', { bot: 1, minor: 1, nocreate: 1, summary: 'test edit' });
  417. // exmaple 4: append a new section
  418. await enwiki.edit('section content', {
  419. section: 'new',
  420. sectiontitle: 'section title',
  421. nocreate : 1,
  422. summary: 'test edit',
  423. });
  424. // </code>
  425. *
  426. * @example <caption>edit page: method 2: modify summary inside function</caption>
  427. // <code>
  428. const enwiki = new Wikiapi;
  429. await enwiki.login('bot name', 'password', 'en');
  430. await enwiki.edit_page('Wikipedia:Sandbox', function (page_data) {
  431. this.summary += ': You may set additional summary inside the function';
  432. delete this.minor;
  433. return page_data.wikitext
  434. + '\nTest edit using {{GitHub|kanasimi/wikiapi}}.';
  435. }, { bot: 1, nocreate: 1, minor: 1, redirects: 1, summary: 'test edit' });
  436. // </code>
  437. *
  438. * @memberof Wikiapi.prototype
  439. */
  440. function Wikiapi_edit_page(title, content, options) {
  441. function Wikiapi_edit_page_executor(resolve, reject) {
  442. const wiki = this[KEY_wiki_session];
  443. // console.trace([title, content]);
  444. // console.trace(`Wikiapi_edit_page 1: ${wiki_API.title_link_of(title)}, ${wiki.actions.length} actions, ${wiki.running}/${wiki.thread_count}/${wiki.actions[wiki_API.KEY_waiting_callback_result_relying_on_this]}.`);
  445. // console.trace(title);
  446. // CeL.set_debug(6);
  447. if (title) {
  448. // console.trace(wiki);
  449. options = { ...options, error_with_symbol: true };
  450. if (false && options.page_to_edit && wiki_API.title_of(options.page_to_edit) !== wiki_API.title_of(title)) {
  451. console.trace('delete options.page_to_edit!');
  452. delete options.page_to_edit;
  453. }
  454. // 預防 page 本身是非法的頁面標題。當 session.page() 出錯時,將導致沒有 .last_page。
  455. if (wiki_API.content_of.had_fetch_content(title)) {
  456. options.page_to_edit = title;
  457. } else {
  458. options.page_title_to_edit = title;
  459. options.page_to_edit = wiki_API.VALUE_set_page_to_edit;
  460. // 設定個僅 debug 用、無功能的註記。
  461. //options[Symbol('page title to edit')] = title;
  462. }
  463. // call wiki_API_prototype_method() @ CeL.application.net.wiki.list
  464. wiki.page(title, (page_data, error) => {
  465. // console.trace(`Set .page_to_edit: ${wiki_API.title_link_of(page_data)} (${title}) (${wiki_API.title_link_of(options.page_to_edit)})`);
  466. // console.trace(options);
  467. // console.log([page_data, error]);
  468. // console.log(wiki.actions[0]);
  469. // if (!page_data) console.trace(page_data);
  470. // 手動指定要編輯的頁面。避免多執行續打亂 wiki.last_page。
  471. // Will set at wiki_API.prototype.next
  472. //options.page_to_edit = page_data;
  473. }, options);
  474. }
  475. // console.trace(`Wikiapi_edit_page 2: ${wiki_API.title_link_of(title)}, ${wiki.actions.length} actions, ${wiki.running}/${wiki.thread_count}/${wiki.actions[wiki_API.KEY_waiting_callback_result_relying_on_this]}.`);
  476. // console.trace(wiki);
  477. // console.trace(wiki.last_page);
  478. // wiki.edit(page contents, options, callback)
  479. wiki.edit(typeof content === 'function' ? function (page_data) {
  480. // console.trace(`Get page_data of ${wiki_API.title_link_of(page_data)} (${title})`);
  481. return content.call(this, set_page_data_attributes(page_data, wiki));
  482. } : content, options, (title, error, result) => {
  483. // console.trace(`Wikiapi_edit_page: callbacked: ${wiki_API.title_link_of(title)} (${wiki.running})`);
  484. // CeL.set_debug(6);
  485. if (!reject_edit_error(reject, error, result)) {
  486. // console.log('Wikiapi_edit_page: resolve');
  487. resolve(title);
  488. }
  489. // console.log('Wikiapi_edit_page: callback() return');
  490. });
  491. // console.trace(`Wikiapi_edit_page 3: ${wiki_API.title_link_of(title)}, ${wiki.actions.length} actions, ${wiki.running}/${wiki.thread_count}/${wiki.actions[wiki_API.KEY_waiting_callback_result_relying_on_this]}.`);
  492. }
  493. return new Promise(Wikiapi_edit_page_executor.bind(this));
  494. }
  495. // <code>return Wikiapi.skip_edit;</code> as a symbol to skip this edit, do not generate
  496. // warning message.
  497. // 可以利用 ((return [ wiki_API.edit.cancel, 'reason' ];)) 來回傳 reason。
  498. // ((return [ wiki_API.edit.cancel, 'skip' ];)) 來跳過 (skip) 本次編輯動作,不特別顯示或處理。
  499. // 被 skip/pass 的話,連警告都不顯現,當作正常狀況。
  500. /**
  501. * @description Return <code>Wikiapi.skip_edit</code> when we running edit function, but do not want to edit current page.
  502. *
  503. * @memberof Wikiapi
  504. */
  505. Wikiapi.skip_edit = [wiki_API.edit.cancel, 'skip'];
  506. // --------------------------------------------------------
  507. /**
  508. * @alias move_page
  509. * @description Move page <code>move_from_title</code> to <code>move_to_title</code>.
  510. *
  511. * @param {Object|String} move_from_title - move from title
  512. * @param {Object|String} move_to_title - move to title
  513. * @param {Object} [options] - options to run this function
  514. *
  515. * @returns {Promise} Promise object represents {String} result of MediaWiki API
  516. *
  517. * @example <caption>Move <code>move_from_title</code> to <code>move_to_title</code>.</caption>
  518. // <code>
  519. await wiki.move_page(move_from_title, move_to_title, { reason, noredirect: true, movetalk: true });
  520. // </code>
  521. *
  522. * @memberof Wikiapi.prototype
  523. */
  524. function Wikiapi_move_page(move_from_title, move_to_title, options) {
  525. function Wikiapi_move_page_executor(resolve, reject) {
  526. const wiki = this[KEY_wiki_session];
  527. // using wiki_API.prototype.move_page()
  528. wiki.move_page(move_from_title, move_to_title, options, (data, error) => {
  529. if (error) {
  530. /**
  531. * <code>
  532. e.g., { code: 'articleexists', info: 'A page of that name already exists, or the name you have chosen is not valid. Please choose another name.', '*': '...' }
  533. e.g., { code: 'missingtitle', info: "The page you specified doesn't exist.", '*': '...' }
  534. </code>
  535. */
  536. reject(error);
  537. } else {
  538. /**
  539. * <code>
  540. e.g., { from: 'from', to: 'to', reason: 'move', redirectcreated: '', moveoverredirect: '' }
  541. </code>
  542. */
  543. resolve(data);
  544. }
  545. }, options);
  546. }
  547. return new Promise(Wikiapi_move_page_executor.bind(this));
  548. }
  549. /**
  550. * @alias move_to
  551. * @description Move to <code>move_to_title</code>. <em>Must call {@link Wikiapi#page} first!</em>
  552. *
  553. * @param {Object|String} move_to_title - move to title
  554. * @param {Object} [options] - options to run this function
  555. *
  556. * @returns {Promise} Promise object represents {String} result of MediaWiki API
  557. *
  558. * @example <caption>Move <code>move_from_title</code> to <code>move_to_title</code>.</caption>
  559. // <code>
  560. page_data = await wiki.page(move_from_title);
  561. await wiki.move_to(move_to_title, { reason: reason, noredirect: true, movetalk: true });
  562. // </code>
  563. *
  564. * @memberof Wikiapi.prototype
  565. */
  566. function Wikiapi_move_to(move_to_title, options) {
  567. function Wikiapi_move_to_executor(resolve, reject) {
  568. const wiki = this[KEY_wiki_session];
  569. if (!wiki.last_page) {
  570. reject(new Error(Wikiapi_move_to.name + ': Must call .page() first! '
  571. // gettext_config:{"id":"cannot-move-to-$1"}
  572. + CeL.gettext('Cannot move to %1', wiki_API.title_link_of(move_to_title))));
  573. return;
  574. }
  575. // using wiki_API.prototype.move_to()
  576. wiki.move_to(move_to_title, options, (data, error) => {
  577. if (error) {
  578. /**
  579. * <code>
  580. e.g., { code: 'articleexists', info: 'A page of that name already exists, or the name you have chosen is not valid. Please choose another name.', '*': '...' }
  581. e.g., { code: 'missingtitle', info: "The page you specified doesn't exist.", '*': '...' }
  582. </code>
  583. */
  584. reject(error);
  585. } else {
  586. /**
  587. * <code>
  588. e.g., { from: 'from', to: 'to', reason: 'move', redirectcreated: '', moveoverredirect: '' }
  589. </code>
  590. */
  591. resolve(data);
  592. }
  593. }, options);
  594. }
  595. return new Promise(Wikiapi_move_to_executor.bind(this));
  596. }
  597. // --------------------------------------------------------
  598. /**
  599. * @alias query
  600. * @description query MediaWiki API manually
  601. *
  602. * @param {Object} parameters - parameters to call MediaWiki API
  603. * @param {Object} [options] - options to run this function
  604. *
  605. * @returns {Promise} Promise object represents {Object} result of MediaWiki API
  606. *
  607. * @example <caption>query flow-parsoid-utils</caption>
  608. // <code>
  609. const wiki = new Wikiapi('mediawiki');
  610. const results = await wiki.query({
  611. action: "flow-parsoid-utils",
  612. content: "<b>bold</b> &amp; <i>italic</i>",
  613. title: "MediaWiki", from: "html", to: "wikitext"
  614. });
  615. // </code>
  616. *
  617. * @memberof Wikiapi.prototype
  618. */
  619. function Wikiapi_query(parameters, options) {
  620. function Wikiapi_query_executor(resolve, reject) {
  621. const wiki = this[KEY_wiki_session];
  622. wiki.query_API(parameters, (data, error) => {
  623. if (error) {
  624. reject(error);
  625. } else {
  626. resolve(data);
  627. }
  628. }, {
  629. post_data_only: true,
  630. ...options
  631. });
  632. }
  633. return new Promise(Wikiapi_query_executor.bind(this));
  634. }
  635. // --------------------------------------------------------
  636. /**
  637. * @alias purge
  638. * @description Purge the cache for the given title.
  639. *
  640. * @param {Object} title - page title
  641. * @param {Object} [options] - options to run this function
  642. *
  643. * @returns {Promise} Promise object represents {Object} page_data
  644. *
  645. * @example <caption>query flow-parsoid-utils</caption>
  646. // <code>
  647. const metawiki = new Wikiapi('meta');
  648. let page_data = await metawiki.purge('Project:Sandbox');
  649. // </code>
  650. *
  651. * @memberof Wikiapi.prototype
  652. */
  653. function Wikiapi_purge(title, options) {
  654. if (CeL.is_Object(title) && !options) {
  655. // shift arguments.
  656. [title, options] = [null, title];
  657. }
  658. function Wikiapi_purge_executor(resolve, reject) {
  659. const wiki = this[KEY_wiki_session];
  660. if (title) {
  661. wiki.page(title);
  662. }
  663. // using wiki_API.purge
  664. wiki.purge((data, error) => {
  665. if (error) {
  666. reject(error);
  667. } else {
  668. resolve(data);
  669. }
  670. }, options);
  671. }
  672. return new Promise(Wikiapi_purge_executor.bind(this));
  673. }
  674. // --------------------------------------------------------
  675. /**
  676. * @description Bind properties to {@link wiki_API} data entity.
  677. * 設定 wikidata entity object,讓我們能直接操作 entity.modify(),並且避免洩露 wiki_API session。
  678. *
  679. * @param {Object} data_entity - wiki_API data entity
  680. *
  681. * @ignore
  682. * @inner
  683. */
  684. function setup_data_entity(data_entity) {
  685. if (!data_entity)
  686. return;
  687. // assert: data_entity[KEY_SESSION].host === this
  688. // console.trace(data_entity[KEY_SESSION].host === this);
  689. delete data_entity[KEY_SESSION];
  690. Object.defineProperties(data_entity, {
  691. [KEY_wiki_session]: { value: this },
  692. modify: { value: modify_data_entity },
  693. });
  694. }
  695. /**
  696. * @description Modify data entity
  697. *
  698. * @param {Object} data_entity - wiki_API data entity
  699. * @param {Object} [options] - options to run this function
  700. *
  701. * @returns {Promise} Promise object represents {Object} result data entity
  702. *
  703. * @ignore
  704. * @inner
  705. */
  706. function modify_data_entity(data_to_modify, options) {
  707. function modify_data_entity_executor(resolve, reject) {
  708. const wiki = this[KEY_wiki_session];
  709. // console.trace(wiki);
  710. // using function wikidata_edit() @
  711. // https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/data.js
  712. // wiki.edit_data(id, data, options, callback)
  713. wiki.data(this).edit_data(data_to_modify || this, options, (data_entity, error) => {
  714. // console.trace([data_entity, error]);
  715. if (error) {
  716. reject(error);
  717. } else {
  718. setup_data_entity.call(wiki, data_entity);
  719. resolve(data_entity);
  720. }
  721. });
  722. }
  723. return new Promise(modify_data_entity_executor.bind(this));
  724. }
  725. /**
  726. * @alias data
  727. * @description Get wikidata entity / property
  728. *
  729. * @param {Object} data_entity - wiki_API data entity
  730. * @param {Object} [options] - options to run this function
  731. *
  732. * @returns {Promise} Promise object represents {Object} wikidata entity / property
  733. *
  734. * @example <caption>Get wikidata entity method 1</caption>
  735. // <code>
  736. const wiki = new Wikiapi;
  737. const data_entity = await wiki.data('Q1');
  738. // Work with other language
  739. console.assert(CeL.wiki.data.value_of(data_entity.labels.zh) === '宇宙');
  740. // </code>
  741. *
  742. * @example <caption>Get wikidata entity of [[Human]]</caption>
  743. // <code>
  744. const wiki = new Wikiapi;
  745. const page_data = await wiki.page('Human');
  746. const data_entity = await wiki.data(page_data);
  747. console.assert(CeL.wiki.data.value_of(data_entity.labels.zh) === '人類');
  748. // </code>
  749. *
  750. * @example <caption>Get wikidata entity method 2: Get P1419 of wikidata entity: 'Universe'</caption>
  751. // <code>
  752. const wiki = new Wikiapi;
  753. // Read, access by title (English), access property P1419
  754. let data = await wiki.data('Universe', 'P1419');
  755. // assert: {Array}data = [ 'shape of the universe', '', ... ]
  756. console.assert(data.includes('shape of the universe'));
  757. // </code>
  758. *
  759. * @example <caption>update wikidata</caption>
  760. // <code>
  761. // Just for test
  762. delete CeL.wiki.query.default_maxlag;
  763. const wiki = new Wikiapi;
  764. await wiki.login('user', 'password', 'test');
  765. // Get https://test.wikidata.org/wiki/Q7
  766. let entity = await wiki.data('Q7');
  767. // search [ language, label ]
  768. //entity = await wiki.data(['en', 'Earth']);
  769. // Reset claim
  770. entity = await wiki.data('Q1841');
  771. await entity.modify({ claims: [{ P3: "old.wav", remove: true }] }, { bot: 1, summary: 'test edit: Remove specific value' });
  772. // Warning: If you want to perform multiple operations on the same property, you need to get the entity again!
  773. entity = await wiki.data('Q1841');
  774. await entity.modify({ claims: [{ P3: "new.wav" }] }, { bot: 1, summary: 'test edit: Add value' });
  775. // Update claim
  776. await entity.modify({ claims: [{ P17: 'Q213280' }] }, { bot: 1, summary: 'test edit: Update claim' });
  777. // Update claim: set country (P17) to 'Test Country 1' (Q213280) ([language, label] as entity)
  778. await entity.modify({ claims: [{ language: 'en', country: [, 'Test Country 1'] }] }, { summary: '' });
  779. // Remove country (P17) : 'Test Country 1' (Q213280)
  780. await entity.modify({ claims: [{ language: 'en', country: [, 'Test Country 1'], remove: true }] }, { summary: '' });
  781. // Update label
  782. await entity.modify({ labels: [{ language: 'zh-tw', value: '地球' }] }, { summary: '' });
  783. // </code>
  784. *
  785. * @memberof Wikiapi.prototype
  786. */
  787. function Wikiapi_data(key, property, options) {
  788. if (CeL.is_Object(property) && !options) {
  789. // shift arguments.
  790. [property, options] = [null, property];
  791. }
  792. function Wikiapi_data_executor(resolve, reject) {
  793. const wiki = this[KEY_wiki_session];
  794. if (false && wiki_API.is_page_data(key)) {
  795. // get entity (wikidata item) of page_data: key
  796. // .page(key): 僅僅設定 .last_page,不會真的再獲取一次頁面內容。
  797. wiki.page(key);
  798. }
  799. if (key.title && !key.site) {
  800. // @see function wikidata_entity() @ CeL.application.net.wiki.data
  801. // 確保引用到的是本 wiki session,不會引用到其他 site。
  802. key = { ...key, site: this.site_name() };
  803. }
  804. // using wikidata_entity() → wikidata_datavalue()
  805. wiki.data(key, property, (data_entity, error) => {
  806. if (error) {
  807. reject(error);
  808. } else {
  809. setup_data_entity.call(wiki, data_entity);
  810. resolve(data_entity);
  811. }
  812. }, options);
  813. }
  814. return new Promise(Wikiapi_data_executor.bind(this));
  815. }
  816. /**
  817. * @alias new_data_entity
  818. * @description Create new entity or property
  819. *
  820. * @param {Object} data_to_modify - Initial data.
  821. * @param {Object} [options] - options to run this function
  822. *
  823. * @returns {Promise} Promise object represents {Object} new entity or property.
  824. *
  825. * @example <caption>Create new entity</caption>
  826. // <code>
  827. const new_entity = await wiki.new_data_entity({ labels: { en: "Evolution in Mendelian Populations" }, P698: "17246615", P932: "1201091" }, { new: 'item' });
  828. // </code>
  829. *
  830. * @memberof Wikiapi.prototype
  831. */
  832. function Wikiapi_new_data_entity(data_to_modify, options) {
  833. function Wikiapi_new_data_entity_executor(resolve, reject) {
  834. options = { new: 'item', ...options };
  835. const wiki = this[KEY_wiki_session];
  836. wiki.edit_data({}, options, (data_entity, error) => {
  837. if (error) {
  838. reject(error);
  839. } else if (data_to_modify) {
  840. delete options.new;
  841. //console.trace([data_entity, options]);
  842. wiki.edit_data(data_entity, data_to_modify, options, (result, error) => {
  843. if (error) {
  844. reject(error);
  845. } else if (false && options.retrieve_entity) {
  846. // reget modified data
  847. this.data(data_entity.id, options).then(resolve, reject);
  848. } else {
  849. //console.trace([data_entity, result]);
  850. //data_entity.latest_result = result;
  851. // data_entity: e.g.,
  852. // {"type":"item","id":"Q123456","labels":{},"descriptions":{},"aliases":{},"claims":{},"sitelinks":{},"lastrevid":123456}
  853. resolve(data_entity);
  854. }
  855. });
  856. } else {
  857. setup_data_entity.call(wiki, data_entity);
  858. resolve(data_entity);
  859. }
  860. });
  861. }
  862. return new Promise(Wikiapi_new_data_entity_executor.bind(this));
  863. }
  864. // --------------------------------------------------------
  865. /**
  866. * @alias SPARQL
  867. * @description Query wikidata via SPARQL
  868. *
  869. * @param {Object} SPARQL - SPARQL to query. Please test it on <a href="https://query.wikidata.org/">Wikidata Query Service</a> first.
  870. * @param {Object} [options] - options to run this function
  871. *
  872. * @returns {Promise} Promise object represents {Array} query result of `SPARQL`.
  873. *
  874. * @example <caption>Get cats</caption>
  875. // <code>
  876. const wikidata_item_list = await wiki.SPARQL(`
  877. SELECT ?item ?itemLabel WHERE {
  878. ?item wdt:P31 wd:Q146.
  879. SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
  880. }
  881. `);
  882. // </code>
  883. *
  884. * @example <caption>Get specific DOI</caption>
  885. // <code>
  886. // for case-insensitive DOI
  887. const wikidata_item_list = await wiki.search('haswbstatement:' + JSON.stringify('P356=10.1371/journal.pone.0029797'), { namespace: 0 });
  888. //wikidata_item_list.map(item => item.title)
  889. // for case-sensitive DOI
  890. const wikidata_item_list = await wiki.SPARQL(`
  891. SELECT ?doi ?item ?itemLabel WHERE {
  892. VALUES ?doi { "10.1371/JOURNAL.PONE.0029797" }
  893. ?item wdt:P356 ?doi.
  894. SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
  895. }`, {
  896. // options.API_URL: custom SPARQL endpoint
  897. API_URL: ''
  898. });
  899. //wikidata_item_list.id_list()
  900. // </code>
  901. *
  902. * @memberof Wikiapi.prototype
  903. */
  904. function Wikiapi_SPARQL(SPARQL, options) {
  905. function Wikiapi_SPARQL_executor(resolve, reject) {
  906. wiki_API.SPARQL(SPARQL, (result, error) => {
  907. if (error) {
  908. reject(error);
  909. } else {
  910. resolve(result);
  911. }
  912. }, this.append_session_to_options(options));
  913. }
  914. return new Promise(Wikiapi_SPARQL_executor.bind(this));
  915. }
  916. // --------------------------------------------------------
  917. /**
  918. * @description Tool function to access page list.<br />
  919. * Please refer to <a href="https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/list.js#:~:text=get_list.type%20=">all supported types</a> (search "get_list.type =").
  920. *
  921. * @example <caption>get list of [[w:en:Category:Chemical_elements]]</caption>
  922. // <code>
  923. const wiki = new Wikiapi;
  924. let page_list = await wiki.categorymembers('Chemical elements');
  925. console.log(page_list);
  926. // Working on multiple pages
  927. await wiki.for_each_page(
  928. // {Array} title liat / page data list
  929. page_list,
  930. page_data => {
  931. // ...
  932. });
  933. // </code>
  934. *
  935. * @example <caption>get pages transcluding {{w:en:Periodic table}}</caption>
  936. // <code>
  937. const wiki = new Wikiapi;
  938. let page_list = await wiki.embeddedin('Template:Periodic table');
  939. console.log(page_list);
  940. // </code>
  941. *
  942. * @example <caption>Process each page of the category.</caption>
  943. // <code>
  944. // Get the list of all pages at once first.
  945. const page_list = await wiki.categorymembers('Category:Articles not listed in the vital article list');
  946. await page_list.each((page_data) => { }, options);
  947. // Imperative code, for huge pages. 用於巨量的頁面。
  948. for await (const page_data of wiki.categorymembers('Category:Articles not listed in the vital article list')) {
  949. console.trace(`page_data #${count}:`, page_data);
  950. }
  951. // Declarative code(?), for huge pages.
  952. await wiki.categorymembers('Category:Articles not listed in the vital article list', {
  953. for_each_page(page_data) {
  954. console.log('page_data:', page_data);
  955. }
  956. });
  957. await wiki.allpages({
  958. async for_each_slice(page_list) {
  959. }
  960. });
  961. // </code>
  962. *
  963. * @example <caption>Process all pages.</caption>
  964. // <code>
  965. let count = 0;
  966. for await (const page_data of wiki.allpages({ namespace: 'Talk', apfrom: wiki.remove_namespace('ABC') })) {
  967. console.trace(`page_data #${count}:`, page_data);
  968. if (++count > 5) break;
  969. }
  970. console.log('Done.');
  971. // </code>
  972. *
  973. * @example <caption>Process all pages with batch_size.</caption>
  974. // <code>
  975. let count = 0;
  976. for await (const page_list of wiki.allpages({ namespace: 'Talk', apfrom: wiki.remove_namespace('ABC'), batch_size: 5 })) {
  977. console.trace('page_list:', page_list);
  978. if (++count > 2) break;
  979. }
  980. console.log('Done.');
  981. // </code>
  982. *
  983. */
  984. // Warning: Won't throw if title is not existed!
  985. // @inner
  986. function Wikiapi_list(list_type, title, options) {
  987. function Wikiapi_list_executor(resolve, reject) {
  988. options = CeL.setup_options(options);
  989. // console.trace(options.for_each_page);
  990. // const wiki = this[KEY_wiki_session];
  991. //console.trace([ list_type, title ]);
  992. wiki_API.list(title, (list/* , target, options */) => {
  993. // console.trace(list);
  994. if (list.error) {
  995. reject(list.error);
  996. } else {
  997. resolve(list);
  998. }
  999. }, this.append_session_to_options({
  1000. type: list_type,
  1001. // namespace: '0|1',
  1002. ...options
  1003. }));
  1004. /**
  1005. * <code>
  1006. // method 2: 使用循環取得資料版:
  1007. wiki.cache({
  1008. // Do not write cache file to disk.
  1009. cache: false,
  1010. type: list_type,
  1011. list: title
  1012. }, (list, error) => {
  1013. if (error) {
  1014. reject(error);
  1015. } else {
  1016. resolve(list);
  1017. }
  1018. },
  1019. // default options === this
  1020. //{ namespace : '0|1' }
  1021. options);
  1022. // NG: 不應使用單次版
  1023. wiki[list_type](title, (list, error) => {
  1024. if (error) {
  1025. reject(error);
  1026. } else {
  1027. resolve(list);
  1028. }
  1029. }, {
  1030. limit: 'max', ...options
  1031. });
  1032. </code>
  1033. */
  1034. }
  1035. return new Promise(Wikiapi_list_executor.bind(this));
  1036. }
  1037. /**
  1038. * @alias for_each_page_in_list
  1039. * @description Syntactic sugar for several kinds of lists.
  1040. *
  1041. * @param {String} type - list type
  1042. * @param {String} [title] - page title if necessary.
  1043. * @param {Function} for_each_page - Executing for each page.
  1044. * @param {Object} [options] - options to run this function.
  1045. * @returns {Promise}
  1046. *
  1047. * @deprecated Please use {@link Wikiapi_list}.
  1048. *
  1049. * @example <caption>List all redirected categories</caption>
  1050. // <code>
  1051. await wiki.for_each_page_in_list('allredirects', page_data => console.log('page_data: ', page_data), { namespace: 'Category' });
  1052. // </code>
  1053. */
  1054. function Wikiapi_for_each_page_in_list(type, title, for_each_page, options) {
  1055. if (options === undefined && typeof title === 'function') {
  1056. // shift arguments
  1057. options = for_each_page;
  1058. for_each_page = title;
  1059. title = undefined;
  1060. }
  1061. return Wikiapi_list.call(this, type, title, {
  1062. for_each_page: for_each_page,
  1063. ...options
  1064. });
  1065. }
  1066. // wrapper for sync functions
  1067. for (const function_name of ('namespace|remove_namespace|is_article|is_namespace|to_namespace|is_talk_namespace|to_talk_page|talk_page_to_main|normalize_title|redirect_target_of|aliases_of_page|is_template'
  1068. // CeL.run('application.net.wiki.featured_content');
  1069. // [].map(wiki.to_talk_page.bind(wiki))
  1070. + '|get_featured_content_configurations').split('|')) {
  1071. Wikiapi.prototype[function_name] = function wrapper() {
  1072. const wiki = this[KEY_wiki_session];
  1073. return wiki[function_name].apply(wiki, arguments);
  1074. };
  1075. }
  1076. const default_list_wait_size = 100;
  1077. // assert: default_list_wait_size >= 1
  1078. // @see get_list.type @
  1079. // https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/list.js
  1080. for (const type of wiki_API.list.type_list) {
  1081. // Cannot use `= (title, options) {}` !
  1082. // arrow function expression DO NOT has this, arguments, super, or
  1083. // new.target keywords.
  1084. Wikiapi.prototype[type] = Wikiapi_get_page_list_of_type;
  1085. function Wikiapi_get_page_list_of_type(title, options) {
  1086. if (options === undefined && CeL.is_Object(title) && !wiki_API.is_page_data(title)) {
  1087. // shift arguments
  1088. options = title;
  1089. title = undefined;
  1090. }
  1091. const _this = this;
  1092. //console.trace(arguments);
  1093. let done, page_queue = [], resolve_queue, waiting_resolve;
  1094. // 餵頁面資料給 resolve()
  1095. function feed_page_data() {
  1096. //console.trace([resolve_queue.length, done, page_queue.length, options.batch_size]);
  1097. if (resolve_queue.length === 0)
  1098. return;
  1099. if (!done && !(page_queue.length >= (options.batch_size >= 1 ? options.batch_size : 1))) {
  1100. //CeL.info(`${feed_page_data.name}: 尚未累積足夠的頁面資料。 (${page_queue.length},${options.batch_size})`);
  1101. //CeL.set_debug(6);
  1102. return;
  1103. }
  1104. const feed_done = page_queue.abort || page_queue.length === 0;
  1105. // 由最早的開始給。
  1106. const value = options.batch_size >= 1 ? page_queue.splice(0, options.batch_size) : page_queue.shift();
  1107. // .shift(): 由最早的開始餵。
  1108. resolve_queue.shift()(feed_done ? { done } : { value });
  1109. }
  1110. function for_each_page(page_data) {
  1111. if (page_queue.abort)
  1112. return CeL.wiki.list.exit;
  1113. //console.trace(page_data, done, page_queue, resolve_queue);
  1114. const return_value = original_for_each_page?.apply(this, arguments);
  1115. if (resolve_queue) {
  1116. if (return_value === CeL.wiki.list.exit) {
  1117. page_queue.abort = done = true;
  1118. page_queue.truncate();
  1119. if (waiting_resolve) {
  1120. //CeL.info(`${for_each_page.name}: 清掉前面累積的。例如 wiki_API.list 剎不住。`);
  1121. waiting_resolve();
  1122. }
  1123. } else {
  1124. page_queue.push(page_data);
  1125. //console.trace(page_queue.length, 'pages in queue');
  1126. feed_page_data();
  1127. // assert: default_list_wait_size >= 1
  1128. const wait_size = options.batch_size > default_list_wait_size ? options.batch_size : default_list_wait_size;
  1129. if (page_queue.length > wait_size) {
  1130. if (waiting_resolve) {
  1131. //CeL.info(`${for_each_page.name}: 清掉前面累積的。例如 wiki_API.list 剎不住。`);
  1132. waiting_resolve();
  1133. }
  1134. //CeL.info(`${for_each_page.name}: 已經累積太多頁面資料(${page_queue.length} > ${wait_size}),該緩緩了。`);
  1135. return new Promise(resolve => { waiting_resolve = resolve; });
  1136. }
  1137. }
  1138. }
  1139. return return_value;
  1140. }
  1141. const original_for_each_page = options?.for_each_page;
  1142. options = { ...options, for_each_page, get_list: options?.get_list || !original_for_each_page };
  1143. /**
  1144. * @example <code>
  1145. const page_list = await wiki.embeddedin(template_name, options);
  1146. await page_list.each((page_data) => { }, options);
  1147. * </code>
  1148. */
  1149. const promise = Wikiapi_list.call(this, type, title, options)
  1150. .then((page_list) => {
  1151. // console.trace(page_list);
  1152. // console.trace(page_list.length, 'pages');
  1153. // console.trace(page_queue, resolve_queue);
  1154. if (resolve_queue) {
  1155. done = true;
  1156. while (resolve_queue.length > 0)
  1157. feed_page_data();
  1158. }
  1159. page_list.each = Wikiapi_for_each_page.bind(_this, page_list);
  1160. return page_list;
  1161. });
  1162. // Imperative code, for huge pages.
  1163. // 依照標準實作,會先執行一次 next()。之後待 for_each_page() 將此 betch 全 push 進 page_queue 後,再依序 call next()。因此 resolve_queue.length <= 1。
  1164. // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
  1165. promise[Symbol.asyncIterator] = () => {
  1166. resolve_queue = [];
  1167. return {
  1168. next() {
  1169. //console.trace(done, page_queue, resolve_queue);
  1170. if (done && page_queue.length === 0)
  1171. return { done };
  1172. return new Promise( /* executor */ function (resolve, reject) {
  1173. //console.trace(done, page_queue, resolve_queue);
  1174. // 依照標準實作, resolve_queue.length === 0。
  1175. resolve_queue.push(resolve);
  1176. feed_page_data();
  1177. if (waiting_resolve) {
  1178. // 可以繼續接收頁面資料了。
  1179. waiting_resolve();
  1180. waiting_resolve = null;
  1181. }
  1182. });
  1183. },
  1184. return() {
  1185. // e.g., break loop
  1186. page_queue.abort = done = true;
  1187. return { done };
  1188. },
  1189. throw(error) {
  1190. CeL.error('Wikiapi list TODO: Not yet tested');
  1191. page_queue.abort = done = true;
  1192. return { done };
  1193. }
  1194. };
  1195. };
  1196. return promise;
  1197. }
  1198. }
  1199. // --------------------------------------------------------
  1200. /**
  1201. * @alias category_tree
  1202. * @description Get structural category tree with sub-categories of <code>root_category</code>. This is powerful than categorymembers. Get sub-categories with {@link Wikiapi.KEY_subcategories}.
  1203. *
  1204. * @param {String} root_category - category name
  1205. * @param {Object} [options] - options to run this function.
  1206. *
  1207. * @returns {Promise} Promise object represents {Array} category_tree.
  1208. *
  1209. * @example <caption>Checking if [[Category:Countries in North America]] including [[Mexico]].</caption>
  1210. // <code>
  1211. const enwiki = new Wikiapi('en');
  1212. const page_list = await enwiki.category_tree('Countries in North America', 1);
  1213. assert(page_list.some(page_data => page_data.title === 'United States'), 'list category tree: [[Category:Countries in North America]] must includes [[United States]]');
  1214. assert('Mexico' in page_list[Wikiapi.KEY_subcategories], 'list category tree: [[Category:Mexico]] is a subcategory of [[Category:Countries in North America]]');
  1215. // </code>
  1216. *
  1217. * @example <caption>Get all sub-categories of [[Category:Echinodermata]] with depth=2.</caption>
  1218. // <code>
  1219. const wiki = new Wikiapi('commons');
  1220. const all_sub_categories = (await wiki.category_tree('Echinodermata', { depth: 2, cmtype: 'subcat', get_flat_subcategories: true })).flat_subcategories;
  1221. // </code>
  1222. *
  1223. * @memberof Wikiapi.prototype
  1224. */
  1225. function Wikiapi_category_tree(root_category, options) {
  1226. function Wikiapi_category_tree_executor(resolve, reject) {
  1227. const wiki = this[KEY_wiki_session];
  1228. // using wiki_API.prototype.category_tree
  1229. wiki.category_tree(root_category, (list, error) => {
  1230. if (error) {
  1231. reject(error);
  1232. } else {
  1233. resolve(list);
  1234. }
  1235. }, options);
  1236. }
  1237. return new Promise(Wikiapi_category_tree_executor.bind(this));
  1238. }
  1239. /**
  1240. * export key for subcategory 子分類 used in {@link Wikiapi#category_tree}
  1241. *
  1242. * @example
  1243. // <code>
  1244. const KEY_subcategories = Wikiapi.KEY_subcategories;
  1245. // </code>
  1246. */
  1247. Wikiapi.KEY_subcategories = wiki_API.KEY_subcategories;
  1248. // --------------------------------------------------------
  1249. /**
  1250. * @alias search
  1251. * @description search pages include <code>key</code>
  1252. *
  1253. * @param {String} key - key to search
  1254. * @param {Object} [options] - options to run this function.
  1255. *
  1256. * @returns {Promise} Promise object represents {Array} page_list.
  1257. *
  1258. * @example <caption>search pages include key: 霍金</caption>
  1259. // <code>
  1260. const zhwikinews = new Wikiapi('zh.wikinews');
  1261. const page_list = await zhwikinews.search('"霍金"');
  1262. // </code>
  1263. *
  1264. * @memberof Wikiapi.prototype
  1265. */
  1266. function Wikiapi_search(key, options) {
  1267. function Wikiapi_search_executor(resolve, reject) {
  1268. const wiki = this[KEY_wiki_session];
  1269. // using wiki_API.search
  1270. wiki.search(key, (list, error) => {
  1271. if (error) {
  1272. reject(error);
  1273. } else {
  1274. resolve(list);
  1275. }
  1276. }, options);
  1277. }
  1278. return new Promise(Wikiapi_search_executor.bind(this));
  1279. }
  1280. // --------------------------------------------------------
  1281. /**
  1282. * @alias redirects_root
  1283. * @description Get redirects target of <code>title</code>.
  1284. *
  1285. * @param {String} title - page title
  1286. * @param {Object} [options] - options to run this function
  1287. *
  1288. * @returns {Promise} Promise object represents {String} page title or {Object} page data
  1289. *
  1290. * @example <caption>Get redirects target of [[WP:SB]]</caption>
  1291. // <code>
  1292. const redirects_taregt = await enwiki.redirects_root('WP:SB', { get_page_data: true });
  1293. // </code>
  1294. *
  1295. * @memberof Wikiapi.prototype
  1296. */
  1297. function Wikiapi_redirects_root(title, options) {
  1298. function Wikiapi_redirects_root_executor(resolve, reject) {
  1299. // const wiki = this[KEY_wiki_session];
  1300. // using wiki_API.redirects_root
  1301. wiki_API.redirects_root(title, (_title, page_data, error) => {
  1302. if (error) {
  1303. reject(error);
  1304. } else if (options && options.get_page_data) {
  1305. page_data.query_title = title;
  1306. resolve(page_data);
  1307. } else {
  1308. resolve(_title);
  1309. }
  1310. }, this.append_session_to_options(options));
  1311. }
  1312. return new Promise(Wikiapi_redirects_root_executor.bind(this));
  1313. }
  1314. // --------------------------------------------------------
  1315. /**
  1316. * @alias redirect_to
  1317. * @description Get the page that <code>title</code> redirects to.
  1318. *
  1319. * @param {String} title - page title
  1320. * @param {Object} [options] - options to run this function
  1321. *
  1322. * @returns {Promise} Promise object represents {Array} redirect_list
  1323. *
  1324. * @example <caption>Get the page that [[WP:SB]] redirects to.</caption>
  1325. // <code>
  1326. const redirects_list = await enwiki.redirect_to('WP:SB');
  1327. // </code>
  1328. *
  1329. * @memberof Wikiapi.prototype
  1330. */
  1331. function Wikiapi_redirect_to(title, options) {
  1332. function Wikiapi_redirect_to_executor(resolve, reject) {
  1333. // const wiki = this[KEY_wiki_session];
  1334. // using wiki_API.redirect_to
  1335. wiki_API.redirect_to(title, (redirect_data, page_data, error) => {
  1336. if (error) {
  1337. reject(error);
  1338. } else {
  1339. //console.trace(redirect_data);
  1340. //console.trace(page_data);
  1341. resolve(page_data || redirect_data);
  1342. }
  1343. }, this.append_session_to_options(options));
  1344. }
  1345. return new Promise(Wikiapi_redirect_to_executor.bind(this));
  1346. }
  1347. // --------------------------------------------------------
  1348. /**
  1349. * @alias redirects_here
  1350. * @description Get all pages redirects to <code>title</code>.
  1351. *
  1352. * @param {String} title - page title
  1353. * @param {Object} [options] - options to run this function
  1354. *
  1355. * @returns {Promise} Promise object represents {Array} redirect_list
  1356. *
  1357. * @example <caption>Get all pages redirects to [[Wikipedia:Sandbox]]</caption>
  1358. // <code>
  1359. const redirects_list = await enwiki.redirects_here('Wikipedia:Sandbox');
  1360. // </code>
  1361. *
  1362. * @memberof Wikiapi.prototype
  1363. */
  1364. function Wikiapi_redirects_here(title, options) {
  1365. function Wikiapi_redirects_here_executor(resolve, reject) {
  1366. // const wiki = this[KEY_wiki_session];
  1367. // using wiki_API.redirects_here
  1368. wiki_API.redirects_here(title, (root_page_data, redirect_list, error) => {
  1369. if (error) {
  1370. reject(error);
  1371. } else {
  1372. //console.trace(root_page_data);
  1373. //console.trace(redirect_list);
  1374. //console.assert(!redirect_list || redirect_list === root_page_data.redirect_list);
  1375. resolve(redirect_list || root_page_data);
  1376. }
  1377. }, this.append_session_to_options({
  1378. // Making .redirect_list[0] the redirect target.
  1379. include_root: true,
  1380. ...options
  1381. }));
  1382. }
  1383. return new Promise(Wikiapi_redirects_here_executor.bind(this));
  1384. }
  1385. // --------------------------------------------------------
  1386. /**
  1387. * @alias register_redirects
  1388. * @description register page alias. usually used for templates
  1389. *
  1390. * @param {Array|String} page_title_list - list of page titles
  1391. * @param {Object} [options] - options to run this function.
  1392. *
  1393. * @returns {Promise} Promise object represents the operations are done.
  1394. *
  1395. * @example <caption>Do something for categories embedded in template `template_name`.</caption>
  1396. // <code>
  1397. const wiki_session = new Wikiapi;
  1398. const redirect_target_page_data = await wiki_session.register_redirects(template_name, { namespace: 'Template' });
  1399. // Do NOT use `await wiki_session.embeddedin(template_name)`: `template_name` may redirect to redirect_target_page_data.title. We need all categories embedded in redirect_target_page_data.title, not just `template_name`.
  1400. const category_list = await wiki_session.embeddedin(redirect_target_page_data.title, { namespace: 'Category' });
  1401. await wiki_session.for_each_page(category_list, for_category);
  1402. // </code>
  1403. *
  1404. * @example <caption>Register template redirects and get tokens of the templates.</caption>
  1405. // <code>
  1406. const wiki_session = new Wikiapi;
  1407. // e.g., await wiki_session.register_redirects(['Section link', 'Broken anchors'], { namespace: 'Template' });
  1408. await wiki_session.register_redirects([template_name_1, template_name_2, template_name_3], { namespace: 'Template' });
  1409. // ...
  1410. const page_data = await wiki_session.page(page_title);
  1411. // {Array} parsed page content 頁面解析後的結構。
  1412. const parsed = page_data.parse();
  1413. parsed.each('Template:' + template_name_1, function (token, index, parent) {
  1414. // ...
  1415. });
  1416. parsed.each('template', function (token, index, parent) {
  1417. if (wiki_session.is_template(template_name_1, token)) {
  1418. // ...
  1419. return;
  1420. }
  1421. if (wiki_session.is_template(template_name_2, token)) {
  1422. // ...
  1423. return;
  1424. }
  1425. // alternative method:
  1426. switch (wiki_session.redirect_target_of(token)) {
  1427. case wiki_session.redirect_target_of(template_name_1):
  1428. break;
  1429. case wiki_session.redirect_target_of(template_name_2):
  1430. break;
  1431. case wiki_session.redirect_target_of(template_name_3):
  1432. break;
  1433. }
  1434. });
  1435. // </code>
  1436. *
  1437. * @memberof Wikiapi.prototype
  1438. */
  1439. function Wikiapi_register_redirects(page_title_list, options) {
  1440. function Wikiapi_register_redirects_executor(resolve, reject) {
  1441. const wiki = this[KEY_wiki_session];
  1442. wiki.register_redirects(page_title_list, (redirect_list, error) => {
  1443. if (error) {
  1444. reject(error);
  1445. } else {
  1446. // console.trace( redirect_list);
  1447. resolve(redirect_list);
  1448. }
  1449. }, options);
  1450. }
  1451. return new Promise(Wikiapi_register_redirects_executor.bind(this));
  1452. }
  1453. // --------------------------------------------------------
  1454. /**
  1455. * @alias upload
  1456. * @description Upload specified local file to the target wiki.
  1457. *
  1458. * @param {Object} file_data - Upload configurations.<br />
  1459. * Warning: When you are update a file, only the file content will changed. The <code>comment</code> will only show in the file page. The <code>text</code>, ... till <code>categories</code> will <em>all ignored</em>. If you want to update the content of file page, please consider <code>Variable_Map</code> as mentioned in the sample code.<br />
  1460. {<br />
  1461. <ul>
  1462. <li><code>file_path</code>: string - Local path.</li>
  1463. <li><code>media_url</code>: string - URL path. Alternative to <code>file_path</code>.</li>
  1464. <li><code>comment</code>: string - Upload comment.</li>
  1465. <li><code>text</code>: string or {Object} - Either {String}wikitext to fill the file's page,<br />
  1466. or {Object}parameters of <a href="https://commons.wikimedia.org/wiki/Template:Information" target="_blank">{{Information}}</a>:<br />
  1467. {<br />
  1468. <ul>
  1469. <li><code>description</code>: string - File description.</li>
  1470. <li><code>date</code>: date string - YYYY-MM-DD, e.g., <code>new Date()</code> || <code>'2021-01-01'</code>.</li>
  1471. <li><code>source_url</code>: string - Source where the file comes from, typically an URL.</li>
  1472. <li><code>author</code>: string - Author's name or username in wikicode, e.g., URL or <code>'[[User:Yoda|Yoda]]'</code>.</li>
  1473. <li><code>permission</code>: string - License and other usage limitations and warnings, e.g., <code>'{{cc-by-sa-2.5}}'</code>.</li>
  1474. <li><code>other_versions</code>: string - Wikicode links to files with very similar content or derived files.</li>
  1475. <li><code>other_fields</code>: string - Additional table fields added on the bottom of the template.</li>
  1476. </ul>
  1477. }
  1478. </li>
  1479. <li><code>license</code>: array of strings - License under which the file is uploaded, e.g., <code>['{{cc-by-sa-2.5}}']</code>.</li>
  1480. <li><code>additional_text</code>: string - Additional wikitext to place before <code>categories</code>.</li>
  1481. <li><code>categories</code>: array of strings - Categories for this file, e.g., <code>['[[Category:test images]]']</code>.</li>
  1482. <li><code>ignorewarnings</code>: boolean - Set to 1 will overwrite existing files.</li>
  1483. </ul>
  1484. }<br />
  1485. <br />
  1486. See <a href="https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/edit.js" target="_blank">edit.js</a> and search for <q>file_data</q> for other <code>file_data</code> options.
  1487. *
  1488. * @returns {Promise} Promise object represents {String} result of MediaWiki API
  1489. *
  1490. * @example <caption><span id="example__Upload file / media">Upload file / media</span></caption>
  1491. // <code>
  1492. const wiki = new Wikiapi;
  1493. await wiki.login('user', 'password', 'test');
  1494. // Upload a local file directly:
  1495. //let result = await wiki.upload({ file_path: '/local/file/path', comment: '', text: '' || {description: '', ...} });
  1496. let result = await wiki.upload({
  1497. file_path: '/local/file/path', comment: '',
  1498. filename: 'Will set via .file_path or .media_url if not settled.',
  1499. description: '', date: new Date() || '2021-01-01', source_url: 'https://github.com/kanasimi/wikiapi', author: '[[User:user]]', permission: '{{cc-by-sa-2.5}}', other_versions: '', other_fields: '',
  1500. license: ['{{cc-by-sa-2.5}}'], categories: ['[[Category:test images]]'],
  1501. bot: 1, tags: "tag1|tag2",
  1502. // To overwrite existing file
  1503. ignorewarnings: 1,
  1504. });
  1505. // Upload file from URL:
  1506. result = await wiki.upload({ media_url: 'https://media.url/name.jpg', comment: '', text: '' });
  1507. // </code>
  1508. *
  1509. * @example <caption>Upload file and then update content of file page</caption>
  1510. // <code>
  1511. const wiki = new Wikiapi;
  1512. await wiki.login('user', 'password', 'test');
  1513. const variable_Map = new CeL.wiki.Variable_Map();
  1514. variable_Map.set('description', '...');
  1515. //variable_Map.set('date', '...');
  1516. // ...
  1517. //variable_Map.set('other_fields', '...');
  1518. let result = await wiki.upload({
  1519. file_path: '/local/file/path',
  1520. // The <code>comment</code> will only show in the file page when updating file. It is read-only and cannot be modified.
  1521. comment: '',
  1522. // <code>CeL.wiki.Variable_Map</code> is used to update content when update pages or files. It will insert comments around the value, prevent others from accidentally editing the text that will be overwritten.
  1523. // <code>description</code> till <code>other_fields</code> will be auto-setted as values assigned above.
  1524. // The code to do the conversion is in <code>wiki_API.upload</code> @ https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/edit.js
  1525. // There are some examples: https://github.com/kanasimi/wikibot/blob/master/routine/20181016.import_earthquake_shakemap.js https://github.com/kanasimi/wikibot/blob/master/routine/20190629.import_hurricane_track_maps.js
  1526. // More examples to use <code>CeL.wiki.Variable_Map</code>: https://github.com/kanasimi/wikibot/blob/master/routine/20191129.check_language_convention.js
  1527. variable_Map,
  1528. // When set .variable_Map, after successful update, the content of file page will be auto-updated too.
  1529. // To overwrite existing file
  1530. ignorewarnings: 1,
  1531. });
  1532. // </code>
  1533. *
  1534. * @memberof Wikiapi.prototype
  1535. */
  1536. function Wikiapi_upload(file_data) {
  1537. // 2021/3/25 renamed from old name: Wikiapi_upload_file(),
  1538. // Wikiapi_upload_file_executor()
  1539. function Wikiapi_upload_executor(resolve, reject) {
  1540. const wiki = this[KEY_wiki_session];
  1541. wiki.upload(file_data, (result, error) => {
  1542. if (error) {
  1543. reject(error);
  1544. } else {
  1545. resolve(result);
  1546. }
  1547. });
  1548. }
  1549. return new Promise(Wikiapi_upload_executor.bind(this));
  1550. }
  1551. // --------------------------------------------------------
  1552. /**
  1553. * @alias download
  1554. * @description Download file to local path.
  1555. *
  1556. * @param {String} file_title - file title starts with "File:"
  1557. * @param {Object} [options] - options to run this function. Refer to example codes.
  1558. *
  1559. * @returns {Promise} Promise object represents [ {Object}file informations ]
  1560. *
  1561. * @example <caption>Download original file / media to current directory.</span></caption>
  1562. // <code>
  1563. const wiki = new Wikiapi('commons');
  1564. await wiki.download('File:Example.svg');
  1565. // </code>
  1566. *
  1567. * @example <caption>Download file / media with options</span></caption>
  1568. // <code>
  1569. const wiki = new Wikiapi('commons');
  1570. // Download non-vector version of .svg
  1571. await wiki.download('File:Example.svg', { width: 80 });
  1572. // Change width / height
  1573. await wiki.download('File:Example.png', {
  1574. file_name: 'example.png', directory: '/tmp/',
  1575. // reget and overwrite existed file.
  1576. reget: true,
  1577. width: 80,// height: 80
  1578. });
  1579. // Download all files from a (Commons) category and its subcategories WITH directory structure.
  1580. const file_data_list = await wiki.download('Category:name', {
  1581. directory: './',
  1582. max_threads: 4,
  1583. // depth of categories
  1584. depth: 4,
  1585. // Only download files with these formats.
  1586. //download_derivatives : ['wav', 'mp3', 'ogg'],
  1587. // Warning: Will skip downloading if there is no new file!
  1588. download_derivatives : 'mp3',
  1589. // A function to filter result pages. Return `true` if you want to keep the element.
  1590. page_filter(page_data) {
  1591. return page_data.title.includes('word');
  1592. }
  1593. });
  1594. // Download all files from a (Commons) category WITHOUT directory structure.
  1595. for (const page_data of await wiki.categorymembers('Category:name', { namespace: 'File' })) {
  1596. try {
  1597. //if (wiki.is_namespace(page_data, 'File'))
  1598. const file_data = await wiki.download(page_data, { directory: './' });
  1599. } catch (e) { console.error(e); }
  1600. }
  1601. // also
  1602. const categorymembers = await wiki.categorymembers('Category:name', { namespace: 'File' });
  1603. const file_data_list = await wiki.download(categorymembers, { directory: './', no_category_tree: true });
  1604. // </code>
  1605. *
  1606. * @memberof Wikiapi.prototype
  1607. */
  1608. function Wikiapi_download(file_title, options) {
  1609. function Wikiapi_download_executor(resolve, reject) {
  1610. const wiki = this[KEY_wiki_session];
  1611. wiki.download(file_title, options, (result, error) => {
  1612. if (error) {
  1613. // return result.error_titles
  1614. reject(result && result.error_titles && result || error);
  1615. } else {
  1616. resolve(result);
  1617. }
  1618. });
  1619. }
  1620. return new Promise(Wikiapi_download_executor.bind(this));
  1621. }
  1622. // --------------------------------------------------------
  1623. /**
  1624. * @alias for_each_page
  1625. * @description Edit / process pages listing in <code>page_list</code>. Will get the content of multiple pages at once to save transmission times. 一次取得多個頁面內容,以節省傳輸次數。<br />
  1626. * <b>You might be interested in {@link Wikiapi_list}</b>.
  1627. *
  1628. * @param {Array} page_list - title list or page_data list
  1629. * @param {Function} for_each_page - processor for each page. for_each_page(page_data with contents)
  1630. * @param {Object} [options] - options to run this function. Refer to example codes.
  1631. *
  1632. * @returns {Promise} Promise object represents the operations are done.
  1633. *
  1634. * @example <caption>read / edit multiple pages</caption>
  1635. // <code>
  1636. const enwiki = new Wikiapi('en');
  1637. const link_from = await wiki.redirects_here('ABC');
  1638. await wiki.for_each_page(link_from, page_data => {
  1639. // Return `Wikiapi.skip_edit` if you just want to get the page data.
  1640. return Wikiapi.skip_edit;
  1641. return 'You may also modify page contents for each page';
  1642. }, {
  1643. // The options below are sample, not default configuration.
  1644. // denotes we do not edit pages
  1645. no_edit: true,
  1646. // Only needed if you want to modify page.
  1647. summary: 'test edit',
  1648. // Allow content to be emptied. 允許內容被清空。白紙化。
  1649. allow_blanking: true,
  1650. // If the content is not changed, using `skip_nochange` will skip the actual edit. Otherwise, a null edit will be made.
  1651. skip_nochange: true,
  1652. tags: 'bot trial',
  1653. // prevent creating new pages
  1654. // Throw an error if the page doesn't exist.
  1655. // 若頁面不存在/已刪除,則產生錯誤。
  1656. nocreate: 1,
  1657. // denotes this is a bot edit. 標記此編輯為機器人編輯。
  1658. bot: 1,
  1659. minor: 1,
  1660. // options to get page revisions
  1661. page_options: { redirects: 1, rvprop: 'ids|content|timestamp|user' }
  1662. // <code>.for_each_page()</code> will generate a report. It can be written to the specified page.
  1663. log_to: 'log to this page',
  1664. // no warning messages on console. e.g., hide "wiki_API_page: No contents: [[title]]" messages
  1665. no_warning: true,
  1666. // no warning messages and debug messages on console
  1667. no_message: true,
  1668. // For `{{bots|optout=n1,n2}}`
  1669. notification_name: 'n1|n2',
  1670. });
  1671. // </code>
  1672. *
  1673. * @memberof Wikiapi.prototype
  1674. */
  1675. function Wikiapi_for_each_page(page_list, for_each_page, options) {
  1676. function Wikiapi_for_each_page_executor(resolve, reject) {
  1677. options = typeof options === 'string' ? { summary: options } : CeL.setup_options(options);
  1678. const wiki = this[KEY_wiki_session];
  1679. const append_to_this = Array.isArray(for_each_page) && for_each_page[1];
  1680. if (Array.isArray(for_each_page))
  1681. for_each_page = for_each_page[0];
  1682. // console.trace(for_each_page);
  1683. const work_config = {
  1684. log_to: null,
  1685. no_message: options.no_edit,
  1686. ...options,
  1687. //is_async_each: CeL.is_async_function(for_each_page),
  1688. each: [function each(page_data/* , messages, config */) {
  1689. // assert: arguments[2] === work_config && work_config === config
  1690. // assert: config.initial_target_length > 0
  1691. set_page_data_attributes(page_data, wiki);
  1692. return for_each_page.apply(this, arguments);
  1693. }, append_to_this],
  1694. // Run after all list items (pages) processed.
  1695. last(error) {
  1696. // this === options
  1697. // console.trace('last(error)');
  1698. // console.error(error);
  1699. // console.trace('Wikiapi_for_each_page_executor finish:');
  1700. // console.log(options);
  1701. // 提早執行 resolve(), reject() 的話,可能導致後續的程式碼 `options.last`
  1702. // 延後執行,程式碼順序錯亂。
  1703. if (typeof options.last === 'function')
  1704. options.last.call(this, error);
  1705. if (error) {
  1706. if (options.throw_error) {
  1707. reject(error);
  1708. return;
  1709. }
  1710. console.error(typeof error === 'object' ? error : new Error(error));
  1711. }
  1712. resolve(this);
  1713. }
  1714. };
  1715. //console.trace([ options, work_config, ]);
  1716. wiki.work(work_config, page_list);
  1717. }
  1718. return new Promise(Wikiapi_for_each_page_executor.bind(this));
  1719. }
  1720. // --------------------------------------------------------
  1721. /**
  1722. * @alias convert_Chinese
  1723. * @description convert text to traditional Chinese / simplified Chinese.
  1724. *
  1725. * @param {String|Array|Object} text - text or objects to convert. Will convert to {String} using JSON.stringify().
  1726. * @param {Object} [options] - options to run this function
  1727. *
  1728. * @returns {Promise} Promise object represents the converted text.
  1729. *
  1730. * @example <caption>繁簡轉換</caption>
  1731. // <code>
  1732. const wiki = new Wikiapi('en');
  1733. const converted_text = await wiki.convert_Chinese('中国', { uselang: 'zh-hant' });
  1734. const converted_text = await wiki.convert_Chinese('中國', { uselang: 'zh-hans' });
  1735. const converted_list = await wiki.convert_Chinese(['繁體', '簡體'], { uselang: 'zh-hans' });
  1736. // </code>
  1737. *
  1738. * @memberof Wikiapi.prototype
  1739. */
  1740. function Wikiapi_convert_Chinese(text, options) {
  1741. function Wikiapi_convert_Chinese(resolve, reject) {
  1742. if (typeof options === 'string') {
  1743. options = { uselang: options };
  1744. }
  1745. const site_name = this.site_name({ get_all_properties: true });
  1746. // node.js v12.22.7: Cannot use "?."
  1747. if (site_name && site_name.language === 'zh') {
  1748. // 不用再重新造出一個實體。
  1749. options = this.append_session_to_options(options);
  1750. }
  1751. // using wiki_API.search
  1752. wiki_API.convert_Chinese(text, (text, error) => {
  1753. if (error) {
  1754. reject(error);
  1755. } else {
  1756. resolve(text);
  1757. }
  1758. }, options);
  1759. }
  1760. return new Promise(Wikiapi_convert_Chinese.bind(this));
  1761. }
  1762. // --------------------------------------------------------
  1763. // May only test in the [https://tools.wmflabs.org/ Wikimedia Toolforge]
  1764. function Wikiapi_run_SQL(SQL, for_each_row/* , options */) {
  1765. function Wikiapi_run_SQL_executor(resolve, reject) {
  1766. const wiki = this[KEY_wiki_session];
  1767. function run_callback() {
  1768. wiki.SQL_session.SQL(SQL, (error, rows/* , fields */) => {
  1769. if (error) {
  1770. reject(error);
  1771. } else {
  1772. rows.forEach(for_each_row);
  1773. }
  1774. });
  1775. resolve();
  1776. }
  1777. if (wiki.SQL_session) {
  1778. run_callback();
  1779. return;
  1780. }
  1781. wiki.SQL_session = new wiki_API.SQL((error, rows, fields) => {
  1782. if (error) {
  1783. reject(error);
  1784. } else {
  1785. run_callback();
  1786. }
  1787. }, wiki);
  1788. }
  1789. return new Promise(Wikiapi_run_SQL_executor.bind(this));
  1790. }
  1791. // --------------------------------------------------------
  1792. /**
  1793. * @alias setup_layout_element_to_insert
  1794. * @description Setup layout element to insert before insert layout element.
  1795. *
  1796. * @example <caption>insert layout element</caption>
  1797. // <code>
  1798. const layout_element = CeL.wiki.parse('{{Authority control}}');
  1799. await wiki_session.setup_layout_element_to_insert(layout_element);
  1800. const page_data = await wiki.page(page_data);
  1801. const parsed = page_data.parse();
  1802. parsed.insert_layout_element(layout_element);
  1803. parsed.toString();
  1804. // </code>
  1805. *
  1806. * @memberof Wikiapi.prototype
  1807. */
  1808. function Wikiapi_setup_layout_element_to_insert(layout_element, options) {
  1809. function Wikiapi_setup_layout_element_to_insert_executor(resolve, reject) {
  1810. const wiki = this[KEY_wiki_session];
  1811. wiki.setup_layout_element_to_insert(layout_element, (location, error) => {
  1812. if (error)
  1813. reject(error);
  1814. else
  1815. resolve(location);
  1816. }, options);
  1817. }
  1818. return new Promise(Wikiapi_setup_layout_element_to_insert_executor.bind(this));
  1819. }
  1820. // --------------------------------------------------------
  1821. /**
  1822. * @alias get_featured_content
  1823. * @description Get featured content.
  1824. *
  1825. * @param {String|Object} [options] - options to run this function.
  1826. * {String}type (FFA|GA|FA|FL)
  1827. * || {type,on_conflict(FC_title, {from,to})}
  1828. *
  1829. * @returns {Promise} Promise object represents {Object} featured content data hash
  1830. *
  1831. * @example <caption>Get featured content of current wiki site.</caption>
  1832. // <code>
  1833. // MUST including wiki.featured_content first to get featured content!
  1834. CeL.run('application.net.wiki.featured_content');
  1835. // ...
  1836. const FC_data_hash = await wiki.get_featured_content();
  1837. console.assert(FC_data_hash === wiki.FC_data_hash);
  1838. // </code>
  1839. *
  1840. * @memberof Wikiapi.prototype
  1841. */
  1842. function Wikiapi_get_featured_content(options) {
  1843. if (!options || !options.type) {
  1844. const session = this;
  1845. return Wikiapi_get_featured_content.default_types
  1846. .reduce((promise, type) => promise.then(Wikiapi_get_featured_content.bind(session, { ignore_missed: true, ...options, type, })), Promise.resolve());
  1847. if (false) {
  1848. let promise = Promise.resolve();
  1849. Wikiapi_get_featured_content.default_types.forEach(type => {
  1850. promise = promise.then(Wikiapi_get_featured_content.bind(session, { ...options, type }));
  1851. });
  1852. return promise;
  1853. }
  1854. }
  1855. function Wikiapi_get_featured_content_executor(resolve, reject) {
  1856. const wiki = this[KEY_wiki_session];
  1857. wiki.get_featured_content(options, FC_data_hash => {
  1858. try {
  1859. this.FC_data_hash = FC_data_hash;
  1860. resolve(FC_data_hash);
  1861. } catch (e) {
  1862. reject(e);
  1863. }
  1864. });
  1865. }
  1866. return new Promise(Wikiapi_get_featured_content_executor.bind(this));
  1867. }
  1868. Wikiapi_get_featured_content.default_types = 'FFA|GA|FA|FL'.split('|');
  1869. // --------------------------------------------------------
  1870. /**
  1871. * @alias site_name
  1872. * @description Get site name / project name of this {@link Wikiapi} instance.
  1873. *
  1874. * @param {String} [language] - language code of wiki session
  1875. * @param {Object} [options] - options to run this function
  1876. *
  1877. * @returns {String}site name
  1878. *
  1879. * @example <caption>Get site name of {@link Wikiapi} instance.</caption>
  1880. // <code>
  1881. console.log(Wikiapi.site_name('zh', { get_all_properties: true }));
  1882. const wiki = new Wikiapi('en');
  1883. console.assert(wiki.site_name() === 'enwiki');
  1884. console.log(wiki.site_name({ get_all_properties: true }));
  1885. console.assert(wiki.site_name({ get_all_properties: true }).language === 'en');
  1886. // </code>
  1887. *
  1888. * @memberof Wikiapi.prototype
  1889. */
  1890. function Wikiapi_site_name(language, options) {
  1891. return wiki_API.site_name(language, options);
  1892. }
  1893. Wikiapi.site_name = Wikiapi_site_name;
  1894. // --------------------------------------------------------
  1895. // administration functions 管理功能。
  1896. /**
  1897. * @alias delete
  1898. * @description delete page
  1899. *
  1900. * @param {String} title - page title
  1901. * @param {Object} [options] - options to run this function
  1902. *
  1903. * @returns {Promise} Promise object represents response of delete.
  1904. *
  1905. * @example <caption>delete page [[Page to delete]]</caption>
  1906. // <code>
  1907. const testwiki = new Wikiapi('test');
  1908. await testwiki.delete('Page to delete', { reason: 'test' });
  1909. // { title: 'Page to delete', reason: 'test', logid: 00000 }
  1910. // </code>
  1911. *
  1912. * @memberof Wikiapi.prototype
  1913. */
  1914. function Wikiapi_delete(title, options) {
  1915. function Wikiapi_delete_executor(resolve, reject) {
  1916. const wiki = this[KEY_wiki_session];
  1917. // using wiki_API.delete
  1918. wiki.page(title).delete(options, (response, error) => {
  1919. if (error) {
  1920. reject(error);
  1921. } else {
  1922. resolve(response);
  1923. }
  1924. }, options);
  1925. }
  1926. return new Promise(Wikiapi_delete_executor.bind(this));
  1927. }
  1928. // --------------------------------------------------------
  1929. // exports
  1930. Object.assign(Wikiapi.prototype, {
  1931. append_session_to_options(options) {
  1932. // Object.assign({ [KEY_SESSION]: wiki }, options)
  1933. // return { ...options, [KEY_SESSION]: this[KEY_wiki_session] };
  1934. return wiki_API.add_session_to_options(this[KEY_wiki_session], options);
  1935. },
  1936. site_name(options) {
  1937. return Wikiapi_site_name(this[KEY_wiki_session], options);
  1938. },
  1939. login: Wikiapi_login,
  1940. query: Wikiapi_query,
  1941. page: Wikiapi_page,
  1942. tracking_revisions: Wikiapi_tracking_revisions,
  1943. edit_page: Wikiapi_edit_page,
  1944. /**
  1945. * @description edits content of target page.<br />
  1946. * <em>MUST using directly after {@link Wikiapi#page} without any complicated operation! Or rather, the {@link Wikiapi#edit_page} should be used instead.</em><br />
  1947. * Note: for multiple pages, you should use {@link Wikiapi#for_each_page}.<br />
  1948. * Note: The function will check sections of [[User talk:user name/Stop]] if somebody tells us needed to stop edit. See <a href="https://en.wikipedia.org/wiki/User:Cewbot/Stop">mechanism to stop operations</a>.
  1949. *
  1950. * @param {String|Function} content - 'wikitext page content' || page_data => 'wikitext'
  1951. * @param {Object} [options] - options to run this function. e.g., { summary: '', bot: 1, nocreate: 1, minor: 1 }
  1952. *
  1953. * @returns {Promise} Promise object represents {Object} result of MediaWiki API
  1954. *
  1955. * @memberof Wikiapi.prototype
  1956. */
  1957. edit(content, options) {
  1958. return this.edit_page(null, content, options);
  1959. },
  1960. move_to: Wikiapi_move_to,
  1961. move_page: Wikiapi_move_page,
  1962. purge: Wikiapi_purge,
  1963. /**
  1964. * @description Find comment by talk page title and anchor.
  1965. *
  1966. * @example <code>
  1967. const comment_list = await wiki.find_comment(token);
  1968. </code>
  1969. *
  1970. * @param {String|Array}talk_page_and_anchor
  1971. * talk page and anchor: "title#anchor" | [title,anchor]
  1972. * @param {Object}[options]
  1973. * 附加參數/設定選擇性/特殊功能與選項
  1974. *
  1975. * @returns {Promise|Undefined} resolve({Array}comment_list)
  1976. *
  1977. * @memberof Wikiapi.prototype
  1978. */
  1979. find_comment(talk_page_and_anchor, options) {
  1980. const wiki = this[KEY_wiki_session];
  1981. return wiki.find_comment.apply(wiki, arguments);
  1982. },
  1983. /**
  1984. * @description Listen to page modification. 監視最近更改的頁面。<br />
  1985. * wrapper for {@link wiki_API}#listen
  1986. *
  1987. * @param {Function} listener - function(page_data) { return quit_listening; }
  1988. * @param {Object} [options] - options to run this function. e.g., { summary: '', bot: 1, nocreate: 1, minor: 1 }
  1989. *
  1990. * @example <caption>listen to new edits</caption>
  1991. // <code>
  1992. const wiki = new Wikiapi;
  1993. wiki.listen(function for_each_row() {
  1994. // ...
  1995. }, {
  1996. // 檢查的延遲時間。
  1997. delay: '2m',
  1998. filter: function filter_row(row) {
  1999. // The format of `row` is basically the same as page_data.
  2000. },
  2001. // also get diff
  2002. with_diff: { LCS: true, line: true },
  2003. // only for articles (0:main namespace) and talk pages
  2004. namespace: '0|talk',
  2005. });
  2006. // </code>
  2007. *
  2008. * @memberof Wikiapi.prototype
  2009. */
  2010. listen(listener, options) {
  2011. const wiki = this[KEY_wiki_session];
  2012. wiki.listen(listener, options);
  2013. },
  2014. category_tree: Wikiapi_category_tree,
  2015. search: Wikiapi_search,
  2016. redirects_root: Wikiapi_redirects_root,
  2017. redirect_to: Wikiapi_redirect_to,
  2018. // Warning: 採用 wiki_API.redirects_here(title) 才能追溯重新導向的標的。
  2019. // wiki.redirects() 無法追溯重新導向的標的!
  2020. redirects_here: Wikiapi_redirects_here,
  2021. register_redirects: Wikiapi_register_redirects,
  2022. upload: Wikiapi_upload,
  2023. download: Wikiapi_download,
  2024. get_featured_content: Wikiapi_get_featured_content,
  2025. for_each_page: Wikiapi_for_each_page,
  2026. for_each_page_in_list: Wikiapi_for_each_page_in_list,
  2027. delete: Wikiapi_delete,
  2028. data: Wikiapi_data,
  2029. new_data_entity: Wikiapi_new_data_entity,
  2030. SPARQL: Wikiapi_SPARQL,
  2031. convert_Chinese: Wikiapi_convert_Chinese,
  2032. run_SQL: Wikiapi_run_SQL,
  2033. setup_layout_element_to_insert: Wikiapi_setup_layout_element_to_insert,
  2034. });
  2035. // wrapper for properties
  2036. for (const property_name of ('task_configuration|latest_task_configuration').split('|')) {
  2037. Object.defineProperty(Wikiapi.prototype, property_name, {
  2038. get() {
  2039. const wiki = this[KEY_wiki_session];
  2040. return wiki[property_name];
  2041. }
  2042. });
  2043. }
  2044. module.exports = Wikiapi;
  2045. // export default Wikiapi;