# wikiapi.js

    ﻿/**
     * @name Wikiapi.js
     *
     * @fileoverview Main codes of module wikiapi (class Wikiapi)
     */

    'use strict';

    /**
     * @description CeJS controller
     *
     * @type Function
     * @inner
     *
     * @see https://github.com/kanasimi/CeJS
     */
    let CeL;

    try {
    	// Load CeJS library.
    	CeL = require('cejs');
    	if (typeof CeL.then === 'function' && typeof window === "object" && window.CeL) {
    		// assert: @Snowpack
    		CeL = window.CeL;
    	}

    } catch (e) /* istanbul ignore next: Only for debugging locally */ {
    	// https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
    	// const Wikiapi = require('./wikiapi.js');
    	require('./_CeL.loader.nodejs.js');
    	CeL = globalThis.CeL;
    }
    // assert: typeof CeL === 'function'

    // Load modules.
    // @see `wiki loader.js`:
    // https://github.com/kanasimi/wikibot/blob/master/wiki%20loader.js
    CeL.run(['interact.DOM', 'application.debug',
    	// 載入不同地區語言的功能 for wiki.work()。
    	'application.locale',
    	// 載入操作維基百科的主要功能。
    	'application.net.wiki',
    	// Optional 可選功能
    	'application.net.wiki.data', 'application.net.wiki.admin',
    	// Add color to console messages. 添加主控端報告的顏色。
    	'interact.console',
    	// for 'application.platform.nodejs': CeL.env.arg_hash, wiki_API.cache(),
    	// CeL.fs_mkdir(), wiki_API.read_dump()
    	'application.storage']);

    // --------------------------------------------------------

    /**
     * @description syntactic sugar for CeJS MediaWiki module. CeL.net.wiki === CeL.wiki
     *
     * @inner
     */
    const wiki_API = CeL.net.wiki;
    /**
     * key to get {@link wiki_API} operator when using {@link wiki_API}.
     *
     * @type Symbol
     *
     * @inner
     */
    const KEY_SESSION = wiki_API.KEY_SESSION;

    // Set default language. 改變預設之語言。
    wiki_API.set_language('en');

    /**
     * @description key to get {@link wiki_API} operator inside {@link Wikiapi}.
     * <code>this[KEY_wiki_session]</code> inside module code will get {@link wiki_API} operator.
     *
     * @type Symbol
     *
     * @inner
     */
    const KEY_wiki_session = Symbol('wiki_API session');
    // for debug
    // Wikiapi.KEY_wiki_session = KEY_wiki_session;

    /**
     * @description main Wikiapi operator 操作子.
     *
     * @param {String|Object} [API_URL]	- language code or service endpoint of MediaWiki project.<br />
     *            Input {Object} will be treat as options.
     *
     * @class
     */
    function Wikiapi(API_URL) {
    	const wiki_session = new wiki_API(null, null, API_URL);
    	// this[KEY_wiki_session] = new wiki_API(null, null, API_URL);
    	setup_wiki_session.call(this, wiki_session);
    }

    // --------------------------------------------------------

    /**
     * @description Bind {@link wiki_API} instance to {@link Wikiapi} instance
     *
     * @param {wiki_API} wiki_session	- wiki_API session
     *
     * @inner
     */
    function setup_wiki_session(wiki_session) {
    	Object.defineProperty(this, KEY_wiki_session, {
    		value: wiki_session,
    		writable: true,
    	});
    }

    /**
     * @alias login
     * @description login into the target MediaWiki API using the provided username and password.
     * For bots, see [[Special:BotPasswords]] on your wiki.
     *
     * @param {String} user_name	- Account username.
     * @param {String} password		- Account's password.
     * @param {String} [API_URL]	- API URL of target wiki site.
     *
     * @returns {Promise} Promise object represents {String} login_name
     *
     * @example <caption><span id="example__Login to wiki site 1">Login to wiki site method 1.</span></caption>
    // <code>
    const wiki = new Wikiapi;
    const login_options = {
    	user_name: '', password: '', API_URL: 'en',
    	// Ror lingualibre only. @see https://github.com/kanasimi/wikibot/blob/master/wiki%20configuration.sample.js
    	//data_API_URL: 'https://lingualibre.org/api.php',
    	//SPARQL_API_URL: 'https://lingualibre.org/bigdata/namespace/wdq/sparql',
    	// Calling in another domain
    	origin: '*'
    };
    await wiki.login(login_options);
    // </code>
     *
     * @example <caption><span id="example__Login to wiki site 2">Login to wiki site method 2.</span></caption>
    // <code>
    const wiki = new Wikiapi;
    await wiki.login('user_name', 'password', 'en');
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_login(user_name, password, API_URL) {
    	let options;
    	if (!password && !API_URL && CeL.is_Object(user_name)) {
    		options = user_name;
    	} else if (CeL.is_Object(API_URL)) {
    		options = { ...API_URL, user_name, password };
    	} else {
    		options = { user_name, password, API_URL };
    	}

    	function Wikiapi_login_executor(resolve, reject) {
    		const wiki_session = wiki_API.login({
    			preserve_password: true,
    			...options,

    			API_URL: options.API_URL || this[KEY_wiki_session].API_URL,
    			callback(login_name, error) {
    				if (error) {
    					reject(error);
    				} else {
    					resolve(login_name);
    				}
    			},
    			// task_configuration_page: 'page title',
    		});
    		setup_wiki_session.call(this, wiki_session);
    	}

    	return new Promise(Wikiapi_login_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @description attributes of {Object} page_data, will setup by {@link set_page_data_attributes}.
     *
     * @type Object
     *
     * @inner
     */
    const page_data_attributes = {
    	/**
    	 * @description get {String}page content, maybe undefined.
    	 * 條目/頁面內容 = wiki_API.revision_content(revision)
    	 *
    	 * @type String
    	 */
    	wikitext: {
    		get() {
    			// console.trace(this);
    			// console.log(wiki_API.content_of(this, 0));
    			return wiki_API.content_of(this, 0);
    		}
    	},
    	/**
    	 * @description get {Object}revisions
    	 *
    	 * @type Object
    	 */
    	revision: {
    		value: function revision(revision_NO) {
    			return wiki_API.content_of(this, revision_NO);
    		}
    	},
    	/**
    	 * @description get {Attay} parsed data of page_data
    	 *
    	 * @type Array
    	 */
    	parse: {
    		value: function parse(options) {
    			// this === page_data

    			// options = { ...options, [KEY_SESSION]: this[KEY_wiki_session] };
    			options = Wikiapi.prototype.append_session_to_options.call(this, options);

    			// using function parse_page(options) @ wiki_API
    			return wiki_API.parser(this, options).parse();
    			// return {Array}parsed
    		}
    	},
    };

    /**
     * @description Bind {@link page_data_attributes} to <code>page_data</code>
     *
     * @param {Object} page_data	- page data
     * @param {wiki_API} wiki		- wiki_API session
     *
     * @returns {Promise} Promise object represents {Object} page's data
     *
     * @inner
     */
    function set_page_data_attributes(page_data, wiki) {
    	// `page_data` maybe non-object when error occurres.
    	if (page_data) {
    		page_data[KEY_wiki_session] = wiki;
    		Object.defineProperties(page_data, page_data_attributes);
    	}
    	return page_data;
    }

    /**
     * @alias page
     * @description given a title, returns the page's data.
     *
     * @param {String} title		- page title
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} page's data
     *
     * @example <caption>load page</caption>
    // <code>
    // on Wikipedia...
    const wiki = new Wikiapi('en');
    // ...or other MediaWiki websites
    //const wiki = new Wikiapi('https://awoiaf.westeros.org/api.php');
    let page_data = await wiki.page('Universe', {
    	// You may also set rvprop.
    	//rvprop: 'ids|content|timestamp|user',
    });
    console.log(page_data.wikitext);
    // </code>
     *
     * @example <caption>Get multi revisions</caption>
    // <code>
    const wiki = new Wikiapi;
    let page_data = await wiki.page('Universe', {
    	// Get multi revisions
    	revisions: 2
    });
    console.log(page_data.wikitext);
    // </code>
     *
     * @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>
    // <code>
    // Usage with other language
    const zhwiki = new Wikiapi('zh');
    await zhwiki.login('user', 'password');
    let page_data = await zhwiki.page('Universe');

    // `page_data.parse(options)` will startup the parser process, create page_data.parsed. After .parse(), we can use parsed.each().
    const parsed = page_data.parse();

    // See all type in wiki_toString @ https://github.com/kanasimi/CeJS/tree/master/application/net/wiki/parser/wikitext.js
    // List all template name.
    parsed.each('template', token => console.log(token.name));
    // List all [[Template:Tl]] token.
    parsed.each('Template:Tl', token => console.log(token));
    // </code>
     *
     * @example <caption>Get information from Infobox template</caption>
    // <code>
    const wiki = new Wikiapi('en');
    const page_data = await wiki.page('JavaScript');
    const parsed = page_data.parse();
    let infobox;
    // Read Infobox templates, convert to JSON.
    parsed.each('template', template_token => {
    	if (template_token.name.startsWith('Infobox')) {
    		infobox = template_token.parameters;
    		return parsed.each.exit;
    	}
    });
    for (const [key, value] of Object.entries(infobox))
    	infobox[key] = value.toString();
    // print json of the infobox
    console.log(infobox);
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_page(title, options) {
    	function Wikiapi_page_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.page(title, (page_data, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(set_page_data_attributes(page_data, wiki));
    			}
    		}, {
    			// node.js v12.22.7: Cannot use "?."
    			rvlimit: options && options.revisions,
    			...options
    		});
    	}

    	return new Promise(Wikiapi_page_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias tracking_revisions
     * @description tracking revisions to lookup what revision had added / removed <code>to_search</code>.
     *
     * @param {String} title		- page title
     * @param {String} to_search	- filter / text to search. to_search(diff, revision, old_revision): `diff` 為從舊的版本 `old_revision` 改成 `revision` 時的差異。
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} newer_revision,
     *          newer_revision.page: page_data
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_tracking_revisions(title, to_search, options) {
    	function Wikiapi_tracking_revisions_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.tracking_revisions(title, to_search, (revision, page_data, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				if (!revision)
    					revision = Object.create(null);
    				revision.page = page_data;
    				resolve(revision);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_tracking_revisions_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @description Handle the result of MediaWiki API when executing edit operation.
     *
     * @param {Function} reject	- reject function
     * @param {any} error		- error object / message
     * @param {any} [result]	- result of MediaWiki API
     *
     * @returns {Boolean} Return <code>true</code> if the edit operation failed.
     *
     * @inner
     */
    function reject_edit_error(reject, error, result) {
    	// skip_edit is not error
    	if (!error
    		// @see wiki_API_edit.check_data
    		|| Array.isArray(error) && error[0] === Wikiapi.skip_edit[0]) {
    		return;
    	}

    	if (Array.isArray(error) && typeof error[1] === 'string') {
    		// console.log('' + reject);
    		// console.trace(error);
    		error = error[1];
    		const error_object = new Error(error);
    		error_object.from_string = error;
    		error = error_object
    		// console.log(error);
    	}

    	if (result && typeof error === 'object')
    		error.result = result;
    	reject(error);
    	return true;
    }

    /**
     * @alias edit_page
     * @description edits content of target page.<br />
     * Note: for multiple pages, you should use {@link Wikiapi#for_each_page}.<br />
     * Note: The function will check sections of [[User talk:user name/Stop]] if somebody tells us needed to stop edit. See <a href="https://zh.wikipedia.org/wiki/User:Cewbot/Stop">mechanism to stop operations</a>.
     *
     * @param {String} title			- page title
     * @param {String|Function} content	- 'wikitext page content' || page_data => 'wikitext'
     * @param {Object} [options]		- options to run this function. e.g., { summary: '', bot: 1, nocreate: 1, minor: 1 }
     *
     * @returns {Promise} Promise object represents {Object} result of MediaWiki API
     *
     * @example <caption>edit page: method 1: basic operation</caption>
    // <code>
    const enwiki = new Wikiapi;
    await enwiki.login('bot name', 'password', 'en');

    const SB_page_data = await enwiki.page('Wikipedia:Sandbox');
    // You may do some operations on SB_page_data
    const parsed = SB_page_data.parse();
    parsed.each('template', template_token => {
    	// modify template token
    });
    // and then edit it. ** You MUST call enwiki.page() before enwiki.edit()! **
    await enwiki.edit(parsed.toString(), { bot: 1, minor: 1, nocreate: 1 });

    // exmaple 2: append text in the tail of page content
    await enwiki.edit(page_data => {
    	return page_data.wikitext
    		+ '\nTest edit using {{GitHub|kanasimi/wikiapi}}.';
    }, { bot: 1 });

    // exmaple 3: replace page content
    await enwiki.edit('Just replace by this wikitext', { bot: 1, minor: 1, nocreate: 1, summary: 'test edit' });

    // exmaple 4: append a new section
    await enwiki.edit('section content', {
    	section: 'new',
    	sectiontitle: 'section title',
    	nocreate : 1,
    	summary: 'test edit',
    });
    // </code>
     *
     * @example <caption>edit page: method 2: modufy summary inside function</caption>
    // <code>
    const enwiki = new Wikiapi;
    await enwiki.login('bot name', 'password', 'en');
    await enwiki.edit_page('Wikipedia:Sandbox', function (page_data) {
    	this.summary += ': You may set additional summary inside the function';
    	delete this.minor;
    	return page_data.wikitext
    		+ '\nTest edit using {{GitHub|kanasimi/wikiapi}}.';
    }, { bot: 1, nocreate: 1, minor: 1, redirects: 1, summary: 'test edit' });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_edit_page(title, content, options) {
    	function Wikiapi_edit_page_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];

    		// console.trace([title, content]);
    		// 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]}.`);
    		// console.trace(title);
    		// CeL.set_debug(6);
    		if (title) {
    			// console.trace(wiki);
    			options = { ...options, error_with_symbol: true };
    			// 預防 page 本身是非法的頁面標題。當 session.page() 出錯時，將導致沒有 .last_page。
    			if (wiki_API.is_page_data(title))
    				options.task_page_data = title;
    			// call wiki_API_prototype_method() @ CeL.application.net.wiki.list
    			wiki.page(title, (page_data, error) => {
    				// console.trace('Set .page_to_edit:');
    				// console.log([title, page_data, error]);
    				// console.log(wiki.actions[0]);

    				// 手動指定要編輯的頁面。避免多執行續打亂 wiki.last_page。
    				options.page_to_edit = page_data;
    			}, options);
    		}
    		// 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]}.`);
    		// console.trace(wiki);
    		// console.trace(wiki.last_page);

    		// wiki.edit(page contents, options, callback)
    		wiki.edit(typeof content === 'function' ? function (page_data) {
    			return content.call(this, set_page_data_attributes(page_data, wiki));
    		} : content, options, (title, error, result) => {
    			// console.trace('Wikiapi_edit_page: callbacked');
    			// console.log(title);
    			// console.log(wiki.running);
    			// CeL.set_debug(6);

    			if (!reject_edit_error(reject, error, result)) {
    				// console.log('Wikiapi_edit_page: resolve');
    				resolve(title);
    			}
    			// console.log('Wikiapi_edit_page: callback() return');
    		});

    		// 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]}.`);
    	}

    	return new Promise(Wikiapi_edit_page_executor.bind(this));
    }

    // <code>return Wikiapi.skip_edit;</code> as a symbol to skip this edit, do not generate
    // warning message.
    // 可以利用 ((return [ wiki_API.edit.cancel, 'reason' ];)) 來回傳 reason。
    // ((return [ wiki_API.edit.cancel, 'skip' ];)) 來跳過 (skip) 本次編輯動作，不特別顯示或處理。
    // 被 skip/pass 的話，連警告都不顯現，當作正常狀況。
    /**
     * @description Return <code>Wikiapi.skip_edit</code> when we running edit function, but do not want to edit current page.
     *
     * @memberof Wikiapi
     */
    Wikiapi.skip_edit = [wiki_API.edit.cancel, 'skip'];

    // --------------------------------------------------------

    /**
     * @alias move_page
     * @description Move page <code>move_from_title</code> to <code>move_to_title</code>.
     *
     * @param {Object|String} move_from_title	- move from title
     * @param {Object|String} move_to_title		- move to title
     * @param {Object} [options]				- options to run this function
     *
     * @returns {Promise} Promise object represents {String} result of MediaWiki API
     *
     * @example <caption>Move <code>move_from_title</code> to <code>move_to_title</code>.</caption>
    // <code>
    await wiki.move_page(move_from_title, move_to_title, { reason, noredirect: true, movetalk: true });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_move_page(move_from_title, move_to_title, options) {
    	function Wikiapi_move_page_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		// using wiki_API.prototype.move_page()
    		wiki.move_page(move_from_title, move_to_title, options, (data, error) => {
    			if (error) {
    				/**
    				 * <code>

    				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.', '*': '...' }
    				e.g., { code: 'missingtitle', info: "The page you specified doesn't exist.", '*': '...' }

    				</code>
    				 */
    				reject(error);
    			} else {
    				/**
    				 * <code>

    				e.g., { from: 'from', to: 'to', reason: 'move', redirectcreated: '', moveoverredirect: '' }

    				</code>
    				 */
    				resolve(data);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_move_page_executor.bind(this));

    }

    /**
     * @alias move_to
     * @description Move to <code>move_to_title</code>. <em>Must call {@link Wikiapi#page} first!</em>
     *
     * @param {Object|String} move_to_title	- move to title
     * @param {Object} [options]			- options to run this function
     *
     * @returns {Promise} Promise object represents {String} result of MediaWiki API
     *
     * @example <caption>Move <code>move_from_title</code> to <code>move_to_title</code>.</caption>
    // <code>
    page_data = await wiki.page(move_from_title);
    await wiki.move_to(move_to_title, { reason: reason, noredirect: true, movetalk: true });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_move_to(move_to_title, options) {
    	function Wikiapi_move_to_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		if (!wiki.last_page) {
    			reject(new Error(Wikiapi_move_to.name + ': Must call .page() first! '
    				// gettext_config:{"id":"cannot-move-to-$1"}
    				+ CeL.gettext('Cannot move to %1', wiki_API.title_link_of(move_to_title))));
    			return;
    		}

    		// using wiki_API.prototype.move_to()
    		wiki.move_to(move_to_title, options, (data, error) => {
    			if (error) {
    				/**
    				 * <code>

    				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.', '*': '...' }
    				e.g., { code: 'missingtitle', info: "The page you specified doesn't exist.", '*': '...' }

    				</code>
    				 */
    				reject(error);
    			} else {
    				/**
    				 * <code>

    				e.g., { from: 'from', to: 'to', reason: 'move', redirectcreated: '', moveoverredirect: '' }

    				</code>
    				 */
    				resolve(data);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_move_to_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias query
     * @description query MediaWiki API manually
     *
     * @param {Object} parameters	- parameters to call MediaWiki API
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} result of MediaWiki API
     *
     * @example <caption>query flow-parsoid-utils</caption>
    // <code>
    const wiki = new Wikiapi('mediawiki');
    const results = await wiki.query({
    	action: "flow-parsoid-utils",
    	content: "<b>bold</b> &amp; <i>italic</i>",
    	title: "MediaWiki", from: "html", to: "wikitext"
    });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_query(parameters, options) {
    	function Wikiapi_query_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.query_API(parameters, (data, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(data);
    			}
    		}, {
    			post_data_only: true,
    			...options
    		});
    	}

    	return new Promise(Wikiapi_query_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias purge
     * @description Purge the cache for the given title.
     *
     * @param {Object} title		- page title
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} page_data
     *
     * @example <caption>query flow-parsoid-utils</caption>
    // <code>
    const metawiki = new Wikiapi('meta');
    let page_data = await metawiki.purge('Project:Sandbox');
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_purge(title, options) {
    	if (CeL.is_Object(title) && !options) {
    		// shift arguments.
    		[title, options] = [null, title];
    	}

    	function Wikiapi_purge_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		if (title) {
    			wiki.page(title);
    		}
    		// using wiki_API.purge
    		wiki.purge((data, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(data);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_purge_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @description Bind properties to {@link wiki_API} data entity.
     * 設定 wikidata entity object，讓我們能直接操作 entity.modify()，並且避免洩露 wiki_API session。
     *
     * @param {Object} data_entity	- wiki_API data entity
     *
     * @inner
     */
    function setup_data_entity(data_entity) {
    	if (!data_entity)
    		return;
    	// assert: data_entity[KEY_SESSION].host === this
    	// console.trace(data_entity[KEY_SESSION].host === this);
    	delete data_entity[KEY_SESSION];

    	Object.defineProperties(data_entity, {
    		[KEY_wiki_session]: { value: this },
    		modify: { value: modify_data_entity },
    	});
    }

    /**
     * @description Modify data entity
     *
     * @param {Object} data_entity	- wiki_API data entity
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} result data entity
     *
     * @inner
     */
    function modify_data_entity(data_to_modify, options) {
    	function modify_data_entity_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		// console.trace(wiki);

    		// using function wikidata_edit() @
    		// https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/data.js
    		// wiki.edit_data(id, data, options, callback)
    		wiki.data(this).edit_data(data_to_modify || this, options, (data_entity, error) => {
    			// console.trace([data_entity, error]);
    			if (error) {
    				reject(error);
    			} else {
    				setup_data_entity.call(wiki, data_entity);
    				resolve(data_entity);
    			}
    		});
    	}

    	return new Promise(modify_data_entity_executor.bind(this));
    }

    /**
     * @alias data
     * @description Get wikidata entity / property
     *
     * @param {Object} data_entity	- wiki_API data entity
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} wikidata entity / property
     *
     * @example <caption>Get wikidata entity method 1</caption>
    // <code>
    const wiki = new Wikiapi;
    const data_entity = await wiki.data('Q1');
    // Work with other language
    console.assert(CeL.wiki.data.value_of(data_entity.labels.zh) === '宇宙');
    // </code>
     *
     * @example <caption>Get wikidata entity of [[Human]]</caption>
    // <code>
    const wiki = new Wikiapi;
    const page_data = await wiki.page('Human');
    const data_entity = await wiki.data(page_data);
    console.assert(CeL.wiki.data.value_of(data_entity.labels.zh) === '人類');
    // </code>
     *
     * @example <caption>Get wikidata entity method 2: Get P1419 of wikidata entity: 'Universe'</caption>
    // <code>
    const wiki = new Wikiapi;
    // Read, access by title (English), access property P1419
    let data = await wiki.data('Universe', 'P1419');
    // assert: {Array}data = [ 'shape of the universe', '', ... ]
    console.assert(data.includes('shape of the universe'));
    // </code>
     *
     * @example <caption>update wikidata</caption>
    // <code>
    // Just for test
    delete CeL.wiki.query.default_maxlag;
    const wiki = new Wikiapi;
    await wiki.login('user', 'password', 'test');

    // Get https://test.wikidata.org/wiki/Q7
    let entity = await wiki.data('Q7');
    // search [ language, label ]
    //entity = await wiki.data(['en', 'Earth']);

    // Update claim
    await entity.modify({ claims: [{ P17: 'Q213280' }] }, { bot: 1, summary: '' });
    // Update claim: set country (P17) to 'Test Country 1' (Q213280) ([language, label] as entity)
    await entity.modify({ claims: [{ language: 'en', country: [, 'Test Country 1'] }] }, { summary: '' });
    // Remove country (P17) : 'Test Country 1' (Q213280)
    await entity.modify({ claims: [{ language: 'en', country: [, 'Test Country 1'], remove: true }] }, { summary: '' });

    // Update label
    await entity.modify({ labels: [{ language: 'zh-tw', value: '地球' }] }, { summary: '' });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_data(key, property, options) {
    	if (CeL.is_Object(property) && !options) {
    		// shift arguments.
    		[property, options] = [null, property];
    	}

    	function Wikiapi_data_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		if (false && wiki_API.is_page_data(key)) {
    			// get entity (wikidata item) of page_data: key
    			// .page(key): 僅僅設定 .last_page，不會真的再獲取一次頁面內容。
    			wiki.page(key);
    		}
    		if (key.title && !key.site) {
    			// @see function wikidata_entity() @ CeL.application.net.wiki.data
    			// 確保引用到的是本 wiki session，不會引用到其他 site。
    			key = { ...key, site: this.site_name() };
    		}
    		// using wikidata_entity() → wikidata_datavalue()
    		wiki.data(key, property, (data_entity, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				setup_data_entity.call(wiki, data_entity);
    				resolve(data_entity);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_data_executor.bind(this));
    }

    /**
     * @alias new_data_entity
     * @description Create new entity or property
     *
     * @param {Object} data_to_modify	- Initial data.
     * @param {Object} [options]		- options to run this function
     *
     * @returns {Promise} Promise object represents {Object} new entity or property.
     *
     * @example <caption>Create new entity</caption>
    // <code>
    const new_entity = await wiki.new_data_entity({ labels: { en: "Evolution in Mendelian Populations" }, P698: "17246615", P932: "1201091" }, { new: 'item' });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_new_data_entity(data_to_modify, options) {
    	function Wikiapi_new_data_entity_executor(resolve, reject) {
    		options = { new: 'item', ...options };
    		const wiki = this[KEY_wiki_session];
    		wiki.edit_data({}, options, (data_entity, error) => {
    			if (error) {
    				reject(error);
    			} else if (data_to_modify) {
    				delete options.new;
    				//console.trace([data_entity, options]);
    				wiki.edit_data(data_entity, data_to_modify, options, (result, error) => {
    					if (error) {
    						reject(error);
    					} else if (false && options.retrieve_entity) {
    						// reget modified data
    						this.data(data_entity.id, options).then(resolve, reject);
    					} else {
    						//console.trace([data_entity, result]);
    						//data_entity.latest_result = result;
    						// data_entity: e.g.,
    						// {"type":"item","id":"Q123456","labels":{},"descriptions":{},"aliases":{},"claims":{},"sitelinks":{},"lastrevid":123456}
    						resolve(data_entity);
    					}
    				});
    			} else {
    				setup_data_entity.call(wiki, data_entity);
    				resolve(data_entity);
    			}
    		});
    	}

    	return new Promise(Wikiapi_new_data_entity_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias SPARQL
     * @description Query wikidata via SPARQL
     *
     * @param {Object} SPARQL		- SPARQL to query. Please test it on <a href="https://query.wikidata.org/">Wikidata Query Service</a> first.
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Array} query result of `SPARQL`.
     *
     * @example <caption>Get cats</caption>
    // <code>
    const wikidata_item_list = await wiki.SPARQL(`
    SELECT ?item ?itemLabel WHERE {
      ?item wdt:P31 wd:Q146.
      SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
    }
    `);
    // </code>
     *
     * @example <caption>Get specific DOI</caption>
    // <code>
    // for case-insensitive DOI
    const wikidata_item_list = await wiki.search('haswbstatement:' + JSON.stringify('P356=10.1371/journal.pone.0029797'), { namespace: 0 });
    //wikidata_item_list.map(item => item.title)

    // for case-sensitive DOI
    const wikidata_item_list = await wiki.SPARQL(`
    SELECT ?doi ?item ?itemLabel WHERE {
    	VALUES ?doi { "10.1371/JOURNAL.PONE.0029797" }
    	?item wdt:P356 ?doi.
    	SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
    }`, {
    	// options.API_URL: custom SPARQL endpoint
    	API_URL: ''
    });
    //wikidata_item_list.id_list()
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_SPARQL(SPARQL, options) {
    	function Wikiapi_SPARQL_executor(resolve, reject) {
    		wiki_API.SPARQL(SPARQL, (result, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(result);
    			}
    		}, this.append_session_to_options(options));
    	}

    	return new Promise(Wikiapi_SPARQL_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     *
     * @example <caption>get list of [[w:en:Category:Chemical_elements]]</caption>
    // <code>
    const wiki = new Wikiapi;
    let list = await wiki.categorymembers('Chemical elements');
    console.log(list);
    // Working on multiple pages
    await wiki.for_each_page(
    	// {Array} title liat / page data list
    	list,
    	page_data => {
    		// ...
    	});
    // </code>
     *
     * @example <caption>get pages transcluding {{w:en:Periodic table}}</caption>
    // <code>
    const wiki = new Wikiapi;
    let list = await wiki.embeddedin('Template:Periodic table');
    console.log(list);
    // </code>
     */

    // Warning: Won't throw if title is not existed!
    // @inner
    function Wikiapi_list(list_type, title, options) {
    	function Wikiapi_list_executor(resolve, reject) {
    		options = CeL.setup_options(options);
    		// const wiki = this[KEY_wiki_session];
    		wiki_API.list(title, (list/* , target, options */) => {
    			// console.trace(list);
    			if (list.error) {
    				reject(list.error);
    			} else {
    				resolve(list);
    			}
    		}, this.append_session_to_options({
    			type: list_type,
    			// namespace: '0|1',
    			...options
    		}));

    		/**
    		 * <code>

    		// method 2: 使用循環取得資料版:
    		wiki.cache({
    			// Do not write cache file to disk.
    			cache: false,
    			type: list_type,
    			list: title
    		}, (list, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(list);
    			}
    		},
    			// default options === this
    			//{ namespace : '0|1' }
    			options);

    		// NG: 不應使用單次版
    		wiki[list_type](title, (list, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(list);
    			}
    		}, {
    				limit: 'max', ...options
    			});

    		</code>
    		 */
    	}

    	return new Promise(Wikiapi_list_executor.bind(this));
    }

    // functions for several kinds of lists
    function Wikiapi_for_each(type, title, for_each, options) {
    	return Wikiapi_list.call(this, type, title, {
    		for_each,
    		...options
    	});
    }

    // --------------------------------------------------------

    /**
     * @alias category_tree
     * @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}.
     *
     * @param {String} root_category	- category name
     * @param {Object} [options]		- options to run this function.
     *
     * @returns {Promise} Promise object represents {Array} category_tree.
     *
     * @example <caption>Checking if [[Category:Countries in North America]] including [[Mexico]].</caption>
    // <code>
    const enwiki = new Wikiapi('en');
    const page_list = await enwiki.category_tree('Countries in North America', 1);
    assert(page_list.some(page_data => page_data.title === 'United States'), 'list category tree: [[Category:Countries in North America]] must includes [[United States]]');
    assert('Mexico' in page_list[Wikiapi.KEY_subcategories], 'list category tree: [[Category:Mexico]] is a subcategory of [[Category:Countries in North America]]');
    // </code>
     *
     * @example <caption>Get all sub-categories of [[Category:Echinodermata]] with depth=2.</caption>
    // <code>
    const wiki = new Wikiapi('commons');
    const all_sub_categories = (await wiki.category_tree('Echinodermata', { depth: 2, cmtype: 'subcat', get_flated_subcategories: true })).flated_subcategories;
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_category_tree(root_category, options) {
    	function Wikiapi_category_tree_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		// using wiki_API.prototype.category_tree
    		wiki.category_tree(root_category, (list, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(list);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_category_tree_executor.bind(this));
    }

    /**
     * export key for subcategory 子分類 used in {@link Wikiapi#category_tree}
     *
     * @example
    // <code>
    const KEY_subcategories = Wikiapi.KEY_subcategories;
    // </code>
     */
    Wikiapi.KEY_subcategories = wiki_API.KEY_subcategories;

    // --------------------------------------------------------

    /**
     * @alias search
     * @description search pages include <code>key</code>
     *
     * @param {String} key			- key to search
     * @param {Object} [options]	- options to run this function.
     *
     * @returns {Promise} Promise object represents {Array} page_list.
     *
     * @example <caption>search pages include key: 霍金</caption>
    // <code>
    const zhwikinews = new Wikiapi('zh.wikinews');
    const page_list = await zhwikinews.search('"霍金"');
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_search(key, options) {
    	function Wikiapi_search_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		// using wiki_API.search
    		wiki.search(key, (list, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(list);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_search_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias redirects_root
     * @description Get redirects target of <code>title</code>.
     *
     * @param {String} title		- page title
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {String} page title or {Object} page data
     *
     * @example <caption>Get redirects target of [[WP:SB]]</caption>
    // <code>
    const redirects_taregt = await enwiki.redirects_root('WP:SB', { get_page_data: true });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_redirects_root(title, options) {
    	function Wikiapi_redirects_root_executor(resolve, reject) {
    		// const wiki = this[KEY_wiki_session];
    		// using wiki_API.redirects_root
    		wiki_API.redirects_root(title, (_title, page_data, error) => {
    			if (error) {
    				reject(error);
    			} else if (options && options.get_page_data) {
    				page_data.query_title = title;
    				resolve(page_data);
    			} else {
    				resolve(_title);
    			}
    		}, this.append_session_to_options(options));
    	}

    	return new Promise(Wikiapi_redirects_root_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias redirects_here
     * @description Get all pages redirects to <code>title</code>.
     *
     * @param {String} title		- page title
     * @param {Object} [options]	- options to run this function
     *
     * @returns {Promise} Promise object represents {Array} redirect_list
     *
     * @example <caption>Get all pages redirects to [[Wikipedia:Sandbox]]</caption>
    // <code>
    const redirects_list = await enwiki.redirects_here('Wikipedia:Sandbox');
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_redirects_here(title, options) {
    	function Wikiapi_redirects_here_executor(resolve, reject) {
    		// const wiki = this[KEY_wiki_session];
    		// using wiki_API.redirects_here
    		wiki_API.redirects_here(title, (root_page_data, redirect_list, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				//console.trace(root_page_data);
    				//console.trace(redirect_list);
    				//console.assert(!redirect_list || redirect_list === root_page_data.redirect_list);
    				resolve(redirect_list || root_page_data);
    			}
    		}, this.append_session_to_options({
    			// Making .redirect_list[0] the redirect target.
    			include_root: true,
    			...options
    		}));
    	}

    	return new Promise(Wikiapi_redirects_here_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias register_redirects
     * @description register page alias. usually used for templates
     *
     * @param {Array|String} page_title_list	- list of page titles
     * @param {Object} [options]				- options to run this function.
     *
     * @returns {Promise} Promise object represents the operations are done.
     *
     * @example <caption>Register template redirects and get tokens of the templates.</caption>
    // <code>
    const wiki_session = new Wikiapi;
    // e.g., await wiki_session.register_redirects(['Section link', 'Broken anchors'], { namespace: 'Template' });
    await wiki_session.register_redirects([template_name_1, template_name_2, template_name_3], { namespace: 'Template' });

    // ...

    const page_data = await wiki_session.page(page_title);
    // {Array} parsed page content 頁面解析後的結構。
    const parsed = page_data.parse();

    parsed.each('Template:' + template_name_1, function (token, index, parent) {
    	// ...
    });

    parsed.each('template', function (token, index, parent) {
    	if (wiki_session.is_template(template_name_1, token)) {
    		// ...
    		return;
    	}
    	if (wiki_session.is_template(template_name_2, token)) {
    		// ...
    		return;
    	}

    	// alternative method:
    	switch (wiki_session.redirect_target_of(token)) {
    		case wiki_session.redirect_target_of(template_name_1):
    			break;
    		case wiki_session.redirect_target_of(template_name_2):
    			break;
    		case wiki_session.redirect_target_of(template_name_3):
    			break;
    	}
    });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_register_redirects(page_title_list, options) {
    	function Wikiapi_register_redirects_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.register_redirects(page_title_list, (redirect_list, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				// console.trace( redirect_list);
    				resolve(redirect_list);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_register_redirects_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias upload
     * @description Upload specified local file to the target wiki.
     *
     * @param {Object} file_data	- Upload configurations.<br />
     * 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 />
    {<br />
    <ul>
    <li><code>file_path</code>: string - Local path.</li>
    <li><code>media_url</code>: string - URL path. Alternative to <code>file_path</code>.</li>
    <li><code>comment</code>: string - Upload comment.</li>

    <li><code>text</code>: string or {Object} - Either {String}wikitext to fill the file's page,<br />
     or {Object}parameters of <a href="https://commons.wikimedia.org/wiki/Template:Information" target="_blank">{{Information}}</a>:<br />
    {<br />
    <ul>
    <li><code>description</code>: string - File description.</li>
    <li><code>date</code>: date string - YYYY-MM-DD, e.g., <code>new Date()</code> || <code>'2021-01-01'</code>.</li>
    <li><code>source_url</code>: string - Source where the file comes from, typically an URL.</li>
    <li><code>author</code>: string - Author's name or username in wikicode, e.g., URL or <code>'[[User:Yoda|Yoda]]'</code>.</li>
    <li><code>permission</code>: string - License and other usage limitations and warnings, e.g., <code>'{{cc-by-sa-2.5}}'</code>.</li>
    <li><code>other_versions</code>: string - Wikicode links to files with very similar content or derived files.</li>
    <li><code>other_fields</code>: string - Additional table fields added on the bottom of the template.</li>
    </ul>
    }
    </li>

    <li><code>license</code>: array of strings - License under which the file is uploaded, e.g., <code>['{{cc-by-sa-2.5}}']</code>.</li>
    <li><code>additional_text</code>: string - Additional wikitext to place before <code>categories</code>.</li>
    <li><code>categories</code>: array of strings - Categories for this file, e.g., <code>['[[Category:test images]]']</code>.</li>

    <li><code>ignorewarnings</code>: boolean - Set to 1 will overwrite existing files.</li>
    </ul>
    }<br />
    <br />
    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.
     *
     * @returns {Promise} Promise object represents {String} result of MediaWiki API
     *
     * @example <caption><span id="example__Upload file / media">Upload file / media</span></caption>
    // <code>
    const wiki = new Wikiapi;
    await wiki.login('user', 'password', 'test');
    // Upload a local file directly:
    //let result = await wiki.upload({ file_path: '/local/file/path', comment: '', text: '' || {description: '', ...} });
    let result = await wiki.upload({
    	file_path: '/local/file/path', comment: '',
    	filename: 'Will set via .file_path or .media_url if not settled.',
    	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: '',
    	license: ['{{cc-by-sa-2.5}}'], categories: ['[[Category:test images]]'],
    	bot: 1, tags: "tag1|tag2",
    	// To overwrite existing file
    	ignorewarnings: 1,
    });
    // Upload file from URL:
    result = await wiki.upload({ media_url: 'https://media.url/name.jpg', comment: '', text: '' });
    // </code>
     *
     * @example <caption>Upload file and then update content of file page</caption>
    // <code>
    const wiki = new Wikiapi;
    await wiki.login('user', 'password', 'test');

    const variable_Map = new CeL.wiki.Variable_Map();
    variable_Map.set('description', '...');
    //variable_Map.set('date', '...');
    // ...
    //variable_Map.set('other_fields', '...');

    let result = await wiki.upload({
    	file_path: '/local/file/path',
    	// The <code>comment</code> will only show in the file page when updating file. It is read-only and cannot be modified.
    	comment: '',

    	// <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.
    	// <code>description</code> till <code>other_fields</code> will be auto-setted as values assigned above.
    	// 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
    	// 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
    	// More examples to use <code>CeL.wiki.Variable_Map</code>: https://github.com/kanasimi/wikibot/blob/master/routine/20191129.check_language_convention.js
    	variable_Map,
    	// When set .variable_Map, after successful update, the content of file page will be auto-updated too.

    	// To overwrite existing file
    	ignorewarnings: 1,
    });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_upload(file_data) {
    	// 2021/3/25 renamed from old name: Wikiapi_upload_file(),
    	// Wikiapi_upload_file_executor()
    	function Wikiapi_upload_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.upload(file_data, (result, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(result);
    			}
    		});
    	}

    	return new Promise(Wikiapi_upload_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias download
     * @description Download file to local path.
     *
     * @param {String} file_title	- file title starts with "File:"
     * @param {Object} [options]	- options to run this function. Refer to example codes.
     *
     * @returns {Promise} Promise object represents [ {Object}file informations ]
     *
     * @example <caption>Download original file / media to current directory.</span></caption>
    // <code>
    const wiki = new Wikiapi('commons');
    await wiki.download('File:Example.svg');
    // </code>
     *
     * @example <caption>Download file / media with options</span></caption>
    // <code>
    const wiki = new Wikiapi('commons');

    // Download non-vector version of .svg
    await wiki.download('File:Example.svg', { width: 80 });

    // Change width / height
    await wiki.download('File:Example.png', {
    	file_name: 'example.png', directory: '/tmp/',
    	// reget and overwrite existed file.
    	reget: true,
    	width: 80,// height: 80
    });

    // Download all files from a (Commons) category and its subcategories WITH directory structure.
    const file_data_list = await wiki.download('Category:name', {
    	directory: './',
    	max_threads: 4,
    	// depth of categories
    	depth: 4,
    	// Only download files with these formats.
    	//download_derivatives : ['wav', 'mp3', 'ogg'],
    	// Warning: Will skip downloading if there is no new file!
    	download_derivatives : 'mp3',
    	// A function to filter result pages. Return `true` if you want to keep the element.
    	page_filter(page_data) {
    		return page_data.title.includes('word');
    	}
    });

    // Download all files from a (Commons) category WITHOUT directory structure.
    for (const page_data of await wiki.categorymembers('Category:name', { namespace: 'File' })) {
    	try {
    		//if (wiki.is_namespace(page_data, 'File'))
    		const file_data = await wiki.download(page_data, { directory: './' });
    	} catch (e) { console.error(e); }
    }
    // also
    const categorymembers = await wiki.categorymembers('Category:name', { namespace: 'File' });
    const file_data_list = await wiki.download(categorymembers, { directory: './', no_category_tree: true });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_download(file_title, options) {
    	function Wikiapi_download_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.download(file_title, options, (result, error) => {
    			if (error) {
    				// return result.error_titles
    				reject(result && result.error_titles && result || error);
    			} else {
    				resolve(result);
    			}
    		});
    	}

    	return new Promise(Wikiapi_download_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias for_each_page
     * @description Edit / process pages listing in <code>page_list</code>. Will get the content of multiple pages at once to save transmission times. 一次取得多個頁面內容，以節省傳輸次數。
     *
     * @param {Array} page_list			- title list or page_data list
     * @param {Function} for_each_page	- processor for each page. for_each_page(page_data with contents)
     * @param {Object} [options]		- options to run this function. Refer to example codes.
     *
     * @returns {Promise} Promise object represents the operations are done.
     *
     * @example <caption>read / edit multiple pages</caption>
    // <code>
    const enwiki = new Wikiapi('en');
    const link_from = await wiki.redirects_here('ABC');
    await wiki.for_each_page(link_from, page_data => {
    	// Return `Wikiapi.skip_edit` if you just want to get the page data.
    	return Wikiapi.skip_edit;
    	return 'You may also modify page contents for each page';
    }, {
    	// The options below are sample, not default configuration.

    	// denotes we do not edit pages
    	no_edit: true,

    	// Only needed if you want to modify page.
    	summary: 'test edit',
    	// Allow content to be emptied. 允許內容被清空。白紙化。
    	allow_empty: true,
    	tags: 'bot trial',
    	// prevent creating new pages
    	// Throw an error if the page doesn't exist.
    	// 若頁面不存在/已刪除，則產生錯誤。
    	nocreate: 1,
    	// denotes this is a bot edit. 標記此編輯為機器人編輯。
    	bot: 1,
    	minor: 1,

    	// options to get page revisions
    	page_options: { redirects: 1, rvprop: 'ids|content|timestamp|user' }

    	// <code>.for_each_page()</code> will generate a report. It can be written to the specified page.
    	log_to: 'log to this page',
    	// no warning messages on console. e.g., hide "wiki_API_page: No contents: [[title]]" messages
    	no_warning: true,
    	// no warning messages and debug messages on console
    	no_message: true,
    });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_for_each_page(page_list, for_each_page, options) {
    	function Wikiapi_for_each_page_executor(resolve, reject) {
    		options = typeof options === 'string' ? { summary: options } : CeL.setup_options(options);

    		const wiki = this[KEY_wiki_session];
    		const append_to_this = Array.isArray(for_each_page) && for_each_page[1];
    		if (Array.isArray(for_each_page))
    			for_each_page = for_each_page[0];
    		// console.trace(for_each_page);
    		const work_config = {
    			log_to: null,
    			no_message: options.no_edit,

    			...options,

    			//is_async_each: CeL.is_async_function(for_each_page),
    			each: [function each(page_data/* , messages, config */) {
    				set_page_data_attributes(page_data, wiki);

    				return for_each_page.apply(this, arguments);
    			}, append_to_this],
    			// Run after all list items (pages) processed.
    			last(error) {
    				// this === options
    				// console.trace('last(error)');
    				// console.error(error);
    				// console.trace('Wikiapi_for_each_page_executor finish:');
    				// console.log(options);

    				// 提早執行 resolve(), reject() 的話，可能導致後續的程式碼 `options.last`
    				// 延後執行，程式碼順序錯亂。
    				if (typeof options.last === 'function')
    					options.last.call(this, error);
    				if (error) {
    					if (options.throw_error) {
    						reject(error);
    						return;
    					}
    					console.error(error);
    				}
    				resolve(this);
    			}
    		};

    		wiki.work(work_config, page_list);
    	}

    	return new Promise(Wikiapi_for_each_page_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias convert_Chinese
     * @description convert text to traditional Chinese / simplified Chinese.
     *
     * @param {String|Array|Object} text	- text or objects to convert. Will convert to {String} using JSON.stringify().
     * @param {Object} [options]			- options to run this function
     *
     * @returns {Promise} Promise object represents the converted text.
     *
     * @example <caption>繁簡轉換</caption>
    // <code>
    const wiki = new Wikiapi('en');
    await wiki.convert_Chinese('中国', { uselang: 'zh-hant' });
    await wiki.convert_Chinese('中國', { uselang: 'zh-hans' });
    await wiki.convert_Chinese(['繁體', '簡體'], { uselang: 'zh-hans' });
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_convert_Chinese(text, options) {
    	function Wikiapi_convert_Chinese(resolve, reject) {
    		if (typeof options === 'string') {
    			options = { uselang: options };
    		}
    		const site_name = this.site_name({ get_all_properties: true });
    		// node.js v12.22.7: Cannot use "?."
    		if (site_name && site_name.language === 'zh') {
    			// 不用再重新造出一個實體。
    			options = this.append_session_to_options(options);
    		}

    		// using wiki_API.search
    		wiki_API.convert_Chinese(text, (text, error) => {
    			if (error) {
    				reject(error);
    			} else {
    				resolve(text);
    			}
    		}, options);
    	}

    	return new Promise(Wikiapi_convert_Chinese.bind(this));
    }

    // --------------------------------------------------------

    // May only test in the [https://tools.wmflabs.org/ Wikimedia Toolforge]
    function Wikiapi_run_SQL(SQL, for_each_row/* , options */) {
    	function Wikiapi_run_SQL_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		function run_callback() {
    			wiki.SQL_session.SQL(SQL, (error, rows/* , fields */) => {
    				if (error) {
    					reject(error);
    				} else {
    					rows.forEach(for_each_row);
    				}
    			});
    			resolve();
    		}
    		if (wiki.SQL_session) {
    			run_callback();
    			return;
    		}
    		wiki.SQL_session = new wiki_API.SQL((error, rows, fields) => {
    			if (error) {
    				reject(error);
    			} else {
    				run_callback();
    			}
    		}, wiki);
    	}

    	return new Promise(Wikiapi_run_SQL_executor.bind(this));
    }

    // --------------------------------------------------------

    function Wikiapi_setup_layout_elements(options) {
    	function Wikiapi_setup_layout_elements_executor(resolve, reject) {
    		// const wiki = this[KEY_wiki_session];
    		wiki_API.setup_layout_elements(resolve, this.append_session_to_options(options));
    	}

    	return new Promise(Wikiapi_setup_layout_elements_executor.bind(this));
    }

    // --------------------------------------------------------

    /**
     * @alias get_featured_content
     * @description Get featured content.
     *
     * @param {String|Object} [options]	- options to run this function.
     *            {String}type (FFA|GA|FA|FL)
     *            || {type,on_conflict(FC_title, {from,to})}
     *
     * @returns {Promise} Promise object represents {Object} featured content data hash
     *
     * @example <caption>Get featured content of current wiki site.</caption>
    // <code>
    // MUST including wiki.featured_content first to get featured content!
    CeL.run('application.net.wiki.featured_content');

    // ...

    const FC_data_hash = await wiki.get_featured_content();
    console.assert(FC_data_hash === wiki.FC_data_hash);
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_get_featured_content(options) {
    	if (!options || !options.type) {
    		const session = this;
    		return Wikiapi_get_featured_content.default_types
    			.reduce((promise, type) => promise.then(Wikiapi_get_featured_content.bind(session, { ...options, type })), Promise.resolve());
    		if (false) {
    			let promise = Promise.resolve();
    			Wikiapi_get_featured_content.default_types.forEach(type => {
    				promise = promise.then(Wikiapi_get_featured_content.bind(session, { ...options, type }));
    			});
    			return promise;
    		}
    	}

    	function Wikiapi_get_featured_content_executor(resolve, reject) {
    		const wiki = this[KEY_wiki_session];
    		wiki.get_featured_content(options, FC_data_hash => {
    			try {
    				this.FC_data_hash = FC_data_hash;
    				resolve(FC_data_hash);
    			} catch (e) {
    				reject(e);
    			}
    		});
    	}

    	return new Promise(Wikiapi_get_featured_content_executor.bind(this));
    }

    Wikiapi_get_featured_content.default_types = 'FFA|GA|FA|FL'.split('|');

    // --------------------------------------------------------

    /**
     * @alias site_name
     * @description Get site name / project name of this {Wikiapi}.
     *
     * @param {String} [language]	- language code of wiki session
     * @param {Object} [options]	- options to run this function
     *
     * @returns {String}site name
     *
     * @example <caption>Get site name of {Wikiapi}.</caption>
    // <code>
    console.log(Wikiapi.site_name('zh', { get_all_properties: true }));

    const wiki = new Wikiapi('en');
    console.assert(wiki.site_name() === 'enwiki');
    console.log(wiki.site_name({ get_all_properties: true }));
    console.assert(wiki.site_name({ get_all_properties: true }).language === 'en');
    // </code>
     *
     * @memberof Wikiapi.prototype
     */
    function Wikiapi_site_name(language, options) {
    	return wiki_API.site_name(language, options);
    }

    Wikiapi.site_name = Wikiapi_site_name;

    // --------------------------------------------------------
    // exports

    Object.assign(Wikiapi.prototype, {
    	append_session_to_options(options) {
    		// Object.assign({ [KEY_SESSION]: wiki }, options)
    		// return { ...options, [KEY_SESSION]: this[KEY_wiki_session] };
    		return wiki_API.add_session_to_options(this[KEY_wiki_session], options);
    	},

    	site_name(options) {
    		return Wikiapi_site_name(this[KEY_wiki_session], options);
    	},
    	login: Wikiapi_login,

    	query: Wikiapi_query,
    	page: Wikiapi_page,
    	tracking_revisions: Wikiapi_tracking_revisions,
    	edit_page: Wikiapi_edit_page,
    	/**
    	 * @description edits content of target page.<br />
    	 * <em>MUST using after {@link Wikiapi#page}!</em><br />
    	 * Note: for multiple pages, you should use {@link Wikiapi#for_each_page}.<br />
    	 * Note: The function will check sections of [[User talk:user name/Stop]] if somebody tells us needed to stop edit. See <a href="https://zh.wikipedia.org/wiki/User:Cewbot/Stop">mechanism to stop operations</a>.
    	 *
    	 * @param {String|Function} content	- 'wikitext page content' || page_data => 'wikitext'
    	 * @param {Object} [options]		- options to run this function. e.g., { summary: '', bot: 1, nocreate: 1, minor: 1 }
    	 *
    	 * @returns {Promise} Promise object represents {Object} result of MediaWiki API
    	 *
    	 * @memberof Wikiapi.prototype
    	 */
    	edit(content, options) {
    		return this.edit_page(null, content, options);
    	},
    	move_to: Wikiapi_move_to,
    	move_page: Wikiapi_move_page,
    	purge: Wikiapi_purge,
    	/**
    	 * @description Listen to page modification. 監視最近更改的頁面。<br />
    	 * wrapper for {@link wiki_API}#listen
    	 *
    	 * @param {Function} listener	- function(page_data) { return quit_listening; }
    	 * @param {Object} [options]	- options to run this function. e.g., { summary: '', bot: 1, nocreate: 1, minor: 1 }
    	 *
    	 * @example <caption>listen to new edits</caption>
    // <code>
    const wiki = new Wikiapi;
    wiki.listen(function for_each_row() {
    	// ...
    }, {
    	// 檢查的延遲時間。
    	delay: '2m',
    	filter: function filter_row(row) {
    		// row is the same format as page_data
    	},
    	// also get diff
    	with_diff: { LCS: true, line: true },
    	// only for articles (0:main namespace) and talk pages
    	namespace: '0|talk',
    });
    // </code>
    	 *
    	 * @memberof Wikiapi.prototype
    	 */
    	listen(listener, options) {
    		const wiki = this[KEY_wiki_session];
    		wiki.listen(listener, options);
    	},

    	category_tree: Wikiapi_category_tree,
    	search: Wikiapi_search,

    	redirects_root: Wikiapi_redirects_root,
    	// Warning: 採用 wiki_API.redirects_here(title) 才能追溯重新導向的標的。
    	// wiki.redirects() 無法追溯重新導向的標的！
    	redirects_here: Wikiapi_redirects_here,
    	register_redirects: Wikiapi_register_redirects,

    	upload: Wikiapi_upload,
    	download: Wikiapi_download,

    	get_featured_content: Wikiapi_get_featured_content,

    	for_each_page: Wikiapi_for_each_page,

    	for_each: Wikiapi_for_each,

    	data: Wikiapi_data,
    	new_data_entity: Wikiapi_new_data_entity,
    	SPARQL: Wikiapi_SPARQL,

    	convert_Chinese: Wikiapi_convert_Chinese,

    	run_SQL: Wikiapi_run_SQL,

    	setup_layout_elements: Wikiapi_setup_layout_elements,
    });

    // wrapper for properties
    for (const property_name of ('task_configuration|latest_task_configuration').split('|')) {
    	Object.defineProperty(Wikiapi.prototype, property_name, {
    		get() {
    			const wiki = this[KEY_wiki_session];
    			return wiki[property_name];
    		}
    	});
    }

    // wrapper for sync functions
    for (const function_name of ('namespace|remove_namespace|is_namespace|to_namespace|is_talk_namespace|to_talk_page|talk_page_to_main|normalize_title|redirect_target_of|aliases_of_page|is_template'
    	// CeL.run('application.net.wiki.featured_content');
    	// [].map(wiki.to_talk_page.bind(wiki))
    	+ '|get_featured_content_configurations').split('|')) {
    	Wikiapi.prototype[function_name] = function wrapper() {
    		const wiki = this[KEY_wiki_session];
    		return wiki[function_name].apply(wiki, arguments);
    	};
    }

    // @see get_list.type @
    // https://github.com/kanasimi/CeJS/blob/master/application/net/wiki/list.js
    for (const type of wiki_API.list.type_list) {
    	// Cannot use `= (title, options) {}` !
    	// arrow function expression DO NOT has this, arguments, super, or
    	// new.target keywords.
    	Wikiapi.prototype[type] = function (title, options) {
    		const _this = this;
    		/**
    		 * @example <code>

    		const page_list = await wiki.embeddedin(template_name, options);
    		await page_list.each((page_data) => { }, options);

    		 * </code>
    		 */
    		return Wikiapi_list.call(this, type, title, options)
    			.then((page_list) => {
    				// console.log(page_list);
    				page_list.each = Wikiapi_for_each_page.bind(_this, page_list);
    				return page_list;
    			});
    	};
    }

    module.exports = Wikiapi;

    // export default Wikiapi;