UNPKG

cecc

Version:

繁簡轉換函式庫 追求正確率 先解析詞性再繁簡轉換 繁體中文↔簡體中文轉換 Chinese converter between Traditional Chinese and Simplified Chinese.

2,335 lines 98.5 kB
/*

TODO:
簡化辭典複雜度: 分割個別作品的辭典為特設辭典。
依照前後詞彙再建立 Map(),避免條件式串列過長。這可能得考慮如何合併詞性標註錯誤時的條件式。
+ PoS: "n*" 放在 "n*:" 之下。


https://zhuanlan.zhihu.com/p/95358646
常用的关键词提取算法:TF-IDF算法、TextRank算法
https://blog.csdn.net/vivian_ll/article/details/106647666
利用jieba进行关键字提取时,有两种接口。一个基于TF-IDF算法,一个基于TextRank算法。
https://s.itho.me/techtalk/2017/%E4%B8%AD%E6%96%87%E6%96%B7%E8%A9%9E%EF%BC%9A%E6%96%B7%E5%8F%A5%E4%B8%8D%E8%A6%81%E6%82%B2%E5%8A%87.pdf
某個詞在⼀篇⽂章中出現的頻率⾼,且在其他⽂章中很少出現,則此詞語為具代表性的關鍵詞

*/

'use strict';

// modify from wikiapi.js
let CeL;

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

// 在非 Windows 平台上避免 fatal 錯誤。
CeL.env.ignore_COM_error = true;

// Load modules.
CeL.run(['application.debug',
	// 載入不同地區語言的功能 for wiki.work()。
	'application.locale',
	// Add color to console messages. 添加主控端報告的顏色。
	'interact.console',

	// CeL.data.Convert_Pairs.remove_comments()
	'data.Convert_Pairs',

	// for CeL_CN_to_TW()
	'extension.zh_conversion',

	//for CeL.get_URL()
	'application.net.Ajax',

	// for 'application.platform.nodejs': CeL.env.arg_hash, CeL.wiki.cache(),
	// CeL.fs_mkdir(), CeL.wiki.read_dump()
	'application.storage']);

/** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */
const NOT_FOUND = ''.indexOf('_');

const module_base_path = CeL.append_path_separator(module.path);
const test_directory = CeL.append_path_separator(module_base_path + '_test suite');

// Cache default convertors without CeCC.
const CeL_CN_to_TW = CeL.zh_conversion.CN_to_TW, CeL_TW_to_CN = CeL.zh_conversion.TW_to_CN;

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

// default
const KEY_word = 'word', KEY_PoS_tag = 'tag', KEY_filter_name = 'filter_name';
const DEFAULT_TEST_FILE_EXTENSION = 'txt';

const dictionary_template = {
	TW: 'CN_to_TW.%name.%type.txt',
	CN: 'TW_to_CN.%name.%type.txt'
};
function get_dictionary_file_paths(type) {
	if (!this.parser_name)
		throw new Error('No parser name specified!');

	const dictionary_file_paths = Object.create(null);
	for (const language in dictionary_template) {
		let path = dictionary_template[language]
			.replace('%name', this.parser_name)
			.replace('%type', type || 'PoS');
		if (type === 'filters')
			path = path.replace(/[^.]+$/, 'js');
		dictionary_file_paths[language] = path;
	}

	if (!type)
		this.dictionary_file_paths = dictionary_file_paths;
	return dictionary_file_paths;
}

// CeCC
class Chinese_converter {
	constructor(options) {
		this.convertion_pairs = Object.create(null);
		this.KEY_word = KEY_word;
		this.KEY_PoS_tag = KEY_PoS_tag;

		if (options?.LTP_URL) {
			this.LTP_URL = options.LTP_URL;
			options.using_LTP = options.using_LTP || true;
		}

		if (options?.using_LTP) {
			// 最高正確率
			this.KEY_word = 'text';
			this.KEY_PoS_tag = 'pos';
			this.TAG_punctuation = 'wp';
			this.condition_filter = condition_filter_LTP;
			this.parser_name = 'LTP';
			this.filters = get_dictionary_file_paths.call(this, 'filters');
			for (const language in this.filters) {
				const dictionary_file_path = this.dictionaries_directory + this.filters[language];
				this.filters[language] = require(dictionary_file_path);
			}
			this.generate_condition = generate_condition_LTP;
			load_synonym_dictionary.call(this);
			this.tag_paragraph = tag_paragraph_LTP;
			// .batch_get_tag 批量查詢詞性標記之條件: 1.可接受批量{Array}。 2.單次查詢消耗太大。
			this.batch_get_tag = !this.LTP_URL;

		} else if (options?.CoreNLP_URL) {
			// using Stanford CoreNLP
			this.KEY_PoS_tag = 'pos';
			this.CoreNLP_URL = new URL(options.CoreNLP_URL);
			this.parser_name = 'CoreNLP';
			// https://stanfordnlp.github.io/CoreNLP/corenlp-server.html
			this.CoreNLP_URL_properties = {
				annotators: 'tokenize,ssplit,pos,depparse',
			};
			this.tag_paragraph = tag_paragraph_via_CoreNLP;

		} else {
			// fallback to default: nodejieba
			this.nodejieba_CN = require("nodejieba");
			this.nodejieba_CN.load({ dict: this.dictionaries_directory + 'commons.txt' });
			this.parser_name = 'jieba';
			this.tag_paragraph = tag_paragraph_jieba;
		}

		get_dictionary_file_paths.call(this);

		for (const language in this.dictionary_file_paths) {
			const dictionary_file_path = this.dictionary_file_paths[language]
				= this.dictionaries_directory + this.dictionary_file_paths[language];
			load_dictionary.call(this, dictionary_file_path, { language });
		}
		if (CeL.is_debug()) {
			// 這些是比較耗時的轉換。
			for (const [language, convertion_pairs] of Object.entries(this.convertion_pairs)) {
				CeL.info(`convertion pairs for ${language}:`);
				function show_convertion_pairs(_pairs, tag = 'general') {
					const size = _pairs.size;
					if (size > 0) {
						CeL.log(`\t${tag || 'general'}\t${size} convertion(s)${size < 9 ? '\t' + Array.from(_pairs.keys()).join('\t') : ''}`);
					}
				}
				show_convertion_pairs(convertion_pairs.get(KEY_general_pattern_filter));
				for (const [tag, _pairs] of Object.entries(convertion_pairs.get(KEY_tag_pattern_filter))) {
					show_convertion_pairs(_pairs, tag);
				}
			}
		}

		this.load_default_text_to_check();
	}

	/**
	 * convert to TW
	 * @param {Array}paragraphs [{String}, {String}, ...]
	 * @param {Object}[options]
	 */
	async to_TW(paragraphs, options) {
		return await convert_Chinese.call(this, await paragraphs, { convert_to_language: 'TW', ...options });
	}
	to_TW_sync(paragraphs, options) {
		return convert_Chinese.call(this, paragraphs, { convert_to_language: 'TW', ...options });
	}

	/**
	 * convert to CN
	 * @param {Array}paragraphs [{String}, {String}, ...]
	 * @param {Object}[options]
	 */
	async to_CN(paragraphs, options) {
		return await convert_Chinese.call(this, await paragraphs, { convert_to_language: 'CN', ...options });
	}
	to_CN_sync(paragraphs, options) {
		return convert_Chinese.call(this, paragraphs, { convert_to_language: 'CN', ...options });
	}

	// 自動判斷句子、段落的語境(配合維基百科專有名詞轉換)
	detect_domain(paragraphs, options) {
		// TODO
	}

	static async has_LTP_server(options) {
		if (typeof options === 'string') {
			// treat options as LTP_URL
			options = { LTP_URL: options };
		} else {
			options = { LTP_URL: 'http://localhost:5000/', ...options };
		}

		if (options.skip_server_test) {
			CeL.debug('強制使用 LTP server,跳過對 LTP server 的運作測試。請只在您準備全程使用 cache 的情況下才使用這個選項。', 1, Chinese_converter.has_LTP_server.name);
			return options.LTP_URL;
		}

		try {
			//console.trace(options);
			// 注意: 測試 LTP server 不可包含空白或者英數字元!
			const result = await tag_paragraph_LTP.call(options, '測試繁簡轉換伺服器是否正常運作');
			//console.trace(result);
			return Array.isArray(result) && options.LTP_URL;
		} catch (e) {
			//console.error(e);
		}
	}
	//#parse_condition = parse_condition
}

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

function to_converted_file_path(convert_from_text__file_name) {
	return convert_from_text__file_name.replace(/(\.\w+)$/, '.converted$1');
}

async function regenerate_converted(convert_from_text__file_path, convert_to_text__file_status, options) {
	CeL.info(`${regenerate_converted.name}: Generate a new answer file for ${options.convert_from_text__file_name || convert_from_text__file_path}...`);

	let converted_text = CeL.read_file(convert_from_text__file_path).toString();
	//console.trace(options.convert_options);
	converted_text = options.text_is_TW
		? await this.to_CN(converted_text, options.convert_options || regenerate_converted.default_convert_options)
		: await this.to_TW(converted_text, options.convert_options || regenerate_converted.default_convert_options)
		;
	//console.trace(converted_text.slice(0, 200));
	CeL.write_file(convert_to_text__file_status
		//.replace('.answer.', '.converted.')
		, converted_text, { backup: { directory_name: 'backup' } });
}

regenerate_converted.default_convert_options = {
	cache_directory: CeL.append_path_separator(test_directory + 'cache_data'),
	cache_file_for_short_sentences: true,
	// 超過此長度才創建個別的 cache 檔案,否則會放在 .cache_file_for_short_sentences。
	min_cache_length: 40,
};

function get_convert_to_text__file_status(convert_from_text__file_name, options) {
	options = CeL.setup_options(options);
	const convert_from_text__file_path = this.test_articles_directory + convert_from_text__file_name;
	const convert_from_text__file_status = CeL.fso_status(convert_from_text__file_path);

	const convert_to_text__file_path = options.convert_to_text__file_path
		|| (options.convert_to_text__file_name ? this.test_articles_directory + options.convert_to_text__file_name : Chinese_converter.to_converted_file_path(convert_from_text__file_path));
	const convert_to_text__file_status = CeL.fso_status(convert_to_text__file_path);

	const need_to_generate_new_convert_to_text__file = options.regenerate_converted || !convert_to_text__file_status || convert_from_text__file_status.mtime - convert_to_text__file_status.mtime > 0;
	return { convert_from_text__file_path, convert_from_text__file_status, convert_to_text__file_path, convert_to_text__file_status, need_to_generate_new_convert_to_text__file };
}

async function not_new_article_to_check(convert_from_text__file_name, options) {
	options = CeL.setup_options(options);
	const { convert_from_text__file_path, convert_from_text__file_status, convert_to_text__file_path, convert_to_text__file_status, need_to_generate_new_convert_to_text__file } = get_convert_to_text__file_status.call(this, convert_from_text__file_name, options);
	if (need_to_generate_new_convert_to_text__file) {
		//console.trace('重新生成 .converted.* 解答檔案。');
		await this.regenerate_converted(convert_from_text__file_path, convert_to_text__file_path, { ...options, convert_from_text__file_name, });
	}

	if (options.recheck) {
		// 既然要重新檢查,即便詞典檔是舊的,依然算作有新變化。
		return;
	}

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

	// 檢查上一次測試後,是否有新詞典檔。
	const latest_test_result = options.latest_test_result && options.latest_test_result[options.test_name];
	const latest_test_result_for_file = latest_test_result && latest_test_result.test_results && latest_test_result.test_results[convert_from_text__file_name];
	const latest_test_result_date = latest_test_result_for_file?.error_count === 0 ? Date.parse(latest_test_result_for_file?.date)
		// 檢查是否有比測試檔或 .converted.* 解答檔案更新的新詞典檔。
		: convert_to_text__file_status ? Math.max(convert_from_text__file_status.mtime.getTime(), convert_to_text__file_status.mtime.getTime()) : convert_from_text__file_status.mtime.getTime();
	//console.trace(this.dictionary_file_paths);
	for (const dictionary_file_path of Object.values(this.dictionary_file_paths)) {
		const dictionary_file_status = CeL.fso_status(dictionary_file_path);
		//console.trace(dictionary_file_status);
		//console.trace([dictionary_file_status.mtime - latest_test_result_date, convert_from_text__file_status && convert_from_text__file_status.mtime - dictionary_file_status.mtime]);
		if (dictionary_file_status.mtime - latest_test_result_date > 0) {
			CeL.info(`${not_new_article_to_check.name}: ${convert_from_text__file_name}: 有新詞典檔 ${dictionary_file_path}`);
			if (latest_test_result)
				delete latest_test_result[convert_from_text__file_name];
			return;
		}
	}

	// 檢查上一次測試是否比測試檔更新。
	//console.trace(latest_test_result_date - convert_from_text__file_status.mtime);
	if (latest_test_result_date - convert_from_text__file_status.mtime > 0) {
		//console.trace(!convert_from_text__file_status || latest_test_result_date > convert_from_text__file_status.mtime);
		return !convert_to_text__file_status || latest_test_result_date > convert_to_text__file_status.mtime;
	}
}


const KEY_files_loaded = Symbol('files loaded');

function load_text_to_check(should_be_text__file_name, options) {
	if (CeL.is_Object(should_be_text__file_name)) {
		if (should_be_text__file_name.all) {
			CeL.read_directory(this.test_articles_directory).forEach(from_file_name => {
				const matched = from_file_name.match(/watch_target\.(?<work_title>[^.]+)\.(?<to_language>TW|CN)\.\w+$/);
				if (matched) {
					this.load_text_to_check(from_file_name, {
						export: { work_title: matched.groups.work_title }
					});
				}
			});
			return;
		}

		if (should_be_text__file_name.work_title) {
			options = CeL.setup_options(options);
			if (!options.export)
				options.export = Object.create(null);
			if (!options.export.work_title)
				options.export.work_title = should_be_text__file_name.work_title;
			// e.g., "watch_target.第一序列.TW.txt"
			should_be_text__file_name = `watch_target.${should_be_text__file_name.work_title}.${should_be_text__file_name.convert_to_language}.${DEFAULT_TEST_FILE_EXTENSION}`;
			//console.trace(should_be_text__file_name);
		} else {
			throw new Error(`${load_text_to_check.name}: Invalid should_be_text__file_name: ${JSON.stringify(should_be_text__file_name)}`);
		}
	}

	let check_language = should_be_text__file_name.match(/\.(TW|CN)\.\w+$/);
	//console.trace([should_be_text__file_name, check_language]);
	if (!check_language) {
		CeL.error(`無法判別檔案之語言: ${should_be_text__file_name}`);
		return;
	}

	check_language = check_language[1];

	const convert_to_text__data = get_convert_to_text__file_status.call(this, should_be_text__file_name, options);
	const should_be_text__file_path = convert_to_text__data.convert_from_text__file_path;
	if (!this.generate_condition_for_language
		|| options?.reset && !this.generate_condition_for_language.only_default) {
		//console.trace('初始化。');
		this.generate_condition_for_language = { [KEY_files_loaded]: [], only_default: true };
		if (!options?.is_default)
			this.load_default_text_to_check();
	}
	if (this.generate_condition_for_language[KEY_files_loaded].includes(should_be_text__file_path)) {
		CeL.log(`${load_text_to_check.name}: The file is already loaded, skip ${should_be_text__file_path}`);
		return;
	}
	if (!options?.is_default)
		delete this.generate_condition_for_language.only_default;
	this.generate_condition_for_language[KEY_files_loaded].push(should_be_text__file_path);
	const should_be_texts = get_paragraphs_of_file(should_be_text__file_path, { with_configurations: true });
	if (!should_be_texts)
		return;

	const source_text__file_path = convert_to_text__data.convert_to_text__file_path;
	if (convert_to_text__data.need_to_generate_new_convert_to_text__file) {
		//console.trace('重新生成 .converted.* 解答檔案。');
		return this.regenerate_converted(should_be_text__file_path, source_text__file_path, {
			...options,
			convert_from_text__file_name: should_be_text__file_name, text_is_TW: check_language === 'TW',
			convert_options: {
				...regenerate_converted.default_convert_options,
				cache_directory: CeL.append_path_separator(regenerate_converted.default_convert_options.cache_directory + should_be_text__file_name)
			}
		}).then(setup_generate_condition_for.bind(this));

	} else {
		return setup_generate_condition_for.call(this);
	}

	function setup_generate_condition_for() {
		// should_be_text__file_path: .TW.* 為轉換之答案/標的,因此檢查的是相反語言。 .converted 才是原文!
		const source_texts = get_paragraphs_of_file(source_text__file_path);
		if (!source_texts)
			return;
		if (should_be_texts.length !== source_texts.length) {
			CeL.error(`${should_be_text__file_name} 與 ${should_be_text__file_path} 含有不同數量之字串!此${CeL.gettext.get_alias(check_language)}之標的檔與欲測試之項目數不符,將不採用解答!若檔案為自動生成,您可以刪除舊檔後,重新生成轉換標的檔案。`);
			return;
		}

		//console.log(this.generate_condition_for_language);
		// this.generate_condition_for_language[convert_to_language] = { convert_from_text: should_convert_to_text, ... }
		const generate_condition_for = this.generate_condition_for_language[check_language]
			|| (this.generate_condition_for_language[check_language] = new Map);
		const generate_condition_for__title = `${options?.export?.work_title ? `《${options.export.work_title}》` : '通用 '
			}${CeL.gettext.get_alias(check_language === 'TW' ? 'CN' : 'TW')}→${CeL.gettext.get_alias(check_language)}`;
		should_be_texts.forEach((should_convert_to_text, index) => {
			const configuration = should_be_texts.configurations[should_convert_to_text];
			//console.trace([should_convert_to_text, configuration]);
			let text = source_texts[index];
			if (false && configuration) {
				console.trace([text, should_convert_to_text, configuration]);
			}
			if (configuration?.原文) {
				if (configuration.原文 === text) {
					CeL.info(`${setup_generate_condition_for.name}: 轉換前後文字相同,無需設定"原文" ${JSON.stringify(text)}: ${JSON.stringify(configuration)}`);
				} else {
					configuration.original_text_converted = text;
					text = configuration.原文;
				}
			}
			//console.trace([check_language === 'TW' ? CeL_CN_to_TW(text) : CeL_TW_to_CN(text), should_convert_to_text]);
			if (generate_condition_for.has(text)) {
				CeL.log(`${setup_generate_condition_for.name}: ${generate_condition_for__title}: 重複設定 ${JSON.stringify(text)}`);
			}
			generate_condition_for.set(text, { should_convert_to_text, ...options?.export, ...configuration });
		});
		//console.trace(generate_condition_for);
		const totle_count = generate_condition_for.size;
		CeL.info(`${load_text_to_check.name}: 自動檢核 ${should_be_texts.length}個${generate_condition_for__title
			} 之字串。${totle_count === should_be_texts.length ? '' : `總共檢核 ${totle_count}個。`} From ${should_be_text__file_path}`);
		//console.trace(this.generate_condition_for_language);
		return this.generate_condition_for_language;
	}
}

// 會在每次轉換都測試是否有相符之文字。
function load_default_text_to_check() {
	this.text_to_check_files.forEach(from_file_name => this.load_text_to_check(from_file_name, { is_default: true }));
}

// 顯示用函數。
function report_text_to_check(options) {
	if (!this.generate_condition_for_language)
		return;

	const SGR_style = CeL.interact.console.SGR_style;
	const normal_style = (new SGR_style('fg=green;bg=black')).toString(), NG_style = (new SGR_style('fg=red;bg=white')).toString(), reset_style = (new SGR_style({ reset: true })).toString();

	const generate_condition_for = this.generate_condition_for_language[options.convert_to_language];
	//console.trace(generate_condition_for);
	// lost_texts: 用來記錄、顯示還有哪些尚未處理。
	const lost_texts = [], multi_matched = Object.create(null);
	let OK_count = 0, NG_count = 0;
	for (const [convert_from, convert_data] of generate_condition_for.entries()) {
		if (!convert_data.work_title) {
			// e.g., 常出錯詞語 @ this.text_to_check_files
			continue;
		}
		const { check_result } = convert_data;
		if (!check_result) {
			lost_texts.push(convert_data.should_convert_to_text);
			continue;
		}
		if (check_result.NG.length > 0 || check_result.OK.length /* + check_result.NG.length */ > 1) {
			multi_matched[convert_from] = check_result.OK.length;
			if (check_result.NG.length > 0)
				multi_matched[convert_from] += ` + ${NG_style}${check_result.NG.length} NG${normal_style}`;
		}
		if (check_result.NG.length > 0)
			NG_count++;
		else
			OK_count++;
	}

	const message = `${report_text_to_check.name}: ${OK_count} OK, ${NG_count
		} NG.${lost_texts.length > 0 ? ` ${lost_texts.length} lost:\n\t${lost_texts.join('\n\t')}` : ''}`;
	if (NG_count > 0) {
		CeL.error(message);
	} else {
		CeL.log(message);
	}
	const multi_matched_keys = Object.keys(multi_matched);
	if (multi_matched_keys.length > 0) {
		// 這裡可以計算某個值出現幾次。
		CeL.log(`multi matched counts:\n${normal_style
			}${multi_matched_keys.map(convert_from => `\t${convert_from}: \t${multi_matched[convert_from]}`).join('\n')
			}${reset_style}`);
	}
	return { lost_texts, OK_count, NG_count };
}

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

const condition_delimiter = '+';

/*
conditions will be split by `condition_delimiter`:

word
PoS:word
PoS:
// "~": 指示此 condition 為標的文字(is_target)
~PoS:word
// "!": 指示選出不符合此條件的(not_match)
!PoS:word
~!PoS:word

// 末尾的"?": 表示此條件可有可無、可以跳過(is_optional)
~!PoS:word?

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

word:
文字
/search_pattern/flags
/search_pattern/replace_to/flags
// "~/改成了錯誤的繁體pattern/正確的繁體replace_to/flags$" 表示先進行繁簡轉換再執行此處的替代,僅僅適用於標的文字(is_target)
~/pattern/replace_to/flags
文字~/pattern/replace_to/flags
/search_pattern/flags~/pattern/replace_to/flags

文字<filter_name>filter_target

*/

// [ condition, is target, not match, tag (PoS), word / pattern, is optional / repeat range ]
const PATTERN_condition = /^(?<is_target>~)?(?<not_match>!)?(?:(?<tag>[^:+<>]+):)?(?<word>.*?)(?<is_optional>\?)?$/;
// [ all, word, do_after_converting ]
const PATTERN_do_after_converting = new RegExp('^(?<word>.*?)~(?<do_after_converting>' + CeL.PATTERN_RegExp_replacement.source.slice(1, -1) + ')?$');

// JSON.stringify(): for "\n"
function stringify_condition(condition_text) {
	// .replace(/\r/g, '\\r').replace(/\n/g, '\\n')
	return JSON.stringify(condition_text).slice(1, -1).replace(/\\"/g, '"');
}

function word_data_to_condition(word_data, options) {
	const tag = word_data[this.KEY_PoS_tag];
	return (tag ? tag + ':' : '')
		+ (options?.including_prefix_spaces && word_data[KEY_prefix_spaces] ? stringify_condition(word_data[KEY_prefix_spaces]) : '')
		+ (typeof word_data[this.KEY_word] === 'string' &&
			stringify_condition(word_data[this.KEY_word]) || word_data[this.KEY_word] || '');
}

// parse rule
// convert {String}full_condition_text to {Object}word_data or {Object}condition
function parse_condition(full_condition_text, options) {
	let target_index;

	function set_as_target(condition_data) {
		condition_data.is_target = true;
		condition_data.full_condition_text = full_condition_text;
		if (options?.matched_condition)
			condition_data.matched_condition = options.matched_condition;
	}

	const condition = [];
	const full_condition_splited = full_condition_text.split(condition_delimiter);
	for (let index = 0, accumulated_target_index_diff = 0; index < full_condition_splited.length; index++) {
		let token = full_condition_splited[index];
		let matched = token.match(PATTERN_condition).groups;
		if (/^\//.test(matched.tag) && /\(\?$/.test(matched.tag)) {
			// e.g., "/^(?:a)$/"
			matched.word = matched.tag + ':' + matched.word;
			matched.tag = undefined;
			//console.trace(matched);
		}
		if (/^\/(\\\/|[^\/])+$/.test(matched.word)) {
			// 處理 RegExp pattern 中包含 condition_delimiter 的情況。
			// e.g., ~里+/^许.+河$/	v:卷+m:/^[\\d〇一二三四五六七八九零十]+$/+~裡
			const full_condition_splited_expanded = Array.isArray(options.full_condition_splited) ? full_condition_splited.concat(options.full_condition_splited.slice(options.index + 1)) : full_condition_splited;
			for (let combined_token = token, next_index = index; next_index < full_condition_splited_expanded.length;) {
				const next_token = full_condition_splited_expanded[++next_index];
				combined_token += condition_delimiter + next_token;
				const _matched = combined_token.match(PATTERN_condition).groups;
				if (CeL.PATTERN_RegExp.test(_matched.word) || CeL.PATTERN_RegExp_replacement.test(_matched.word)) {
					token = combined_token;
					matched = _matched;
					accumulated_target_index_diff += next_index - index;
					index = next_index;
					//console.trace([token, matched]);
				}
			}
			if (index >= full_condition_splited.length) {
				// e.g., ~干<role.type:A1>/那.+何事$/
				options.combined_token_count = index - full_condition_splited.length + 1;
			}
			//console.log([full_condition_splited_expanded, full_condition_splited, options.full_condition_splited?.slice(options.index + 1), options]);
			//console.trace([index, target_index, accumulated_target_index_diff, token, matched]);
		}

		const condition_data = Object.create(null);
		if (matched.is_target && !options?.no_target) {
			set_as_target(condition_data);
			if (target_index >= 0)
				CeL.warn(`${parse_condition.name}: Multiple targets: ${full_condition_text}`);
			else
				target_index = index - accumulated_target_index_diff;
		}

		let do_after_converting = matched.word && matched.word.match(PATTERN_do_after_converting);
		if (do_after_converting) {
			do_after_converting = do_after_converting.groups;
			matched.word = do_after_converting.word;
			if (do_after_converting = do_after_converting.do_after_converting?.to_RegExp({ allow_replacement: true }))
				condition_data.do_after_converting = do_after_converting;
		}
		if (matched.word) {
			let filter = matched.word.match(/^(?<word>.*?)<(?<filter_name>[^<>]+)>(?<filter_target>.*?)$/);
			if (filter) {
				if (!this.condition_filter)
					throw new Error('No .condition_filter set but set filter: ' + matched.word);
				filter = filter.groups;
				const _options = { no_target: true, full_condition_splited, index };
				Object.assign(condition_data, {
					[this.KEY_word]: filter.word,
					[KEY_filter_name]: filter.filter_name,
					filter_target: parse_condition.call(this, filter.filter_target, _options)
				});
				//console.trace(condition_data);
				if (_options.combined_token_count > 0) {
					token = full_condition_splited.slice(index, index + _options.combined_token_count + 1).join(condition_delimiter);
					accumulated_target_index_diff += _options.combined_token_count;
					index += _options.combined_token_count;
				}
			} else {
				//const replace_pattern = matched.word.match();
				condition_data[this.KEY_word] = CeL.PATTERN_RegExp.test(matched.word) || CeL.PATTERN_RegExp_replacement.test(matched.word)
					? matched.word.to_RegExp({ allow_replacement: true })
					// allow '\n', '\t' in filter.
					: matched.word.replace(/\\\w/g, char => JSON.parse(`"${char}"`));
			}
		}

		condition_data.condition_text = token;

		if (matched.not_match) {
			// !!: 採用字串作XOR運算,可能出現錯誤。 ('!'^true)===1
			condition_data.not_match = !!matched.not_match;
			//console.trace([matched, condition_data]);
		}
		if (matched.tag)
			condition_data[this.KEY_PoS_tag] = matched.tag;
		if (matched.is_optional)
			condition_data.is_optional = true;

		//console.trace(condition_data);
		condition.push(condition_data);
	}

	if (!(target_index >= 0) && !options?.no_target) {
		// 當僅僅只有單一 token 時,預設即為當前標的。
		set_as_target(condition[0]);
	}

	if (condition.length === 1) {
		return condition[0];
	}

	if (!options?.no_target) {
		// default: set [0] as target.
		condition.target_index = target_index || 0;
	}

	return condition;
}

// ------------------------------------------------------------------
// 顯示用函數。

const KEY_matched_condition = 'matched condition';
function print_correction_condition(correction_condition, options) {
	//console.trace(correction_condition);
	const to_word_data = correction_condition.parsed[KEY_matched_condition];
	let matched_condition_mark;
	if (to_word_data) {
		//console.log(correction_condition);
		//console.log(correction_condition.parsed.parents);
		//console.trace(to_word_data);
		matched_condition_mark = ` 匹配的條件式: ${to_word_data.matched_condition ? `${to_word_data.matched_condition} → ` : ''}${to_word_data.full_condition_text}`;
		CeL.warn(`Matched condition${matched_condition_mark}`);
	}
	// 自動提供可符合答案之候選條件式。
	CeL.info(`Candidate correction for ${JSON.stringify(correction_condition.parsed.text)}→${JSON.stringify(correction_condition.target)} (錯誤轉換為 ${JSON.stringify(correction_condition.error_converted_to)}):`);
	const {
		work_title,
		original_sentence_word_list,
		tagged_convert_from_text,
	} = options;
	if (tagged_convert_from_text) {
		const list = correction_condition.slice(1).filter(correction => !correction.includes('<←'));
		list.push(tagged_convert_from_text.join(condition_delimiter));
		CeL.info(`//${matched_condition_mark ? ' 解析錯誤 @' : ''}${work_title ? ` 《${work_title}》` : ''}	${original_sentence_word_list} (${list.join(' ')})${matched_condition_mark || ''}`);
	}
	CeL.info(correction_condition.join('\t'));
}

// 展示有問題的項目。
function print_section_report(configuration, options) {
	const { tagged_word_list, condition_list, convert_from_text, convert_to_text, should_convert_to_text, show_tagged_word_list,
		start_index, end_index, distance_token_header_to_metched } = configuration;
	const { index_hash } = condition_list;

	const SGR_style = CeL.interact.console.SGR_style;
	const normal_style_tagged = (new SGR_style('fg=blue;bg=cyan')).toString(), marked_style_row = 'fg=red;bg=white', marked_style = (new SGR_style(marked_style_row)).toString(), reset_style = (new SGR_style({ reset: true })).toString();
	const normal_style_convert_from_text_row = 'fg=green;bg=black';
	const ansi_convert_from_text = new CeL.interact.console.SGR(convert_from_text);

	let backward = 0, forward = 0;
	const is_fragment = start_index >= 0 && should_convert_to_text.chars().length <= 4;
	if (is_fragment) {
		// 當截取的詞彙太短,自動擴張成一整句。
		// assert: 0 <= start_index < end_index
		let index = start_index;
		// 向前找尋標點符號。
		while (index > 0) {
			const word_data = tagged_word_list[--index];
			if (word_data[this.KEY_PoS_tag] === this.TAG_punctuation) {
				if (index < start_index && /[、,;:。?!…]$/.test(word_data[this.KEY_word]))
					index++;
				break;
			}
		}
		backward = start_index - index;
		// assert: 0 <= backward <= start_index

		// start from next tagged_word_list[], at least move 1 step.
		// 向後找尋標點符號。
		index = end_index;
		while (index < tagged_word_list.length) {
			const word_data = tagged_word_list[index++];
			if (word_data[this.KEY_PoS_tag] === this.TAG_punctuation) {
				break;
			}
		}
		forward = index - end_index;
		//console.trace([start_index, backward, end_index, forward]);
	}

	const tagged_word_list_pieces = start_index >= 0 ? tagged_word_list.slice(start_index - backward, end_index + forward) : tagged_word_list;
	//console.trace(tagged_word_list_pieces);

	let offset = convert_from_text.match(/^\s*/)[0].length, original_sentence_word_list = [];
	const tagged_convert_from_text = [];
	const matched_conditions = [];
	//console.trace([convert_from_text, offset, distance_token_header_to_metched, start_index, backward]);
	CeL.log(`${normal_style_tagged
		}${CeL.gettext.get_alias(options.convert_to_language === 'TW' ? 'CN' : 'TW').slice(0, 1)
		}\t${tagged_word_list_pieces.map((word_data, index) => {
			const prefix_spaces = index > 0 && word_data[KEY_prefix_spaces] || '';
			// condition filter 預設會排除 prefix spaces,因此將 prefix_spaces 另外列出。
			// @see match_single_condition()
			const text = stringify_condition(prefix_spaces) + word_data_to_condition.call(this, word_data);
			tagged_convert_from_text.push(text);
			original_sentence_word_list.push(prefix_spaces + word_data[this.KEY_word]);
			const matched_condition_data = word_data[KEY_matched_condition];
			if (matched_condition_data) {
				//console.trace(matched_condition_data);
				matched_conditions.push(matched_condition_data.matched_condition + ' → ' + matched_condition_data.condition_text);
			}
			if (backward && (index -= backward) < 0) {
				return text;
			}

			if (prefix_spaces)
				offset += prefix_spaces.length;
			const start_offset = offset;
			offset += word_data[this.KEY_word].length;
			if (index === 0) {
				// assert: convert_from_text.trimStart().startsWith(word_data_to_condition.call(this, word_data).slice(distance_token_header_to_metched));
				if (distance_token_header_to_metched) {
					//console.trace([distance_token_header_to_metched, prefix_spaces.length, word_data]);
					// assert: distance_token_header_to_metched >= prefix_spaces.length
					offset -= distance_token_header_to_metched - (word_data[KEY_prefix_spaces] || '').length;
				}
			}
			if (!index_hash[start_index >= 0 ? start_index + index : index]) {
				return text;
			}

			//console.trace([word_data, index_hash[index], start_offset, offset, ansi_convert_from_text.style]);
			if (ansi_convert_from_text.style_at(start_offset, true)) {
				// assert: 不間斷連續匹配到。先前已設定過 .style_at(start_offset, normal_style_convert_from_text_row)
				// assert: ansi_convert_from_text.style_at(start_offset, true).toString() === normal_style_convert_from_text_row.toString()
				ansi_convert_from_text.style_at(start_offset, null);
			} else {
				ansi_convert_from_text.style_at(start_offset, marked_style_row);
			}
			ansi_convert_from_text.style_at(offset, normal_style_convert_from_text_row);
			//console.trace([start_offset, offset, convert_from_text.slice(start_offset, offset), text, convert_from_text.slice(word_data.offset, word_data.offset + word_data[this.KEY_word].length)]);
			return marked_style + text + normal_style_tagged;
		}).join(condition_delimiter)
		}${reset_style}`);

	original_sentence_word_list = original_sentence_word_list.join('');
	if (is_fragment) {
		// show 全句
		CeL.log(`\t原文⇒${reset_style}${JSON.stringify(original_sentence_word_list)}`);
	}

	//console.log(ansi_convert_from_text);
	//CeL.log(`\t${JSON.stringify(convert_from_text)}`);
	CeL.log(`${(new SGR_style(normal_style_convert_from_text_row)).toString()
		}原文:\t ${ansi_convert_from_text.toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}${reset_style}`);

	// 為轉換前後的差異文字著色。
	CeL.coloring_diff(JSON.stringify(convert_to_text), JSON.stringify(should_convert_to_text), {
		headers: [
			`→ ${CeL.gettext.get_alias(options.convert_to_language).slice(0, 1)}\t`,
			` ${options.message_should_be || '應為'}\t`
		],
		header_style: { fg: 'cyan' }, print: true
	});

	let { work_title } = options;
	if (!work_title && (work_title = configuration.test_title)) {
		work_title = work_title.match(/watch_target\.(.+)\.(TW|CN)/);
		if (work_title)
			work_title = work_title[1];
	}
	condition_list.forEach(condition => print_correction_condition(condition, {
		work_title,
		original_sentence_word_list,
		tagged_convert_from_text,
	}));

	if (matched_conditions.length > 0) {
		matched_conditions.unshift('匹配的條件式:');
		CeL.log(matched_conditions.join('\n\t'));
	}

	if (!is_fragment) {
		CeL.log(`單純 zh_conversion 轉換過程:`);
		CeL.log('單純:\t ' + (options.convert_to_language === 'TW' ? CeL_CN_to_TW : CeL_TW_to_CN)(convert_from_text, { show_matched: true }));
	}

	if (show_tagged_word_list) {
		CeL.debug(beautify_tagged_word_list(tagged_word_list_pieces), 1);
	}
}

/** {Boolean}跳過長度不同的測試。 e.g., 地區習慣用詞轉換 */
const skip_tests_convert_to_different_length = true;
function check_convert_to_different_length(converted_text_String, should_be_text, no_warning, is_convert_from) {
	if (converted_text_String.chars().length === should_be_text?.chars().length) {
		return;
	}

	if (no_warning) {
		;
	} else if (skip_tests_convert_to_different_length) {
		// 轉換前後。
		CeL.warn(`${check_convert_to_different_length.name}: 預設解答與轉換後之文字長度不符!`);
	} else {
		// ,刪除解答
		CeL.error(`${check_convert_to_different_length.name}: 預設解答與轉換後之文字長度不符,跳過此項!`);
	}
	// 為差異文字著色。
	CeL.coloring_diff(converted_text_String, should_be_text, {
		headers: [
			(is_convert_from ? '轉換前:' : '轉換後:') + '\t',
			`解答:\t`,
		],
		header_style: { fg: 'yellow' }, print: true
	});
	// return true: Skip this test.
	return !skip_tests_convert_to_different_length;
}

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

const KEY_tag_filter = Symbol('tag filter'), KEY_tag_pattern_filter = Symbol('tag pattern filter'), KEY_general_pattern_filter = Symbol('general pattern filter'), KEY_pattern = 'pattern';

function get_convert_to_conditions(options) {
	const { word_data, convertion_pairs }
		// incase "Variable 'options' is null checked here, but its property is accessed without null check prior"
		= options === null ? Object.create(null) : options;
	let convertion_set, key = word_data[this.KEY_word], pattern;
	const KEY_PoS_tag = this.KEY_PoS_tag;

	function set_tag_convertion(KEY) {
		convertion_set = convertion_pairs.get(KEY);
		if (!convertion_set[word_data[KEY_PoS_tag]]) {
			if (!options?.create)
				return;
			convertion_set[word_data[KEY_PoS_tag]] = new Map;
		}
		//console.trace(convertion_set);
		return convertion_set = convertion_set[word_data[KEY_PoS_tag]];
	}

	if (CeL.is_RegExp(key) || options?.search_pattern) {
		if (options?.try_tag && word_data[KEY_PoS_tag]) {
			if (!set_tag_convertion(KEY_tag_pattern_filter))
				return;
		} else {
			convertion_set = convertion_pairs.get(KEY_general_pattern_filter);
		}

		if (CeL.is_RegExp(key)) {
			pattern = key;
			key = key.toString().replace(/(\w)+$/, flags => flags.replace(/[g]/, ''));
		} else {
			const all_matched_conditions = [];
			for (const convert_to_conditions of convertion_set.values()) {
				//console.trace([key, convert_to_conditions[KEY_pattern]]);
				// assert {Array}convert_to_conditions
				if (convert_to_conditions[KEY_pattern].test(key)) {
					if (!options.get_all_matched_conditions)
						return convert_to_conditions;
					all_matched_conditions.push(convert_to_conditions);
				}
			}
			if (all_matched_conditions.length > 0) {
				if (all_matched_conditions.length > 1) {
					//console.trace(all_matched_conditions);
				}
				return all_matched_conditions;
			}
		}

	} else {
		if (options?.try_tag && word_data[KEY_PoS_tag]) {
			if (!set_tag_convertion(KEY_tag_filter))
				return;
		} else {
			convertion_set = convertion_pairs;
		}
	}

	if (!convertion_set.has(key)) {
		if (!options?.create)
			return;
		// 初始化 initialization
		const convert_to_conditions = [];
		if (pattern)
			convert_to_conditions[KEY_pattern] = pattern;
		convertion_set.set(key, convert_to_conditions);
		//console.trace(convertion_set);
	}

	const convert_to_conditions = convertion_set.get(key);
	// assert: {Array}convert_to_conditions
	return options.get_all_matched_conditions ? [convert_to_conditions] : convert_to_conditions;
}

const KEY_postfix = Symbol('postfix');
function load_dictionary(file_path, options) {
	const word_list = get_paragraphs_of_file(file_path);
	// 初始化 initialization: convertion_pairs
	const convertion_pairs = this.convertion_pairs[options.language] = new Map;
	convertion_pairs.set(KEY_tag_filter, Object.create(null));
	convertion_pairs.set(KEY_tag_pattern_filter, Object.create(null));
	convertion_pairs.set(KEY_general_pattern_filter, new Map);
	convertion_pairs.set(KEY_postfix, []);

	for (let conditions of word_list) {
		conditions = conditions.split('\t');
		const matched_condition = conditions[0].trim();
		if (conditions.length < 2 || !matched_condition) {
			CeL.error(`${load_dictionary.name}: 未設定轉換條件: ${conditions.join('\t')}`);
			continue;
		}
		const filter = parse_condition.call(this, matched_condition);
		if (filter.filter_name === 'postfix') {
			//console.trace(filter);
		} else if (!filter[this.KEY_word] && !filter[this.KEY_PoS_tag]) {
			// assert: !!matched_condition === true
			CeL.error(`Invalid word filter: ${matched_condition}`);
			continue;
		}
		if (filter.not_match)
			throw new Error('NYI: not_match');

		const convert_to_conditions = filter.filter_name === 'postfix' ? convertion_pairs.get(KEY_postfix)
			: get_convert_to_conditions.call(this, { word_data: filter, convertion_pairs, create: true, try_tag: true });
		for (let index = 1; index < conditions.length; index++) {
			let condition = conditions[index];
			if (!condition.trim()) {
				CeL.error(`${load_dictionary.name}: Empty condition[${index}] in ${JSON.stringify(conditions)}`);
				continue;
			}
			condition = parse_condition.call(this, condition, { matched_condition });
			if (condition.do_after_converting || convert_to_conditions.length === 0 || !convert_to_conditions[convert_to_conditions.length - 1].do_after_converting) {
				// TODO: 將 {Array} 之 pattern 轉成 {Regexp} 之 pattern,採用 .replace(pattern, token => match_condition(token))。
				convert_to_conditions.push(condition);
			} else {
				// 應該將有 .do_after_converting 的擺到後面。
				let index = convert_to_conditions.length - 1;
				while (index > 0 && convert_to_conditions[index - 1].do_after_converting)
					index--;
				// assert: !convert_to_conditions[index - 1].do_after_converting && convert_to_conditions[index].do_after_converting
				// 將沒有 .do_after_converting 的插入到有 .do_after_converting 的之前。
				convert_to_conditions.splice(index, 0, condition);
			}
		}
		//console.trace(convert_to_conditions);
	}

	//console.trace(this.convertion_pairs);
}

const KEY_synonym_pattern = Symbol('synonym pattern');

function load_synonym_dictionary() {
	// this.synonyms_of_language['TW'] = {Map} { '台灣' => [ '臺灣' ] }
	if (!this.synonyms_of_language)
		this.synonyms_of_language = Object.create(null);

	const file_paths = Object.create(null);
	for (const language in dictionary_template) {
		if (!this.synonyms_of_language[language]) {
			// initialization
			this.synonyms_of_language[language] = new Map;
			this.synonyms_of_language[language][KEY_synonym_pattern] = [];
		}
		const synonyms_Map = this.synonyms_of_language[language];
		let synonym_data = CeL.read_file(this.dictionaries_directory + `synonym.${language}.txt`);
		if (!synonym_data)
			continue;

		synonym_data = CeL.data.Convert_Pairs.remove_comments(synonym_data.toString().replace(/\r/g, ''));
		synonym_data.split('\n').forEach(line => {
			if (!line)
				return;
			const synonyms = line.split('\t');
			const 正字正詞 = synonyms.shift();
			if (synonyms.length === 0) {
				if (CeL.PATTERN_RegExp_replacement.test(正字正詞)) {
					// {RegExp}通同字/同義詞pattern
					synonyms_Map[KEY_synonym_pattern].push(正字正詞.to_RegExp({ allow_replacement: true }));
				} else {
					CeL.error(`${load_synonym_dictionary.name}: No synonym settle: ${正字正詞}`)
				}
				return;
			}

			// 有設定`正字正詞`時,僅允許轉換成`正字正詞`,不可轉換為俗寫。
			const allowed_synonyms = 正字正詞 ? [正字正詞] : synonyms;
			synonyms.forEach(synonym => {
				if (!synonym)
					return;
				if (synonyms_Map.has(synonym))
					CeL.error(`${load_synonym_dictionary.name}: 重複設定: ${JSON.stringify(synonym)}`);
				synonyms_Map.set(synonym, allowed_synonyms);
			});
		});
	}

	//console.log(this.synonyms_of_language);
}

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

// @inner 須配合 generate_condition_LTP()。
function condition_filter_LTP(single_condition, word_data, options) {
	//console.trace(options);
	if (single_condition.filter_name in this.filters[options.convert_to_language])
		return true;

	//console.trace([single_condition, word_data, options]);
	//console.trace(options.convertion_pairs.get(KEY_tag_filter).v.get('干'));

	const { tagged_word_list } = options;
	// assert: word_data === tagged_word_list[options.index_of_tagged_word_list]
	const tagged_word_list_index_offset = options.index_of_tagged_word_list - word_data.id;

	if (single_condition.filter_name === word_data.relation) {
		// 指定關係。
		//console.trace([single_condition.filter_target, tagged_word_list[word_data.parent]]);
		// e.g., ~只<ATT>b:/表/
		return match_single_condition.call(this, single_condition.filter_target, tagged_word_list[tagged_word_list_index_offset + word_data.parent], options);
	}

	let matched;

	matched = single_condition.filter_name.match(/^←(.+)$/);
	if (matched) {
		matched = matched[1];
		// 搜尋反向關係。
		for (let index = tagged_word_list_index_offset, latest_id = -1; index < tagged_word_list.length; index++) {
			const word_data_to_test = tagged_word_list[index];
			if (latest_id >= word_data_to_test.id) {
				// tagged_word_list 可能是 recover_original_paragraphs() 多次查詢拼合起來的。當 (latest_id > word_data_to_test.id) 的時候,已經超越本次查詢的範圍。
				// assert: word_data_to_test.id === 0
				return;
			}
			// assert: word_data_to_test.id === latest_id + 1
			latest_id = word_data_to_test.id;
			if (word_data_to_test.parent === word_data.id
				&& word_data_to_test.relation === matched
				&& match_single_condition.call(this, single_condition.filter_target, word_data_to_test, options)
			) {
				return true;
			}
		}
	}

	matched = single_condition.filter_name.match(/(?<property_name>(?:role|parent))(?:\.(?<sub_property_name>[^:]+):(?<sub_property_value>.+))?/);
	if (matched) {
		matched = matched.groups;
		const filter_target = single_condition.filter_target;
		//console.trace([single_condition, matched, word_data]);
		// e.g., 沖<role.type:A1>/[水浴杯]/
		// 搜尋 roles / parents。
		return word_data[matched.property_name + 's'].some(token => {
			const parent_index = tagged_word_list_index_offset + token.parent;
			if (parent_index in tagged_word_list) {
				// 這可能造成 JSON.stringify(tagged_word_list) 出問題 (TypeError: Converting circular structure to JSON)。
				Object.assign(token, tagged_word_list[parent_index]);
			}
			return (!matched.sub_property_name || token[matched.sub_property_name] === matched.sub_property_value)
				&& match_single_condition.call(this, filter_target, token, options);
		});
	}
}

function match_single_condition(single_condition, word_data, options) {
	//if (!single_condition) console.trace([single_condition, word_data, options]);

	if (single_condition[KEY_filter_name]) {
		return this.condition_filter && this.condition_filter(single_condition, word_data, options);
	}

	let filter;

	// 依照最佳詞性轉換。
	// ICTPOS3.0词性标记集 https://gist.github.com/luw2007/6016931 http://ictclas.nlpir.org/
	// CKIP中文斷詞系統 詞類標記列表 http://ckipsvr.iis.sinica.edu.tw/cat.htm https://github.com/ckiplab/ckiptagger/wiki/POS-Tags
	// NLPIR 词性类别: 计算所汉语词性标记集 http://103.242.175.216:197/nlpir/html/readme.htm
	filter = single_condition[this.KEY_PoS_tag];
	if (filter
		&& !CeL.fit_filter(filter, word_data[this.KEY_PoS_tag])) {
		//if (single_condition.not_match) console.trace([single_condition, filter, word_data, CeL.fit_filter(filter, word_data[this.KEY_PoS_tag])]);
		return single_condition.not_match;
	}

	filter = single_condition[this.KEY_word];
	if (!filter
		// .is_target 時, [this.KEY_word] 可能是欲改成的字串,此時不做篩選。
		|| single_condition.is_target && !CeL.is_RegExp(filter)) {
		return true;
	}

	//if (single_condition.not_match && /上/.test(filter)) console.trace([single_condition, filter, word_data, CeL.fit_filter(filter, word_data[this.KEY_word]), single_condition.not_match ^ CeL.fit_filter(filter, word_data[this.KEY_word])]);
	// console.trace([single_condition, filter, CeL.fit_filter(filter, word_data[this.KEY_word])]);
	return single_condition.not_match ^ (CeL.fit_filter(filter, word_data[this.KEY_word])
		// 接受 condition filter 包含 prefix spaces 的情況。
		//|| word_data[KEY_prefix_spaces] && typeof word_data[this.KEY_word] === 'string' && CeL.fit_filter(filter, word_data[KEY_prefix_spaces] + word_data[this.KEY_word])
	);
}

function match_condition(options) {
	const { conditions, word_data, tagged_word_list } = options;
	//console.trace([conditions, word_data]);
	if (!Array.isArray(conditions))
		return match_single_condition.call(this, conditions, word_data, options) && conditions;

	const target_index = conditions.target_index || 0;
	//console.assert(conditions[target_index]);
	//if (!conditions[target_index]) console.trace(conditions);

	// 檢查當前 part。
	if (!match_single_condition.call(this, conditions[target_index], word_data, options))
		return;

	// 向後檢查。
	for (let index_of_condition = target_index + 1, index_of_target = options.index_of_tagged_word_list + 1; index_of_condition < conditions.length; index_of_condition++) {
		if (index_of_target >= tagged_word_list.length)
			return;
		const condition = conditions[index_of_condition];
		if (match_single_condition.call(this, condition, tagged_word_list[index_of_target], options)) {
			index_of_target++;
		} else {
			if (!condition.is_optional)
				return;
			// Skip the condition, try next condition.
		}
	}

	// 向前檢查。
	for (let index_of_condition = target_index - 1, index_of_target = options.index_of_tagged_word_list - 1; index_of_condition >= 0; index_of_condition--) {
		if (index_of_target < 0)
			return;
		const condition = conditions[index_of_condition];
		if (match_single_condition.call(this, condition, tagged_word_list[index_of_target], options)) {
			index_of_target--;
		} else {
			if (!condition.is_optional)
				return;
			// Skip the condition, try next condition.
		}
	}

	return conditions[target_index];
}

function get_matched_condition(options) {
	let all_convert_to_conditions = get_convert_to_conditions.call(this, { ...options, get_all_matched_conditions: true });
	//console.trace([word_data, all_convert_to_conditions]);
	//console.trace(all_convert_to_conditions);
	if (!all_convert_to_conditions) {
		return;
	}

	const all_matched_conditions = [];
	all_convert_to_conditions.forEach(convert_to_conditions => {
		// assert: convert_to_conditions = [{ [this.KEY_word]: '詞', [this.KEY_PoS_tag]: '詞性' }, { [this.KEY_word]: '詞', [this.KEY_PoS_tag]: '詞性' }, ...]
		for (let index_of_conditions = 0; index_of_conditions < convert_to_conditions.length; index_of_conditions++) {
			const conditions = convert_to_conditions[index_of_conditions];
			const matched_condition = match_condition.call(this, { ...options, conditions });
			if (matched_condition) {
				//console.trace([matched_condition, convert_to_conditions, convert_to_conditions.pattern]);
				all_matched_conditions.push(matched_condition);
				if (!convert_to_conditions.pattern) {
					// 對於非 pattern,僅取第一個 matched 的。
					return;
				}
			}
		}
	});
	if (all_matched_conditions.length > 0) {
		//console.trace([all_matched_conditions, all_convert_to_conditions]);
		return { all_matched_conditions, all_convert_to_conditions };
	}

	return { all_convert_to_conditions };
}

/*
辭典檔應用順序:先測試整個詞相同的情況,再測試 {RegExp}。先測試包含詞性標注的條件式,再測試泛用情況(不論詞性)。
所有條件式皆依照辭典檔中的出現順序依序檢測。

1.	詞性相同 + 詞彙相同
2.	僅詞彙相同
3.	檢測所有詞性相同的 {RegExp}
4.	檢測所有泛用的 {RegExp}

*/
const get_all_possible_matched_condition_options = [
	{ try_tag: true },
	,
	{ try_tag: true, search_pattern: true },
	{ search_pattern: true }
];
function get_all_possible_matched_condition(options) {
	let best_matched_data;
	for (const _options of get_all_possible_matched_condition_options) {
		// 引用 options 主要是為了 options.convert_to_language @ condition_filter_LTP()。
		const matched_data = get_matched_condition.call(this, { ...options, ..._options });
		if (matched_data) {
			if (matched_data.all_matched_conditions)
				return matched_data;
			best_matched_data = best_matched_data || matched_data;
		}
	}

	return best_matched_data;
}

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

//const web_request_queues_count = new Map;
const web_request_queues = new Map;
// 控制流量,依照順序傳輸,別一次全部衝上去。
function add_new_web_request(host, promise) {
	//console.trace(web_request_queues);
	if (web_request_queues.has(host)) {
		//console.log([web_request_queues_count, web_request_queues, promise]);
		//web_request_queues_count.set(host, web_request_queues_count.get(host) + 1);
		//console.log(`Set ${web_request_queues_count.get(host)} ${host}`);
		const _promise = promise;
		promise = web_request_queues.get(host)
			// clean error
			.catch(() => null)
			//.then(() => { web_request_queues_count.set(host, web_request_queues_count.get(host) - 1); console.log(`Requesting ${web_request_queues_count.get(host)} ${host}`) })
			.then(() => _promise);
		//web_request_queues_count.set(host, 0);
	}
	web_request_queues.set(host, promise);
	return promise;
}

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

// POS tagging 词性标注 詞性標注
function tag_paragraph_jieba(paragraph, options) {
	return this.nodejieba_CN.tag(paragraph);
}

// word_data 會被寫入 cache,因此 KEY_prefix_spaces 必須為 JSON 可接受之 key(即 {String}),否則會漏失資訊!
const KEY_prefix_spaces = 'prefix spaces';
function recover_spaces(parsed, paragraph) {
	const KEY_word = this.KEY_word;
	let offset = 0;
	for (let parsed_index = 0; parsed_index < parsed.length; parsed_index++) {
		const word_data = parsed[parsed_index];
		const word = word_data[KEY_word];
		const index = paragraph.indexOf(word, offset);
		if (index === offset) {
			offset += word.length;
			continue;
		}

		if (index > offset) {
			word_data[KEY_prefix_spaces] = paragraph.slice(offset, index);
			offset = index + word.length;
			continue;
		}

		console.log(parsed);
		console.log([parsed_index, word_data]);
		throw new Error(`Not found: ${JSON.stringify(word)} in ${JSON.stringify(paragraph)}`);
	}

	//assert: offset <= paragraph.length
	if (offset < paragraph.length) {
		// append tail spaces
		const tail = paragraph.slice(offset).match(/^(\s*)([\s\S]*?)$/);
		// assert: tail[2] === ''
		const word_data = { [KEY_prefix_spaces]: tail[1], [KEY_word]: tail[2] };
		//console.trace([offset, paragraph.length, word_data]);
		parsed.push(word_data);
	}
	//console.trace(parsed);
}

// @inner 自動生成辭典用的候選條件式。須配合 condition_filter_LTP()。
function generate_condition_LTP(configuration, options) {
	const synonyms_Map = options.skip_check_for_synonyms && this.synonyms_of_language[options.convert_to_language];
	const synonym_pattern_list = synonyms_Map && synonyms_Map[KEY_synonym_pattern];
	//console.trace(configuration);
	const { tagged_word_list, converted_text, should_be_text } = configuration;
	//console.trace({ tagged_word_list, converted_text, should_be_text });
	const start_index = configuration.start_index >= 0 ? configuration.start_index : 0;
	const end_index = isNaN(configuration.end_index) ? tagged_word_list.length : Math.min(tagged_word_list.length, configuration.end_index);
	//console.trace([configuration, options.paragraph_index, start_index, end_index]);
	const tagged_word_list_index_offset = start_index - tagged_word_list[start_index].id;
	//assert: tagged_word_list[tagged_word_list_index_offset].id === 0

	const diff_list = CeL.LCS(converted_text.slice(start_index, end_index).join(''), should_be_text, { diff: true });
	//console.trace({ converted_text.slice(start_index, end_index), should_be_text, diff_list });
	const condition_list = [], index_hash = Object.create(null);
	condition_list.index_hash = index_hash;
	for (let index = start_index,
		/** {Number} offset of converted_to slice in converted_text */
		index_of_converted_to_slice = 0; index < end_index; index++) {
		const word_data = tagged_word_list[index];
		const converted_to = converted_text[index];
		//console.trace([word_data, converted_to]);

		// 應對 converted_to slice 與 should_be_slice 長度不同的情況。
		const should_be_slice = should_be_text.slice(
			CeL.LCS.corresponding_index(diff_list, index_of_converted_to_slice),
			CeL.LCS.corresponding_index(diff_list, index_of_converted_to_slice += converted_to.length)
		);
		if (false) {
			console.trace([
				[index_of_converted_to_slice, index_of_converted_to_slice + converted_to.length, converted_to],
				[CeL.LCS.corresponding_index(diff_list, index_of_converted_to_slice - converted_to.length), CeL.LCS.corresponding_index(diff_list, index_of_converted_to_slice), should_be_slice]
			]);
		}

		//console.trace([should_be_slice, converted_to, word_data]);
		if (should_be_slice === converted_to) {
			continue;
		}
		//console.trace([should_be_slice, converted_to, word_data]);
		const target = should_be_slice.trim();
		// 不檢查/跳過通同字/同義詞,通用詞彙不算錯誤。用於無法校訂原始文件的情況。
		if (synonyms_Map) {
			// .trimStart(): 只有 word_data[KEY_prefix_spaces]
			if (synonyms_Map.has(target) && synonyms_Map.get(target).includes(converted_to.trimStart())) {
				//console.trace(`為可接受之通同字/同義詞,可跳過 ${JSON.stringify(target)}。`);
				continue;
			}

			if (synonym_pattern_list.some(synonym_pattern =>
				synonym_pattern.test(target)
				// assert: pattern has .replace_to
				&& synonym_pattern.replace(target) === converted_to.trimStart()
			)) {
				//console.trace(`匹配可接受之通同字/同義詞,可跳過 ${JSON.stringify(target)}。`);
				continue;
			}
		}

		const condition = [word_data_to_condition.call(this, word_data)];
		//console.trace([condition, target]);
		//const stringified_target = stringify_condition(target);
		const base_condition = '~' + stringify_condition(target);
		word_data.condition = condition;
		if (word_data.parent >= 0) {
			condition.push(`${base_condition}<${word_data.relation}>${word_data_to_condition.call(this, tagged_word_list[tagged_word_list_index_offset + word_data.parent])}`);
		}
		word_data.roles.forEach(role =>
			condition.push(`${base_condition}<role.type:${role.type}>${word_data_to_condition.call(this, role)}`)
		);
		word_data.parents.forEach(parent => {
			const parent_condition = word_data_to_condition.call(this, parent);
			if (parent_condition) {
				// assert: parent === tagged_word_list[tagged_word_list_index_offset + parent.id]
				condition.push(`${base_condition}<parent.relate:${parent.relate}>${parent_condition}`);
				//console.log(word_data);
				//console.trace(parent);
			} else {
				// 可能有 `{ parent: 103, relate: 'eSUCC' }` 之類。
				//console.trace(parent);
			}
		});
		// 反向關係。
		for (let _index = tagged_word_list_index_offset, latest_id = -1; _index < tagged_word_list.length; _index++) {
			const word_data_to_test = tagged_word_list[_index];
			if (latest_id >= word_data_to_test.id) {
				// tagged_word_list 可能是 recover_original_paragraphs() 多次查詢拼合起來的。當 (latest_id > word_data_to_test.id) 的時候,已經超越本次查詢的範圍。
				// assert: word_data_to_test.id === 0
				break;
			}
			// assert: word_data_to_test.id === latest_id + 1
			latest_id = word_data_to_test.id;
			// tagged_word_list 可能是 recover_original_paragraphs() 多次查詢拼合起來的。`word_data_to_test.parent` 實際指向的應該是 `word_data`。
			if (word_data_to_test.parent === word_data.id) {
				//console.trace(word_data_to_test);
				condition.push(`${base_condition}<←${word_data_to_test.relation}>${word_data_to_condition.call(this, word_data_to_test)}`);
			}
		}
		if (condition.length === 1) {
			// 完全沒有相符的,只好給一個避免空窗。 e.g., n:干脆面
			condition.push(base_condition);
		}
		//CeL.info(`${generate_condition_LTP.name}: Condition for ${word_data[this.KEY_word]}→${base_condition}:`);
		Object.assign(condition, {
			parsed: word_data, target, error_converted_to: converted_to,
			//should_be_slice
		});
		//CeL.log(condition.join('\t'));
		//console.trace(condition);
		condition_list.push(condition);
		index_hash[index] = condition;
	}

	return condition_list;
}

function recover_original_paragraphs(parsed, options) {
	const { token_count_array } = options;
	if (!token_count_array)
		return parsed;

	// 警告: 這種合併 merge / combine 可能造成不可靠的 .id, .offset, .parent 等!在 condition_filter_LTP() 中以 tagged_word_list_index_offset 處理此問題。
	return token_count_array.map((length, index) => {
		let parsed_index = index === 0 ? 0 : token_count_array[index - 1];
		const result_token = parsed[parsed_index];
		//Object.freeze(result_token.parents);
		while (++parsed_index < length) {
			result_token.append(parsed[parsed_index]);
		}
		return result_token;
	});
}

function parse_LTP_result(parsed, options) {
	if (typeof parsed === 'string')
		parsed = JSON.parse(parsed);
	//console.trace(parsed);

	let result = parsed.map((parsed_paragraph, index) => {
		const words = parsed_paragraph.words;
		if (!options.ignore_spaces && words[words.length - 1].offset + words[words.length - 1].length !== options.paragraphs_before_convert[index].length) {
			//https://github.com/HIT-SCIR/ltp/issues/201
			//https://github.com/HIT-SCIR/ltp/issues/204
			//https://github.com/HIT-SCIR/ltp/issues/408#issuecomment-686915450
			//console.trace(`Need recover spaces: ${options.paragraphs_before_convert[index]}→${words.map(word_data => word_data[this.KEY_word]).join('')}`);
			//console.trace(options.paragraphs_before_convert);
			recover_spaces.call(this, words, options.paragraphs_before_convert[index]);
		}
		return words;
	});

	result = recover_original_paragraphs(result, options);
	//console.trace(JSON.stringify(result));

	if (!options.is_Array) {
		// assert: result.length === 1
		return result[0];
	}

	return result;
}

// http://ltp.ai/docs/quickstart.html#ltp-server
function get_LTP_data(options) {
	//console.trace(options);
	const parsed_array = [];
	let promise;
	options.paragraphs_before_convert.forEach((paragraph, paragraph_index, list) => {
		if (typeof paragraph !== 'string' || !paragraph) {
			// Should not go to here!
			CeL.error(`${get_LTP_data.name}: Did not set text: ${paragraph_index + 1}/${list.length}`);
			return;
		}

		if (parsed_array[paragraph_index] = options.cached_tagged_word_list_Map?.get(paragraph)) {
			// Using cache and process next paragraph.
			return;
		}

		promise = add_new_web_request(this.LTP_URL, new Promise((resolve, reject) => {
			CeL.log_temporary(`${get_LTP_data.name}: ${list.length > 1 ? `${paragraph_index + 1}/${list.length} ` : ''}Query LTP server for ${paragraph.length} chars text.`);
			CeL.get_URL(this.LTP_URL, (XMLHttp, error) => {
				if (error) {
					reject(error);
				} else {
					//console.log(paragraph);
					parsed_array[paragraph_index] = JSON.parse(XMLHttp.responseText);
					resolve();
				}
			}, null, { text: paragraph }, {
				// CeL.to_millisecond('60 min')
				timeout: 60 * 60 * 1000,
				error_retry: 4,
				headers: {
					'Content-Type': 'application/json; charset=utf-8'
				}
			})
		}));
	});

	return promise && promise.then(parse_LTP_result.bind(this, parsed_array, options))
		|| parse_LTP_result.bind(this, parsed_array, options);
}

// @see resources/ltp_parse.py
const MARK_result_starts = 'Parsed JSON:';
// assert: LTP_paragraph_MAX_LENGTH <= 510
const LTP_paragraph_MAX_LENGTH = 500;

function preserve_tail(tail) {
	return tail.match(/^["'’”」:\s]*/)[0];
}

function fill_cached_tagged_word_list_Map(paragraphs, options, cached_tagged_word_list_Map) {
	if (!cached_tagged_word_list_Map)
		return;

	const cached_tagged_word_list = [];
	if (paragraphs.every((paragraph, index) => {
		const _options = set__options_tagged_word_list__via_cache.call(this, paragraph, options)[0];
		const tagged_word_list = _options.tagged_word_list;
		if (tagged_word_list) {
			cached_tagged_word_list_Map.set(paragraph, tagged_word_list);
			cached_tagged_word_list[index] = tagged_word_list;
			return true;
		}
	})) {
		return options.isArray ? cached_tagged_word_list : cached_tagged_word_list[0];
	}
}

function tag_paragraph_LTP(paragraphs, options) {
	// 避免污染,重新造一個 options。
	options = { ...options, is_Array: Array.isArray(paragraphs) };
	//console.trace([this.LTP_URL, options]);

	// LTP 一次只能處理大約500字左右,因此必須適度切分。
	// https://github.com/HIT-SCIR/ltp/issues/407#issuecomment-686864300
	// bert 类的transformers都有512个最大字符长度的限制,然后我们的web demo运行的是base模型
	// https://github.com/HIT-SCIR/ltp/issues/388
	// 实际上也是510,但是在输入时进行tokenize时,对于数字和英文会产生子词,所以使用字符数估计长度并不准确,另外这段话可以先进行分句操作来避免报错。
	if (!options.is_Array) {
		paragraphs = [paragraphs];
	}

	const cached_tagged_word_list_Map = new Map;
	let cached_tagged_word_list = fill_cached_tagged_word_list_Map.call(this, paragraphs, options, cached_tagged_word_list_Map);
	if (cached_tagged_word_list) {
		return cached_tagged_word_list;
	}

	// @see https://github.com/HIT-SCIR/ltp/blob/master/ltp/utils/sent_split.py
	if (paragraphs.some(paragraph => paragraph.length > LTP_paragraph_MAX_LENGTH)) {
		const paragraphs_before_convert = [];
		const token_count_array = [];
		paragraphs.forEach(paragraph => {
			while (paragraph.length > LTP_paragraph_MAX_LENGTH) {
				const piece = paragraph.slice(0, LTP_paragraph_MAX_LENGTH);
				let token = piece.replace(/[^。?!…]*$/, preserve_tail)
					|| piece.replace(/[^.?!,]*$/, preserve_tail)
					|| piece;
				paragraph = paragraph.slice(token.length);
				const matched = paragraph.match(/^\s+/);
				if (matched) {
					paragraph = paragraph.slice(matched[0].length);
					token += matched[0];
				}
				paragraphs_before_convert.push(token);
			}
			if (paragraph) {
				paragraphs_before_convert.push(paragraph);
			}
			token_count_array.push(paragraphs_before_convert.length);
		});

		cached_tagged_word_list = fill_cached_tagged_word_list_Map.call(this, paragraphs_before_convert, options, cached_tagged_word_list_Map);
		if (cached_tagged_word_list) {
			return cached_tagged_word_list;
		}

		Object.assign(options, { token_count_array, paragraphs_before_convert });

	} else {
		options.paragraphs_before_convert = paragraphs;
	}

	Object.assign(options, { cached_tagged_word_list_Map });

	//console.trace(this.LTP_URL);
	if (this.LTP_URL) {
		//console.trace([paragraphs, options]);
		return get_LTP_data.call(this, options);
	}

	let parsed = require('child_process').execFileSync('python3', [module_base_path + 'resources/ltp_parse.py', '-j', JSON.stringify(options.paragraphs_before_convert)]);
	parsed = parsed.toString();
	//console.trace(parsed);
	parsed = parsed.between(MARK_result_starts);
	return parse_LTP_result.call(this, parsed, options);
}

function convert_CoreNLP_result(result) {
	result = JSON.parse(result).sentences;
	const tokens = result[0].tokens;
	// free
	delete result[0].tokens;
	tokens.result = result;
	for (let index = 1; index < result.length; index++) {
		tokens.append(result[index].tokens);
		// free
		delete result[index].tokens;
	}

	//console.log(tokens.map(word_data => word_data.word).join(' '));
	//"basicDependencies":[...,{"dep":"nummod","governor":8,"governorGloss":"小猫","dependent":4,"dependentGloss":"三"},...]
	//console.trace(JSON.stringify(tokens.result[0]));

	return tokens;
}

// using Stanford CoreNLP
function tag_paragraph_via_CoreNLP(paragraph, options) {
	//paragraph = encodeURIComponent(paragraph);
	return add_new_web_request(this.CoreNLP_URL, new Promise((resolve, reject) => {
		this.CoreNLP_URL_properties.date = new Date;
		this.CoreNLP_URL.searchParams.set('properties', JSON.stringify(this.CoreNLP_URL_properties));
		this.CoreNLP_URL.searchParams.set('pipelineLanguage', 'zh');
		//console.trace(this.CoreNLP_URL.toString());
		CeL.log_temporary(`${tag_paragraph_via_CoreNLP.name}: Query CoreNLP server for ${paragraph.length} chars text.`);
		CeL.get_URL(this.CoreNLP_URL, (XMLHttp, error) => {
			if (error) {
				reject(error);
			} else {
				//console.log(paragraph);
				resolve(convert_CoreNLP_result(XMLHttp.responseText));
			}
		}, null, paragraph, {
			error_retry: 4
		})
	}));
}

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

// 強制轉換段落/sentence。
function forced_convert_to_TW(paragraph, index, tagged_word_list, options) {
	//CeL.log(`${paragraph}→${CeL_CN_to_TW(paragraph)}`);

	// 採行 CeL.CN_to_TW() 的原因:這是經過調試,比較準確的轉換器。
	// 採用的辭典見 https://github.com/kanasimi/CeJS/blob/master/extension/zh_conversion/corrections.txt 。
	return CeL_CN_to_TW(paragraph, options);
}
forced_convert_to_TW.base_function = CeL_CN_to_TW;

function forced_convert_to_CN(paragraph, index, tagged_word_list, options) {
	return CeL_TW_to_CN(paragraph, options);
}
forced_convert_to_CN.base_function = CeL_CN_to_TW;

// 詞性標注結果換行,方便查詢檢視。
function beautify_tagged_word_list(tagged_word_list) {
	return JSON.stringify(tagged_word_list).replace(/,{"id":/g, ',\n{"id":');
}

function load_general_word_list_cache_file(/*cache_file_for_short_sentences*/cache_file_path, options, rebuild_cache) {
	if (rebuild_cache || !this.general_word_list_cache)
		this.general_word_list_cache = Object.create(null);
	if (rebuild_cache || !this.general_word_list_cache_files_loaded)
		this.general_word_list_cache_files_loaded = [];

	if (this.general_word_list_cache_files_loaded.includes(cache_file_path))
		return;

	let cache_data = CeL.read_file(cache_file_path);
	if (!cache_data)
		return;

	CeL.info(`${load_general_word_list_cache_file.name}: ${cache_file_path}`);
	cache_data = JSON.parse(cache_data.toString());
	// assert: cache_data = { paragraph: {Object}tagged_word_list, ...};
	Object.assign(this.general_word_list_cache, cache_data);
	//console.trace(this.general_word_list_cache);
	this.general_word_list_cache_files_loaded.push(cache_file_path);
}

function set__options_tagged_word_list__via_cache(paragraph, options) {
	let { cache_directory } = options;
	if (!cache_directory) {
		return [options, cache_directory];
	}

	//console.trace([cache_directory, CeL.append_path_separator(cache_directory)]);
	cache_directory = CeL.append_path_separator(cache_directory);
	//console.trace(cache_directory);

	if (options.cache_file_for_short_sentences) {
		if (options.cache_file_for_short_sentences === true)
			options.cache_file_for_short_sentences = '!short_sentences_word_list.json';
		load_general_word_list_cache_file.call(this, cache_directory + options.cache_file_for_short_sentences, options, this.latest_cache_directory !== cache_directory);
	}
	this.latest_cache_directory = cache_directory;

	if (this.general_word_list_cache && this.general_word_list_cache[paragraph]) {
		// 重新造一個 options 以避免污染。
		options = {
			...options,
			// deep clone. 避免後續 this.general_word_list_cache 內容被更動。
			tagged_word_list: Object.clone(this.general_word_list_cache[paragraph], true)
		};
		//options.tagged_word_list.is_cache = true;

	} else if (!options.tagged_word_list
		// 超過此長度才創建個別的 cache 檔案,否則會放在 .cache_file_for_short_sentences。
		&& (!options.min_cache_length || paragraph.length >= options.min_cache_length)
	) {
		// 可自附 options.tagged_word_list,或者會由 set__options_tagged_word_list__via_cache() 依 cache_directory 下的資料填入。

		// 重新造一個 options 以避免污染。
		options = {
			...options,
			cache_file_path: cache_directory + CeL.to_file_name(paragraph.slice(0, 40) + '.' + paragraph.hashCode() + '.json')
		};
		let cache_data = CeL.read_file(options.cache_file_path);
		if (cache_data) {
			try {
				cache_data = JSON.parse(cache_data.toString());
				//console.trace(options);
				//console.trace(`Using cache file: ${options.cache_file_path}`);
				options.tagged_word_list = cache_data;
				options.tagged_word_list.is_cache = true;
			} catch (e) {
				CeL.error(`${set__options_tagged_word_list__via_cache.name}: Skip ${options.cache_file_path}: ${e}`);
			}
		}
	}
	return [options, cache_directory];
}

function save_cache_file_for_short_sentences(cache_file_path) {
	//console.trace([cache_file_path, Object.keys(this.general_word_list_cache).length]);
	CeL.write_file(cache_file_path, this.general_word_list_cache);
	delete this.save_cache_file_for_short_sentences;
}

/**
 * 轉換段落文字。
 * @param {String}paragraph 段落文字
 * @param {Object}[options]
 */
function convert_paragraph(paragraph, options) {
	let cache_directory;
	[options, cache_directory] = set__options_tagged_word_list__via_cache.call(this, paragraph, options);

	const tagged_word_list = options.tagged_word_list || this.tag_paragraph(paragraph, options);
	if (CeL.is_thenable(tagged_word_list)) {
		return tagged_word_list.then(
			tagged_word_list => convert_paragraph.call(this, paragraph, { ...options, tagged_word_list })
		);
	}
	//console.trace(tagged_word_list);

	// ---------------------------------------------
	// Write cache to file

	//console.trace([cache_directory, options]);
	// options.cache_file_path maybe undefined!
	if (cache_directory) {
		if (options.cache_file_path) {
			if (!options.tagged_word_list.is_cache) {
				CeL.create_directory(cache_directory);
				//console.trace(options);
				//console.trace(`Write tagged data to ${options.cache_file_path}`);
				CeL.write_file(options.cache_file_path, beautify_tagged_word_list(tagged_word_list));
			}

		} else if (options.cache_file_for_short_sentences) {
			if (!this.general_word_list_cache[paragraph]) {
				// deep clone. 避免後續 this.general_word_list_cache 內容被更動。
				this.general_word_list_cache[paragraph] = Object.clone(tagged_word_list, true);
				const cache_file_path = cache_directory + options.cache_file_for_short_sentences;
				//console.trace([cache_file_path, paragraph]);
				if (options.delay_save_file) {
					// 延遲儲存檔案。 Must save the file yourself!
					this.save_cache_file_for_short_sentences = save_cache_file_for_short_sentences.bind(this, cache_file_path);
				} else {
					save_cache_file_for_short_sentences.call(this, cache_file_path);
				}
			}
		}
	}

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

	const convertion_pairs = this.convertion_pairs[options.convert_to_language];
	const [forced_convert, max_convert_word_length] = (() => {
		let converter = options.convert_to_language === 'TW'
			? this.CN_to_TW || forced_convert_to_TW
			: this.TW_to_CN || forced_convert_to_CN;
		// initialization
		converter('');
		//console.trace(converter.base_function.max_convert_word_length);
		return [converter.bind(this), converter.base_function.max_convert_word_length || 20];
	})();

	// default (undefined) or 'word': 每個解析出的詞單獨作 zh_conversion。
	// 'combine': 結合未符合分詞辭典規則之詞一併轉換。converter 必須有提供輸入陣列的功能。
	// false: 按照原始輸入,不作 zh_conversion。
	const word_convert_mode = options.forced_convert_mode === undefined ? true : options.forced_convert_mode;
	// 檢查辭典檔的規則。debug 用,會拖累效能。
	const { check_dictionary } = options;
	// node.js: 直接開 `conversion.convert(text)` 速度相同,且還包含 .special_keys_Map 的轉換,較完整。
	const word_mode_options = options /* { mode: 'word_first', ...options } */;

	const generate_condition_for = options.generate_condition_for || this.generate_condition_for_language && this.generate_condition_for_language[options.convert_to_language];
	//console.trace(generate_condition_for);

	let converted_text = [], waiting_queue = [];
	tagged_word_list.forEach((word_data, index_of_tagged_word_list) => {
		// assert: word_data === tagged_word_list[index_of_tagged_word_list]
		const matched_condition_data = get_all_possible_matched_condition.call(this, { ...options, word_data, convertion_pairs, index_of_tagged_word_list, tagged_word_list });
		// 維持與輸入相同格式: 用於補全失落的空白字元。
		const prefix_spaces = word_data[KEY_prefix_spaces];
		if (!matched_condition_data) {
			if (word_convert_mode === 'combine') {
				// 把所有沒匹配到的詞合起來,一起採用 CeL.zh_conversion.CeL_CN_to_TW() 或 CeL_TW_to_CN()。
				waiting_queue.push(prefix_spaces ? prefix_spaces + word_data[this.KEY_word] : word_data[this.KEY_word]);
				return;
			}
			// assert: word_convert_mode === 'word' || !word_convert_mode
			const processed_word = word_convert_mode ? forced_convert(word_data[this.KEY_word], index_of_tagged_word_list, tagged_word_list, word_mode_options) : word_data[this.KEY_word];
			converted_text.push(prefix_spaces ? prefix_spaces + processed_word : processed_word);
			return;
		}

		//console.trace(matched_condition_data);
		const { all_matched_conditions } = matched_condition_data;
		if (all_matched_conditions) {
			//console.trace(all_matched_conditions);
			if (all_matched_conditions.length > 1) {
				//console.trace(all_matched_conditions);
			}
			let word = word_data[this.KEY_word], had_forced_converted;
			all_matched_conditions.forEach(/*matched_condition*/to_word_data => {
				if (options.generate_condition || generate_condition_for) {
					//console.trace(to_word_data);
					word_data[KEY_matched_condition] = to_word_data;
				}

				if (to_word_data.filter_name in this.filters[options.convert_to_language]) {
					return this.filters[options.convert_to_language][to_word_data.filter_name].call(this, { word_data, index_of_tagged_word_list, tagged_word_list, matched_condition_data, options });
				}

				const to_word = to_word_data[this.KEY_word];
				if (to_word) {
					if (to_word.replace_to) {
						// {RegExp}to_word
						word = to_word.replace(word);
					} else if (typeof to_word === 'string') {
						word = to_word;
					} else {
						throw new Error('Invalid KEY_word: ' + to_word);
					}
				}
				const do_after_converting = to_word_data.do_after_converting;
				if (do_after_converting) {
					if (!had_forced_converted) {
						had_forced_converted = true;
						word = forced_convert(word, index_of_tagged_word_list, tagged_word_list, word_mode_options);
					}
					// assert: pattern has .replace_to
					word = do_after_converting.replace(word);
				}
			});

			if (prefix_spaces)
				word = prefix_spaces + word;

			let converted_word_list;
			if (waiting_queue.length > 0) {
				//console.trace(waiting_queue);
				converted_word_list = forced_convert(waiting_queue, index_of_tagged_word_list, tagged_word_list, word_mode_options);
				//console.log([waiting_queue.join('').length, converted_word_list.join('').length]);
				//console.trace([waiting_queue.join(''), converted_word_list.join('')]);
				converted_text.append(converted_word_list);
			}

			if (check_dictionary) {
				// 檢查這條 rule 是否有必要。在更改 zh_conversion 辭典時可使用。
				if (converted_word_list) {
					converted_word_list.push(word);
				} else {
					converted_word_list = [word];
				}
				// 在上一個 rule 有作用的情況下,無須向前回溯。但這可能依情況有變,因此必須重複執行至所有冗餘  rule 皆處理過。
				waiting_queue.push(prefix_spaces ? prefix_spaces + word_data[this.KEY_word] : word_data[this.KEY_word]);
				for (let append_index = index_of_tagged_word_list, length_left = max_convert_word_length - word.length; length_left > 0 && ++append_index < tagged_word_list.length;) {
					const word_to_append = tagged_word_list[append_index][this.KEY_word];
					length_left -= word_to_append.length;
					waiting_queue.push(word_to_append);
				}

				let converted_word_list_without_rule = forced_convert(waiting_queue, index_of_tagged_word_list, tagged_word_list, word_mode_options);
				// 最後切到這個長度,剛好包含本 rule 的作用點。
				converted_word_list_without_rule = converted_word_list_without_rule.slice(0, converted_word_list.length);
				// 按照正常 zh_conversion 轉換若能獲得相同結果,則無必要。
				if (converted_word_list.join('') === converted_word_list_without_rule.join('')) {
					const to_word_data = word_data[KEY_matched_condition];
					CeL.info(`It seems the rule is unnecessary: ${to_word_data.matched_condition ? `${to_word_data.matched_condition} → ` : ''}${to_word_data.full_condition_text}`);
					//console.trace(word_data);
				}

				// TODO: 可以檢查單字個別轉換與完整連貫文本轉換的差異。
			}

			if (waiting_queue.length > 0) {
				// reset
				waiting_queue = [];
			}

			converted_text.push(word);
			return;
		}

		if (word_convert_mode === 'combine') {
			waiting_queue.push(prefix_spaces ? prefix_spaces + word_data[this.KEY_word] : word_data[this.KEY_word]);
			return;
		}
		// assert: word_convert_mode === 'word' || !word_convert_mode
		const processed_word = word_convert_mode ? forced_convert(word_data[this.KEY_word], index_of_tagged_word_list, tagged_word_list, word_mode_options) : word_data[this.KEY_word];
		converted_text.push(prefix_spaces ? prefix_spaces + processed_word : processed_word);
	});
	if (waiting_queue.length > 0) {
		const converted_word_list = forced_convert(waiting_queue, tagged_word_list.length, tagged_word_list, word_mode_options);
		//console.log([waiting_queue.join('').length, converted_word_list.join('').length]);
		//console.trace([waiting_queue.join(''), converted_word_list.join('')]);
		converted_text.append(converted_word_list);
		waiting_queue = null;
	}

	// 事後轉換函數。
	//console.trace([this.convertion_pairs, options]);
	convertion_pairs.get(KEY_postfix).forEach(single_condition => {
		if (single_condition.filter_name in this.filters[options.convert_to_language]) {
			//console.trace(single_condition);
			this.filters[options.convert_to_language][single_condition.filter_name].call(this, { single_condition, converted_text, tagged_word_list, options });
		}
	});

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

	if (generate_condition_for && this.generate_condition) {
		// 長度累加紀錄。
		let tagged_word_list_length_accumulation, converted_text_length_accumulation;
		for (let [convert_from_text, should_convert_to] of generate_condition_for.entries()) {
			const should_convert_to_is_Object = CeL.is_Object(should_convert_to);
			const should_convert_to_text = should_convert_to_is_Object ? should_convert_to.should_convert_to_text : should_convert_to;
			//console.trace([convert_from_text, should_convert_to_text]);
			// 勣 → '𪟝'.length === 2
			if (convert_from_text.chars().length !== should_convert_to_text.chars().length) {
				if (!skip_tests_convert_to_different_length) {
					CeL.error(`${convert_paragraph.name}: 預設解答與轉換前之文字長度不符,刪除解答:`);
					check_convert_to_different_length(convert_from_text, should_convert_to_text, true, true);
					generate_condition_for.delete(convert_from_text);
					continue;
				}

				if (!should_convert_to_is_Object || !should_convert_to.warned_convert_to_different_length) {
					// 設定已經警告過轉換長度不同。
					if (should_convert_to_is_Object)
						should_convert_to.warned_convert_to_different_length = true;
					CeL.warn(`${convert_paragraph.name}: 預設解答與轉換前之文字長度不符:`);
					check_convert_to_different_length(convert_from_text, should_convert_to_text, true, true);
				}
			}

			// 遍歷所有相符的。例如對於主要角色人名、專有名詞、成語成句就需要此種操作。
			for (let offset = 0; (offset = paragraph.indexOf(convert_from_text, offset)) !== NOT_FOUND;
				// 避免下次從原處開始搜尋。
				offset += convert_from_text.length) {
				//console.log([paragraph, convert_from_text, offset]);
				if (!tagged_word_list_length_accumulation) {
					let length = 0;
					// 初始化。
					tagged_word_list_length_accumulation = [length];
					tagged_word_list.forEach(word_data => tagged_word_list_length_accumulation.push(length += (word_data[KEY_prefix_spaces] ? word_data[KEY_prefix_spaces].length : 0) + word_data[this.KEY_word].length));
				}
				if (!converted_text_length_accumulation) {
					// 初始化。
					converted_text_length_accumulation = [0];
					converted_text.reduce((accumulated_length, token) => { converted_text_length_accumulation.push(accumulated_length += token.length); return accumulated_length; }, 0);
				}

				// 找出轉換後文字對應的位置。

				/** {Number}start index of converted_text */
				let start_index = offset;
				tagged_word_list_length_accumulation.search_sorted(offset, {
					found(index, is_near) {
						// 轉換前後字數可能不同。 the diff is usually +0.
						start_index += converted_text_length_accumulation[index] - tagged_word_list_length_accumulation[index];
						// for (paragraph === '李世𪟝正式吹响了出征高句丽的号角。' && convert_from_text === '出征'),
						// start_index += -1: '𪟝'.length === 2
					}
				});

				//console.assert(converted_text.join('').slice(start_index).startsWith(should_convert_to_text));

				let should_be_text = should_convert_to_text, end_index;
				converted_text_length_accumulation.search_sorted(start_index + should_be_text.length, {
					found(index, is_near) {
						//console.log(paragraph);
						//console.log([start_index, should_be_text.length, converted_text_length_accumulation]);
						//console.log([is_near, index, converted_text]);
						if (is_near) {
							should_be_text += converted_text[index].slice(start_index + should_be_text.length - converted_text_length_accumulation[index]);
							end_index = index + 1;
						} else {
							end_index = index;
						}
					}
				});
				/** {Number}在converted_text上,從本token開頭到匹配的文字中間的位移距離。token包括prefix_spaces。 */
				let distance_token_header_to_metched = 0;
				// 避免中間切斷,移到本token之首。
				converted_text_length_accumulation.search_sorted(start_index, {
					found(index, is_near) {
						if (is_near) {
							distance_token_header_to_metched = start_index - converted_text_length_accumulation[index];
							// assert: distance_token_header_to_metched > 0
							//console.trace([start_index, converted_text_length_accumulation[index], converted_text[index].slice(0, distance_token_header_to_metched), should_be_text]);
							should_be_text = converted_text[index].slice(0, distance_token_header_to_metched) + should_be_text;
						}
						// assert: should_be_text.startsWith(converted_text[index]);
						start_index = index;
					}
				});
				const converted_text_String = converted_text.slice(start_index, end_index).join('');
				if (check_convert_to_different_length(converted_text_String, should_be_text)) {
					continue;
				}

				if (!should_convert_to_is_Object) {
					// 初始化。
					generate_condition_for.set(convert_from_text, should_convert_to = { should_convert_to_text });
				}
				if (!should_convert_to.check_result) {
					// 初始化。
					should_convert_to.check_result = { OK: [], NG: [] };
				}
				if (converted_text_String === should_be_text) {
					//console.trace('紀錄已處理過的項目。');
					should_convert_to.check_result.OK.push(true);
					continue;
				}

				//CeL.info(`檢查: ${convert_from_text}→${should_be_text}`);
				//console.trace({ tagged_word_list, converted_text, should_be_text, start_index, end_index });
				const condition_list = this.generate_condition({ tagged_word_list, converted_text, should_be_text, start_index, end_index }, options);
				//console.trace(condition_list);
				if (condition_list.length === 0) {
					//console.trace('只有通同字/同義詞。');
					should_convert_to.check_result.OK.push(true);
					continue;
				}

				//console.trace({ distance_token_header_to_metched, converted_text_String });
				should_convert_to.check_result.NG.push(true);
				this.print_section_report({
					tagged_word_list,
					condition_list,
					convert_from_text,
					convert_to_text: converted_text_String.slice(distance_token_header_to_metched)
						// remove word_data[KEY_prefix_spaces]
						.trimStart(),
					should_convert_to_text,
					start_index, end_index, distance_token_header_to_metched
				}, options);
			}

		}

	}

	if (options.generate_condition && this.generate_condition) {
		if (options.should_be) {
			//should_be_text: should_convert_to_text
			const should_be_text = options.should_be[options.paragraph_index];
			const converted_text_String = converted_text.join('');
			if (check_convert_to_different_length(converted_text_String, should_be_text)) {
				;

			} else if (converted_text_String !== should_be_text) {
				const condition_list = this.generate_condition({ tagged_word_list, converted_text, should_be_text }, options);
				if (condition_list.length > 0) {
					if (!options.should_be.correction_conditions)
						options.should_be.correction_conditions = [];
					options.should_be.correction_conditions[options.paragraph_index] = condition_list;
				} else {
					// 只有通同字/同義詞。
				}

			}

		} else {
			CeL.error(`convert_paragraph: 未設定 options.should_be!`);
		}
	}

	converted_text = converted_text.join('');

	if (!word_convert_mode) {
		converted_text = forced_convert(converted_text);
	}
	return converted_text;
}

function convert_Chinese(paragraphs, options) {
	if (!paragraphs)
		return paragraphs === 0 ? String(paragraphs) : '';

	if (Array.isArray(options.converted_paragraphs)) {
		return return_converted_paragraphs(options, options.converted_paragraphs);
	}

	if (typeof paragraphs === 'string') {
		options.input_string = true;
		paragraphs = [paragraphs];
	}

	const domain = this.detect_domain(paragraphs, options);

	if (this.batch_get_tag && !Array.isArray(options.tagged_word_list_of_paragraphs)) {
		// 批量取得詞性標注。
		const tagged_word_list_of_paragraphs = this.tag_paragraph(paragraphs, options);
		if (CeL.is_thenable(tagged_word_list_of_paragraphs)) {
			return options.tagged_word_list_of_paragraphs.then(tagged_word_list_of_paragraphs => {
				options.tagged_word_list_of_paragraphs = tagged_word_list_of_paragraphs;
				return execute_convert_Chinese.call(this, paragraphs, options);
			});
		}
		options.tagged_word_list_of_paragraphs = tagged_word_list_of_paragraphs;
	}

	return execute_convert_Chinese.call(this, paragraphs, options);
}

function execute_convert_Chinese(paragraphs, options) {
	let converted_paragraphs = [];
	const { tagged_word_list_of_paragraphs } = options;
	let some_async;
	//console.trace(paragraphs);
	for (let paragraph_index = 0; paragraph_index < paragraphs.length; paragraph_index++) {
		const paragraph = paragraphs[paragraph_index];
		let _options = options;
		if (Array.isArray(tagged_word_list_of_paragraphs)) {
			_options = { ..._options, tagged_word_list: tagged_word_list_of_paragraphs[paragraph_index] };
		}
		if (options.generate_condition) {
			_options = { ..._options, paragraph_index };
		}
		const converted_paragraph = convert_paragraph.call(this, paragraph, _options);
		//console.trace(converted_paragraph);
		converted_paragraphs.push(converted_paragraph);
		some_async = some_async || CeL.is_thenable(converted_paragraph);
	}

	if (!some_async)
		return return_converted_paragraphs(options, converted_paragraphs);

	//console.trace(`Using Promise to wait for ${converted_paragraphs.length} / ${paragraphs.length}: ${converted_paragraphs}`);
	return Promise.all(converted_paragraphs).then(
		converted_paragraphs => return_converted_paragraphs(options, converted_paragraphs)
	);
}

function return_converted_paragraphs(options, converted_paragraphs) {
	//console.trace(converted_paragraphs);
	if (options.input_string && Array.isArray(converted_paragraphs) && converted_paragraphs.length === 1)
		converted_paragraphs = converted_paragraphs[0];

	if (options.get_full_data)
		return { converted_paragraphs, tagged_word_list_of_paragraphs: options.tagged_word_list_of_paragraphs };

	// free
	//delete options.tagged_word_list_of_paragraphs;

	return converted_paragraphs;
}

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

// 注意:LTP 於末尾有無句號、數個句子是合併或拆分解析,會有不同解析結果。
// (?:[。?!]|……)[\r\n]*|
function get_paragraphs_of_text(text, options) {
	if (!text)
		return;

	text = text.toString();
	const with_configurations = !!options?.with_configurations;
	if (with_configurations) {
		text = text
			// @see CeL.data.Convert_Pairs.remove_comments(text)
			.replace(/\/\*[\s\S]*?\*\//g, '');
	} else {
		text = CeL.data.Convert_Pairs.remove_comments(text);
	}

	text = text.trim();
	if (!text) return;

	const configurations = with_configurations && Object.create(null);
	let this_config;

	const paragraphs = text
		// .split(/[\n\s]+/)
		.split('\n')

		//.map(text => text.trim()).filter(text => !!text)
		.reduce(with_configurations ? (filtered, text) => {
			if (text = text.trim()) {
				// 可用 `// {"原文":"___"}` 來設定下一行文句的屬性。
				if (text.startsWith('//')) {
					this_config = null;
					// 2 === '//'.length
					text = text.slice(2).trimStart();
					//console.trace([text]);
					if (text.endsWith('}') && /^{\s*"/.test(text)) {
						try {
							this_config = JSON.parse(text);
							//console.trace(this_config);
						} catch {
							CeL.warn(`${get_paragraphs_of_text.name}: Invalid JSON? ${text}`);
						}
					}
					return filtered;
				}

				text = text.replace(/\/\/.*/, '').trim();
				filtered.push(text);
				if (this_config) {
					//console.trace([text, this_config]);
					configurations[text] = this_config;
					this_config = null;
				}
			}
			this_config = null;
			return filtered;

		} : (filtered, text) => {
			if (text = text.trim()) filtered.push(text);
			return filtered;
		}, [])
		;

	//console.trace([paragraphs, configurations]);
	if (paragraphs.length > 0) {
		if (with_configurations) {
			paragraphs.configurations = configurations;
			//console.trace(paragraphs);
		}
		return paragraphs;
	}
}

function get_paragraphs_of_file(file_name, options) {
	return get_paragraphs_of_text(CeL.read_file(file_name), options);
}

// 前期轉換函數: 將網頁原始碼轉成分詞用的文字。
function normalize_HTML(html) {
	html = CeL.HTML_to_Unicode(html)
		//<br /> → "\n"
		.replace(/<br(?:\s[^<>]*)?>/ig, '\n')
		//.trim()
		//去掉 "\r",全部轉為 "\n"。
		.replace(/\r\n?/g, '\n')
		//最多允許兩個 "\n" 以為分段。
		.replace(/\n{3,}/g, '\n\n');

	return html;
}

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

Object.assign(Chinese_converter, {
	//KEY_matched_condition,
	//KEY_prefix_spaces,
	//print_correction_condition,

	get_paragraphs_of_text, get_paragraphs_of_file,
	beautify_tagged_word_list,
	to_converted_file_path,
});

Object.assign(Chinese_converter.prototype, {
	tag_paragraph_jieba, tag_paragraph_via_CoreNLP,

	dictionaries_directory: CeL.append_path_separator(module_base_path + 'dictionaries'),

	test_articles_directory: CeL.append_path_separator(test_directory + 'articles'),
	// 這些是特別的檔案: 會自動檢核。
	text_to_check_files: ['watch_target.TW.txt', 'watch_target.CN.txt'],

	print_section_report,

	regenerate_converted, not_new_article_to_check,
	load_text_to_check, load_default_text_to_check, report_text_to_check,
});

module.exports = Chinese_converter;

// export { Chinese_converter };