User:SunAfterRain/js/noteTA.js

注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// <nowiki>
// Covert From https://zh.wikipedia.org/w/index.php?title=MediaWiki:Gadget-noteTA.js&oldid=63601886
$.when(
	$.ready,
	mw.loader.using(['mediawiki.api', 'ext.gadget.HanAssist', 'oojs-ui', 'jquery.makeCollapsible'])
).then((_$, require) => {
	const HanAssist = require('ext.gadget.HanAssist');

	const api = new mw.Api();

	/** @type {Map<string, OO.ui.ProcessDialog>} */
	const viewerMap = new Map();
	const windowManager = new OO.ui.WindowManager();
	windowManager.$element.appendTo(document.body);

	/**
	 * @param {any} value
	 * @param {string} valueName 
	 * @return {asserts value} 
	 */
	function assert(value, valueName) {
		if (!value) {
			throw new Error(`Assert Fail, ${valueName} == false.`);
		}
	}

	class ApiRetryFailError extends Error {
		get name() {
			return 'ApiRetryFailError';
		}

		/**
		 * @param {string[]} errors
		 */
		constructor(errors) {
			super(`Api calls failed ${errors.length} time(s) in a row.`);
			this.errors = errors;
		}

		toJQuery() {
			const errorCount = this.errors.length;
			return $('<div>')
				.attr({
					class: 'error'
				})
				.append(
					$('<p>')
						.text(HanAssist.conv({
							hans: `Api 调用连续失败 ${errorCount} 次,${errorCount} 次调用的错误分别为:`,
							hant: `Api 調用連續失敗 ${errorCount} 次,${errorCount} 次調用的錯誤分別為:`,
							other: `Api calls failed ${errorCount} time(s) in a row. Errors: `
						})),
					$('<ol>')
						.append(this.errors.map(v => $('<li>').append(v.split('\n').map(v => $('<p>').text(v)))))
				);
		}
	}

	/**
	 * @typedef {{ [K in keyof C]: C[K] extends (...args: any[]) => any ? K : never; }[keyof C]} GetClassMethods
	 * @template C
	 */

	/**
	 * @template {GetClassMethods<mw.Api>} M
	 * @param {M} method
	 * @param {Parameters<mw.Api[M]>} args
	 * @param {number} count
	 * @param {string[]} previousErrors
	 * @return {Promise<Awaited<ReturnType<mw.Api[M]>>>}
	 */
	function retryApiRequestES6Warp(method, args, count = 3, previousErrors = []) {
		if (!count) {
			return $.Deferred().reject(new ApiRetryFailError(previousErrors));
		}
		const deferred = $.Deferred();
		api[method](...args).then(deferred.resolve, error => {
			console.error(error);
			if (error && typeof error === 'object' && 'stack' in error) {
				previousErrors.push(error.stack);
			} else {
				previousErrors.push(String(error));
			}
			retryApiRequestES6Warp(method, args, --count, previousErrors)
				.then(deferred.resolve, deferred.reject);
		});
		return deferred;
	}

	/**
	 * @template {GetClassMethods<mw.Api>} M
	 * @param {M} method
	 * @param {Parameters<mw.Api[M]>} args
	 * @return {Promise<Awaited<ReturnType<mw.Api[M]>>>}
	 */
	function retryApiRequest(method, ...args) {
		return retryApiRequestES6Warp(method, args);
	}

	/**
	 * @template T
	 * @param {Promise<T>} promise
	 * @return {JQuery.Promise<T>}
	 */
	function nativePromiseToJQueryDeferred(promise) {
		const deferred = $.Deferred();
		promise.then(deferred.resolve, deferred.reject);
		return deferred;
	}

	/**
	 * @param {string} hash
	 */
	function getViewer(hash) {
		if (viewerMap.has(hash)) {
			const viewer = viewerMap.get(hash);
			assert(viewer, 'viewer');
			return viewer;
		}

		const dom = document.getElementById(`noteTA-${hash}`);
		if (!dom) {
			throw new Error(`Can\'t get Element "#noteTA-${hash}".`);
		}
		const $dom = $(dom);

		class NoteTAViewer extends OO.ui.ProcessDialog {
			constructor() {
				super({
					size: 'larger'
				});
				this.hash = hash;
				this.dataIsLoaded = false;
				this.collapse = true;

				this.$realContent = $('<div>');
				
				if ('MutationObserver' in window) {
					this.mutationObserver = new MutationObserver(() => {
						this.updateSize();
					});
					this.mutationObserver.observe(this.$realContent.get(0), {
						subtree: true,
						childList: true
					});
				}
			}

			initialize() {
				super.initialize();

				this.content = new OO.ui.PanelLayout({
					padded: true,
					expanded: false
				});
				this.$realContent.appendTo(this.content.$element);

				this.$body.append(this.content.$element);

				return this;
			}

			destroy() {
				if (this.mutationObserver) {
					this.mutationObserver.disconnect();
				}
			}

			getNoteTAParseText() {
				if (this.noteTAParseText) {
					return $.Deferred().resolve(this.noteTAParseText);
				}

				const $noteTAtitle = $dom.find('.noteTA-title');
				const actualTitle = mw.config.get('wgPageName').replace(/_/g, ' ');
				let wikitext = '';

				const titleDeferred = $.Deferred();
				if ($noteTAtitle.length) {
					const titleConv = $noteTAtitle.attr('data-noteta-code');
					assert(titleConv, 'titleConv');
					let titleDesc = $noteTAtitle.attr('data-noteta-desc');
					if (titleDesc) {
						titleDesc = '(' + titleDesc + ')';
					} else {
						titleDesc = '';
					}
					wikitext += '<span style="float: right;">{{edit|' + actualTitle + '|section=0}}</span>\n';
					wikitext += '; 本文使用[[Help:中文维基百科的繁简、地区词处理#條目標題|标题手工转换]]\n';
					wikitext += '* 转换标题为:-{D|' + titleConv + '}-' + titleDesc + '\n';
					wikitext += '* 实际标题为:-{R|' + actualTitle + '}-;当前显示为:-{|' + titleConv + '}-\n';
					titleDeferred.resolve();
				} else {
					retryApiRequest('parse', '{{noteTA/multititle|' + actualTitle + '}}', {
						title: actualTitle,
						variant: 'zh'
					}).then(resultHtml => {
						const $multiTitle = $($.parseHTML(resultHtml)).find('.noteTA-multititle');

						if ($multiTitle.length) {
							/** @type {Record<string, string[]>} */
							const textVariant = {};
							/** @type {Record<string, string|null>} */
							const variantText = {};
							wikitext += '; 本文[[Help:中文维基百科的繁简、地区词处理#條目標題|标题可能经过转换]]\n* 转换标题为:';
							for (const li of $multiTitle.children().toArray()) {
								const $li = $(li);
								const variant = $li.attr('data-noteta-multititle-variant');
								assert(variant, 'variant');
								const text = $li.text().trim();
								variantText[variant] = text;
								if (textVariant[text]) {
									textVariant[text].push(variant);
								} else {
									textVariant[text] = [variant];
								}
							}

							const multiTitle = [];
							const titleConverted = variantText[mw.config.get('wgUserVariant')];
							for (const variant in variantText) {
								const text = variantText[variant];
								if (text === null) {
									continue;
								}

								const variants = textVariant[text];

								for (const variant of textVariant[text]) {
									variantText[variant] = null;
								}

								const variantsName = variants.map((variant) => `-{R|{{MediaWiki:Variantname-${variant}}}}-`).join('、');
								multiTitle.push(variantsName + ':-{R|' + text + '}-');
							}
							wikitext += multiTitle.join(';');
							wikitext += '\n* 实际标题为:-{R|' + actualTitle + '}-;当前显示为:-{R|' + titleConverted + '}-\n';
						}
						titleDeferred.resolve();
					}).catch(titleDeferred.reject);
				}

				const deferred = $.Deferred();
				titleDeferred.then(() => {
					const $noteTAgroups = $dom.find('.noteTA-group > *[data-noteta-group]');
					if ($noteTAgroups.length > 1) {
						this.collapse = true;
					}
					for (const ele of $noteTAgroups) {
						const $ele = $(ele);
						switch ($ele.attr('data-noteta-group-source')) {
							case 'template':
								wikitext += '{{CGroup/' + $ele.attr('data-noteta-group') + '}}\n';
								break;
							case 'module':
								wikitext += '{{#invoke:CGroupViewer|dialog|' + $ele.attr('data-noteta-group') + '}}\n';
								break;
							case 'none':
								wikitext += '; 本文使用的公共转换组“' + $ele.attr('data-noteta-group') + '”尚未创建\n';
								wikitext += '* {{edit|Module:CGroup/' + $ele.attr('data-noteta-group') + '|创建公共转换组“' + $ele.attr('data-noteta-group') + '”}}\n';
								break;
							default:
								wikitext += '; 未知公共转换组“' + $ele.attr('data-noteta-group') + '”来源“' + $ele.attr('data-noteta-group-source') + '”\n';
						}
					}

					const $noteTAlocal = $dom.find('.noteTA-local');
					if ($noteTAlocal.length) {
						this.collapse = true;
						wikitext += '<span style="float: right;">{{edit|' + actualTitle + '|section=0}}</span>\n';
						wikitext += '; 本文使用[[Help:中文维基百科的繁简、地区词处理#控制自动转换的代碼|全文手工转换]]\n';
						const $noteTAlocals = $noteTAlocal.children('*[data-noteta-code]');
						for (const that of $noteTAlocals.toArray()) {
							const $this = $(that);
							const localConv = $this.attr('data-noteta-code');
							let localDesc = $this.attr('data-noteta-desc');
							if (localDesc) {
								localDesc = '(' + localDesc + ')';
							} else {
								localDesc = '';
							}
							wikitext += '* -{D|' + localConv + '}-' + localDesc + '当前显示为:-{' + localConv + '}-\n';
						}
					}

					wikitext += '{{noteTA/footer}}\n';

					this.noteTAParseText = wikitext;

					deferred.resolve(wikitext);
				}).catch(deferred.reject);
				return deferred;
			}

			doExecute() {
				if (this.dataIsLoaded) {
					return $.Deferred().resolve();
				}

				this.$realContent.empty().append(
					$('<p>').text(HanAssist.conv({ hans: '正在加载...', hant: '正在載入...' }))
				);

				return this.getNoteTAParseText()
					.then(wikitext => retryApiRequest('parse', wikitext, {
						title: 'Template:CGroup/-',
						variant: mw.config.get('wgUserVariant')
					}))
					.then(parsedHtml => {
						this.$realContent.empty().html(parsedHtml);
						this.$realContent.find('.mw-collapsible').makeCollapsible();
						
						if (!this.mutationObserver) {
							this.updateSize();
						}

						this.dataIsLoaded = true;
					})
					.catch(error => {
						if (error instanceof ApiRetryFailError) {
							throw new OO.ui.Error(error.toJQuery(), { recoverable: true });
						} else {
							throw new OO.ui.Error(String(error), { recoverable: false });
						}
					});
			}

			doExecuteWrap() {
				if (!this.executePromise) {
					this.executePromise = this.doExecute();
					delete this.lastError;

					const deferred = $.Deferred();
					this.executePromise
						.then(deferred.resolve)
						.catch(error => {
							if (error instanceof OO.ui.Error) {
								this.lastError = error;
							} else {
								deferred.reject(error);
							}
						})
						.always(() => {
							delete this.executePromise;
						});
					return deferred;
				} else {
					const deferred = $.Deferred();
					this.executePromise
						.then(deferred.resolve)
						.catch(error => {
							if (!(error instanceof OO.ui.Error)) {
								deferred.reject(error);
							} else {
								deferred.resolve();
							}
						})
						.always(() => {
							delete this.executePromise;
						});
					return deferred;
				}
			}

			getSetupProcess(data) {
				return super.getSetupProcess(data).next(() => {
					this.doExecuteWrap();
					this.executeAction('main');
				});
			}

			getActionProcess(action) {
				return new OO.ui.Process()
					.next(() => {
						if (action === 'main') {
							return this.doExecuteWrap();
						}
					})
					.next(() => {
						if (action === 'main' && this.lastError) {
							return this.lastError;
						}

						return super.getActionProcess(action).execute();
					});
			}
		}

		NoteTAViewer.static = Object.create(OO.ui.ProcessDialog.static);
		NoteTAViewer.static.name = 'NoteTALoader-' + hash;
		NoteTAViewer.static.title = HanAssist.conv({ hans: '字词转换', hant: '字詞轉換' });
		NoteTAViewer.static.actions = [
			{
				label: mw.msg('ooui-dialog-process-dismiss'),
				flags: 'primary'
			}
		];

		const viewer = new NoteTAViewer();
		windowManager.addWindows([viewer]);
		viewerMap.set(hash, viewer);

		return viewer;
	}

	function resetAllViewer() {
		for (const viewer of viewerMap.values()) {
			viewer.destroy();
		}
		viewerMap.clear();
		windowManager.clearWindows();
	}

	const skin = mw.config.get('skin');
	let portletId = null;
	let $insertPortlet = null;
	const xNoteTAViewer = 'x-noteTA-viewer';
	let insertedClass = 'x-noteTA-inserted';
	let globalInit = () => {};
	let globalDeinit = () => {};
	let afterPortletAdd = () => {};
	if (
		(skin === 'vector-2022' || skin === 'minerva')
		&& document.getElementById('p-associated-pages')
	) {
		portletId = 'p-associated-pages';
		globalDeinit = () => {
			$(document.getElementsByClassName(xNoteTAViewer)).remove();
		};
	} else if (skin.startsWith('vector')) {
		portletId = 'p-noteTA';
		let $vectorNoteTATab;
		globalInit = () => {
			if ($vectorNoteTATab) {
				return;
			}
			$vectorNoteTATab = $(mw.util.addPortlet(portletId));
			$('#p-variants').after(
				$vectorNoteTATab
					.removeClass(['mw-portlet-p-noteTA'])
					.addClass(
						skin === 'vector'
							? ['mw-portlet-noteTA', 'vector-menu-tabs', 'vector-menu-tabs-legacy']
							: ['mw-portlet-noteTA', 'vector-menu-tabs']
						)
			);
		};
		globalDeinit = () => {
			if (!$vectorNoteTATab) {
				return;
			}
			$vectorNoteTATab.find('ul').empty();
			mw.util.hidePortlet(portletId);
		};
	} else if (skin === 'minerva') {
		portletId = 'p-views';
		globalDeinit = () => {
			$(document.getElementsByClassName(xNoteTAViewer)).remove();
		};
		const $insertBeforeElement = $('#language-selector').next();
		afterPortletAdd = ($portlet) => {
			$portlet
				.insertBefore($insertBeforeElement)
				.addClass(['page-actions-menu__list-item']);
		};
	}
	
	if (portletId) {
		$insertPortlet = $(document.getElementById(portletId));
	}

	function addPortletItem(hash) {
		const $portlet = $(mw.util.addPortletLink(
			portletId,
			'#',
			'汉/漢',
			`ca-noteTA-${hash}`
		));
		$portlet
			.addClass(xNoteTAViewer)
			.find('a')
				.empty()
				.append(
					$('<div>')
						.append(
							$('<span>')
								.css({
									padding: '1px 3px',
									background: '#d3e3f4',
									color: '#000000',
									height: '85%'
								}).text('汉'),
							$('<span>')
								.css({
									padding: '1px 3px',
									background: '#e9e9e9',
									color: '#434343',
									height: '85%'
								}).text('漢')
						)
				);
		afterPortletAdd($portlet);
		$insertPortlet.addClass([insertedClass]);
		return $portlet;
	}

	function noteTAViewer() {
		resetAllViewer();

		if (portletId) {
			$insertPortlet.removeClass([insertedClass]);
		}
		globalDeinit();
		globalInit();

		if (skin === 'minerva') {
			for (const ele of $('.noteTA[id^=noteTA-]').toArray()) {
				const hash = ele.id.replace(/^noteTA-/, '');
				const $ele = addPortletItem(hash);
				$ele.on('click', (e) => {
					e.preventDefault();
					getViewer(hash).open();
				});
			}
		} else {
			for (const ele of $('.mw-indicator[id^=mw-indicator-noteTA-]').toArray()) {
				const hash = ele.id.replace(/^mw-indicator-noteTA-/, '');
				let $ele = $(ele);
				if (portletId) {
					$ele.hide();
					$ele = addPortletItem(hash);
				} else {
					// https://zh.wikipedia.org/w/index.php?title=MediaWiki_talk:Gadget-noteTA.js&oldid=82017438#c-YFdyh000-20240324184400-SunAfterRain-20240324174800
					$ele.css('cursor', 'pointer');
				}
				$ele.on('click', (e) => {
					e.preventDefault();
					getViewer(hash).open();
				});
			}
		}
	}

	noteTAViewer.get = getViewer;
	noteTAViewer.reset = resetAllViewer;
	noteTAViewer.globalInit = globalInit;
	noteTAViewer.globalDeinit = globalDeinit;
	noteTAViewer.addPortletItem = addPortletItem;

	mw.libs.noteTAViewer = noteTAViewer;

	mw.hook('wikipage.content').add(function ($content) {
		noteTAViewer();
	});
});
// </nowiki>