MediaWiki:Gadget-InvertImages.js

From Honkai: Nexus Anima Wiki
Revision as of 17:17, 10 October 2026 by ReisuDesign (talk | contribs) (Created page with "(function () { 'use strict'; // Prevent duplicate initialization if (window.PetitPlanetInvertImagesLoaded) { return; } window.PetitPlanetInvertImagesLoaded = true; // ============================================================ // CONFIG // ============================================================ var JSON_PAGE = 'MediaWiki:Gadget-InvertImages.json'; var DAY_CLASS = 'day-invert-image'; var NIGHT_CLASS = 'night-invert-image'; // ==================...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function () {
	'use strict';

	// Prevent duplicate initialization
	if (window.PetitPlanetInvertImagesLoaded) {
		return;
	}

	window.PetitPlanetInvertImagesLoaded = true;


	// ============================================================
	// CONFIG
	// ============================================================

	var JSON_PAGE = 'MediaWiki:Gadget-InvertImages.json';

	var DAY_CLASS = 'day-invert-image';
	var NIGHT_CLASS = 'night-invert-image';


	// ============================================================
	// STATE
	// ============================================================

	var dayImages = new Set();
	var nightImages = new Set();

	var loaded = false;


	// ============================================================
	// NORMALIZE FILE NAME
	// ============================================================

	function normalizeFileName(name) {
		if (!name) {
			return '';
		}

		name = String(name);

		// Decode URL encoding
		try {
			name = decodeURIComponent(name);
		} catch (e) {
			// Ignore decoding errors
		}

		// Remove File: namespace
		name = name.replace(/^File:/i, '');

		// MediaWiki treats underscores and spaces equivalently
		name = name.replace(/_/g, ' ');

		return name.trim();
	}


	// ============================================================
	// GET FILE NAME FROM IMAGE
	// ============================================================

	function getFileName(img) {
		if (!img) {
			return null;
		}


		// --------------------------------------------------------
		// 1. MediaWiki's File link
		// --------------------------------------------------------

		/*
		 * This is the most reliable method.
		 *
		 * Normal wiki images:
		 *
		 * <a href="/File:Crab_Icon.png">
		 *     <img ...>
		 * </a>
		 *
		 * Category pages / galleries use the same structure.
		 */

		var link = img.closest('a.mw-file-description');

		if (!link) {
			link = img.closest('a');
		}

		if (link) {

			// Check href
			var href = link.getAttribute('href');

			if (href) {

				/*
				 * Matches:
				 *
				 * /File:Crab_Icon.png
				 * /wiki/File:Crab_Icon.png
				 * ./File:Crab_Icon.png
				 */

				var match = href.match(
					/(?:^|\/)(?:wiki\/)?File:([^#?]+)/i
				);

				if (match) {
					return normalizeFileName(match[1]);
				}
			}


			// Check title as another MediaWiki fallback
			var title = link.getAttribute('title');

			if (title && /^File:/i.test(title)) {
				return normalizeFileName(title);
			}
		}


		// --------------------------------------------------------
		// 2. MediaWiki resource attribute
		// --------------------------------------------------------

		var resource = img.getAttribute('resource');

		if (resource) {

			var resourceMatch = resource.match(
				/(?:File:|\/File:)([^#?]+)/i
			);

			if (resourceMatch) {
				return normalizeFileName(resourceMatch[1]);
			}
		}


		// --------------------------------------------------------
		// 3. File-specific data attributes
		// --------------------------------------------------------

		var dataAttributes = [
			'data-file-name',
			'data-filename',
			'data-file'
		];

		for (var i = 0; i < dataAttributes.length; i++) {

			var value = img.getAttribute(
				dataAttributes[i]
			);

			if (value) {
				return normalizeFileName(value);
			}
		}


		// --------------------------------------------------------
		// 4. Image source
		// --------------------------------------------------------

		var sources = [
			img.currentSrc,
			img.src,
			img.getAttribute('data-src'),
			img.getAttribute('data-lazy-src')
		];

		for (var j = 0; j < sources.length; j++) {

			var source = sources[j];

			if (!source) {
				continue;
			}

			try {

				var url = new URL(
					source,
					window.location.href
				);

				var path = url.pathname;


				/*
				 * Thumbnail URL:
				 *
				 * /thumb/a/ab/
				 * Crab_Icon.png/
				 * 150px-Crab_Icon.png
				 */

				var thumbMatch = path.match(
					/\/thumb\/[^/]+\/[^/]+\/([^/]+)\/[^/]+$/i
				);

				if (thumbMatch) {
					return normalizeFileName(
						thumbMatch[1]
					);
				}


				// Last path component
				var parts = path.split('/');
				var filename = parts[parts.length - 1];

				if (filename) {

					// Remove thumbnail size prefix
					filename = filename.replace(
						/^\d+px-/i,
						''
					);

					return normalizeFileName(filename);
				}

			} catch (e) {
				// Ignore invalid URLs
			}
		}


		return null;
	}


	// ============================================================
	// PROCESS ONE IMAGE
	// ============================================================

	function processImage(img) {

		if (!img || img.nodeType !== 1) {
			return;
		}

		if (
			!img.tagName ||
			img.tagName.toLowerCase() !== 'img'
		) {
			return;
		}


		var fileName = getFileName(img);

		if (!fileName) {
			return;
		}


		// --------------------------------------------------------
		// Determine desired class
		// --------------------------------------------------------

		var desiredClass = null;

		if (nightImages.has(fileName)) {

			desiredClass = NIGHT_CLASS;

		} else if (dayImages.has(fileName)) {

			desiredClass = DAY_CLASS;
		}


		// --------------------------------------------------------
		// Apply desired state
		// --------------------------------------------------------

		if (desiredClass === NIGHT_CLASS) {

			img.classList.add(NIGHT_CLASS);
			img.classList.remove(DAY_CLASS);

		} else if (desiredClass === DAY_CLASS) {

			img.classList.add(DAY_CLASS);
			img.classList.remove(NIGHT_CLASS);

		} else {

			// Not in either JSON list
			img.classList.remove(DAY_CLASS);
			img.classList.remove(NIGHT_CLASS);
		}
	}


	// ============================================================
	// PROCESS ALL IMAGES
	// ============================================================

	function processAllImages(root) {

		root = root || document;


		// Root itself
		if (
			root.nodeType === 1 &&
			root.tagName &&
			root.tagName.toLowerCase() === 'img'
		) {
			processImage(root);
		}


		if (!root.querySelectorAll) {
			return;
		}


		var images = root.querySelectorAll('img');

		for (var i = 0; i < images.length; i++) {
			processImage(images[i]);
		}
	}


	// ============================================================
	// LOAD JSON
	// ============================================================

	function loadJSON() {

		var url = mw.util.getUrl(
			JSON_PAGE,
			{
				action: 'raw'
			}
		);


		fetch(url, {
			credentials: 'same-origin',
			cache: 'no-cache'
		})

		.then(function (response) {

			if (!response.ok) {
				throw new Error(
					'Failed to load ' +
					JSON_PAGE +
					' (' +
					response.status +
					')'
				);
			}

			return response.text();
		})


		.then(function (text) {

			var data;

			try {

				data = JSON.parse(text);

			} catch (e) {

				throw new Error(
					'Invalid JSON in ' +
					JSON_PAGE
				);
			}


			// ----------------------------------------------------
			// Day
			// ----------------------------------------------------

			dayImages.clear();

			if (Array.isArray(data.day)) {

				data.day.forEach(function (fileName) {

					var normalized =
						normalizeFileName(fileName);

					if (normalized) {
						dayImages.add(normalized);
					}
				});
			}


			// ----------------------------------------------------
			// Night
			// ----------------------------------------------------

			nightImages.clear();

			if (Array.isArray(data.night)) {

				data.night.forEach(function (fileName) {

					var normalized =
						normalizeFileName(fileName);

					if (normalized) {
						nightImages.add(normalized);
					}
				});
			}


			loaded = true;


			// Process everything currently rendered
			processAllImages(document);

		})


		.catch(function (error) {

			console.error(
				'[InvertImages] ' +
				error.message
			);

		});
	}


	// ============================================================
	// DYNAMIC CONTENT / LAZY LOADING
	// ============================================================

	var observer = new MutationObserver(function (mutations) {

		if (!loaded) {
			return;
		}


		for (var i = 0; i < mutations.length; i++) {

			var mutation = mutations[i];


			// ----------------------------------------------------
			// Newly inserted elements
			// ----------------------------------------------------

			if (mutation.type === 'childList') {

				for (
					var j = 0;
					j < mutation.addedNodes.length;
					j++
				) {

					var node = mutation.addedNodes[j];

					if (node.nodeType === 1) {
						processAllImages(node);
					}
				}
			}


			// ----------------------------------------------------
			// Lazy-loaded / changed image
			// ----------------------------------------------------

			if (
				mutation.type === 'attributes' &&
				mutation.target &&
				mutation.target.tagName &&
				mutation.target.tagName.toLowerCase() === 'img'
			) {

				processImage(mutation.target);
			}
		}
	});


	observer.observe(
		document.documentElement,
		{
			childList: true,
			subtree: true,
			attributes: true,
			attributeFilter: [
				'src',
				'srcset',
				'resource',
				'data-src',
				'data-lazy-src',
				'data-file-name',
				'data-filename',
				'data-file'
			]
		}
	);


	// ============================================================
	// START
	// ============================================================

	loadJSON();

})();