User:SunAfterRain/js/WikidataDesc.js

注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
 * WikidataDesc
 * 在條目頂端顯示維基資料描述
 * 
 * 基於 https://zh.wikipedia.org/w/index.php?title=MediaWiki:Gadget-WikidataDesc.js&oldid=83140265 (原作者:User:Alexander Misel/改進:User:逆襲的天邪鬼)
 * 
 * @author SunAfterRain
 * 
 * 註:因為使用了 assert=user,沒有維基數據帳號也無法正常自動建立帳號的人可能會無法正常使用。
 *     可以指定 `window.wgWikidataDescForceAnonymous = true;` 改為發出匿名請求,
 *     代價是會失去未來可能實現的編輯的功能。
 * 
 * TODO:實現編輯
 */
$.when(
	$.ready,
	mw.loader.using(['mediawiki.ForeignApi', 'ext.gadget.HanAssist'])
).then(async (_$, require) => {
	const HanAssist = require('ext.gadget.HanAssist');

	if (mw.config.get('wgNamespaceNumber') !== 0 || mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId') || !!mw.config.get('wgDiffNewId')) {
		return;
	}

	const messages = HanAssist.batchConv({
		descriptionPlaceholder: {
			hans: '维基数据描述',
			hant: '維基數據描述'
		},
		descriptionEmpty: {
			hans: '(没有描述)',
			hant: '(沒有描述)'
		},
		descriptionEmptyAbbrTitle: {
			hans: '没有描述',
			hant: '沒有描述'
		},

		loadFail: {
			hans: '无法载入描述:',
			hant: '無法載入描述:'
		},

		isLanguage: {
			hans: '($1)',
			hant: '($1)',
		},
		convertedFromLanguage: {
			hans: '(转换拼写自$1)',
			hant: '(轉換拼寫自$1)',
		},

		buttonExpand: {
			hans: '展开所有变体',
			hant: '展開所有變體'
		},
		buttonCollapse: {
			hans: '合并',
			hant: '合併'
		},
		buttonSave: {
			hans: '保存',
			hant: '儲存'
		},

		saving: {
			hans: '正在保存描述……',
			hant: '正在儲存描述……'
		},
		saved: {
			hans: '描述保存成功。',
			hant: '描述儲存成功。'
		},
		saveFail: {
			hans: '无法编辑描述:',
			hant: '無法編輯描述:'
		}
	});
	const chineseVariants = [
		'zh',
		'zh-hans', 'zh-cn', 'zh-my', 'zh-sg',
		'zh-hant', 'zh-tw', 'zh-hk', 'zh-mo',
	];
	const wgUserLanguage = mw.config.get('wgUserLanguage', 'zh');
	const wgUserVariant = mw.config.get('wgUserVariant', wgUserLanguage);
	const getLanguageName = (() => {
		const languageNames = {
			'zh': '中文',
			'zh-hans': '中文(简体)',
			'zh-cn': '中文(大陆)',
			'zh-my': '中文(大马)',
			'zh-sg': '中文(新加坡)',
			'zh-hant': '中文(繁體)',
			'zh-tw': '中文(臺灣)',
			'zh-hk': '中文(香港)',
			'zh-mo': '中文(澳門)'
		};
		let displayNames;
		return function getLanguageName(langCode) {
			if (chineseVariants.includes(langCode)) {
				return languageNames[langCode];
			}
			
			if (!displayNames) {
				displayNames = new Intl.DisplayNames(wgUserLanguage, { type: 'language' });
			}
			return displayNames.of(langCode);
		};
	})();
	const useVariant = chineseVariants.includes(wgUserVariant) ? wgUserVariant : 'zh';

	const allowPostEdit = mw.user.isNamed() && !window.wgWikidataDescForceAnonymous;

	const pageName = mw.config.get('wgPageName');
	const api = new mw.Api();
	const wikidataApi = new mw.ForeignApi(
		'//www.wikidata.org/w/api.php',
		allowPostEdit
			? {
				parameters: {
					assert: 'user'
				}
			}
			: {
				anonymous: true
			}
	);
	
	function deepClone(object) {
		if (!object || typeof object !== 'object') {
			return object;
		} else if (Array.isArray(object)) {
			return object.map(deepClone);
		}
		
		const objectType = Object.prototype.toString.call(object);
		if (objectType !== '[object Object]') {
			throw new Error(`Fail to clone ${objectType}: Not implemented.`);
		}
	
		const clonedObj = Object.create(Object.getPrototypeOf(object));
		for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(object))) {
			if (descriptor.get || descriptor.set) {
				throw new Error(`Fail to clone object: Unexpected get/set accessor "${key}".`);
			}
			descriptor.value = deepClone(descriptor.value);
			Object.defineProperty(clonedObj, key, descriptor);
		}
		return clonedObj;
	}

	let entityData = null;
	async function loadData() {
		try {
			const data = await wikidataApi.get({
				action: 'wbgetentities',
				props: ['labels', 'descriptions'],
				sites: 'zhwiki',
				titles: pageName,
				languages: chineseVariants,
				languagefallback: true,
				formatversion: '2',
			});

			const [entityId, entity] = Object.entries(data.entities ?? {})[0] ?? [];

			if (!data.success || !entity) {
				throw Object.assgin(
					new Error('Unable to parse the data returned by the api.'),
					{ data }
				);
			}

			if (entityId !== '-1') {
				entityData = entity;
			}
		} catch (error) {
			mw.notify(
				mw.format(messages.loadFail, String(error)),
				{
					title: 'WikidataDesc',
					autoHide: false,
					type: 'error'
				}
			);
			console.error(error);
			return;
		}
		displayDescription();
	}

	function createElement(tagName, attrs = {}, innerText = null) {
		const element = document.createElement(tagName);
		for (const [attrName, attrValue] of Object.entries(attrs)) {
			if (attrValue !== undefined && attrValue !== null) {
				element.setAttribute(attrName, attrValue);
			}
		}
		if (innerText) {
			element.innerText = innerText;
		}
		return element;
	}

	let desc = null;
	let descBox = null;
	function displayDescription() {
		if (!desc) {
			mw.util.addCSS('#wikidatadesc .text { color: var(--color-subtle,#54595d); font-size: medium; } #wikidatadesc .collapsed { display: none; } #wikidatadesc .option { font-size: smaller; }');
			desc = createElement('div', {
				id: 'wikidatadesc',
				class: 'noprint'
			});
			descBox = createElement('div', {
				id: 'wikidatadesc-descbox'
			});
			desc.append(descBox);
			$("#siteSub").show().get(0).replaceChildren(desc);
		}

		const fragment = document.createDocumentFragment();
		const singleBox = createElement('div');
		const allVariantsBox = createElement('div', {
			class: 'collapsed'
		});
		fragment.append(singleBox, allVariantsBox);

		let descriptions = entityData ? deepClone(entityData.descriptions) : {};
		const userLangDescription = deepClone(descriptions[useVariant]);
		if (descriptions.zh?.language === 'zh') {
			// 有中文資料,清理回退鏈
			for (const [key, description] of Object.entries(descriptions)) {
				if ('source-language' in description) {
					delete descriptions[key];
				}
			}
		} else {
			// 沒有中文資料
			descriptions = {};
		}
		singleBox.append(
			createElement(
				'span',
				{
					class: 'text'
				},
				userLangDescription.value
			)
		);
		if (userLangDescription.language !== useVariant) {
			singleBox.append(
				document.createTextNode(' '),
				createElement(
					'span',
					{},
					mw.format(messages.isLanguage, getLanguageName(userLangDescription.language))
				)
			);
		} else if ('source-language' in userLangDescription) {
			singleBox.append(
				document.createTextNode(' '),
				createElement(
					'span',
					{},
					mw.format(messages.convertedFromLanguage, getLanguageName(userLangDescription['source-language']))
				)
			);
		}
		for (const variant of chineseVariants) {
			const description = descriptions[variant];
			const value = description?.['source-language'] ? undefined : description?.value;
			const wrap = createElement('div');
			wrap.append(
				document.createTextNode(`${getLanguageName(variant)}:`),
				typeof value !== 'undefined'
					? createElement(
						'span',
						{
							class: 'text'
						},
						value
					)
					: createElement(
						'abbr',
						{
							class: 'text',
							title: messages.descriptionEmptyAbbrTitle
						},
						messages.descriptionEmpty
					)
			);
			allVariantsBox.append(wrap);
		}

		const expandButton = createElement(
			'a',
			{
				href: '#',
				class: 'option'
			},
			`[${messages.buttonExpand}]`
		);
		$(expandButton).on('click', (ev) => {
			ev.preventDefault();
			singleBox.classList.add('collapsed');
			allVariantsBox.classList.remove('collapsed');
		});
		singleBox.append(
			document.createTextNode(' '),
			expandButton
		);

		const collapseButton = createElement(
			'a',
			{
				href: '#',
				class: 'option'
			},
			`[${messages.buttonCollapse}]`
		);
		$(collapseButton).on('click', (ev) => {
			ev.preventDefault();
			singleBox.classList.remove('collapsed');
			allVariantsBox.classList.add('collapsed');
		});
		allVariantsBox.append(collapseButton, createElement('hr'));

		desc.replaceChildren(fragment);
	}

	loadData();
});