MediaWiki:Gadget-InvertImages.js
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();
})();