MediaWiki:Common.js: Difference between revisions
From Sanatan Hindu Dharma
No edit summary |
No edit summary |
||
| Line 640: | Line 640: | ||
} | } | ||
}); | }); | ||
}); | });// === Auto-link existing page titles inside paragraphs (MediaWiki 1.39 compatible) === | ||
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'jquery.title'], function () { | |||
(function ($, mw) { | |||
// Config | |||
const MIN_TOKEN_LEN = 3; // min length to consider | |||
const MAX_TOKENS = 150; // max distinct candidates to query (avoid huge pages) | |||
const BATCH_SIZE = 50; // titles per API query (query limits) | |||
const contentSelector = '#mw-content-text'; | |||
if (mw.config.get('wgNamespaceNumber') !== 0) { | |||
// Only run on main namespace by default; modify if you want other namespaces | |||
// (remove this guard to run everywhere) | |||
} | |||
// === | // Helper: get visible text nodes under root, skipping certain elements | ||
function getTextNodes(root) { | |||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { | |||
acceptNode: function (node) { | |||
// ignore empty / whitespace-only | |||
if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; | |||
// ignore text in these parent tags | |||
const badParents = ['A', 'CODE', 'PRE', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT']; | |||
let p = node.parentNode; | |||
while (p && p !== root) { | |||
if (badParents.indexOf(p.nodeName) !== -1) return NodeFilter.FILTER_REJECT; | |||
if ($(p).closest('.no-autolink').length) return NodeFilter.FILTER_REJECT; | |||
p = p.parentNode; | |||
} | |||
return NodeFilter.FILTER_ACCEPT; | |||
} | |||
}); | |||
const nodes = []; | |||
let cur; | |||
while ((cur = walker.nextNode())) nodes.push(cur); | |||
return nodes; | |||
} | |||
// Helper: extract candidate tokens (capitalized words and multiword sequences) | |||
function extractCandidates(text) { | |||
// Match sequences of words that start with uppercase letter, possibly containing spaces and punctuation | |||
// e.g. "Hindu Temple", "Sanatan Hindu Dharma", "MediaWiki" etc. | |||
const regex = /(?:\b[A-Z][A-Za-z0-9&'’\-]{2,}(?:\s+[A-Z][A-Za-z0-9&'’\-]{2,}){0,3})/g; | |||
const set = new Set(); | |||
let m; | |||
while ((m = regex.exec(text))) { | |||
const t = m[0].trim(); | |||
if (t.length >= MIN_TOKEN_LEN) set.add(t); | |||
if (set.size >= MAX_TOKENS) break; | |||
} | |||
return Array.from(set); | |||
} | |||
// Batch check which titles exist using action=query | |||
async function filterExistingTitles(titles) { | |||
const api = new mw.Api(); | |||
const exist = []; | |||
for (let i = 0; i < titles.length; i += BATCH_SIZE) { | |||
const batch = titles.slice(i, i + BATCH_SIZE); | |||
// Use normalized/title mapping via query | |||
const res = await api.get({ | |||
action: 'query', | |||
format: 'json', | |||
titles: batch.join('|'), | |||
redirects: 1 | |||
}); | |||
if (!res || !res.query) continue; | |||
// iterate pages returned | |||
for (const pageId in res.query.pages) { | |||
const p = res.query.pages[pageId]; | |||
if (p.missing === undefined) { // exists (no missing flag) | |||
const titleText = p.title; // normalized title | |||
exist.push(titleText); | |||
} | |||
} | |||
} | |||
return exist; | |||
} | |||
// Replace occurrences in text nodes using DOM operations | |||
function replaceTextNodesWithLinks(textNodes, titleSet) { | |||
// Create a regex that matches any of the titles (escape) | |||
const titles = Array.from(titleSet).sort((a, b) => b.length - a.length); // longest first | |||
if (!titles.length) return; | |||
const escaped = titles.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); | |||
const wordRegex = new RegExp('\\b(' + escaped.join('|') + ')\\b', 'g'); | |||
textNodes.forEach(node => { | |||
const parent = node.parentNode; | |||
if (!parent) return; | |||
const text = node.nodeValue; | |||
let match; | |||
let lastIndex = 0; | |||
const frag = document.createDocumentFragment(); | |||
while ((match = wordRegex.exec(text)) !== null) { | |||
const before = text.slice(lastIndex, match.index); | |||
if (before) frag.appendChild(document.createTextNode(before)); | |||
const matched = match[0]; | |||
// create link | |||
const a = document.createElement('a'); | |||
a.href = mw.util.getUrl(matched); | |||
a.textContent = matched; | |||
a.className = 'autolinked-page'; | |||
frag.appendChild(a); | |||
lastIndex = wordRegex.lastIndex; | |||
} | |||
if (lastIndex === 0) return; // no replacement | |||
const after = text.slice(lastIndex); | |||
if (after) frag.appendChild(document.createTextNode(after)); | |||
parent.replaceChild(frag, node); | |||
}); | |||
} | |||
let | // Main runner | ||
async function runAutolink() { | |||
const root = document.querySelector(contentSelector); | |||
if (!root) return; | |||
// get text nodes | |||
const nodes = getTextNodes(root); | |||
if (!nodes.length) return; | |||
// sample text to extract candidates | |||
let sampleText = ''; | |||
for (let i = 0; i < Math.min(nodes.length, 200); i++) { | |||
sampleText += ' ' + nodes[i].nodeValue; | |||
} | |||
const candidates = extractCandidates(sampleText); | |||
if (!candidates.length) return; | |||
// check which exist | |||
const existing = await filterExistingTitles(candidates); | |||
if (!existing.length) return; | |||
// map normalized titles to check exact casing versions in page text | |||
const existingSet = new Set(existing); | |||
// Now find all text nodes again and replace matches | |||
const allNodes = getTextNodes(root); | |||
replaceTextNodesWithLinks(allNodes, existingSet); | |||
} | |||
// | // Delay run until DOM ready and when content is ready (VE may alter DOM but this runs after) | ||
$(function () { | |||
// run after a short delay in case VE modifies DOM | |||
setTimeout(runAutolink, 800); | |||
// | // also run after VisualEditor activation to handle VE editing preview | ||
mw.hook('ve.activationComplete').add(function () { | |||
setTimeout(runAutolink, 1000); | |||
}); | |||
}); | }); | ||
})(jQuery, mw); | |||
}); | |||
}); | }); | ||
Revision as of 14:05, 4 November 2025
mw.loader.using('mediawiki.util').done(function () {
console.log("Common.js loaded safely");
});
/* Any JavaScript here will be loaded for all users on every page load. */
/*
document.addEventListener("DOMContentLoaded", function() {
const btn = document.querySelector(".toggle-btn");
const content = document.querySelector(".toggle-content");
if (btn && content) {
btn.addEventListener("click", function() {
content.style.display = (content.style.display === "block") ? "none" : "block";
});
}
});
// Auto-add parent category when editing/creating a subpage
( function () {
if ( mw.config.get('wgAction') !== 'edit' ) return;
// wgTitle contains title without namespace, e.g. "Ancient-education/Subpage"
var title = mw.config.get('wgTitle') || '';
if ( title.indexOf('/') === -1 ) return; // not a subpage
var parent = title.split('/')[0]; // "Ancient-education"
// jQuery available
$( function () {
var textarea = $('#wpTextbox1');
if ( !textarea.length ) return;
// Only append if not present
var current = textarea.val() || '';
var catTag = '\n[[Category:' + parent + ']]';
if ( current.indexOf(catTag.trim()) === -1 ) {
// Insert the category at the end of the text area (preserve existing text)
textarea.val(current + catTag);
}
} );
}() );
$(document).ready(function () {
// Skip special pages
if (mw.config.get('wgNamespaceNumber') < 0) return;
var $content = $('#mw-content-text');
// Fetch all page titles from the API (main namespace only)
$.get(mw.util.wikiScript('api'), {
action: 'query',
list: 'allpages',
aplimit: 'max',
format: 'json'
}).done(function (data) {
var titles = data.query.allpages.map(function (p) { return p.title; });
var html = $content.html();
titles.forEach(function (title) {
// Safe regex for whole words
var safeTitle = title.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var regex = new RegExp('\\b(' + safeTitle + ')\\b', 'g');
html = html.replace(regex, '<a href="/wiki/' + encodeURIComponent(title.replace(/ /g, '_')) + '">$1</a>');
});
$content.html(html);
});
});
$(document).ready(function() {
if (mw.config.get('wgNamespaceNumber') >= 0) { // Only show on normal pages
var pageName = mw.config.get('wgPageName');
var uploadUrl = mw.util.getUrl('Form:UploadVideo', { 'page': pageName });
$('<div style="position:fixed; bottom:20px; right:20px; background:#007bff; color:white; padding:10px; border-radius:5px; cursor:pointer; font-weight:bold;">Upload a Video</div>')
.click(function() {
window.location.href = uploadUrl;
}).appendTo('body');
}
});
*/
/* Functions of Slider One */
(function () {
function initSliders() {
var sliders = document.querySelectorAll('.mw-slider');
sliders.forEach(function (slider) {
// Avoid double-init
if (slider._mwSliderInited) return;
slider._mwSliderInited = true;
var viewport = slider.querySelector('.mw-slider-viewport');
var track = slider.querySelector('.mw-slider-track');
var items = Array.from(slider.querySelectorAll('.mw-slider-item'));
var btnPrev = slider.querySelector('.mw-slider-btn.prev');
var btnNext = slider.querySelector('.mw-slider-btn.next');
var currentIndex = 0;
var itemsToShow = getItemsToShow();
var gap = parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap || 16);
function getItemsToShow() {
var w = window.innerWidth;
if (w <= 600) return 1;
if (w <= 900) return 2;
return 3;
}
function updateSizes() {
itemsToShow = getItemsToShow();
// compute single item width including gap
if (!items[0]) return;
var itemRect = items[0].getBoundingClientRect();
gap = parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap || 16);
var single = itemRect.width + gap;
// ensure currentIndex in range
var maxIndex = Math.max(0, items.length - itemsToShow);
currentIndex = Math.min(currentIndex, maxIndex);
// apply transform
var translateX = -currentIndex * single;
track.style.transform = 'translateX(' + translateX + 'px)';
updateButtons();
}
function updateButtons() {
var maxIndex = Math.max(0, items.length - itemsToShow);
if (btnPrev) btnPrev.disabled = currentIndex <= 0;
if (btnNext) btnNext.disabled = currentIndex >= maxIndex;
}
function gotoIndex(index) {
var maxIndex = Math.max(0, items.length - itemsToShow);
currentIndex = Math.max(0, Math.min(maxIndex, index));
updateSizes();
}
if (btnPrev) btnPrev.addEventListener('click', function () {
gotoIndex(currentIndex - 1);
});
if (btnNext) btnNext.addEventListener('click', function () {
gotoIndex(currentIndex + 1);
});
// Keyboard support
slider.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') gotoIndex(currentIndex - 1);
if (e.key === 'ArrowRight') gotoIndex(currentIndex + 1);
});
// Resize handling
var resizeTimeout;
window.addEventListener('resize', function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(updateSizes, 120);
});
// Touch / drag support
(function () {
var startX = 0;
var currentTranslate = 0;
var dragging = false;
var pointerId = null;
function pointerDown(e) {
if (e.pointerType === 'mouse' && e.button !== 0) return;
dragging = true;
pointerId = e.pointerId;
startX = e.clientX;
track.style.transition = 'none';
track.setPointerCapture && track.setPointerCapture(pointerId);
}
function pointerMove(e) {
if (!dragging || e.pointerId !== pointerId) return;
var dx = e.clientX - startX;
var itemRect = items[0] && items[0].getBoundingClientRect();
var single = itemRect ? (itemRect.width + gap) : 0;
var baseTranslate = -currentIndex * single;
track.style.transform = 'translateX(' + (baseTranslate + dx) + 'px)';
}
function pointerUp(e) {
if (!dragging || e.pointerId !== pointerId) return;
dragging = false;
track.style.transition = '';
var dx = e.clientX - startX;
var threshold = Math.max(40, (items[0] ? items[0].getBoundingClientRect().width : 200) * 0.15);
if (dx > threshold) {
gotoIndex(currentIndex - 1);
} else if (dx < -threshold) {
gotoIndex(currentIndex + 1);
} else {
updateSizes();
}
try { track.releasePointerCapture(pointerId); } catch (err) {}
pointerId = null;
}
track.addEventListener('pointerdown', pointerDown);
window.addEventListener('pointermove', pointerMove);
window.addEventListener('pointerup', pointerUp);
track.addEventListener('pointercancel', pointerUp);
})();
// Initial sizes
setTimeout(updateSizes, 60);
});
}
// init on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSliders);
} else {
initSliders();
}
// If new content is loaded via AJAX on the wiki, re-init
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(initSliders);
}
})();
/* Full width Slider */
(function () {
function initFullSliders() {
var sliders = document.querySelectorAll('.mw-fullslider');
sliders.forEach(function (slider) {
if (slider._fullSliderInit) return;
slider._fullSliderInit = true;
var track = slider.querySelector('.mw-fullslider-track');
var slides = Array.from(track.children);
var btnPrev = slider.querySelector('.mw-fullslider-btn.prev');
var btnNext = slider.querySelector('.mw-fullslider-btn.next');
var dots = Array.from(slider.querySelectorAll('.mw-fullslider-dots .dot'));
var current = 0;
var slideCount = slides.length;
function goTo(index, animate) {
current = (index % slideCount + slideCount) % slideCount;
var x = -current * slider.clientWidth;
if (animate === false) track.style.transition = 'none';
else track.style.transition = '';
track.style.transform = 'translateX(' + x + 'px)';
updateControls();
if (animate === false) {
// force reflow then restore
void track.offsetWidth;
track.style.transition = '';
}
}
function updateControls() {
if (btnPrev) btnPrev.disabled = false;
if (btnNext) btnNext.disabled = false;
// update dots
dots.forEach(function (d) {
d.classList.toggle('active', +d.getAttribute('data-index') === current);
d.setAttribute('aria-pressed', (+d.getAttribute('data-index') === current).toString());
});
}
if (btnPrev) btnPrev.addEventListener('click', function () { goTo(current - 1); });
if (btnNext) btnNext.addEventListener('click', function () { goTo(current + 1); });
dots.forEach(function (dot) {
dot.addEventListener('click', function () {
var idx = parseInt(this.getAttribute('data-index'), 10);
goTo(idx);
});
});
// Resize handling: ensure slide width matches viewport
var resizeTimer;
function onResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
// Recompute translate in case width changed
goTo(current, false);
}, 80);
}
window.addEventListener('resize', onResize);
// Touch / drag support
(function () {
var startX = 0, startTranslate = 0, dragging = false, pointerId = null;
function down(e) {
if (e.pointerType === 'mouse' && e.button !== 0) return;
dragging = true;
pointerId = e.pointerId;
startX = e.clientX;
var style = window.getComputedStyle(track);
var matrix = new WebKitCSSMatrix(style.transform);
startTranslate = matrix.m41 || 0;
track.style.transition = 'none';
e.target.setPointerCapture && e.target.setPointerCapture(pointerId);
}
function move(e) {
if (!dragging || e.pointerId !== pointerId) return;
var dx = e.clientX - startX;
track.style.transform = 'translateX(' + (startTranslate + dx) + 'px)';
}
function up(e) {
if (!dragging || e.pointerId !== pointerId) return;
dragging = false;
track.style.transition = '';
var dx = e.clientX - startX;
var threshold = Math.max(40, slider.clientWidth * 0.12);
if (dx > threshold) goTo(current - 1);
else if (dx < -threshold) goTo(current + 1);
else goTo(current);
try { e.target.releasePointerCapture(pointerId); } catch (err) {}
pointerId = null;
}
track.addEventListener('pointerdown', down);
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
track.addEventListener('pointercancel', up);
})();
// Autoplay (optional): change interval or set autoplay = false
var autoplay = true;
var autoplayInterval = 4500; // ms
var autoplayTimer = null;
function startAutoplay() {
if (!autoplay) return;
stopAutoplay();
autoplayTimer = setInterval(function () { goTo(current + 1); }, autoplayInterval);
}
function stopAutoplay() {
if (autoplayTimer) { clearInterval(autoplayTimer); autoplayTimer = null; }
}
slider.addEventListener('mouseenter', stopAutoplay);
slider.addEventListener('mouseleave', startAutoplay);
slider.addEventListener('focusin', stopAutoplay);
slider.addEventListener('focusout', startAutoplay);
// Respect prefers-reduced-motion
var rmq = window.matchMedia('(prefers-reduced-motion: reduce)');
if (rmq.matches) autoplay = false;
// Ensure initial sizing: make each slide exactly slider.clientWidth
function layoutSlides() {
var w = slider.clientWidth;
slides.forEach(function (s) { s.style.minWidth = w + 'px'; s.style.maxWidth = w + 'px'; });
goTo(current, false);
}
// Wait for images then layout
function imgsReady(cb) {
var imgs = Array.from(slider.querySelectorAll('img'));
var rem = imgs.length;
if (!rem) return cb();
imgs.forEach(function (img) {
if (img.complete) { rem--; if (!rem) cb(); }
else {
img.addEventListener('load', function () { rem--; if (!rem) cb(); });
img.addEventListener('error', function () { rem--; if (!rem) cb(); });
}
});
}
imgsReady(function () {
layoutSlides();
startAutoplay();
window.addEventListener('resize', onResize);
});
// expose for debug (optional)
slider._goTo = goTo;
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initFullSliders);
else initFullSliders();
if (window.mw && mw.hook) mw.hook('wikipage.content').add(initFullSliders);
})();
$(document).ready(function() {
// Prevent multiple inserts
if ($('#mw-custom-footer').length) return;
// Load HTML footer template safely
$.ajax({
url: '/index.php?title=Template:Custom-footer&action=render',
type: 'GET',
success: function(data) {
// Wrap the footer in a container for CSS control
var footer = $('<div id="mw-custom-footer"></div>').html(data);
// Append after the content area but before closing body
// Prefer the footer tag or body end if missing
if ($('#footer').length) {
$('#footer').before(footer);
} else {
$('body').append(footer);
}
},
error: function() {
console.warn('Custom footer could not be loaded.');
}
});
});
// === Auto Parent Page Logic for VisualEditor & Source Editor with Autocomplete ===
mw.loader.using(['mediawiki.util', 'mediawiki.api']).then(function () {
console.log("✅ Common.js loaded safely.");
const api = new mw.Api();
// --- 1️⃣ Show Reminder Banner ---
mw.hook('wikipage.content').add(function ($content) {
if (mw.config.get('wgAction') === 'edit' || mw.config.get('wgAction') === 'submit') {
const banner = $('<div>')
.css({
background: '#fff8c4',
border: '1px solid #e0c14b',
padding: '10px',
marginBottom: '10px',
borderRadius: '6px',
textAlign: 'center',
fontWeight: '600'
})
.text('🪶 Tip: Remember to set a parent page before saving.');
$content.prepend(banner);
}
});
// --- 2️⃣ Parent Page Prompt with Autocomplete ---
function askForParent(title) {
if (window.parentPromptShown) return;
window.parentPromptShown = true;
// Overlay UI
const overlay = $('<div>').css({
position: 'fixed',
top: 0, left: 0, width: '100%', height: '100%',
background: 'rgba(0,0,0,0.5)',
display: 'flex', justifyContent: 'center', alignItems: 'center',
zIndex: 999999
});
const box = $('<div>').css({
background: '#fff', padding: '20px', borderRadius: '10px',
width: '400px', textAlign: 'center', boxShadow: '0 4px 10px rgba(0,0,0,0.3)'
});
box.append('<h3>Select Parent Page</h3>');
const input = $('<input type="text" placeholder="Type to search existing pages...">').css({
width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '5px'
});
const suggestionBox = $('<ul>').css({
listStyle: 'none', margin: '10px 0 0', padding: 0,
maxHeight: '150px', overflowY: 'auto', border: '1px solid #ddd',
borderRadius: '5px', display: 'none', textAlign: 'left'
});
const confirmBtn = $('<button>Confirm</button>').css({
marginTop: '10px', background: '#007bff', color: '#fff',
border: 'none', padding: '8px 14px', borderRadius: '5px', cursor: 'pointer'
});
const skipBtn = $('<button>No Parent</button>').css({
marginLeft: '8px', marginTop: '10px', background: '#6c757d', color: '#fff',
border: 'none', padding: '8px 14px', borderRadius: '5px', cursor: 'pointer'
});
box.append(input, suggestionBox, $('<div>').append(confirmBtn, skipBtn));
overlay.append(box);
$('body').append(overlay);
// --- Autocomplete Logic ---
input.on('input', function () {
const query = input.val().trim();
suggestionBox.empty();
if (query.length < 2) {
suggestionBox.hide();
return;
}
api.get({
action: 'opensearch',
search: query,
limit: 8,
namespace: 0
}).done(function (data) {
const results = data[1] || [];
suggestionBox.empty();
if (results.length) {
suggestionBox.show();
results.forEach(function (page) {
const li = $('<li>').text(page).css({
padding: '6px 10px', cursor: 'pointer'
}).hover(
function () { $(this).css('background', '#f0f0f0'); },
function () { $(this).css('background', ''); }
).click(function () {
input.val(page);
suggestionBox.hide();
});
suggestionBox.append(li);
});
} else {
suggestionBox.hide();
}
});
});
// --- Confirm button handler ---
confirmBtn.on('click', function () {
const parent = input.val().trim();
if (!parent) {
alert('Please select or type a parent page name, or click "No Parent".');
return;
}
const newTitle = parent + '/' + title;
if (confirm('Create under: ' + newTitle + '?')) {
const newUrl = mw.util.getUrl(newTitle) + '?veaction=edit';
console.log("🟢 Redirecting to VisualEditor:", newUrl);
window.location.href = newUrl;
}
});
// --- Skip button handler ---
skipBtn.on('click', function () {
if (confirm('Proceed without a parent page?')) {
const newUrl = mw.util.getUrl(title) + '?veaction=edit';
console.log("🟢 No parent selected — opening VisualEditor:", newUrl);
window.location.href = newUrl;
}
});
// --- Prevent accidental close ---
overlay.on('click', function (e) {
if (e.target === overlay[0]) {
alert('Please choose a parent or click "No Parent" to continue.');
}
});
}
// --- 3️⃣ Detect new page creation ---
function handleParentPrompt() {
const title = mw.config.get('wgPageName');
const action = mw.config.get('wgAction');
const isNewPage = mw.config.get('wgCurRevisionId') === 0;
if (action === 'edit' && isNewPage) {
console.log("🟡 New page detected, showing parent selector...");
setTimeout(function () {
askForParent(title);
}, 1200);
}
}
// --- 4️⃣ Handle both editors ---
if (mw.config.get('wgAction') === 'edit') handleParentPrompt();
mw.hook('ve.activationComplete').add(function () {
console.log("✅ VisualEditor ready, checking for new page.");
handleParentPrompt();
});
});
// --- Disable Tools Option for all users except admin ---
mw.loader.using(['mediawiki.user'], function () {
$(function () {
mw.user.getGroups().then(function (groups) {
// If the user is NOT an admin (sysop)
if (groups.indexOf('sysop') === -1) {
// Disable Tools menu and its dropdown items
$('a, span').filter(function () {
return $(this).text().trim() === 'Tools';
}).each(function () {
const link = $(this);
link.css({
'pointer-events': 'none',
'opacity': '0.5',
'cursor': 'not-allowed'
});
link.attr('title', 'Restricted to admins');
link.closest('.dropdown').find('a').css({
'pointer-events': 'none',
'opacity': '0.5',
'cursor': 'not-allowed'
});
});
// Disable any link to Special:SpecialPages
$('a[href*="Special:SpecialPages"]').css({
'pointer-events': 'none',
'opacity': '0.5',
'cursor': 'not-allowed'
}).attr('title', 'Restricted to admins');
// Disable MediaWiki namespace links
$('a[href*="MediaWiki:"]').css({
'pointer-events': 'none',
'opacity': '0.5',
'cursor': 'not-allowed'
}).attr('title', 'Restricted to admins');
}
});
});
});
// Collapsible Related Pages on Mobile
mw.loader.using('jquery', function () {
$(function () {
var header = $('.page-links-header');
var content = $('.page-links-content');
if (header.length && content.length) {
header.on('click', function () {
$(this).toggleClass('active');
content.toggleClass('open');
});
}
});
});// === Auto-link existing page titles inside paragraphs (MediaWiki 1.39 compatible) ===
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'jquery.title'], function () {
(function ($, mw) {
// Config
const MIN_TOKEN_LEN = 3; // min length to consider
const MAX_TOKENS = 150; // max distinct candidates to query (avoid huge pages)
const BATCH_SIZE = 50; // titles per API query (query limits)
const contentSelector = '#mw-content-text';
if (mw.config.get('wgNamespaceNumber') !== 0) {
// Only run on main namespace by default; modify if you want other namespaces
// (remove this guard to run everywhere)
}
// Helper: get visible text nodes under root, skipping certain elements
function getTextNodes(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: function (node) {
// ignore empty / whitespace-only
if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
// ignore text in these parent tags
const badParents = ['A', 'CODE', 'PRE', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'];
let p = node.parentNode;
while (p && p !== root) {
if (badParents.indexOf(p.nodeName) !== -1) return NodeFilter.FILTER_REJECT;
if ($(p).closest('.no-autolink').length) return NodeFilter.FILTER_REJECT;
p = p.parentNode;
}
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
let cur;
while ((cur = walker.nextNode())) nodes.push(cur);
return nodes;
}
// Helper: extract candidate tokens (capitalized words and multiword sequences)
function extractCandidates(text) {
// Match sequences of words that start with uppercase letter, possibly containing spaces and punctuation
// e.g. "Hindu Temple", "Sanatan Hindu Dharma", "MediaWiki" etc.
const regex = /(?:\b[A-Z][A-Za-z0-9&'’\-]{2,}(?:\s+[A-Z][A-Za-z0-9&'’\-]{2,}){0,3})/g;
const set = new Set();
let m;
while ((m = regex.exec(text))) {
const t = m[0].trim();
if (t.length >= MIN_TOKEN_LEN) set.add(t);
if (set.size >= MAX_TOKENS) break;
}
return Array.from(set);
}
// Batch check which titles exist using action=query
async function filterExistingTitles(titles) {
const api = new mw.Api();
const exist = [];
for (let i = 0; i < titles.length; i += BATCH_SIZE) {
const batch = titles.slice(i, i + BATCH_SIZE);
// Use normalized/title mapping via query
const res = await api.get({
action: 'query',
format: 'json',
titles: batch.join('|'),
redirects: 1
});
if (!res || !res.query) continue;
// iterate pages returned
for (const pageId in res.query.pages) {
const p = res.query.pages[pageId];
if (p.missing === undefined) { // exists (no missing flag)
const titleText = p.title; // normalized title
exist.push(titleText);
}
}
}
return exist;
}
// Replace occurrences in text nodes using DOM operations
function replaceTextNodesWithLinks(textNodes, titleSet) {
// Create a regex that matches any of the titles (escape)
const titles = Array.from(titleSet).sort((a, b) => b.length - a.length); // longest first
if (!titles.length) return;
const escaped = titles.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const wordRegex = new RegExp('\\b(' + escaped.join('|') + ')\\b', 'g');
textNodes.forEach(node => {
const parent = node.parentNode;
if (!parent) return;
const text = node.nodeValue;
let match;
let lastIndex = 0;
const frag = document.createDocumentFragment();
while ((match = wordRegex.exec(text)) !== null) {
const before = text.slice(lastIndex, match.index);
if (before) frag.appendChild(document.createTextNode(before));
const matched = match[0];
// create link
const a = document.createElement('a');
a.href = mw.util.getUrl(matched);
a.textContent = matched;
a.className = 'autolinked-page';
frag.appendChild(a);
lastIndex = wordRegex.lastIndex;
}
if (lastIndex === 0) return; // no replacement
const after = text.slice(lastIndex);
if (after) frag.appendChild(document.createTextNode(after));
parent.replaceChild(frag, node);
});
}
// Main runner
async function runAutolink() {
const root = document.querySelector(contentSelector);
if (!root) return;
// get text nodes
const nodes = getTextNodes(root);
if (!nodes.length) return;
// sample text to extract candidates
let sampleText = '';
for (let i = 0; i < Math.min(nodes.length, 200); i++) {
sampleText += ' ' + nodes[i].nodeValue;
}
const candidates = extractCandidates(sampleText);
if (!candidates.length) return;
// check which exist
const existing = await filterExistingTitles(candidates);
if (!existing.length) return;
// map normalized titles to check exact casing versions in page text
const existingSet = new Set(existing);
// Now find all text nodes again and replace matches
const allNodes = getTextNodes(root);
replaceTextNodesWithLinks(allNodes, existingSet);
}
// Delay run until DOM ready and when content is ready (VE may alter DOM but this runs after)
$(function () {
// run after a short delay in case VE modifies DOM
setTimeout(runAutolink, 800);
// also run after VisualEditor activation to handle VE editing preview
mw.hook('ve.activationComplete').add(function () {
setTimeout(runAutolink, 1000);
});
});
})(jQuery, mw);
});
