Wikipedia testwiki https://test.wikipedia.org/wiki/Main_Page MediaWiki 1.46.0-wmf.24 first-letter Media Special Talk User User talk Wikipedia Wikipedia talk File File talk MediaWiki MediaWiki talk Template Template talk Help Help talk Category Category talk Thread Thread talk Summary Summary talk Test namespace 1 Test namespace 1 talk Test namespace 2 Test namespace 2 talk Draft Draft talk Campaign Campaign talk TimedText TimedText talk Module Module talk SecurePoll SecurePoll talk CNBanner CNBanner talk Translations Translations talk Event Event talk Topic Newsletter Newsletter talk Wikipedia:Requests/Permissions 4 32559 739238 737328 2026-04-24T12:11:06Z Wooze 54732 /* Requests for user rights */add 739238 wikitext text/x-wiki <noinclude>{{Shortcut|WP:R/P|WP:RfP|WP:RfA|WP:PERM|WP:RFR|WP:RFPERM}}</noinclude> {{Wikipedia:Requests/Top}} == Requests for user rights == * Subpages: [[Wikipedia:Requests/Permissions/All|All (current and archived)]] * Request: <inputbox> type=create prefix=Wikipedia:Requests/Permissions/ preload=Template:PA2 buttonlabel=Requests for user rights placeholder=Enter your username </inputbox> After creating the subpage, come back here and transclude the page below (<code><nowiki>{{Wikipedia:Requests/Permissions/Example}}</nowiki></code>). <!-- Please transclude your requests below this line, LATEST AT THE TOP, in the form {{Wikipedia:Requests/Permissions/USERNAME}} --> {{Wikipedia:Requests/Permissions/Wooze}} <!-- NEW ENTRIES AT THE TOP, NOT HERE --> a4fsnspb2sgbv8u6xy0nijkzrm076sm Bug 33151 0 54430 739258 733878 2026-04-24T13:14:46Z ~2026-23062-17 73557 showcaptcha 739258 wikitext text/x-wiki {{subst:No footnotes/auto}}{{subst:Unreferenced/auto}}'''a''b'''c'' teste this is an edit 0i2rxvga6k1dd86k6a3huljrlpvl8jm Letter-small reclam talk:Q0 0 78506 739240 217971 2026-04-24T12:14:42Z ~2026-23062-17 73557 739240 wikitext text/x-wiki <!--please use this page only for test--> this is an edit blah blah 2wb8s5841a4xz5y6p46w41igbi974s6 739241 739240 2026-04-24T12:17:13Z ~2026-23062-17 73557 739241 wikitext text/x-wiki <!--please use this page only for test--> this is an edit continue editing blah blah blah n7doqyxxy6n5jehkd95ymqgxwx38c5w 739242 739241 2026-04-24T12:17:15Z AutoModeratorTest 61468 Reverted edits by [[Special:Contributions/~2026-23062-17|~2026-23062-17]] ([[User talk:~2026-23062-17|talk]]) to last revision by [[User:Dicto23456|Dicto23456]] 217971 wikitext text/x-wiki <!--please use this page only for test--> qmxhi1speatknx5oxiw60vgvtp22lah Alınca, Ovacık 0 82654 739233 688373 2026-04-24T12:04:19Z ~2026-23062-17 73557 showcaptcha 739233 wikitext text/x-wiki <!--- DİKKAT! BU ŞABLONU KAYNAK GÖSTERMEDEN GÜNCELLEMEYİNİZ! ---> {{Türkiye köy bilgi kutusu |ad = Alınca |harita = |harita boyut = |harita açıklama = |harita1 = |harita1 boyut = |harita1 açıklama = |harita2 = Karabük in Turkey.svg |harita2 boyut = 250px |harita2 açıklama = Karabük | latd = 41 | latm = 3 | lats = 49 | latNS = N | longd = 32 | longm = 52 | longs = 56 | longEW = E |il = Karabük |ilçe = Ovacık |bölge = Karadeniz |muhtar = Satılmış Karadağ |yüzölçümü = |yüzölçümü_ref = |rakım = 1310 |rakım_ref = <ref></ref> |nüfus = 26 |nüfus yoğunluğu = |nüfus_ref = <ref name="YERELNET"></ref> |nüfus_yıl = 2012 |alan kodu = 0370 |plaka kodu = |posta kodu = 78300 |website = [http://www.yerelnet.org.tr/koyler/koy.php?koyid=267193] }} '''Alınca''', [[Karabük (il)|Karabük]] ilinin [[Ovacık, Karabük|Ovacık]] ilçesine bağlı bir [[köy]]dür. this is an edit == Tarihçe == == Kültür == == Coğrafya == == İklim == Köyün iklimi, [[Türkiye'de Karadeniz İklimi|Karadeniz iklimi]] etki alanı içerisindedir. == Nüfus == {| class="wikitable" width=200 ! colspan=2 |Yıllara göre köy nüfus verileri |- | align="center" | [[2007]] | align="right" | 23 |- | align="center" | [[2000]] | align="right" | 29 |- | align="center" | [[1997]] | align="right" | 43 |} == Ekonomi == Köyün ekonomisi [[tarım]] ve [[hayvancılık|hayvancılığa]] dayalıdır. == Altyapı bilgileri == Köyde [[ilköğretim]] okulu yoktur. Köyde, [[içme suyu şebekesi]] ve [[kanalizasyon]] şebekesi vardır. [[PTT]] şubesi ve PTT acentesi yoktur. [[Sağlık ocağı]] ve sağlık evi de yoktur. Köye ulaşımı sağlayan yol [[asfalt]] olup köyde [[elektrik]] ve sabit [[telefon]] vardır.köyümüz muhtar Satılmış karadağnın öncülüğü ile orköy tarafından hazırlanan proje ile güneş enerji sisteminede 2008 yılında kavuşmuştur == Dış bağlantılar == {{portal|Türkiye}} [[Kategori:Ovacık (Karabük) belde ve köyleri]] test tjcdx1bv2vo2tivwpj45c9bh87dtazj 739234 739233 2026-04-24T12:05:09Z ~2026-23062-17 73557 showcaptcha 739234 wikitext text/x-wiki <!--- DİKKAT! BU ŞABLONU KAYNAK GÖSTERMEDEN GÜNCELLEMEYİNİZ! ---> {{Türkiye köy bilgi kutusu |ad = Alınca |harita = |harita boyut = |harita açıklama = |harita1 = |harita1 boyut = |harita1 açıklama = |harita2 = Karabük in Turkey.svg |harita2 boyut = 250px |harita2 açıklama = Karabük | latd = 41 | latm = 3 | lats = 49 | latNS = N | longd = 32 | longm = 52 | longs = 56 | longEW = E |il = Karabük |ilçe = Ovacık |bölge = Karadeniz |muhtar = Satılmış Karadağ |yüzölçümü = |yüzölçümü_ref = |rakım = 1310 |rakım_ref = <ref></ref> |nüfus = 26 |nüfus yoğunluğu = |nüfus_ref = <ref name="YERELNET"></ref> |nüfus_yıl = 2012 |alan kodu = 0370 |plaka kodu = |posta kodu = 78300 |website = [http://www.yerelnet.org.tr/koyler/koy.php?koyid=267193] }} '''Alınca''', [[Karabük (il)|Karabük]] ilinin [[Ovacık, Karabük|Ovacık]] ilçesine bağlı bir [[köy]]dür. == Tarihçe == == Kültür == == Coğrafya == == İklim == Köyün iklimi, [[Türkiye'de Karadeniz İklimi|Karadeniz iklimi]] etki alanı içerisindedir. == Nüfus == {| class="wikitable" width=200 ! colspan=2 |Yıllara göre köy nüfus verileri |- | align="center" | [[2007]] | align="right" | 23 |- | align="center" | [[2000]] | align="right" | 29 |- | align="center" | [[1997]] | align="right" | 43 |} == Ekonomi == Köyün ekonomisi [[tarım]] ve [[hayvancılık|hayvancılığa]] dayalıdır. == Altyapı bilgileri == Köyde [[ilköğretim]] okulu yoktur. Köyde, [[içme suyu şebekesi]] ve [[kanalizasyon]] şebekesi vardır. [[PTT]] şubesi ve PTT acentesi yoktur. [[Sağlık ocağı]] ve sağlık evi de yoktur. Köye ulaşımı sağlayan yol [[asfalt]] olup köyde [[elektrik]] ve sabit [[telefon]] vardır.köyümüz muhtar Satılmış karadağnın öncülüğü ile orköy tarafından hazırlanan proje ile güneş enerji sisteminede 2008 yılında kavuşmuştur == Dış bağlantılar == {{portal|Türkiye}} [[Kategori:Ovacık (Karabük) belde ve köyleri]] test m4y7uc0r5653e4ymzj0z61mtkm7ku80 Test patrol 4 0 91804 739259 728062 2026-04-24T13:18:46Z ~2026-23062-17 73557 showcaptcha 739259 wikitext text/x-wiki One more time...testing this is an edit tvofz94d9qp4gdl5tzqmgoxue79jhzu Collapsible 0 97136 739249 667609 2026-04-24T12:34:08Z ~2026-23062-17 73557 739249 wikitext text/x-wiki blank this is an edit f1423px2jfxkewegx7cnauw9eo001pz 739256 739249 2026-04-24T13:08:05Z Wooze 54732 Reverted edit by [[Special:Contributions/~2026-23062-17|~2026-23062-17]] ([[User talk:~2026-23062-17|talk]]) to last revision by [[User:~2025-30384-3|~2025-30384-3]] 667609 wikitext text/x-wiki blank be349kjqdpuoc3b9ab5hvqfdz93txpb 739257 739256 2026-04-24T13:08:19Z Wooze 54732 Reverted edit by [[Special:Contributions/Wooze|Wooze]] ([[User talk:Wooze|talk]]) to last revision by [[User:~2026-23062-17|~2026-23062-17]] 739249 wikitext text/x-wiki blank this is an edit f1423px2jfxkewegx7cnauw9eo001pz User:Sam Sailor/test.js 2 98186 739280 739074 2026-04-24T18:40:30Z Sam Sailor 26820 Test 739280 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); const entity = wikiData.entities[qid]; const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const target = await getPriorityTarget(name, 'surname'); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 383jyoyj5uguua8kmmfn04f35w67353 739281 739280 2026-04-24T18:49:59Z Sam Sailor 26820 Test 739281 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); const entity = wikiData.entities[qid]; const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> ezpz0sq4loq9566kpdlvgdxgnf92akh 739284 739281 2026-04-24T18:58:34Z Sam Sailor 26820 Test 739284 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); const entity = wikiData.entities[qid]; const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> g9n1eyu0b0l8f8r9ufavj4j295uox65 739288 739284 2026-04-24T21:13:49Z Sam Sailor 26820 Test 739288 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 0, formatversion: 2 }); const basePage = baseRes.query.pages[0]; const baseExists = !basePage.missing; const isRedirectToSelf = baseExists && basePage.redirect && basePage.title === baseTitle; const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (!baseExists || isRedirectToSelf) { if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` is free. This disambiguation may be unnecessary.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` is a redlink, but I found ${competitors.length} potential competitors (e.g., `, $('<i>').text(competitors[0].title), `).`)); $content.append($('<span>').text('Consider if a disambiguation page or primary topic move is needed.')); } $container.append($header, $content); appendDismiss($container); } else if (basePage.redirect) { // Scenario: Base redirects elsewhere $header.append($('<strong>').text('Conflict:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` redirects to `, $('<i>').text(basePage.title), `. Verify primary topic status.`)); $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> krfaj2ar9aq0fc5spewclmkk03udylw 739289 739288 2026-04-24T21:33:52Z Sam Sailor 26820 Test 739289 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 0, formatversion: 2 }); const basePage = baseRes.query.pages[0]; const baseExists = !basePage.missing; let baseRedirectsToSelf = false; if (baseExists && basePage.redirect) { const redirRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); if (redirRes.query.pages[0].title === currentTitle) { baseRedirectsToSelf = true; } } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (!baseExists || baseRedirectsToSelf) { if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${baseRedirectsToSelf ? 'redirect to this page' : 'redlink'}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> on45mry8tkkq7mfct0d0223z86i2aik 739290 739289 2026-04-24T21:40:03Z Sam Sailor 26820 Test 739290 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 0, formatversion: 2 }); const basePage = baseRes.query.pages[0]; const baseExists = !basePage.missing; let baseRedirectsToSelf = false; if (baseExists && basePage.redirect) { const redirRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); if (redirRes.query.pages[0].title === currentTitle) { baseRedirectsToSelf = true; } } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; const isCurrent = t === currentTitle; const isBase = t === baseTitle; const isDisambiguatedVersion = t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('); return !isCurrent && !isBase && isDisambiguatedVersion; }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${baseRedirectsToSelf ? 'redirect to this page' : 'redlink'}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 3t7dswj4il7biagzxdrqonru5iach76 739291 739290 2026-04-24T21:41:09Z Sam Sailor 26820 Restored revision 739288 by [[Special:Contributions/Sam Sailor|Sam Sailor]] ([[User talk:Sam Sailor|talk]]): Test (TwinkleGlobal) 739291 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 0, formatversion: 2 }); const basePage = baseRes.query.pages[0]; const baseExists = !basePage.missing; const isRedirectToSelf = baseExists && basePage.redirect && basePage.title === baseTitle; const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (!baseExists || isRedirectToSelf) { if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` is free. This disambiguation may be unnecessary.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` is a redlink, but I found ${competitors.length} potential competitors (e.g., `, $('<i>').text(competitors[0].title), `).`)); $content.append($('<span>').text('Consider if a disambiguation page or primary topic move is needed.')); } $container.append($header, $content); appendDismiss($container); } else if (basePage.redirect) { // Scenario: Base redirects elsewhere $header.append($('<strong>').text('Conflict:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` redirects to `, $('<i>').text(basePage.title), `. Verify primary topic status.`)); $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> krfaj2ar9aq0fc5spewclmkk03udylw 739292 739291 2026-04-24T21:41:54Z Sam Sailor 26820 Undid revision [[Special:Diff/739291|739291]] by [[Special:Contributions/Sam Sailor|Sam Sailor]] ([[User talk:Sam Sailor|talk]]) 739292 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 0, formatversion: 2 }); const basePage = baseRes.query.pages[0]; const baseExists = !basePage.missing; let baseRedirectsToSelf = false; if (baseExists && basePage.redirect) { const redirRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); if (redirRes.query.pages[0].title === currentTitle) { baseRedirectsToSelf = true; } } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; const isCurrent = t === currentTitle; const isBase = t === baseTitle; const isDisambiguatedVersion = t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('); return !isCurrent && !isBase && isDisambiguatedVersion; }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${baseRedirectsToSelf ? 'redirect to this page' : 'redlink'}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 3t7dswj4il7biagzxdrqonru5iach76 739293 739292 2026-04-24T21:47:58Z Sam Sailor 26820 Test 739293 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> ebhx4p5u9s7ydpy1yb1gxbd1m7x93w5 739295 739293 2026-04-25T06:29:50Z Sam Sailor 26820 Test 739295 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const talkRes = await api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }); const page = talkRes.query.pages[0]; if (page.missing) return; const talkWikitext = page.revisions[0].content; const isStubRated = /\|class\s*=\s*Stub/i.test(talkWikitext); if (isStubRated) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $header.append($('<div>').append($('<strong>').text('Quality Mismatch:'), ` ORES predicts `, $('<b>').text(oresClass), ` but talk page says Stub. `, $btn)); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> to5pw03xjwxr1vpuv2hx27bm7xpnc8r 739297 739295 2026-04-25T07:26:54Z Sam Sailor 26820 Test 739297 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>'); let message = `ORES suggests <b>${oresClass}</b>. `; let $btn; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; $btn = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); } $header.append($content.append($('<strong>').text('Quality:'), " ", message, $btn)); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> n5pobrki0exkq0fj6gsabnk6c238bpb 739298 739297 2026-04-25T07:50:12Z Sam Sailor 26820 Test 739298 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>'); let message = `ORES suggests <b>${oresClass}</b>. `; let $btn; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $header.append($content.append($('<strong>').text('Quality:'), " ", message, $btnOres, " or ", $btnStart)); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $header.append($content.append($('<strong>').text('Quality:'), " ", message, $btnOres, " or ", $btnC)); } else { $header.append($content.append($('<strong>').text('Quality:'), " ", message, $btnOres)); } } $header.append($content.append($('<strong>').text('Quality:'), " ", message, $btn)); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 3e2gqyz3qhv65sg62k3qaosk5fv1w4h 739299 739298 2026-04-25T07:57:11Z Sam Sailor 26820 Test 739299 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append(" or ", $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 0qycug3z3jg7tv19o0nng54i695wph4 739300 739299 2026-04-25T08:04:36Z Sam Sailor 26820 Test 739300 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append($('<span>').text(' or ').css('margin', '0 5px'), $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> f86l0xf9omzkd03zgz66rk2f1o1runc 739301 739300 2026-04-25T08:07:48Z Sam Sailor 26820 Test 739301 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append($('<span>').text(' or ').css('margin', '0 10px'), $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 7brep01sam9gpm5q482mnmm5aq48zp0 739302 739301 2026-04-25T08:13:47Z Sam Sailor 26820 Test 739302 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append($('<span>').text(' or ').css('margin', '0 5px 0 20px'), $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> okhbfl61qh7i7aq4tf63hvss2rxo1p4 739303 739302 2026-04-25T08:14:54Z Sam Sailor 26820 Test 739303 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append($('<span>').text(' or ').css('margin', '0 5px 0 50px'), $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> n28hc5dhmy4ppdwugrpf5x2sm5g9n3f 739304 739303 2026-04-25T08:20:06Z Sam Sailor 26820 Test 739304 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append($('<span>').text(' or ').css({ 'display': 'inline-block', 'margin': '0 5px 0 50px' }), $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 8cnlotsf9rmipfeqva7jbhica99ydy0 739305 739304 2026-04-25T08:22:13Z Sam Sailor 26820 Test 739305 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append(" or ", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append($('<span>').text(' or ').css({ 'display': 'inline-block', 'margin-left': '50px', 'margin-right': '5px' }), $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> hjxpx3thkhb17w8gr67ttn5qgdfwj62 739306 739305 2026-04-25T08:26:05Z Sam Sailor 26820 Test 739306 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append("\u00A0\u00A0or\u00A0", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append("\u00A0\u00A0or\u00A0", $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> 7w8ducjwl07fpckfotgqcet62pyyhkt 739307 739306 2026-04-25T08:26:52Z Sam Sailor 26820 Test 739307 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet).then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append("\u00A0\u00A0\u00A0or\u00A0", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append("\u00A0\u00A0\u00A0or\u00A0", $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> q9abcyy9kp5p0uxbhqs5mpxmvb0psf7 739309 739307 2026-04-25T11:55:40Z Sam Sailor 26820 Test 739309 javascript text/javascript //<nowiki> (function() { mw.util.addCSS(` .ambox-Orphan { display: block !important; } .comrade-container { box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px; padding: 10px 15px; margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5; } .comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; } .comrade-content { display: flex; flex-direction: column; gap: 5px; width: 100%; } .comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; } .comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; } .comrade-info { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; } .comrade-container button:not(.comrade-dismiss) { background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px; } .comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; } .comrade-dismiss { background: transparent; border: none; color: #d33; font-weight: bold; cursor: pointer; font-size: 0.9em; } .comrade-dismiss:hover { text-decoration: underline; } .comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; } `); function appendDismiss($el) { $('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() { $(this).closest('.comrade-container').fadeOut(); }).appendTo($el.find('.comrade-header')); $("#siteSub").after($el); } async function checkNamingPrecision(currentTitle, isHuman) { const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/); if (isHuman || !disambigMatch) return; const baseTitle = disambigMatch[1].trim(); const api = new mw.Api(); try { const baseRes = await api.get({ action: 'query', titles: baseTitle, redirects: 1, formatversion: 2 }); const page = baseRes.query.pages[0]; const baseExists = !page.missing; let baseRedirectsToSelf = false; if (baseRes.query.redirects) { baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle); } const searchRes = await api.get({ action: 'query', list: 'allpages', apprefix: baseTitle, aplimit: 50, formatversion: 2 }); const competitors = searchRes.query.allpages.filter(p => { const t = p.title; return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' (')); }); if (!baseExists || baseRedirectsToSelf) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (competitors.length === 0) { $header.append($('<strong>').text('Precision:')); const reason = baseRedirectsToSelf ? "already redirects here" : "is free"; $content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`)); const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, { wpNewTitle: baseTitle, wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]' }); $content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank'))); } else { $header.append($('<strong>').text('Observation:')); const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink'; $content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`)); $content.append($('<span>').text('Consider if a disambiguation page is needed.')); } $container.append($header, $content); appendDismiss($container); } } catch (e) { console.warn("Comrade: Precision check failed", e); } } async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText, customWikitext) { const api = new mw.Api(); const res = await api.get({ action: 'query', titles: redirTitle, formatversion: 2 }); if (res.query.pages[0].missing) { const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-'); const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId); const $header = $('<div>').addClass('comrade-header'); const summary = `Created redirect from ${labelText.toLowerCase()} to [[${targetTitle}]]`; const $btn = $('<button>').text('Create redirect').on('click', () => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext)); $header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn)); $container.append($header); appendDismiss($container); } } async function checkDomainRedirect(qid, currentTitle) { let domainCandidate = ""; if (qid) { try { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetclaims', entity: qid, property: 'P856', format: 'json', origin: '*' }); const res = await fetch(url).then(r => r.json()); if (res.claims?.P856) { domainCandidate = res.claims.P856[0].mainsnak.datavalue.value; } } catch (e) { console.warn("Comrade: Wikidata API call failed."); } } if (!domainCandidate) { domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href"); } if (domainCandidate) { try { const url = new URL(domainCandidate); let domain = url.hostname.replace(/^www\d*\./, ""); if (domain.includes(".") && url.pathname === "/") { const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate); try { const response = await fetch(citationURL); if (response.ok) { await checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain"); } } catch (e) { console.warn("Comrade: Citation API check failed."); } } } catch (e) { console.warn("Comrade: Error parsing URL object."); } } } async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan, resolvedTitle) { const api = new mw.Api(); const candidates = resolvedTitle ? [resolvedTitle] : [`${name} (${type})`, `${name} (${type.toLowerCase()})`, `${name} (name)`, name]; const checked = new Set(); for (const title of candidates) { if (checked.has(title)) continue; checked.add(title); const res = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2, redirects: 0 }); const page = res.query.pages[0]; if (page && !page.missing) { const text = page.revisions[0].content; const foundTitle = page.title; const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text); const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text); if (isNameMatch || isNameDab) { const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]'); const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text); if (alreadyListed) break; const $container = $('<div>').addClass('comrade-container'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').addClass('comrade-content'); if (isOrphan) { $container.addClass('comrade-nudge'); $header.append($('<strong>').text('Comrade nudge:')); } else { $container.addClass('comrade-info'); $header.append($('<strong>').text('Informative:')); } $content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({ href: mw.util.getUrl(foundTitle), target: '_blank' }).text(foundTitle), "; should it be?")); const snippet = '* {{anbl|' + currentTitle + '}}'; const $snippetWrapper = $('<div>').css({ 'display': 'flex', 'align-items': 'center', 'gap': '10px', 'margin-top': '4px' }); const $instructionSpan = $('<span>').append('If the Short description is good, consider adding ', $('<code>').text(snippet).css({ 'background-color': '#f8f9fa', 'border': '1px solid #eaecf0', 'border-radius': '2px', 'padding': '1px 4px' })); const $copyBtn = $('<button>').text('Copy & insert').css('margin-left', '0').on('click', function() { navigator.clipboard.writeText(snippet + '\n').then(() => { const $this = $(this); const originalText = $this.text(); $this.text('Copied!').prop('disabled', true); const editUrl = mw.util.getUrl(foundTitle, { action: 'edit', summary: 'Adding ' + snippet }); window.open(editUrl, '_blank'); setTimeout(() => { $this.text(originalText).prop('disabled', false); }, 1500); }); }); $snippetWrapper.append($instructionSpan, $copyBtn); $content.append($snippetWrapper); if (!hasShortDesc) { $content.append($('<span>').css({ 'color': '#d33', 'font-weight': 'bold', 'margin-top': '5px' }).text('⚠️ Missing short description: Please add a concise one before listing.')); } $container.append($header, $content); appendDismiss($container); break; } } } } async function checkQualityConsistency() { const oresClass = getOresPrediction(); if (!oresClass || oresClass === 'Stub') return; const talkTitle = 'Talk:' + mw.config.get('wgPageName'); const api = new mw.Api(); try { const [talkRes, pageRes] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: talkTitle, rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }) ]); const talkPage = talkRes.query.pages[0]; const wikitext = pageRes.query.pages[0].revisions[0].content; const hasStubTag = /\{\{[^}]*stub\}\}/i.test(wikitext); const $raterLink = $('#ca-rater, #t-rater'); const isRaterAvailable = (typeof window.rater !== 'undefined' || $raterLink.length > 0); let talkClass = null; if (!talkPage.missing) { const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i); talkClass = classMatch ? classMatch[1].charAt(0).toUpperCase() + classMatch[1].slice(1).toLowerCase() : null; } const isStubRated = talkClass === 'Stub'; const lingeringStubTag = (talkClass && talkClass !== 'Stub' && oresClass !== 'Stub' && hasStubTag); if (talkPage.missing || isStubRated || lingeringStubTag) { const $container = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $content = $('<div>').append($('<strong>').text('Quality:'), " "); let message = `ORES suggests <b>${oresClass}</b>. `; if (talkPage.missing) { message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `; let $btn; if (isRaterAvailable) { $btn = $('<button>').text('Open Rater').on('click', () => $raterLink.find('a')[0].click()); } else { $btn = $('<button>').text('Install Rater').on('click', () => window.open('https://en.wikipedia.org/wiki/User:Evad37/rater', '_blank')); message += "Consider installing <i>Rater</i> for easy assessment."; } $content.append(message, $btn); } else if (lingeringStubTag && !isStubRated) { message = `Article is ${talkClass}-class, but stub tags remain. `; const $btn = $('<button>').text('Remove stub tags').on('click', () => performStubRemoval()); $content.append(message, $btn); } else if (isStubRated) { message = `Talk page says Stub, but ORES predicts <b>${oresClass}</b>. `; const $btnOres = $('<button>').text(`Promote to ${oresClass}`).on('click', () => performPromotion(oresClass)); $content.append(message, $btnOres); if (oresClass === 'C') { const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => performPromotion('Start')); $content.append("\u00A0\u00A0\u00A0or\u00A0", $btnStart); } else if (oresClass === 'B') { const $btnC = $('<button>').text(`Promote to C`).on('click', () => performPromotion('C')); $content.append("\u00A0\u00A0\u00A0or\u00A0", $btnC); } } $header.append($content); $container.append($header); appendDismiss($container); } } catch (e) { console.error("Comrade quality check failed", e); } } async function performStubRemoval() { const api = new mw.Api(); try { const pageData = await api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }); let wikitext = pageData.query.pages[0].revisions[0].content; const newWikitext = wikitext.replace(/\{\{[^}]*stub\}\}\n?/gi, '').trim(); if (wikitext === newWikitext) { mw.notify("No stub tags found to remove."); return; } await api.postWithToken('csrf', { action: 'edit', title: mw.config.get("wgPageName"), text: newWikitext, summary: `Removing lingering stub tags per ORES/Talk assessment`, nocreate: true }); location.reload(); } catch (err) { console.error("Comrade failed to remove stub tags:", err); mw.notify("Error removing stub tags.", { type: 'error' }); } } async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) { const api = new mw.Api(); const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`; const summary = customSummary || `Created redirect from [[${redirTitle}]]`; try { await api.postWithEditToken({ action: 'edit', title: redirTitle, text: content, summary: summary, createonly: true }); mw.notify("Redirect created!", { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); $(`#${elementId}`).fadeOut(); } catch (e) { mw.notify("Failed to create redirect.", { type: 'error' }); } } function getOresPrediction() { // Dependency: [[m:User:EpochFail/ArticleQuality-system.js]] renders the prediction inside .article_quality const text = $('.article_quality').text(); if (!text) return null; if (text.includes('Start')) return 'Start'; if (text.includes('C')) return 'C'; if (text.includes('B')) return 'B'; return null; } function getSmartSortKey(fullName, entity) { const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || []; const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean); const allRelevantIds = [...countryIds, ...locationIds, ...placeIds]; const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null; if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", ""); const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i; if (fullName.match(arabicParticles)) { if (birthYear && birthYear > 1900) { const parts = fullName.split(' '); const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben'); if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' '); } else { return fullName; } } const eastAsianQids = ["Q148", "Q184", "Q884", "Q424", "Q881", "Q17"]; if (allRelevantIds.some(id => eastAsianQids.includes(id))) { if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName); const parts = fullName.split(' '); if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`; } return westernSort(fullName); } function westernSort(name) { let cleanName = name; if (name.startsWith("O'")) cleanName = name.replace("O'", "O"); const parts = cleanName.split(' '); if (parts.length < 2) return cleanName; const surname = parts.pop(); if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) { const actualSurname = parts.pop(); return `${actualSurname}, ${parts.join(' ')} ${surname}`; } return `${surname}, ${parts.join(' ')}`; } async function performDeorphan() { const api = new mw.Api(); try { await api.edit(mw.config.get('wgPageName'), function(rev) { let text = rev.content; const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]'; text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, ''); text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|'); text = replaceMI(text); text = text.replace(/\n{3,}/g, '\n\n').trim(); return { text, summary, minor: true }; }); mw.notify('Orphan tag removed!', { type: 'success', tag: 'comrade', classes: ['comrade-success'] }); setTimeout(() => location.reload(), 700); } catch (e) { mw.notify('Failed to remove orphan tag.', { type: 'error' }); } } async function performPromotion(newClass) { const api = new mw.Api(); const pageName = mw.config.get('wgPageName'); const talkName = 'Talk:' + pageName; try { const talkData = await api.get({ action: 'query', prop: 'revisions', titles: talkName, rvprop: 'content', formatversion: 2 }); let talkWikitext = talkData.query.pages[0].revisions[0].content; talkWikitext = talkWikitext.replace(/(\|class\s*=\s*)Stub/i, `$1${newClass}`); await api.postWithEditToken({ action: 'edit', title: talkName, text: talkWikitext, summary: `Promoting article to ${newClass} class per ORES prediction [[WP:ORES]]` }); const artData = await api.get({ action: 'query', prop: 'revisions', titles: pageName, rvprop: 'content', formatversion: 2 }); let artWikitext = artData.query.pages[0].revisions[0].content; const stubRegex = /\{\{[^}]*?-stub\}\}\s*\n?/gi; const genericStubRegex = /\{\{stub\}\}\s*/gi; artWikitext = artWikitext.replace(stubRegex, '').replace(genericStubRegex, ''); await api.postWithEditToken({ action: 'edit', title: pageName, text: artWikitext, summary: `Removing stub tags; article promoted to ${newClass}` }); location.reload(); } catch (e) { alert("Promotion failed. Check console for details."); console.error(e); } } function replaceMI(text) { const miRegex = /\{\{Multiple[ _]issues\s*\|/gi; let result = ''; let lastIndex = 0; let match; while ((match = miRegex.exec(text)) !== null) { const start = match.index; result += text.slice(lastIndex, start); let depth = 0; let i = start; while (i < text.length) { if (text[i] === '{' && text[i + 1] === '{') { depth++; i += 2; } else if (text[i] === '}' && text[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } const end = i; const full = text.slice(start, end); const pipeIdx = full.indexOf('|'); const inner = full.slice(pipeIdx + 1, full.length - 2).trim(); const topLevel = extractTopLevelTemplates(inner); if (topLevel.length === 0) { result += ''; } else if (topLevel.length === 1) { result += topLevel[0].trim(); } else { result += full; } lastIndex = end; } result += text.slice(lastIndex); return result; } function extractTopLevelTemplates(inner) { const templates = []; let i = 0; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { let depth = 0; const start = i; while (i < inner.length) { if (inner[i] === '{' && inner[i + 1] === '{') { depth++; i += 2; } else if (inner[i] === '}' && inner[i + 1] === '}') { depth--; i += 2; if (depth === 0) break; } else { i++; } } templates.push(inner.slice(start, i)); } else { i++; } } return templates; } async function performSortCorrection(newName, type) { const api = new mw.Api(); const title = mw.config.get("wgPageName"); try { const data = await api.get({ action: 'query', prop: 'revisions', titles: title, rvprop: 'content', formatversion: 2 }); let content = data.query.pages[0].revisions[0].content; const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):[^}]+\}\}/i; const newTag = `{{DEFAULTSORT:${newName}}}`; let actionDesc = type === "add" ? "added" : "corrected"; if (dsRegex.test(content)) { content = content.replace(dsRegex, newTag); } else { const catRegex = /\[\[Category:/i; if (catRegex.test(content)) { content = content.replace(catRegex, `${newTag}\n[[Category:`); } else { content += `\n\n${newTag}`; } } await api.postWithEditToken({ action: 'edit', title: title, text: content, summary: `Setting DEFAULTSORT to ${newName}`, nocreate: true }); mw.notify(`DEFAULTSORT ${actionDesc} to ${newName}!`, { title: 'Comrade Success', type: 'success' }); setTimeout(() => location.reload(), 700); } catch (err) { mw.notify('Failed to save DEFAULTSORT.', { type: 'error' }); } } function stripDiacritics(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function renderSortNudge(target, reason) { const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn)); $dsNudge.append($header); appendDismiss($dsNudge); } async function fetchWikidataNameLabels(entity) { const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || []; const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || []; const allNameIds = [...new Set([...familyIds, ...givenIds])]; if (allNameIds.length === 0) return { givens: [], surnames: [] }; const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: allNameIds.join('|'), props: 'labels', languages: 'en', format: 'json', origin: '*' }); const nameData = await fetch(url).then(r => r.json()); return { givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean), surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean) }; } function fixNameCasing(part, referenceName) { return part.split(' ').map(word => { const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i')); if (match) return match[0]; return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word; }).join(' '); } function getLongNameFromWikitext(wikitext) { const firstLine = wikitext.split('\n').find(l => l.includes("'''")); if (!firstLine) return null; const boldMatch = firstLine.match(/'''(.+?)'''/); if (!boldMatch) return null; return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim(); } async function init() { if (mw.config.get("wgDBname") !== "enwiki" || mw.config.get("wgNamespaceNumber") !== 0 || mw.config.get("wgAction") !== "view") return; const api = new mw.Api(); const qid = mw.config.get("wgWikibaseItemId"); try { const [pageData, backlinkData] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), api.get({ action: 'query', list: 'backlinks', blfilterredir: 'nonredirects', bllimit: 50, blnamespace: 0, bltitle: mw.config.get("wgPageName"), formatversion: 2 }) ]); const page = pageData.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const currentTitle = mw.config.get("wgTitle"); const cleanTitle = stripDiacritics(currentTitle); const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext); const backLinkCount = backlinkData.query.backlinks.length; if (isOrphanTagged && backLinkCount >= 1) { const $container = $('<div>').addClass('comrade-container comrade-status'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text('Remove orphan tag').on('click', () => performDeorphan()); $header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn)); $container.append($header); appendDismiss($container); } if (cleanTitle !== currentTitle) await checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic"); await checkDomainRedirect(qid, currentTitle); let isHuman = false; let entity = null; if (qid) { const url = new URL("https://www.wikidata.org/w/api.php"); url.search = new URLSearchParams({ action: 'wbgetentities', ids: qid, props: 'claims|labels', languages: 'en', format: 'json', origin: '*' }); const wikiData = await fetch(url).then(r => r.json()); entity = wikiData.entities[qid]; isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5"); } await checkNamingPrecision(currentTitle, isHuman); if (qid && entity) { const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim(); const nameParts = tClean.split(' '); const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext); const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext); const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i); const isOrphan = isOrphanTagged && backLinkCount < 1; const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]); let targetSortName = ""; if (isHuman) { const { givens, surnames } = await fetchWikidataNameLabels(entity); const refName = entity.labels?.en?.value || currentTitle; if (dsMatch) { const currentDS = dsMatch[1].trim(); targetSortName = currentDS; if (currentDS.includes(',')) { let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim()); if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) { firstPart = firstPart.substring(0, firstPart.length - lastPart.length).trim(); targetSortName = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(firstPart, refName)}`; renderSortNudge(targetSortName, "Redundant surname in given name field."); } else if (givens.length > 0 && lastPart.split(' ').length > 1) { const misplacedGiven = lastPart.split(' ').find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase())); if (misplacedGiven) { const newSurname = lastPart.split(' ').filter(p => p !== misplacedGiven).join(' '); targetSortName = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`; renderSortNudge(targetSortName, `Wikidata suggests "${misplacedGiven}" is a given name.`); } } } } else { targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity); const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge'); const $header = $('<div>').addClass('comrade-header'); const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add")); $header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn)); $dsMissing.append($header); appendDismiss($dsMissing); } const longName = getLongNameFromWikitext(wikitext); if (longName && longName.length > tClean.length) { let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity); const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`; await checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext); } if (targetSortName) { let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle; let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name"; if (targetSortName.includes(',')) { const p = targetSortName.split(','); rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`; } await checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name"); } const finalFamilyNames = new Set(); const finalGivenNames = new Set(); surnames.forEach(fn => { if (tClean.toLowerCase().includes(fn.toLowerCase())) { fn.split(' ').forEach(part => finalFamilyNames.add(part)); finalFamilyNames.add(fn); } }); givens.forEach(gn => { if (tClean.toLowerCase().includes(gn.toLowerCase())) finalGivenNames.add(gn); }); if (finalFamilyNames.size === 0 && nameParts.length >= 2) finalFamilyNames.add(nameParts[nameParts.length - 1]); if (finalGivenNames.size === 0 && nameParts.length >= 2) finalGivenNames.add(nameParts[0]); const getPriorityTarget = async (name, type) => { const titles = type === 'surname' ? [`List of people with surname ${name}`, `List of people with the English surname ${name}`, `${name} (surname)`] : [`List of people with given name ${name}`, `${name} (given name)`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); return titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); }; for (const name of finalGivenNames) { if (name.toLowerCase() === primarySurname.toLowerCase()) continue; const target = await getPriorityTarget(name, 'given'); if (target) await checkNameList(name, 'given name', currentTitle, hasShortDesc, isOrphan, target); } for (const name of finalFamilyNames) { const titles = [name, `${name} (surname)`, `List of people with surname ${name}`]; const res = await api.get({ action: 'query', titles: titles.join('|'), formatversion: 2 }); const target = titles.find(t => res.query.pages.find(pg => pg.title === t && !pg.missing)); if (target) await checkNameList(name, 'surname', currentTitle, hasShortDesc, isOrphan, target); } } const oresWait = setInterval(() => { if ($('.article_quality').length) { clearInterval(oresWait); checkQualityConsistency(); } }, 500); setTimeout(() => clearInterval(oresWait), 5000); } } catch (err) { console.error("Comrade error:", err); } } init(); })(); (function() { if (window.IllWill) return; const STRIKEOUT = true; const HIDE = 'hide'; const IllWill = { Version: "1.1.1", Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>', Summary: "Added interlanguage links using IllWill.js", DataApi: "https://www.wikidata.org/w/api.php", LocalLang: mw.config.get("wgPageContentLanguage"), Disambiguator: / \(.*\)$/, SiteLinks: {}, Config: { MaxLanguages: 5, IgnoreScientific: STRIKEOUT, AutoDiff: true }, css: ` #illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; } .ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; } .illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; } .ill-x { width: 35px; text-align: center; } .ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; } #ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; } #ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; } #ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; } #ill-ok:hover { background: #447ff5; } #ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; } #ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } #ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; } #ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; } #ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; } #ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; } .ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; } `, setup() { if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return; mw.loader.addStyleTag(this.css); const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata"); if (link) { link.addEventListener('click', (e) => { e.preventDefault(); this.makePanel(); }); } }, norm(s) { if (!s) return ""; const t = s.trim().replace(/[\s_]+/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); }, async getRedLinks($textbox) { const txt = $textbox.textSelection('getContents') || ""; const wl = txt.match(/\[\[[^[\]\n]+\]\]/g); if (!wl) return { reds: [], txt }; const api = new mw.Api(); const textToParse = wl.join("").replace(/\|[^\]]+/g, ""); try { const data = await api.post({ action: 'parse', text: textToParse, contentmodel: 'wikitext', formatversion: 2 }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.parse.text; const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => { const urlParams = new URLSearchParams(x.href.split('?')[1]); return urlParams.get('title')?.replace(/_/g, " "); }).filter(Boolean); return { reds: [...new Set(reds)], txt }; } catch (e) { return { reds: [], txt }; } }, async makePanel() { const $editform = $('#editform'); const $textbox = $('#wpTextbox1'); $('#illwill').remove(); $editform.hide(); const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform); const { reds, txt } = await this.getRedLinks($textbox); const wtLinks = []; reds.forEach(title => { const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+"); const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig"); const match = txt.match(re); if (match && !wtLinks.some(z => z.title === title)) { wtLinks.push({ link: match[0], title }); } }); if (!wtLinks.length) { const $back = $('<button>Quit</button>').on('click', () => { $wrapper.remove(); $editform.show(); }); $wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back); return; } const $table = $(` <table id="ill-table" class="wikitable"> <caption>IllWill.js (v${this.Version})</caption> <thead> <tr> <th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th> <th>Input link</th> <th>Possible Wikidata matches</th> <th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th> <th>Resulting wikitext</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td> <td colspan="2" id="ill-buttons"> <button id="ill-cancel">Cancel</button> <button id="ill-ok" disabled>Show changes</button> </td> </tr> </tfoot> </table> `); const $tbody = $table.find('tbody'); wtLinks.forEach((item, i) => { $tbody.append(` <tr id="ill-row-${i}" data-index="${i}"> <td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td> <td class="ill-input">${this.escapeHTML(item.link)}</td> <td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td> <td class="ill-sitelinks"></td> <td class="ill-output"></td> </tr> `); }); $wrapper.empty().append($table); const chunkSize = 5; for (let i = 0; i < wtLinks.length; i += chunkSize) { const chunk = wtLinks.slice(i, i + chunkSize); await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx))); } $table.on('click', '#ill-cancel', () => { $editform.show(); $wrapper.remove(); }); $table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox)); $table.on('change', '#ill-select-all', (e) => { const isChecked = $(e.target).is(':checked'); $table.find('.ill-row-check:not(:disabled)').each(function() { $(this).prop('checked', isChecked).trigger('change'); }); }); $table.on('change', '.ill-row-check', (e) => { this.onCheckbox(e); this.updateOkButtonState($table); }); $table.on('change', 'input[type="radio"]', (e) => { this.showSiteLinks(e); }); }, async fetchWikidataOptimized(title, index) { const query = title.replace(this.Disambiguator, ""); const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); const results = data.search; const $cell = $(`#ill-row-${index} .ill-wd-item`); if (!results || !results.length) { $cell.html('<span class="ill-na">(no results)</span>'); return; } const entityIds = results.map(r => r.id); await this.fetchEntities(entityIds, index, $cell); } catch (e) { $(`#ill-row-${index} .ill-wd-item`).text("Search failed."); } }, async fetchEntities(ids, rowIndex, $cell) { const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`; try { const resp = await fetch(url); const data = await resp.json(); $cell.empty(); ids.forEach(qNum => { const entity = data.entities[qNum]; if (!entity) return; const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814"); if (isScientific && this.Config.IgnoreScientific === HIDE) return; const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || ""; const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum; const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT; const $div = $(` <div class="${isStrikeout ? 'illwill-ignore' : ''}"> <input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}> <label for="radio-${qNum}"> <a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>: <span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span> </label> </div> `); $cell.append($div); this.SiteLinks[qNum] = entity.sitelinks || {}; }); } catch (e) { $cell.text("Detail fetch failed."); } }, updateOkButtonState($table) { const $rows = $table.find('.ill-row-check:not(:disabled)'); const $checkedRows = $rows.filter(':checked'); const anyEnabled = $rows.length > 0; $table.find('#ill-ok').prop('disabled', $checkedRows.length === 0); const $master = $table.find('#ill-select-all'); $master.prop('disabled', !anyEnabled); if (anyEnabled && $rows.length === $checkedRows.length) { $master.prop('checked', true); } else { $master.prop('checked', false); } }, async showSiteLinks(e) { const isManualToggling = e.fromCheckbox || false; const qNum = e.target.value; const $row = $(e.target).closest('tr'); const sitelinks = this.SiteLinks[qNum]; const $checkbox = $row.find('.ill-row-check'); const $sitelinkCell = $row.find('.ill-sitelinks'); const $outputCell = $row.find('.ill-output'); const $table = $('#ill-table'); const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6); const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons')); $sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>'); $outputCell.empty(); if (!isManualToggling) { $checkbox.prop('disabled', true).prop('checked', false); } const candidates = validKeys.slice(0, 25); const sizeMap = []; await Promise.all(candidates.map(async (key) => { const lang = key.replace('wiki', '').replace(/_/g, '-'); const title = sitelinks[key].title; const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`; try { const res = await fetch(endpoint).then(r => r.json()); const page = Object.values(res.query.pages)[0]; sizeMap.push({ lang, title, length: page.length || 0 }); } catch (err) { sizeMap.push({ lang, title, length: 0 }); } })); sizeMap.sort((a, b) => b.length - a.length); const topLinks = sizeMap.slice(0, effectiveMax); $sitelinkCell.html(topLinks.map(item => { const kbValue = Math.round(item.length / 1024); return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`; }).join("")); const inputWikitext = $row.find('.ill-input').text(); const match = inputWikitext.match(/\[\[(.*?)\]\]/); if (match) { const parts = match[1].split('|'); const targetPage = parts[0].trim(); const surfaceName = (parts[1] || parts[0]).trim(); const illLangs = topLinks.map(item => { const isSameTitle = this.norm(item.title) === this.norm(targetPage); return `|${item.lang}|${isSameTitle ? '' : item.title}`; }).join(""); const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`; $outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`); } $checkbox.prop('disabled', !topLinks.length); if (!isManualToggling) { $checkbox.prop('checked', !!topLinks.length); } this.updateOkButtonState($table); }, onCheckbox(e) { const $row = $(e.target).closest('tr'); const $radio = $row.find('input[type="radio"]:checked'); const isChecked = $(e.target).is(':checked'); if (isChecked) { if ($radio.length) { this.showSiteLinks({ target: $radio[0], fromCheckbox: true }); } } else { $row.find('.ill-sitelinks, .ill-output').empty(); } }, onOkButton($editform, $wrapper, $textbox) { let text = $textbox.textSelection('getContents'); let changed = false; $wrapper.find('.ill-row-check:checked').each(function() { const $row = $(this).closest('tr'); const input = $row.find('.ill-input').text(); const output = $row.find('.ill-output').text(); if (output) { const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); text = text.replace(new RegExp(escapedInput, "gi"), output); changed = true; } }); if (changed) { $('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary); $textbox.textSelection('setContents', text); $wrapper.remove(); $editform.show(); if (this.Config.AutoDiff) $('#wpDiff').click(); } }, escapeHTML(str) { return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[m]); } }; window.IllWill = IllWill; mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => { $(() => IllWill.setup()); }); })(); (function() { 'use strict'; const config = { enableNS14: true, rootWhitelists: ['Materials'], userWhitelist: "" }; const categoryDataCache = {}; let scanProgress = 0; let totalToScan = 0; async function verifyTreeMembership() { if (!config.enableNS14) return false; const api = new mw.Api(); const currentTitle = mw.config.get("wgPageName").replace(/_/g, ' '); const fullRoots = config.rootWhitelists.concat(config.userWhitelist.split(',').map(s => s.trim()).filter(s => s)).map(cat => (cat.startsWith('Category:') ? cat : `Category:${cat}`)); if (fullRoots.includes(currentTitle)) return true; let categoriesToTable = mw.config.get('wgCategories') || []; let checked = new Set(); while (categoriesToTable.length > 0) { if (categoriesToTable.some(cat => fullRoots.includes('Category:' + cat))) { return true; } categoriesToTable.forEach(cat => checked.add(cat)); try { const res = await api.get({ formatversion: 2, prop: 'categories', titles: categoriesToTable.map(c => 'Category:' + c), cllimit: 'max' }); let nextBatch = []; if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.categories) { page.categories.forEach(parent => { const parentName = parent.title.substring('Category:'.length); if (!checked.has(parentName)) { nextBatch.push(parentName); } }); } }); } categoriesToTable = [...new Set(nextBatch)]; } catch (e) { return false; } } return false; } function getChemboxBlock(wikitext) { const chemboxRegex = /\{\{\s*Chembox[\s_]*(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const match = wikitext.match(chemboxRegex); return match ? match[0] : null; } function getFormulaFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const formulaMatch = chembox.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i); if (formulaMatch) { let rawFormula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim(); rawFormula = rawFormula.split(/\s+/)[0]; return rawFormula.split(',')[0].trim(); } const propRegex = /\{\{\s*Chembox[\s_]+Properties(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const propSection = chembox.match(propRegex); if (propSection) { const content = propSection[0]; const allElements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'D', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', 'Lu', 'Lv', 'Mc', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', 'Ne', 'Nh', 'Ni', 'No', 'Np', 'O', 'Og', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'Ts', 'U', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']; let data = {}; allElements.forEach(el => { const elRegex = new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d*\\.?\\d+)`, 'i'); const elMatch = content.match(elRegex); if (elMatch && elMatch[1] !== "0") { data[el] = elMatch[1] === "1" ? "" : elMatch[1]; } }); const elementsPresent = Object.keys(data); if (elementsPresent.length > 0) { let formulaArray = []; const hasC = "C" in data; if (hasC) { formulaArray.push("C" + data["C"]); if ("H" in data) formulaArray.push("H" + data["H"]); } elementsPresent.filter(el => !(hasC && (el === "C" || el === "H"))).sort().forEach(el => { formulaArray.push(el + data[el]); }); return formulaArray.join(''); } } return null; } function getCASFromWikitext(wikitext) { const chembox = getChemboxBlock(wikitext); if (!chembox) return null; const idRegex = /\{\{\s*Chembox[\s_]+Identifiers(?:[^{}]|\{\{(?:[^{}]|\{\{[^{}]*\}\})*\}\})*\}\}/i; const idSection = chembox.match(idRegex); if (!idSection) return null; const content = idSection[0]; const nbsp = String.fromCharCode(160); const casMatch = content.match(/\|\s*CASNo\d*\b(?!\s*_)\s*=\s*([^|}\n]+)/i); if (casMatch) { let val = casMatch[1].replace(/\{\{[^}]+\}\}/g, "").replace(/\u00AD/g, "").split("&nbsp;").join(""); while (val.indexOf(nbsp) !== -1) val = val.replace(nbsp, ""); while (val.indexOf(" ") !== -1) val = val.replace(" ", ""); const cleanedCas = val.trim(); return /^\d{2,7}-\d{2}-\d$/.test(cleanedCas) ? cleanedCas : null; } return null; } async function getWikidataValue() { const qid = mw.config.get('wgWikidataItemId'); if (!qid) return null; const api = new mw.ForeignApi('https://www.wikidata.org/w/api.php'); const res = await api.get({ action: 'wbgetentities', ids: qid, props: 'claims', format: 'json', formatversion: 2 }); const claims = res.entities[qid].claims; const data = {}; if (claims.P274) data.formula = claims.P274[0].mainsnak.datavalue.value; if (claims.P231) data.cas = claims.P231[0].mainsnak.datavalue.value; return data; } async function startBackgroundCategoryScan() { const api = new mw.Api(); const titles = $('#mw-pages li a').map((i, el) => $(el).attr('title') || $(el).text()).get(); totalToScan = titles.length; if (totalToScan === 0) return; const chunks = arrayChunk(titles, 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', prop: 'revisions', titles: chunk, rvprop: 'content', formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(page => { if (page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const f = getFormulaFromWikitext(wikitext); const c = getCASFromWikitext(wikitext); if (f || c) categoryDataCache[page.title] = { formula: f, cas: c }; }); } scanProgress += chunk.length; $('#caschemassist-cat-btn').text(`Scanning... ${scanProgress}/${totalToScan}`); } $('#caschemassist-cat-btn').text('Show missing CAS/Chem').prop('disabled', false); } async function renderCategoryAudit() { const api = new mw.Api(); const missingCheckTitles = []; Object.values(categoryDataCache).forEach(data => { if (data.formula) missingCheckTitles.push(data.formula); if (data.cas) missingCheckTitles.push(data.cas); }); if (missingCheckTitles.length === 0) { mw.notify("No valid Chembox data found in this category."); return; } const existenceMap = {}; const chunks = arrayChunk([...new Set(missingCheckTitles)], 50); for (const chunk of chunks) { const res = await api.get({ action: 'query', titles: chunk, formatversion: 2 }); if (res.query && res.query.pages) { res.query.pages.forEach(p => { existenceMap[p.title] = !p.missing; }); } } $('#mw-pages li').each(function() { const title = $(this).find('a').attr('title') || $(this).find('a').text().trim(); const data = categoryDataCache[title]; if (!data) return; if (data.formula && !existenceMap[data.formula]) { $(this).append($('<span>').css({ 'color': 'red', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [${data.formula}]`)); } if (data.cas && !existenceMap[data.cas]) { $(this).append($('<span>').css({ 'color': 'darkred', 'font-size': '0.85em', 'margin-left': '8px', 'font-weight': 'bold' }).text(` [CAS: ${data.cas}]`)); } }); $('#caschemassist-cat-btn').text('Audit complete').prop('disabled', true); } async function handleSingleArticle() { const api = new mw.Api(); const currentTitle = mw.config.get("wgTitle"); const [wikiRes, wikidata] = await Promise.all([ api.get({ action: 'query', prop: 'revisions', titles: mw.config.get("wgPageName"), rvprop: 'content', formatversion: 2 }), getWikidataValue() ]); const page = wikiRes.query.pages[0]; if (!page || page.missing || !page.revisions) return; const wikitext = page.revisions[0].content; const formula = getFormulaFromWikitext(wikitext); const cas = getCASFromWikitext(wikitext); if (wikidata) { let mismatches = []; if (formula && wikidata.formula && formula !== wikidata.formula) { mismatches.push(`Formula mismatch: Chembox "${formula}" vs Wikidata "${wikidata.formula}"`); } if (cas && wikidata.cas && cas !== wikidata.cas) { mismatches.push(`CAS mismatch: Chembox "${cas}" vs Wikidata "${wikidata.cas}"`); } if (mismatches.length > 0) { const $warning = $('<div>').css({ 'background': '#fff2f2', 'border': '2px solid #d33', 'padding': '10px', 'margin-bottom': '1em', 'color': '#b32424' }).append($('<strong>').text('⚠️ Data Mismatch Detected: '), mismatches.join(' | ')); $('#mw-content-text').prepend($warning); } } async function checkAndRender(target, label, customWikitext) { const check = await api.get({ action: 'query', titles: target, formatversion: 2 }); if (check.query.pages[0].missing) { const $container = $('<div>').css({ 'background': '#f8f9fa', 'border': '1px solid #a2a9b1', 'padding': '10px', 'margin-bottom': '1em' }); const $btn = $('<button>').text(`Create redirect: ${target}`).click(async function() { await api.postWithEditToken({ action: 'edit', title: target, text: customWikitext, summary: `Creating redirect to [[${currentTitle}]] (${label})`, createonly: true }); mw.notify(`Created redirect: ${target}`); $container.fadeOut(400, function() { $(this).remove(); }); }); $container.append($('<strong>').text(`${label}: `), `Missing redirect. `, $btn); $('#mw-content-text').prepend($container); } } if (formula && formula !== currentTitle) { const formulaWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from chemical formula}}\n}}`; await checkAndRender(formula, "Chemical formula", formulaWikitext); } if (cas && cas !== currentTitle) { const casWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from CAS Registry Number}}\n{{R unprintworthy}}\n}}`; await checkAndRender(cas, "CAS number", casWikitext); } } function arrayChunk(arr, size) { const result = []; for (let i = 0; i < arr.length; i += size) { result.push(arr.slice(i, i + size)); } return result; } $(document).ready(async function() { const ns = mw.config.get("wgNamespaceNumber"); if (ns === 14) { if (await verifyTreeMembership()) { const $catContainer = $('#mw-pages'); if ($catContainer.length) { const $btn = $('<button>').text('Initializing scan...').attr('id', 'caschemassist-cat-btn').addClass('mw-ui-button').css({ 'margin': '0 10px 10px 0', 'float': 'right', 'position': 'relative', 'z-index': '100' }).prop('disabled', true).click(renderCategoryAudit); $catContainer.prepend($btn); startBackgroundCategoryScan(); } } } else if (ns === 0 && mw.config.get("wgAction") === "view") { handleSingleArticle(); } }); })(); //</nowiki> awkfesbpp7d7f5lvkv2qu3bta6rjvni Jupiter 0 102293 739260 620517 2026-04-24T13:23:42Z ~2026-23062-17 73557 showcaptcha 739260 wikitext text/x-wiki {| class="wikitable" |+ !Nom !Liens !Nombre d'occurrences !Changements depuis la semaine dernière |- | Baidu Baike || baike.baidu.com, baike.baidu.hk || 0 || +0 |} this is an edit cp8ytu7opdizu1269ozytcqczoe6bi7 Missing hl 0 118450 739236 463075 2026-04-24T12:10:22Z ~2026-23062-17 73557 showcaptcha 739236 wikitext text/x-wiki Test page ---- Is there an hl up there? this is an edit klfk1p3rbeqm7c36n7ldpgdg234g50k Mwbot-rs/Save 0 122864 739237 738365 2026-04-24T12:10:59Z Mwbot-rs test 52001 Test suite edit 739237 wikitext text/x-wiki It has been 1777032658 seconds since the epoch. mctzluz6d26n25lr0rryn4y57o8wc4l 739239 739237 2026-04-24T12:11:15Z Mwbot-rs test 52001 Test suite edit 739239 wikitext text/x-wiki It has been 1777032674 seconds since the epoch. l0nn6iop5joffi3gz1qlpk2g55eegcz User:SongVĩ.Bot II 2 124239 739271 739207 2026-04-24T17:00:14Z SongVĩ.Bot II 52414 [[User:SongVĩ.Bot II|Task 0]]: Đã 1579 ngày... 739271 wikitext text/x-wiki Cập nhật lần cuối: 25-04-2026 Đã 1579 ngày... 09jkj7kcc3yq2e157rjyt8hwsnc0c2a Wikipedia:Village pump/topic list 4 146208 739267 739215 2026-04-24T15:23:04Z Cewbot 33876 [[User:Cewbot/log/20170915/configuration|Generate topic list: 6 topics]] 739267 wikitext text/x-wiki <!-- This page is auto-generated by bot. Please contact the bot operator to improve the tool. --> {| class="wikitable sortable mw-collapsible" style="float:left;" |- ! data-sort-type="number" style="font-weight: normal;" | <small>#</small> !! 💭 Title !! <span title="Count of comments">💬</span> !! <span title="Count of peoples in discussion">👥</span> !! 🙋 Last editor !! data-sort-type="isoDate" | <span title="Date/Time">🕒 <small>(UTC)</small></span> |- | style="text-align: right;" | 1 | [[Wikipedia:Village pump#Script|Script]] | style="text-align: right;" | 8 | style="text-align: right;" | 5 | style="background-color: #bbb;" | [[User:LuniZunie|LuniZunie]] | style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-11-09T16:47:00.000Z" | 2025-11-09 <span style="color: blue;">16:47</span> |- | style="text-align: right;" | 2 | [[Wikipedia:Village pump#Report_concerning_Tanbiruzzammn|Report concerning Tanbiruzzammn]] | style="text-align: right;" | 2 | style="text-align: right;" | 2 | style="background-color: #bbb;" | [[User:Barras|Barras]] | style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2025-12-09T21:45:00.000Z" | 2025-12-09 <span style="color: blue;">21:45</span> |- | style="text-align: right;" | 3 | [[Wikipedia:Village pump#Report_concerning_Bucheon|Report concerning Bucheon]] | style="text-align: right;background-color: #fcc;" | 1 | style="text-align: right;background-color: #fcc;" | 1 | style="background-color: #bbb;" | [[User:PieWriter|PieWriter]] | style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-02-19T10:25:00.000Z" | 2026-02-19 <span style="color: blue;">10:25</span> |- | style="text-align: right;" | 4 | [[Wikipedia:Village pump#Versions_and_dates|Versions and dates]] | style="text-align: right;" | 2 | style="text-align: right;background-color: #fcc;" | 1 | style="background-color: #bbb;" | [[Special:Contributions/~2026-13668-13|<span style="color: #c20;">~2026-13668-13</span>]] | style="background-color: #bbb;" data-sort-type="isoDate" data-sort-value="2026-03-03T06:17:00.000Z" | 2026-03-03 <span style="color: blue;">06:17</span> |- | style="text-align: right;" | 5 | style="max-width: 24em" | <small>[[Wikipedia:Village pump#Upcoming_Wikimedia_Café_meetup_regarding_the_the_2026-2027_Wikimedia_Foundation_Annual_Plan|Upcoming Wikimedia Café meetup regarding the the 2026-2027 Wikimedia Foundation Annual Plan]]</small> | style="text-align: right;background-color: #fcc;" | 1 | style="text-align: right;background-color: #fcc;" | 1 | style="background-color: #ddd;" | [[User:Pine|Pine]] | style="background-color: #ddd;" data-sort-type="isoDate" data-sort-value="2026-03-30T03:46:00.000Z" | 2026-03-30 <span style="color: blue;">03:46</span> |- | style="text-align: right;" | 6 | [[Wikipedia:Village pump#Changes_to_electionadmin_userright|Changes to electionadmin userright]] | style="text-align: right;" | 4 | style="text-align: right;" | 4 | [[User:Chaotic Enby|Chaotic Enby]] | data-sort-type="isoDate" data-sort-value="2026-04-23T15:22:00.000Z" | 2026-04-23 <span style="color: blue;">15:22</span> |} {| class="wikitable mw-collapsible mw-collapsed" style="float: left; margin-left: .5em;;{{#if:{{{no_time_legend|}}}|display:none;|}}" ! title="From the latest bot edit" | Legend |- | style="background-color: #efe;" | * In the last hour |- | style="background-color: #eef;" | * In the last day |- | | * In the last week |- | style="background-color: #ddd;" | * In the last month |- | style="background-color: #bbb;" | * More than one month |- ! Manual settings |- | style="max-width: 12em;" | <small>When exceptions occur,<br />please check [[User:Cewbot/log/20170915/configuration|the setting]] first.</small> |- |} {{Clear}} tlxg3wcsu66pp2w0yqmp59tgres118k Vorlage:Coordinate/to DMS 0 148892 739294 564218 2026-04-25T00:03:43Z InternetArchiveBot 34092 Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5 739294 wikitext text/x-wiki {{Infobox Gemeinde in Deutschland |Art = Stadt |Wappen = Reinbek Wappen.svg |Breitengrad = 53.510211 |Längengrad = 10.250318 |Lageplan = Reinbek in OD.svg |Bundesland = Schleswig-Holstein |Kreis = Stormarn |Höhe = 27 |PLZ = 21465 |Vorwahl = 040, 04104 |Gemeindeschlüssel = 01062060 |LOCODE = DE REI |Gliederung = [[Liste der Bezirke und Stadtteile Reinbeks|6 statistische Bezirke und 22 Stadtteile]] |Straße = Hamburger Straße 5–7 |Website = [https://www.reinbek.de/ www.reinbek.de] |Bürgermeister = [[Björn Warmer]] |Partei = SPD }} '''Reinbek''' ([[Niederdeutsche Sprache|niederdeutsch]] ''Reinbeek''), in der südlichen [[Geest]] [[Schleswig-Holstein]]s gelegen, ist mit etwa 28.000 Einwohnern die zweitgrößte Stadt im [[Kreis Stormarn]]. Die [[Mittelstadt]] liegt im östlichen Ballungsraum [[Hamburg]]s und gehört zur [[Metropolregion Hamburg]]. == Geografie == Die Ost- und Südgrenze Reinbeks bildet die zum ''Mühlenteich'' aufgestaute, naturgeschützte [[Bille]]. Die zwischen den Ortsteilen liegenden Flächen werden zum Teil noch landwirtschaftlich genutzt. Geprägt vom angrenzenden [[Sachsenwald]], bietet Reinbek ein grünes, erholsames Stadtbild. Ein Großteil der Stadt ist mit Einzelhäusern bebaut, das Gebiet rings um den Täby-Platz und das Paul Luckow-Stadion besteht zum großen Teil aus mehrstöckigen Mietshäusern, die im Stil der 1960er Jahre erbaut wurden. Das höchste von ihnen, das Sachsenwald-Hochhaus mit 20 Stockwerken, befindet sich in der Hamburger Straße. Zu Reinbek gehören die Stadtteile ''Alt-Reinbek'', [[Hinschendorf]], [[Schönningstedt]], [[Neuschönningstedt]], [[Ohe (Reinbek)|Ohe]] mit [[Büchsenschinken]] und das jüngere Neubaugebiet [[Krabbenkamp]] (→[[Liste der Bezirke und Stadtteile Reinbeks]]). Direkt angrenzend liegen die Hamburger Stadtteile [[Hamburg-Bergedorf|Bergedorf]] und [[Hamburg-Lohbrügge|Lohbrügge]]. == Geschichte == {{Hauptartikel|Geschichte der Stadt Reinbek}} Von der Besiedlung des heutigen Reinbeker Gebietes in bereits vorgeschichtlicher Zeit zeugen zahlreiche [[Hügelgrab|Hügelgräber]]. Die erste urkundlich überlieferte Erwähnung Reinbeks datiert allerdings erst auf das Jahr 1238 und geht auf die Gründung des gleichnamigen [[Zisterzienserinnen]]klosters (siehe [[Kloster Reinbek]]) zurück. Die ältesten bekannten Schreibformen des Ortsnamens sind ''(ville) Reinebec'' (1238), ''(in) Reynebeke'' (1309 und 1350), ''(to deme) Reynenbeke'' (1400) und ''(tome) Rynenbeke'' (1466); der Name wird als Kompositum aus dem Grundwort ''bek'' für „Bach“ und dem Adjektiv „rein“ als Bestimmungswort gedeutet.<ref>[[Wolfgang Laur]]: ''Historisches Ortsnamenlexikon von Schleswig-Holstein'', 2.&nbsp;Aufl., S.&nbsp;538.</ref> Nach der Zerstörung des Klosters (1534) gewann der Ort erst mit dem Bau der Schlossanlage (1572) wieder an Bedeutung. [[Datei:Mühlenteich und Reinbeker Schloss im Winter.jpg|mini|Der Mühlenteich und das Reinbeker Schloss im Winter]] Die Ansiedlung von Handwerkern im späten 18. Jahrhundert brachte endlich wirtschaftliches Wachstum. Einen entscheidenden Impuls für die Entwicklung des Ortes gab jedoch der Bau der Eisenbahnstrecke zwischen Hamburg und [[Berlin]] (1846): Reinbek wurde vorübergehend zum Kurort und beliebten Ausflugsziel. Die alte Schreibweise „Reinbeck“ wurde am 1. September 1877 durch eine Anordnung über die einheitliche Regelung der Schreibweise für Ortsnamen von der Provinzialregierung in Schleswig in „Reinbek“ geändert. Zum Ende des [[Zweiter Weltkrieg|Zweiten Weltkrieges]] wurde Deutschland schrittweise besetzt. Am 3. Mai 1945 besetzten britischen Truppen auch Reinbek, das benachbarte [[Glinde]] sowie den letzten Teil des noch unbesetzten [[Kreis Stormarn|Stormarns]].<ref>[[Hamburger Abendblatt]]: [http://www.abendblatt.de/region/stormarn/article205288543/Vor-siebzig-Jahren-kapitulierte-die-Stadt-Ahrensburg.html Kriegsende. Vor siebzig Jahren kapitulierte die Stadt Ahrensburg], vom: 2. Mai 2015; abgerufen am: 31. Mai 2017</ref> Des Weiteren begann am Nachmittag des Tages auch die Besetzung [[Hamburg]]s, die zuvor in der [[Villa Möllering]] bei [[Lüneburg]] vereinbart worden war. Einen Tag später unterschrieb zudem [[Hans-Georg von Friedeburg]] im Auftrag des letzten [[Reichspräsident]]en [[Karl Dönitz]], der sich zuvor mit der [[Regierung Dönitz|letzten Reichsregierung]] in den [[Sonderbereich Mürwik]] abgesetzt hatte, die [[Teilkapitulation der Wehrmacht für Nordwestdeutschland, Dänemark und die Niederlande]].<ref>[https://web.archive.org/web/20131104080252/http://www.volksbund.de/fileadmin/redaktion/BereichInfo/Textsammlungen/Ausstellungen/0400_ausstellung_timeloberg/Timeloberg.pdf Die Kapitulation auf dem Timeloberg] (PDF, 16. S.; 455&nbsp;kB)</ref> Die [[Bedingungslose Kapitulation der Wehrmacht]] folgte am 8. Mai 1945. Zum Kriegsende erlebte Reinbek einen verstärkten Zuzug von Flüchtlingen und durch Kriegseinwirkung obdachlos gewordenen Hamburgern. Seit den 1960er Jahren wurden mehrere Gewerbegebiete erschlossen und erweitert. Am 28. Juni 1952 erhielt Reinbek das [[Stadtrecht]]. Am 1. Januar 1974 wurden die Gemeinde [[Schönningstedt]] (mit [[Neuschönningstedt]] und [[Ohe (Reinbek)|Ohe]]) sowie ein Teil der Gemeinde Glinde mit damals etwa 100 Einwohnern und ein Teil der aufgelösten Gemeinde Stemwarde eingegliedert.<ref>{{Literatur | Herausgeber = Statistisches Bundesamt | Titel = Historisches Gemeindeverzeichnis für die Bundesrepublik Deutschland. Namens-, Grenz- und Schlüsselnummernänderungen bei Gemeinden, Kreisen und Regierungsbezirken vom 27. Mai 1970 bis 31. Dezember 1982 | Jahr = 1983 | Verlag = W. Kohlhammer GmbH | Ort = Stuttgart/Mainz | ISBN = 3-17-003263-1 | Seiten = 186}}</ref> Im Jahre 1978 kam das bisher landwirtschaftlich genutzte Gebiet Krabbenkamp, das vormals zu Schönningstedt gehörte, als weiterer Stadtteil hinzu. == Religion == Reinbek gehörte ursprünglich zum [[Kirchspiel]] [[Kirchsteinbek|Steinbek]], bis es 1894 zu einer eigenständigen [[Evangelisch-lutherische Kirchen|evangelisch-lutherischen]] [[Kirchengemeinde]] wurde. Die [[Neogotik|neogotische]] Kirche (heute Maria-Magdalenen-Kirche) wurde 1901 errichtet. 1908 gründete sich die [[Römisch-katholische Kirche|römisch-katholische]] Kirchengemeinde, die 1953 die Herz-Jesu-Kirche erbauen ließ. In Reinbek sind 44 % der Bevölkerung evangelisch und 9 % katholisch, 26 % gehören anderen Konfessionen an, 22 % sind ohne Religionszugehörigkeit. Die bedeutendsten Gemeinden der Stadt sind: * [[Ansgar von Bremen|Ansgar]]-Kirchengemeinde Schönningstedt-Ohe (evangelisch-lutherisch) * Kirchengemeinde [[Gethsemane]] Neuschönningstedt (evangelisch-lutherisch) * [[Maria-Magdalenen-Kirche (Reinbek)|Maria-Magdalenen-Kirche]] (evangelisch-lutherisch) * [[Nathan-Söderblom-Kirche (Reinbek)|Nathan-Söderblom-Kirche]] (evangelisch-lutherisch) * [[Herz-Jesu-Kirche (Reinbek)|Herz-Jesu]]-Gemeinde (römisch-katholisch) * [[Evangelisch-Freikirchliche Gemeinde]] ([[Baptisten]]) == Politik == === Stadtvertretung === Die letzten drei Kommunalwahlen [[Kommunalwahlen in Schleswig-Holstein 2018|am 6. Mai 2018]], [[Kommunalwahlen in Schleswig-Holstein 2013|am 26. Mai 2013]]<ref>{{Webarchiv|url=http://www.reinbek.de/wahlen/KW2013.html |wayback=20160304064515 |text=Archivierte Kopie}}</ref> und [[Kommunalwahlen in Schleswig-Holstein 2008|am 25. Mai 2008]]<ref>{{Cite web |title=Archived copy |url=http://www.reinbek.de/files/Wahlen/GKW_25052008.pdf#page=10 |access-date=2023-02-21 |archive-date=2016-03-04 |archive-url=https://web.archive.org/web/20160304074748/http://www.reinbek.de/files/Wahlen/GKW_25052008.pdf#page=10 }}</ref> führten zu folgenden Ergebnissen: {| class="wikitable" | colspan="2" | '''Parteien und Wählergemeinschaften''' | align="center" | '''%<br />2018''' | align="center" | '''Sitze<br />2018''' | align="center" | '''%<br />2013''' | align="center" | '''Sitze<br />2013''' | align="center" | '''%<br />2008''' | align="center" | '''Sitze<br />2008''' | rowspan="12" |{{Wahldiagramm | LAND = DE | TITEL = Kommunalwahl 2018 | JAHRALT = 2013 | JAHRNEU = 2018 | GUV = ja | PARTEI1 = CDU | ERGEBNIS1 = 27.5 | ERGEBNISALT1 = 30.7 | PARTEI3 = SPD | ERGEBNIS3 = 20.7 | ERGEBNISALT3 = 26.6 | PARTEI2 = GRÜNE | ERGEBNIS2 = 22.1 | ERGEBNISALT2 = 17.2 | PARTEI5 = Forum21 | ERGEBNIS5 = 11.0 | ERGEBNISALT5 = 13.2 | FARBE5 = 0000FF | PARTEI4 = FDP | ERGEBNIS4 = 17.0 | ERGEBNISALT4 = 10.9 | PARTEI6 = [[Klaus-Peter Puls|Puls]] | ERGEBNIS6 = 1.7 | ERGEBNISALT6 = 1.5 | FARBE6 = CCCCCC }} | rowspan="12" |{{Sitzverteilung | Land = DE | Überschrift = Sitzverteilung in der Stadtverordnetenversammlung | SPD|Grüne|Forum21|Puls|FDP|CDU| | Legende = ja | SPD = 6 | Grüne = 7 | Forum21 = 3 | Puls = 1 | FDP = 5 | CDU = 9 | Forum21 Farbe = 0000FF | Puls Farbe = CCCCCC | Puls Link = [[Klaus-Peter Puls|Puls]] }} |- style="text-align:right" | style="text-align:left" | CDU | style="text-align:left" | [[Christlich Demokratische Union Deutschlands]] | 27,5 | 9 | 30,7 | 10 | 33,6 | 13 |- style="text-align:right" | style="text-align:left" | SPD | style="text-align:left" | [[Sozialdemokratische Partei Deutschlands]] | 20,7 | 6 | 26,6 | 8 | 24,3 | 9 |- style="text-align:right" | style="text-align:left" | GRÜNE | style="text-align:left" | [[Bündnis 90/Die Grünen]] | 22,1 | 7 | 17,2 | 5 | 15,4 | 5 |- style="text-align:right" | Forum21 | Forum21 | 11,0 | 3 | 13,2 | 4 | 13,0 | 4 |- style="text-align:right" | style="text-align:left" | FDP | style="text-align:left" | [[Freie Demokratische Partei]] | 17,0 | 5 | 10,9 | 3 | 13,8 | 5 |- style="text-align:right" | style="text-align:left" | Puls | style="text-align:left" | Einzelbewerber [[Klaus-Peter Puls]]<ref>http://www.bergedorfer-zeitung.de/printarchiv/reinbek/article188444/Kommunalwahl-am-26-Mai-2013-Vorstellung-der-Reinbeker-Kandidaten-Wahlkreis-13.html</ref><ref>http://www.abendblatt.de/region/stormarn/article115057641/Klaus-Peter-Puls-tritt-aus-der-SPD-aus.html</ref> | 1,7 | 1 | 1,5 | 1 | — | — |- class="hintergrundfarbe5" style="text-align:right" | colspan="2" style="text-align:left" | '''gesamt''' | '''100,0''' | '''31''' | '''100,0''' | '''31''' | '''100,0''' | '''36''' |- ! colspan="2" style="text-align:left" | Wahlbeteiligung in % ! colspan="2" | ! colspan="2" | 45,5 ! colspan="2" | |} [[Datei:Reinbeker Rathaus.JPG|mini|Reinbeker Rathaus]] === Bürgermeister === <!-- Amtsvorgänger bitte mit Amtszeit und Partei nachtragen --> {| class="wikitable" ! colspan="2" | Amtszeit !! rowspan="2" | Name |- ! von !! bis |- | 17. Februar 1931 || 13. September 1945 | Eduard Claußen (NSDAP)<ref>[https://www.museumsverein-reinbek.de/wp-content/uploads/2018/06/Eduard-Claussen.pdf ''Eduard Claußen''], museumsverein-reinbek.de</ref><ref>Claußen half trotz seiner NSDAP-Zugehörigkeit im Rahmen seiner Möglichkeiten mehreren jüdischen Einwohnern und sorgte dafür, dass Reinbek kampflos den Engländern übergeben wurde, vgl. dazu: Detlev Landgrebe: Kückallee 37. Eine Kindheit am Rande des Holocaust. Rheinbach 2009, ISBN 978-3-87062-104-9, S. 163, S. 167 u.&nbsp;a.</ref> |- | 15. Dezember 1945 || 31. Januar 1946 || Wilhelm Kleist |- | 1. Februar 1946 || 22. September 1946 || Carl Dobbertin |- | 23. September 1946 || 11. November 1948 || Alwin Hemken |- | 12. November 1948 || 28. April 1950 || Carl Dobbertin |- | 28. April 1950 || 31. März 1951 || Wilhelm Kleist |- | 1. April 1951 || 31. Dezember 1971 || [[Hermann Körner]] |- | 1. Januar 1972 || 31. Januar 1990 || Günther Kock |- | 1. Februar 1990 || 31. Januar 1996 || Manfred Neumann |- | 1. September 1996 || 31. August 2008 || Detlef Palm |- | 1. September 2008 || 31. August 2014 || [[Axel Bärendorf]] |- | 1. September 2014 || || [[Björn Warmer]] |} === Wappen === [[Blasonierung]]: „In Rot ein silberner Wellenbalken, begleitet von drei im Dreipass mit den Stielen einander zugekehrten Eichenblättern, und zwar zwei oben und einem unten.“<ref>[{{SH-Wappenrolle|262|Stadt Reinbek, Kreis Stormarn|nurLink=1}} Kommunale Wappenrolle Schleswig-Holstein]</ref> Die Blätter, in ihrer Anordnung an das Wappen der Familie [[Bismarck (Adelsgeschlecht)|Bismarck]] angelehnt, versteht man als Symbole für den [[Sachsenwald]], während das Band für die [[Bille]] steht. Eine ähnliche Symbolik findet sich auf den Wappen der Nachbarorte [[Wohltorf]] und [[Aumühle]]; die Farben Rot und Weiß entsprechen den Wappen [[Holstein]]s und [[Kreis Stormarn|Stormarns]]. Das Wappen wurde 1935 genehmigt. === Städtepartnerschaften === * 1956–2011: Städtefreundschaft mit [[Täby]] ([[Schweden]]). Der Marktplatz in Reinbek-Klosterbergen, der ''Täbyplatz'', wurde nach der Partnerstadt benannt. * Seit 1961: Städtefreundschaft mit [[Königslutter am Elm]] ([[Niedersachsen]]). * Seit 1974: Patenschaft zwischen der [[Freiwillige Feuerwehr|Freiwilligen Feuerwehr]] Ohe und der Gemeinde [[Padasjoki]] ([[Finnland]]). * Seit 1999: Städtepartnerschaft mit [[Koło]] ([[Polen]]). == Kultur und Sehenswürdigkeiten == [[Datei:Das Reinbeker Schloss.jpg|mini|Reinbeker Schloss]] [[Datei:Museum Rade.JPG|mini|Ehemaliges Museum Rade]] === Theater, Kino und Museen === * Das Kultur- und Kongresszentrum ''Sachsenwald-Forum'' bietet ein wechselndes Programm von Tournee- und Privattheatern. * Der Filmring Reinbek e.&nbsp;V. führt ehrenamtlich monatlich eine Kinoveranstaltung in der Nathan-Söderblom-Kirche durch. * Das gegenüber vom Schloss gelegene ''Museum Rade'' stellte die Sammlung volkstümlicher Kunst des Hamburger Schriftstellers und Kunstsammlers [[Rolf Italiaander]] aus. Seit Sommer 2017 ist das Museum dauerhaft geschlossen, die Sammlung wurde Ende 2018 ins Schloss Reinbek verlegt [[Datei:Reinbek Sankt Maria Magdalena.JPG|hochkant|mini|Maria-Magdalenen-Kirche]] === Bauwerke === [[Datei:Reinbek dänenbrücke P4070041.JPG|mini|„Dänenbrücke“ von 1793]] [[Datei:RK 1810 P1650787 Bismarcksäule Friedrichsruh.jpg|mini|Bismarcksäule]] Verschont von den Zerstörungswellen des Zweiten Weltkrieges, zeigt Reinbeks Stadtarchitektur ein kontinuierliches Bild durch die Epochen norddeutscher Baugeschichte, angefangen bei der niederländischen Renaissance und alten Bauernkaten, über [[großbürger]]liche Villen der Kaiserzeit, Klinkerexpressionismus der Weimarer Republik und Wohngroßbauten der 1970er bis hin zu einer eher behutsamen Architektur der 1990er Jahre. * Ältestes und bedeutendstes Bauwerk ist das [[Schloss Reinbek]] im Stil der [[Niederländische Renaissance|Niederländischen Renaissance]]. [[Adolf I. (Schleswig-Holstein-Gottorf)|Herzog Adolf I. von Gottorf]] ließ das Schloss zwischen 1572 und 1576 in seiner heute noch vorhandenen Form errichten. Zunächst Nebenwohnsitz des Landesherren, war das Schloss in dänischer Zeit Residenz des Amtmannes und später kurzzeitig der Sitz des Landratsamtes für den [[Kreis Stormarn]]. Heute steht das originalgetreu restaurierte Gebäude für öffentliche Nutzung zur Verfügung. * Über die 1793 erbaute ''Dänenbrücke'', in unmittelbarer Nähe zum Schloss, verlief einst der Verkehr zwischen dem dänischen Amt Reinbek und dem [[Herzogtum Sachsen-Lauenburg]]. * Die ''Schönningstedter Mühle'', erbaut 1886, wurde seit der Stilllegung (1968) als Gaststätte betrieben. Sie wurde durch einen Brand (1991) vollständig zerstört. Sie wurde durch eine andere am Ursprungsort abgebaute auf den Grundmauern der Alten Mühle neu errichtet. * Die [[Bismarcksäule (Friedrichsruh)|Bismarcksäule]] auf dem ''Hammelsberg'' zwischen den Ortsteilen Krabbenkamp und Schönningstedt, in der Nähe des ehemaligen bismarckschen Guts Schönau, wurde 1903 fertiggestellt. Das 19 Meter hohe Monument entspricht dem üblichen Bismarcksäulen-Typus eines Feuerturmes, den [[Wilhelm Kreis]] 1898 entworfen hatte, und wurde aus Mitteln der deutschen Studentenschaft finanziert. Der Turm steht seit 1989 unter Denkmalschutz. * In Reinbek gibt es [[Liste der Stolpersteine in Reinbek|sieben Stolpersteine]] zur Erinnerung an Opfer des [[Nationalsozialismus]].<ref>[http://www.akens.org/akens/texte/stolpersteine/Stolpersteineliste.htm#Reinbek Stolpersteine: Reinbek]</ref> In der [[Liste der Kulturdenkmale in Reinbek]] stehen die in der Denkmalliste des Landes Schleswig-Holstein eingetragenen Kulturdenkmale. === Grünflächen und Naherholung === * Die Wald- und Wiesenlandschaft in und um Reinbek sowie der Schlosspark laden zum Spazieren, Wandern und Radfahren ein. Auf der Bille und auf dem Mühlenteich werden Kanufahrten veranstaltet. * Jährlich wird in Reinbek auf dem Täbyplatz oder am ''Waldhaus'' im Sommer oder im Herbst die sogenannte „Reinbeker Sommersause“ bzw. „Reinbeker Herbstsause“ gefeiert. Bei diesen Festen treten unter anderem regionale Musiker und Coverbands auf. === Sport === * Das ''Freizeitbad Reinbek'' und der angrenzende ''Sport-Park Reinbek'' bieten neben einem Hallenbad mit Außenschwimmbecken auch eine Sauna und verschiedene Sportprogramme an. * Die [[TSV Reinbek]] und der [[FC Voran Ohe]] bieten verschiedene Sportarten an. == Wirtschaft, Infrastruktur, öffentliche Einrichtungen == === Unternehmen === Reinbek zeichnet sich durch eine vielfältige, vorwiegend klein- und mittelständische Wirtschaftsstruktur aus. Zahlreiche bedeutende Firmen hatten bzw. haben hier ihren Sitz, wie zum Beispiel der [[Rowohlt Verlag]] (von 1960 bis März 2019), E.&nbsp;Michaelis & Co. – Papiergroßhandel, [[Almirall]] Almirall Hermal und [[Dermapharm|Allergopharma]] (die seit Mai 2021 an der Herstellung des Impfstoffs von [[Biontech]] beteiligt sind)<ref>[https://www.ndr.de/nachrichten/schleswig-holstein/coronavirus/Corona-Biontech-Impfstoff-kommt-jetzt-auch-aus-Reinbek,spahn290.html ''Corona: Biontech-Impfstoff kommt jetzt auch aus Reinbek''] {{Webarchive|url=https://web.archive.org/web/20220328133934/https://www.ndr.de/nachrichten/schleswig-holstein/coronavirus/Corona-Biontech-Impfstoff-kommt-jetzt-auch-aus-Reinbek,spahn290.html |date=2022-03-28 }}, ndr.de, 30. April 2021</ref>, [[Fürst-Bismarck-Quelle]], Grossmann-Feinkost, [[Kahl Gruppe|Amandus Kahl]] (Neuhaus Neotec), Peek&nbsp;&&nbsp;Cloppenburg (Verteilzentrum) und Lutz Aufzüge (Maschinen- und Anlagentechnik), Wollenhaupt (Teehandel). Ein weiterer großer Arbeitgeber ist das Krankenhaus Reinbek St. Adolf-Stift (Gesundheitswesen). Anfang der 1960er Jahre wurde das gemeinsame Gewerbegebiet Reinbek-[[Glinde]] erschlossen. Seitdem erfolgten immer wieder Erweiterungen und Neuausweisungen von Gewerbeflächen. Zuletzt wurde das Gewerbegebiet Haidland vermarktet (ca. 22&nbsp;ha): bis 2018 sind dort mehr als 30 Firmen angesiedelt worden, dadurch wurden 1200 Arbeitsplätze gesichert und ca. 400 neu geschaffen. Geplant ist die Erweiterung des Gewerbegebietes. Die wirtschaftliche Dynamik Reinbeks zeigt sich unter anderem in der Entwicklung der Gewerbebetriebe: deren Zahl stieg auf 2532 Betriebe (31. August 2018). Auch die positiven Arbeitsmarktdaten sind ein Beweis für die Besonderheit des Standortes. Im Geschäftsstellenbezirk der Arbeitsagentur Bad Oldesloe wird der Bezirk Reinbek mit einer der niedrigsten Arbeitslosenquoten aufgeführt, vergleichbar mit denen süddeutscher Wirtschaftsregionen. In der Region Südstormarn liegen einige der Kommunen mit der höchsten Kaufkraft in Deutschland. Auch Reinbek lag im Jahr 2017 mit einer Kaufkraftkennziffer von 118 über dem Durchschnitt (CIMA Lübeck, Jahresbericht interkommunales Einzelhandelsforum 2017). Reinbek ist perspektivisch weiter ein dynamischer Wirtschaftsstandort mit einer hohen Gewerbeflächennachfrage und steigenden Gewerbesteuereinnahmen, u.&nbsp;a. wegen der verkehrsgünstigen zentralen Lage in der Metropolregion direkt benachbart der Weltstadt Hamburg. Die Arbeitsplatzzentralität ist mit einem knapp 80-%-Anteil an den Beschäftigten hoch. === Öffentliche Einrichtungen === Reinbek ist Sitz eines [[Amtsgericht]]s. === Bildung === In Reinbek gibt es vier [[Grundschule]]n, eine [[Gemeinschaftsschule mit Oberstufe]] (mit auslaufenden Haupt- und Realschulklassen) und ein [[Sachsenwaldschule Gymnasium Reinbek|Gymnasium]]. Außerdem gibt es eine [[Förderschule (Deutschland)|Förderschule]]. Gemeinschaftsschule und Förderschule sind zum Schulzentrum Mühlenredder zusammengefasst. Die ''Volkshochschule Sachsenwald'' hat ein umfangreiches Angebot an Kursen verschiedener Fachrichtungen und deckt auch das Angebot für die Nachbargemeinde [[Wentorf bei Hamburg|Wentorf]] mit ab. Die meisten Kurse finden im eigenen, gut ausgestatteten Haus mitten in Reinbek statt. Die ''Reinbeker Stadtbibliothek'' bietet ein breit gefächertes Angebot aus alten wie neuen Medien und unterhält einen ständigen Bücherflohmarkt aus gespendeten und ausgemusterten Büchern. Seit 1989 besteht der [[Museumsverein Reinbek|Geschichts- und Museumsverein Reinbek e.&nbsp;V.]] === Verkehr === [[Datei:Reinbeker Bahnhof.jpg|mini|Der Reinbeker Bahnhof]] Reinbek liegt in der [[Metropolregion Hamburg]]. Von Reinbek ist die Hamburger Innenstadt mit der [[S-Bahn Hamburg|S-Bahn-Linie]] S&nbsp;21 in 25&nbsp;Minuten zu erreichen. Die S-Bahn verbindet Reinbek mit den Nachbarorten [[Wohltorf]] und [[Bahnhof Aumühle|Aumühle]], innerhalb Reinbeks fahren mehrere Buslinien, die von den zum [[Hamburger Verkehrsverbund|HVV]] gehörenden [[VHH PVG Unternehmensgruppe|VHH]] betrieben werden. Die Fernverkehrsstraßen [[Bundesstraße 5|B&nbsp;5]], [[Bundesautobahn 24|A&nbsp;24]] und [[Bundesautobahn 1|A&nbsp;1]] führen in die Hamburger Innenstadt bzw. in Richtung [[Berlin]], [[Lübeck]] und [[Bremen]]. Der nächstgelegene Fernbahnhof ist [[Bahnhof Hamburg-Bergedorf|Hamburg-Bergedorf]], die [[Bahnstrecke Hamburg–Berlin]] durchquert die Stadt ohne Halt parallel zur S-Bahn. == Persönlichkeiten == === Ehrenbürger === <!-- chronologisch nach Geburtsdatum geordnet --> {{Mehrspaltige Liste|liste= * [[Paul Lingens]] (1895–1976), Stadtverordneter der CDU, Bürgervorsteher * Karl Meißner (1912–2010), Stadtverordneter der SPD, Bürgervorsteher * [[Georges-Arthur Goldschmidt]] (* 1928), französisch-deutscher Schriftsteller, Essayist und Übersetzer * Lothar Zug (1928–2020), Stadtverordneter der CDU, Bürgervorsteher * Helmut Schomann (1932–2009), Stadtverordneter der SPD, Bürgervorsteher }} === Söhne und Töchter der Stadt === <!-- chronologisch nach Geburtsdatum geordnet --> {{Mehrspaltige Liste|liste= * [[Minna Specht]] (1879–1961), Pädagogin und Sozialistin * [[Wilhelm Bisse]] (1881–1946), Reichstagsabgeordneter der NSDAP * [[Horst Seifart]] (1916–2004), Journalist und Fernseh-Regisseur * [[Donat de Chapeaurouge]] (1925–2019), Kunsthistoriker * [[Georges-Arthur Goldschmidt]] (* 1928), französisch-deutscher Schriftsteller, Essayist und Übersetzer * Helmut Schomann (1932–2009), Politiker, Ehrenbürger und Träger des Bundesverdienstkreuzes * [[Hartmut Berg]] (* 1936), Wirtschaftswissenschaftler * [[Ekkehard Wachmann]] (* 1937), Entomologe * [[Wittko Francke]] (1940–2020), Chemiker * [[Hans Klapdor-Kleingrothaus]] (* 1942), Physiker * [[Klaus-Peter Puls]] (* 1943), Politiker * [[Albert Maringer]] (* 1945), Manager * [[Wolfgang Seifert (Japanologe)|Wolfgang Seifert]] (* 1946), Japanologe * [[Claus Peter Ortlieb]] (1947–2019), Mathematiker * [[Eckart Modrow]] (* 1948), Pädagoge und Sachbuchautor * [[Johannes Spallek]] (* 1948), Archivar und Kulturreferent * [[Christine Christ-von Wedel]] (* 1948), Historikerin * [[Dieter Matz]] (* 1948), Sportjournalist * [[Angela Sommer-Bodenburg]] (* 1948), Kinderbuchautorin und Malerin; bekannt wurde sie durch ihre Bücher über den ''Kleinen Vampir'' * [[Christel Hüttemann]] (* 1949), Trägerin des Bundesverdienstkreuzes * [[Mathias Nolte]] (* 1952), Buchautor und Journalist * [[Mathias Petersen]] (* 1955), Politiker * [[Harald Lemke (Politiker, 1956)|Harald Lemke]] (* 1956), Staatssekretär * [[Norbert Meier]] (* 1958), Fußballtrainer und ehemaliger -spieler * [[Sabine Sütterlin-Waack]] (* 1958), Rechtsanwältin und Politikerin (CDU) * [[Martin Rheinheimer]] (* 1960), Historiker * [[Jan van Aken (Politiker)|Jan van Aken]] (* 1961), Politiker * [[Dietrich Becker (Diplomat)|Dietrich Becker]] (* 1961), Diplomat * [[Ralf Sommer (Elektroingenieur)|Ralf Sommer]] (* 1961), Elektroingenieur * [[Thomas Röske]] (* 1962), Kunsthistoriker * [[Gerd Gottlob]] (* 1964), Journalist und Fußballkommentator * [[Gundula Bavendamm]] (* 1965), Historikerin und Kulturmanagerin * [[Christiane Bruns (Medizinerin)|Christiane Bruns]] (* 1965), Chirurgin * [[Kerstin Drechsel]] (* 1966), Malerin * [[Andreas Herbig]] (1966–2022), Produzent, Echopreisträger * [[Birte Karalus]] (* 1966), Journalistin und Moderatorin * [[Lena Johannson]] (* 1967), Schriftstellerin * [[Michael Meyer-Hermann (Physiker)|Michael Meyer-Hermann]] (* 1967), Physiker und Hochschullehrer * [[Thorsten Schröder]] (* 1967), Journalist, Moderator und Sprecher der Tagesschau * [[Lars Uwe Höltich]] (* 1968), TV-Producer * [[Sönke Lieberam-Schmidt]] (* 1969), Wirtschaftsinformatiker und Professor * [[Heiko Nieder]] (* 1972), Koch, mit zwei Sternen im ''Guide Michelin'' ausgezeichnet * [[Christine Berger]] (* 1973), Theater- und Fernsehschauspielerin * [[Andreas Dobberkau]] (* 1975), Schauspieler * [[Helmut Fritz]] (* 1975), fiktiver Popsänger * [[Julian Krafftzig]] (* 1977), Radiomoderator * [[Torben Liebrecht]] (* 1977), Schauspieler, Regisseur und Drehbuchautor * [[Alexander Nerlich]] (* 1979), Regisseur * [[Imke Wedekind]] (* 1984), Volleyballspielerin * [[Ann-Kathrin Karschnick]] (* 1985), Fantasy-Autorin * [[Max Kruse (Fußballspieler)|Max Kruse]] (* 1988), Fußballspieler * [[Marvin Boadu]] (* 1989), Basketballspieler * [[Felix Brügmann]] (* 1992), Fußballspieler * [[Maximilian Buhk]] (* 1992), Automobilrennfahrer * [[Felix von der Laden]] (* 1994), Webvideoproduzent (bekannt als „Dner“), Automobilrennfahrer und Unternehmer * [[Sina Aylin Demirhan]] (* 1994), Politikerin (Bündnis 90/Die Grünen) * [[Larina Aylin Hillemann]] (* 1996), Ruderin * [[Victoria Helene Bergemann]] (* 1997), Komikerin und Autorin * [[Noma Noha Akugue]] (* 2003), Tennisspielerin }} === Mit Reinbek verbunden ===<!-- chronologisch nach Geburtsdatum geordnet --> {{Mehrspaltige Liste|liste= * [[Georg Julius Andresen]] (1815–1882), Autor, Mediziner, Hydrotherapeut und Gründer des Sophienbads * [[Arthur Goldschmidt (Jurist)|Arthur Goldschmidt]] (1873–1947), Jurist und Politiker * [[Hans E. B. Kruse]] (1891–1968), Kaufmann und Hamburger Senator, wohnte und starb in Reinbek * [[Franz Heske]] (1892–1963), Forstwissenschaftler * [[Bernhard Rogge (Marineoffizier)|Bernhard Rogge]] (1899–1982), Admiral * [[Helene Francke-Grosmann]] (1900–1990), Forstwissenschaftlerin * [[Erwin Freytag]] (1907–1987), Autor und evangelisch-lutherischer Theologe * [[Heinrich Maria Ledig-Rowohlt]] (1908–1992), bis 1982 Verleger des [[Rowohlt Verlag]]s * [[Rolf Italiaander]] (1913–1991), Schriftsteller, Übersetzer, Forschungsreisender, Ethnograf * [[Sandro von Lorsch]] (1919–1992), Maler * [[Arwed Imiela]] (1929–1982), Frauenmörder * [[Günter Gaus]] (1929–2004), Journalist, Publizist, Diplomat und Politiker * [[Hans-Jürgen von Maydell]] (''Baron Maydell''; 1932–2010), Forstwissenschaftler * [[Heinz-Georg Keerl]] (1946–2011), General * [[Thomas Straubhaar]] (* 1957), Ökonom * [[Holger Waldenberger]] (* 1967), Quizspieler * [[Bjarne Mädel]] (* 1968), Schauspieler * [[Moritz Bleibtreu]] (* 1971), Schauspieler * [[Ann-Katrin Schröder]] (* 1973), Journalistin und Fernsehmoderatorin * [[Bodo Wartke]] (* 1977), Musik-Kabarettist * [[Martin Habersaat]] (* 1977), Politiker, lebt seit 2014 in Reinbek * [[Julian Reister]] (* 1986), Tennisspieler }} == Literatur == ;Antiquarisch * Mathilde Weise-Minck: ''Kindertage in Reinbek.'' Piper, München 1947, {{DNB|576902853}}. * Curt Davids: ''Festschrift zur 725-Jahrfeier von Reinbek.'' 1963, {{DNB|451252543}}. * Walter Fink: ''Das Amt Reinbek.'' Zentralstelle f. Personen- u. Familiengeschichte, Frankfurt am Main 1969, {{DNB|999410660}}. * Herbert Rathmann: ''Ich bin ein Reinbeker.'' 1978, {{OCLC|248265316}}. * Curt Davids: ''Die Wassermühle in Reinbek.'' 1982, {{DNB|840196717}}. * Hans Heuer: ''Das Kloster Reinbek.'' Beitrag zur Geschichte der Landschaft Stormarn. Wachholtz, Neumünster 1985, ISBN 3-529-02186-5. * [[Dirk Bavendamm]]: ''Reinbek. Geschichte einer holsteinischen Stadt zwischen Hamburg und Sachsenwald.'' 1988, ISBN 3-9801817-0-7. * ''Reinbek in alten Ansichten.'' Bildband. Europäische Bibliothek, Zaltbommel 1996, ISBN 90-288-6082-7. ;Aktuellere Titel * Wolf Gütschow, Michael Zapf: ''Reinbek und der Sachsenwald im Wandel.'' Bildband. Schubert, Hamburg 1997, ISBN 3-929229-44-7. * ''Reinbek gestern und heute.'' Bildband. Europäische Bibliothek, Zaltbommel 2000, ISBN 90-288-6634-5. * Georges-Arthur Goldschmidt: ''Ein Garten in Deutschland.'' 2000, ISBN 3-250-10118-4. * Frank Göhre: ''Endstation Reinbek.'' Krimi. Hamburger Abendblatt, Hamburg 2001, ISBN 3-921305-20-9. * Antje Wendt: ''Das Schloß Reinbek.'' Wachholtz, Neumünster 1994, ISBN 3-529-02739-1. * Detlev Landgrebe: ''Kückallee 37: Eine Kindheit am Rande des Holocaust.'' CMZ, Rheinbach 2009, ISBN 978-3-87062-104-9. == Weblinks == {{Commonscat}} {{Wikivoyage}} * [https://www.reinbek.de/ Website der Stadt Reinbek] == Einzelnachweise == <references /> {{NaviBlock |Navigationsleiste Städte und Gemeinden im Kreis Stormarn |Navigationsleiste Stadtteile von Reinbek}} {{Normdaten|TYP=g|GND=4049222-9|LCCN=n50052582|VIAF=312788140}} [[Kategorie:Ort im Kreis Stormarn]] [[Kategorie:Ort an der Bille]] [[Kategorie:Reinbek| ]] [[Kategorie:Ersterwähnung 1238]] [[Kategorie:Stadt in Schleswig-Holstein]] [[Kategorie:Stadtrechtsverleihung 1952]] 662mj3g32r6t7m0uw1b3ydmnt611kvz User talk:JWBTH/CD test page 3 154341 739272 738869 2026-04-24T17:27:45Z JWBTH 52211 new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]]) 739272 wikitext text/x-wiki == Section 1 == first section comment [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 02:37, 20 November 2024 (UTC) :test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:31, 18 April 2026 (UTC) ::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:31, 18 April 2026 (UTC) :::Hello, World! [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 14:06, 19 April 2026 (UTC) :::Test. [[User:IKhitron|IKhitron]] ([[User talk:IKhitron|talk]]) 14:20, 19 April 2026 (UTC) ::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:29, 19 April 2026 (UTC) :::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:29, 19 April 2026 (UTC) unsigned comment end {{unsigned|user}} : comment to be edited [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 02:38, 20 November 2024 (UTC) :: comment to test buttons [[User:Jack who built the house|Jack who built the house]] ([[User talk:Jack who built the house|talk]]) 02:41, 20 November 2024 (UTC) ::: child comment of comment to test buttons [[User:Jack who built the house|Jack who built the house]] ([[User talk:Jack who built the house|talk]]) 06:09, 27 August 2025 (UTC) ::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 19:32, 16 March 2026 (UTC) :::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 19:32, 16 March 2026 (UTC) : [[#c-Test_account_8-20241120023700-Section_1|Test account 8 @ 02:37, 20 November 2024 (UTC)]] [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 06:43, 28 March 2026 (UTC) === test2 === test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:55, 14 September 2025 (UTC) : [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 20:13, 26 March 2026 (UTC) :: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:17, 8 April 2026 (UTC) === Comment with complex markup === * ̴͍͖̪̭̂ฑεᚹẻ̴̦̜̜͙̰̉̒͠͠иℳἒԊ৩βà̸̩̳̗m̶̧̲̲̬̌̀̈́̀ь β ì̵̛̹̌͛͝«Зᾷу៚ἐฑἒдì̵̛̹̌͛͝ю»ì̵̛̹̌͛͝ ! Ᾰ D̴̞̓̊̀ля чẻ̴̦̜̜͙̰̉̒͠͠рẻ̴̦̜̜͙̰̉̒͠͠счуr̵̢͈͕̺͎̀̅ s̸̢̈́ерьӚz̵͓̫̻͔͠Ԋыᚸ βыΔε௭иm̶̧̲̲̬̌̀̈́̀ь <s>ฑр৩c̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀раԊc̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀β৩</s> ३ᾷβ৩Δь «D̴̞̓̊̀β৩йԊая z̵͓̫̻͔͠à̸̩̳̗௶พь». Ἇ m̶̧̲̲̬̌̀̈́̀৩ иz̵͓̫̻͔͠ Ԋẻ̴̦̜̜͙̰̉̒͠͠k̸̟͔̯̯̖̍̂͐̎͘৩m̶̧̲̲̬̌̀̈́̀৩ᚹыᚸ c̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀ᾷ z̵͓̫̻͔͠а௶พь m̶̧̲̲̬̌̀̈́̀ᾷк ì̵̛̹̌͛͝и ௭ε३εm̶̧̲̲̬̌̀̈́̀ чεᚹعz̵͓̫̻͔͠ k̸̟͔̯̯̖̍̂͐̎͘ᚹᾷй !!! ̴͍͖̪̭̂ <span style="font-family:Calibri; font-size:175%; display: inline-block; letter-spacing: 5px; transform: rotate(10deg); padding: 20px 0px;>[[User:Example|'''<span style="color: Magenta; position: relative; top: -4px;">ঞ</span><span style="color: SpringGreen; position: relative; top: -3px;">ʆ</span><span style="color: red; position: relative; top: -2px;">ἕ</span><span style="color: LimeGreen; position: relative; top: -1px;">ฃ</span><span style="color: DeepPink; position: relative; top: 2px;">r̵̢͈͕̺͎̀̅</span><span style="color: Aqua; position: relative; top: 4px;"> ̴͍͖̪̭̂</span><span style="color: DarkOrange; position: relative; top: 4px;">D̴̞̓̊̀</span><span style="color: DarkOrchid; position: relative; top: 3px;">ἒ</span><span style="color: Chartreuse; position: relative; top: 4px;"> ̴͍͖̪̭̂</span><span style="color: Fuchsia; position: relative; top: 1px;">ໃ</span><span style="color: DarkTurquoise; position: relative; top: 0px;">à̸̩̳̗</span><span style="color: Forestgreen; position: relative; top: -2px;">ʁ</span><span style="color: deeppink; position: relative; top: 2px;">i̵͖̒͆̕͝ͅ</span><span style="color: Turquoise; position: relative; top: -1px;">ń̸̳͑̑͌</span><span style="color: LimeGreen; position: relative; top: -4px;">៩</span><span style="color: Magenta; position: relative; top: 1px;">♥</font>''']]</span> 14:08, 1 April 2026 (UTC) test === Transcluded comments === {{User talk:JWBTH/CD test page/comment}} : test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 10:36, 20 April 2026 (UTC) === Vote === Comment. # Vote 1. [[User:Example|Example]] ([[User talk:Example|talk]]) 06:46, 9 April 2026 (UTC) # Vote 2. [[User:Example|Example]] ([[User talk:Example|talk]]) 06:46, 9 April 2026 (UTC) === Last subsection === test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC) : Comment [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 31 March 2026 (UTC) :: Comment [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:25, 31 March 2026 (UTC) ::: Comment beginning<br> Comment ending [[User:Example|Example]] ([[User talk:Example|talk]]) 09:34, 31 March 2026 (UTC) == Section to add test comments == section [[User:Example|Example]] ([[User talk:Example|talk]]) 02:37, 1 March 2026 (UTC) : Test comment with random number 0.4478961847809999 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 22:01, 19 April 2026 (UTC) : Test comment with random number 0.42841430187725704 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 22:10, 19 April 2026 (UTC) == Section with equals sign (=) for moving == <div class="cd-moveMark">''Moved to [[User talk:JWBTH/CD test page 2#Section with equals sign ({{=}}) for moving]]. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 13:40, 1 April 2026 (UTC)''</div> == test == <div class="cd-moveMark">''Moved to [[User talk:JWBTH/CD test page 2#test]]. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 14:33, 17 April 2026 (UTC)''</div> == Section for moving == test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC) ==Discussion at [[]]== [[File:Farm-Fresh eye.png|15px|link=|alt=]]&nbsp;You are invited to join the discussion at [[]]. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:27, 24 April 2026 (UTC)<!-- [[Template:Please see]] --> lnplnrdaip52fpgpi4cqwv049hdrgmz 739274 739272 2026-04-24T17:33:36Z JWBTH 52211 /* Discussion at [[]] */ delete topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]]) 739274 wikitext text/x-wiki == Section 1 == first section comment [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 02:37, 20 November 2024 (UTC) :test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:31, 18 April 2026 (UTC) ::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:31, 18 April 2026 (UTC) :::Hello, World! [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 14:06, 19 April 2026 (UTC) :::Test. [[User:IKhitron|IKhitron]] ([[User talk:IKhitron|talk]]) 14:20, 19 April 2026 (UTC) ::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:29, 19 April 2026 (UTC) :::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:29, 19 April 2026 (UTC) unsigned comment end {{unsigned|user}} : comment to be edited [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 02:38, 20 November 2024 (UTC) :: comment to test buttons [[User:Jack who built the house|Jack who built the house]] ([[User talk:Jack who built the house|talk]]) 02:41, 20 November 2024 (UTC) ::: child comment of comment to test buttons [[User:Jack who built the house|Jack who built the house]] ([[User talk:Jack who built the house|talk]]) 06:09, 27 August 2025 (UTC) ::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 19:32, 16 March 2026 (UTC) :::: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 19:32, 16 March 2026 (UTC) : [[#c-Test_account_8-20241120023700-Section_1|Test account 8 @ 02:37, 20 November 2024 (UTC)]] [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 06:43, 28 March 2026 (UTC) === test2 === test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:55, 14 September 2025 (UTC) : [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 20:13, 26 March 2026 (UTC) :: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:17, 8 April 2026 (UTC) === Comment with complex markup === * ̴͍͖̪̭̂ฑεᚹẻ̴̦̜̜͙̰̉̒͠͠иℳἒԊ৩βà̸̩̳̗m̶̧̲̲̬̌̀̈́̀ь β ì̵̛̹̌͛͝«Зᾷу៚ἐฑἒдì̵̛̹̌͛͝ю»ì̵̛̹̌͛͝ ! Ᾰ D̴̞̓̊̀ля чẻ̴̦̜̜͙̰̉̒͠͠рẻ̴̦̜̜͙̰̉̒͠͠счуr̵̢͈͕̺͎̀̅ s̸̢̈́ерьӚz̵͓̫̻͔͠Ԋыᚸ βыΔε௭иm̶̧̲̲̬̌̀̈́̀ь <s>ฑр৩c̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀раԊc̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀β৩</s> ३ᾷβ৩Δь «D̴̞̓̊̀β৩йԊая z̵͓̫̻͔͠à̸̩̳̗௶พь». Ἇ m̶̧̲̲̬̌̀̈́̀৩ иz̵͓̫̻͔͠ Ԋẻ̴̦̜̜͙̰̉̒͠͠k̸̟͔̯̯̖̍̂͐̎͘৩m̶̧̲̲̬̌̀̈́̀৩ᚹыᚸ c̴̨͍͇̪̏̿́̽̕m̶̧̲̲̬̌̀̈́̀ᾷ z̵͓̫̻͔͠а௶พь m̶̧̲̲̬̌̀̈́̀ᾷк ì̵̛̹̌͛͝и ௭ε३εm̶̧̲̲̬̌̀̈́̀ чεᚹعz̵͓̫̻͔͠ k̸̟͔̯̯̖̍̂͐̎͘ᚹᾷй !!! ̴͍͖̪̭̂ <span style="font-family:Calibri; font-size:175%; display: inline-block; letter-spacing: 5px; transform: rotate(10deg); padding: 20px 0px;>[[User:Example|'''<span style="color: Magenta; position: relative; top: -4px;">ঞ</span><span style="color: SpringGreen; position: relative; top: -3px;">ʆ</span><span style="color: red; position: relative; top: -2px;">ἕ</span><span style="color: LimeGreen; position: relative; top: -1px;">ฃ</span><span style="color: DeepPink; position: relative; top: 2px;">r̵̢͈͕̺͎̀̅</span><span style="color: Aqua; position: relative; top: 4px;"> ̴͍͖̪̭̂</span><span style="color: DarkOrange; position: relative; top: 4px;">D̴̞̓̊̀</span><span style="color: DarkOrchid; position: relative; top: 3px;">ἒ</span><span style="color: Chartreuse; position: relative; top: 4px;"> ̴͍͖̪̭̂</span><span style="color: Fuchsia; position: relative; top: 1px;">ໃ</span><span style="color: DarkTurquoise; position: relative; top: 0px;">à̸̩̳̗</span><span style="color: Forestgreen; position: relative; top: -2px;">ʁ</span><span style="color: deeppink; position: relative; top: 2px;">i̵͖̒͆̕͝ͅ</span><span style="color: Turquoise; position: relative; top: -1px;">ń̸̳͑̑͌</span><span style="color: LimeGreen; position: relative; top: -4px;">៩</span><span style="color: Magenta; position: relative; top: 1px;">♥</font>''']]</span> 14:08, 1 April 2026 (UTC) test === Transcluded comments === {{User talk:JWBTH/CD test page/comment}} : test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 10:36, 20 April 2026 (UTC) === Vote === Comment. # Vote 1. [[User:Example|Example]] ([[User talk:Example|talk]]) 06:46, 9 April 2026 (UTC) # Vote 2. [[User:Example|Example]] ([[User talk:Example|talk]]) 06:46, 9 April 2026 (UTC) === Last subsection === test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC) : Comment [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 31 March 2026 (UTC) :: Comment [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:25, 31 March 2026 (UTC) ::: Comment beginning<br> Comment ending [[User:Example|Example]] ([[User talk:Example|talk]]) 09:34, 31 March 2026 (UTC) == Section to add test comments == section [[User:Example|Example]] ([[User talk:Example|talk]]) 02:37, 1 March 2026 (UTC) : Test comment with random number 0.4478961847809999 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 22:01, 19 April 2026 (UTC) : Test comment with random number 0.42841430187725704 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 22:10, 19 April 2026 (UTC) == Section with equals sign (=) for moving == <div class="cd-moveMark">''Moved to [[User talk:JWBTH/CD test page 2#Section with equals sign ({{=}}) for moving]]. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 13:40, 1 April 2026 (UTC)''</div> == test == <div class="cd-moveMark">''Moved to [[User talk:JWBTH/CD test page 2#test]]. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 14:33, 17 April 2026 (UTC)''</div> == Section for moving == test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC) pemmwfnqfetdac1bdsr0c4wob07odwg Test 0 155073 739243 739230 2026-04-24T12:19:04Z ~2026-21716-09 73454 showcaptcha 739243 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]]..testtes A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>test O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second 0xj08mab8ck7ixbrn1t4epaiosnrudp 739244 739243 2026-04-24T12:23:09Z ~2026-25025-15 73681 showcaptcha 739244 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]]..testtes A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>test O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second 03vzy45jb224q1iqbow2hp1ptfk6n7e 739245 739244 2026-04-24T12:30:46Z ~2026-21716-09 73454 showcaptcha 739245 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]]..testtesd A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>tes O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second 7jm8wcq1ppie98ldmphyzwa3i07561k 739246 739245 2026-04-24T12:31:08Z ~2026-21716-09 73454 739246 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme. |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]]..testtesd A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>tes O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second iu6oo4c0k6n8hd2xfwevw8wbdk1wwag 739247 739246 2026-04-24T12:31:33Z ~2026-21716-09 73454 showcaptcha 739247 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme. |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]]. A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>tes O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second 1c7z50z92efck8364qnwogvzb42w7g2 739248 739247 2026-04-24T12:32:07Z ~2026-21716-09 73454 showcaptcha 739248 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme. |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]]. A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>tes O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second hlmmclr00m80puq6wxtb4noitbo7wxh 739261 739248 2026-04-24T13:39:31Z ~2026-21716-09 73454 showcaptcha 739261 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme. |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]] A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>tes O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second ozxcodu9tn8h5c90lpiztv57bp3rroy 739262 739261 2026-04-24T13:59:13Z ~2026-25195-23 73696 739262 wikitext text/x-wiki {{Ver desambig|este=o filme de Marcel Camus|a peça teatral de Vinícius de Moraes|Orfeu da Conceição}} {{Info/Filme. |título = Orfeu Negro |título-pt = Orfeu Negro |título-br = Orfeu do Carnaval |imagem = [[Imagem:Orfeu Negro, 1959.jpg|Orfeu Negro, 1959|230px]] |ano = 1959test |duração = 100 |idioma = [[Língua portuguesa|Português]] |país = [[Brasil]] • [[França]] • [[Itália]] |direção = [[Marcel Camus]] |roteiro = Marcel Camus<br />[[Jacques Viot]] |criação original = {{Baseado em|[[Orfeu da Conceição]]|[[Vinicius de Moraes]]}} |produção = Sasha Gordine. |co-produtor = . |produção executivo = |música = [[Tom Jobim]]<br />[[Luiz Bonfá]] |edição = Andrée Feix |diretor de arte = |diretor de fotografia = [[Jean Bourgoin]] |figurino = |precedido_por = |seguido_por =d |estúdio = Dispat Films<br />Gemma Cinematografica<br />Tupan Filmes |elenco = [[Breno Mello]]<br />[[Marpessa Dawn]]<br />[[Lourdes de Oliveira]]<br />[[Léa Garcia]] |código-IMDB = 0053146 |tipo = LF |cor-pb = cor. }} '''''Orfeu Negro'''''<ref>{{Citation|title=Orfeu do Carnaval|url=https://www.adorocinema.com/filmes/filme-261/|accessdate=2023-02-18|language=pt-BR|last=AdoroCinema}}</ref> ou '''''Orfeu do Carnaval'''''<ref>{{Citar web|url=https://web.archive.org/web/20130522152505/http://noticias.r7.com/rio-de-janeiro/noticias/a-espera-de-obama-chapeu-mangueira-e-babilonia-preparam-documentario-e-cartas-ao-presidente-20110316.html|titulo=À espera de Obama, Chapéu Mangueira e Babilônia preparam documentário e cartas ao presidente - Rio de Janeiro - R7|data=2013-05-22|acessodata=2023-02-18|website=web.archive.org}}</ref> (na [[França]], '''''Orphée Noir'''''; na [[Itália]], '''''Orfeo Negro''''') é um [[filme]] ítalo-franco-[[brasil]]eiro de [[1959 no cinema|1959]], dirigido por [[Marcel Camus]] e com [[roteiro]] adaptado por Camus e [[Jacques Viot]] a, partir da [[peça teatral]] ''[[Orfeu da Conceição]]'', de [[Vinícius de Moraes]] A trilha sonora é de [[Tom Jobim]] e [[Luís Bonfá]]. Vinícius e [[Antônio Maria de Araújo Morais|Antônio Maria]] também tiveram músicas incluídas, mas, assim como [[Agostinho dos Santos]], que interpretou a música-tema de Orfeu, "[[Manhã de Carnaval]]", não receberam os créditos. O filme teve outra versão em 1999, sob o nome ''[[Orfeu (filme)|Orfeu]]'', dirigida por [[Cacá Diegues]]<nowiki/>tes O filme ganhou o [[Oscar de Melhor Filme Internacional]] em 1960, representando a França.<ref>{{citar web|título=A França no Oscar: veja a lista dos filmes franceses premiados|url=http://blogs.oglobo.globo.com/paris/post/a-franca-no-oscar-veja-lista-dos-filmes-franceses-premiados-561194.html|acessodata=2 de Junho de 2016}}</ref> Trata-se da primeira produção de [[língua portuguesa]] a conquistar a estatueta do [[Oscar]].<ref>{{citar web|título=Quando os portugueses chegaram aos Óscares|url=http://mag.sapo.pt/cinema/atualidade-cinema/artigos/quando-os-portugueses-chegaram-aos-oscares?artigo-completo=sim|acessodata=2 de Junho de 2016}}</ref> É também, juntamente com ''[[Mustang (filme)|Mustang]]'', ''[[Emilia Pérez|Emilia Perez]]'' e ''[[Un Simple Accident|It Was Just an Accident]]'', um dos filmes não francófonos a representar a França no [[Oscar]]. test test test test test test test test test test test test test test test test test test test == Enredo == O enredo é inspirado na [[mitologia grega]], na história de [[Orfeu]] e [[Eurídice]]. A adaptação ambientou a obra no Brasil, em uma [[favela]] do [[Rio de Janeiro (cidade)|Rio de Janeiro]], na época do [[Carnaval]]. Eurídice vem fugida do [[Sertão brasileiro|sertão nordestino]] para morar na favela com sua prima Serafina. Ela tem medo de um homem que está perseguindo-a e quer matá-la; ela não sabe o motivo, mas pensa que esse homem talvez tenha gostado dela e, como ela não lhe deu confiança, ele agora quer se vingar. Ela apaixona-se perdidamente por Orfeu, que é noivo da bela e sedutora Mira. O tempo passa, Mira passa a perseguir Eurídice, com ciúmes. Serafina ajuda a prima a namorar Orfeu. Eurídice conhece o carnaval [[Carioca (gentílico)|carioca]] ao lado de Orfeu, mas sempre se apavora e corre quando vê que o tal homem está perto. Um dia, ela revela tudo a Orfeu. Ele a protege e diz que vai ficar ao seu lado. O namoro deles é puro e inocente, sem malícia. Passa o tempo. Um dia, se divertindo no último dia de carnaval, Eurídice teme que o homem apareça, e acha melhor voltar para a favela, que fica perto. Ela entra num beco escuro, para subir a favela, mas ela não conhece bem o local e fica assustada. O homem a encontra e a persegue. Ela sai correndo desesperada e entra num galpão velho e escuro. Ela tenta se esconder do homem, mas este a acha. Desesperada, ela pula de um tablado e se segura em um fio de alta tensão. Orfeu chega e liga a tensão, Eurídice cai e morre eletrocutada. Orfeu briga com homem e fica inconsciente, quando acorda se dá conta dos fatos. Ele fica desolado. A ambulância chega e leva o corpo ao [[Instituto Médico Legal]]. Ele não pode ir junto. [[Quarta-feira de cinzas]] e Orfeu só sabe chorar. Ele vai atrás do corpo, faz uma sessão [[Espiritismo|espírita]] na qual Eurídice baixa no corpo de uma senhora, mas, enfim, Orfeu acha seu corpo. Ele sequestra-o e leva à favela. Mira vê, e enfurecida, joga uma pedra na cabeça de Orfeu. Com a pancada ele cai de uma ribanceira com o corpo morto de Eurídice nos braços e morre também.. == Elenco principal == [[Ficheiro:Marpessa Dawn, 1959.tif|miniaturadaimagem|[[Breno Mello]] e [[Marpessa Dawn]] atuando em Orfeu Negro]] * [[Breno Mello]] .... Orfeu * [[Marpessa Dawn]] .... Eurídice * [[Lourdes de Oliveira]] .... Mira * [[Léa Garcia]] .... Serafina * [[Adhemar Ferreira da Silva]] .... Morte * [[Alexandro Constantino]] .... Hermes * [[Waldemar de Souza|Waldir de Souza]] (Waldir 59) .... Chico Bôto * [[Jorge dos Santos]] .... Benedito * [[Aurino Cassiano]] .... Zeca * [[Tião Macalé]] .... Homem vendendo o Gramofone. * [[Cartola_(compositor)|Cartola]] (participação especial) == Principais prêmios e indicações == [[Festival de Cannes]] 1959 (França) * Recebeu a [[Palma de Ouro]]. ''[[Óscar|Oscar]]'' 1960 (EUA) * Vencedor na categoria de melhor filme em língua estrangeira (português/diretor). [[Prêmios Globo de Ouro|Globo de Ouro]] 1960 (EUA) * Venceu na categoria de melhor filme estrangeiro (França). ''[[BAFTA|British Academy of Film and Television Arts]]'' 1961 (Reino Unido) * Indicado na categoria de melhor filme em língua estrangeira (Brasil, França e Itália/produção). == Influência == Orfeu Negro foi citado por [[Jean-Michel Basquiat]] como uma de suas primeiras influências musicais, enquanto [[Barack Obama]] observa em seu livro de memórias [[Dreams from My Father]] (1995) que era o filme favorito de sua mãe.<ref name=":0">{{Citar web|ultimo=|url=https://www.correiobraziliense.com.br/app/noticia/diversao-e-arte/2010/06/05/interna_diversao_arte,196187/obama-e-quase-brasileiro.shtml|titulo=Obama é 'quase' brasileiro|data=18-2-2023|acessodata=2023-02-18|website=Acervo|lingua=pt-BR}}</ref> Obama, no entanto, não compartilhar preferências de sua mãe após a primeira a ver o filme durante seus primeiros anos na [[Universidade Columbia|Universidade de Columbia]]: "de repente eu percebi que a representação dos negros infantis que eu estava vendo agora na tela, a imagem inversa de selvagens escuros de Conrad, era o que minha mãe tinha levado com ela para o [[Havaí]] todos aqueles anos antes, um reflexo das fantasias simples que haviam sido proibidas de uma menina branca, de classe média do [[Kansas]], a promessa de uma outra vida: quente, sensual, exótica, diferente.<ref name=":0" /> == Remakes e adaptações == Em [[1999]], um novo filme, [[Orfeu (filme)|Orfeu]], foi feita por [[Cacá Diegues]], com uma trilha sonora que caracteriza o cantor e compositor brasileiro [[Caetano Veloso]]. O diretor disse que não era um remake de Orfeu Negro, mas um filme baseado na peça original de Vinicius de Moraes, de 1956.<ref>{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq23049921.htm|titulo=Folha de S.Paulo - Cinema - "Orfeu": Filme confirma mito, diz Caetano Veloso - 23/04/1999|acessodata=2023-02-18|website=www1.folha.uol.com.br}}</ref> Em julho de 2014, uma adaptação musical de Broadway Orfeu Negro foi anunciada, a ser escrita por [[Lynn Nottage]] e dirigido por George C. Wolfe.<ref>{{Citar web|ultimo=Archive|primeiro=View Author|ultimo2=Twitter|primeiro2=Follow on|url=https://nypost.com/2020/02/13/antonio-carlos-jobims-music-finally-coming-to-broadway/|titulo=Antônio Carlos Jobim's music finally coming to Broadway|data=2020-02-13|acessodata=2023-02-18|lingua=en-US|ultimo3=feed|primeiro3=Get author RSS}}</ref> == Na cultura popular == Cenas do filme foram utilizados no lyric vídeo da música Afterlife da banda [[Arcade Fire]], de 2013.<ref>{{Citar web|ultimo=G1|primeiro=Do|ultimo2=Paulo|primeiro2=em São|url=http://g1.globo.com/musica/noticia/2013/10/arcade-fire-lanca-clipe-com-imagens-do-filme-orfeu-do-carnaval.html|titulo=Arcade Fire lança clipe com imagens do filme 'Orfeu do Carnaval'|data=2013-10-22|acessodata=2023-02-18|website=Música|lingua=pt-br}}</ref> == Ver também == * [[Lista de indicações brasileiras ao Oscar]] {{Referências}} == Bibliografia == * {{Citar periódico|ultimo=Campos-Muñoz|primeiro=Germán|data=2012|titulo=Contrapuntos órficos: Mitografía brasileña y el mito de Orfeo|url=https://muse.jhu.edu/article/502895|jornal=Latin American Research Review|lingua=es|volume=47|numero=4|paginas=31–48|doi=10.1353/lar.2012.0048|issn=1542-4278}} {{Oscar de melhor filme estrangeiro}} {{Palma de Ouro}} {{Portal3|Cinema|Rio de Janeiro|Brasil|França|Itália}} {{Controle de autoridade}} [[Categoria:Filmes do Brasil de 1959]] [[Categoria:Filmes da França de 1959]] [[Categoria:Filmes da Itália de 1959]] [[Categoria:Filmes de drama da Itália]] [[Categoria:Filmes premiados com o Oscar de melhor filme internacional]] [[Categoria:Filmes premiados com a Palma de Ouro]] [[Categoria:Filmes baseados em peças de teatro]] [[Categoria:Filmes premiados com o Globo de Ouro de melhor filme em língua estrangeira]] [[Categoria:Filmes de drama do Brasil]] [[Categoria:Filmes de drama da França]] [[Categoria:Filmes de fantasia romântica]] [[Categoria:Filmes em língua portuguesa]] [[Categoria:Filmes baseados na mitologia greco-romana]] [[Categoria:Filmes ambientados na cidade do Rio de Janeiro]] [[Categoria:Filmes gravados na cidade do Rio de Janeiro]] [[Categoria:Filmes sobre afro-brasileiros]] [[Category:Wiki_Club_SHUATS]] == Testing == cross-origin edit testing hCaptcha bot detection <references /> T423840-test Second Third 9xwbdqammjt3di51ngognzzz61q6758 User talk:NovemBot 3 160473 739263 738733 2026-04-24T14:51:53Z Novem Linguae 49714 Replaced content with "<div style=clear: both></div>{{{icon|[[File:Information.svg|25px|alt=|link=]]}}} Welcome to Wikipedia. Although everyone is welcome to contribute to Wikipedia, at least one of [[Special:Contributions/{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}|your recent edits]]{{<includeonly>safesubst:</includeonly>#if:{{{1|}}}|, such as the one you made to [[:{{{1}}}]] with <span class="plainlinks">[{{{2}}} this edit]</span>,}} did not appear to be constructive and has b..." 739263 wikitext text/x-wiki <div style=clear: both></div>{{{icon|[[File:Information.svg|25px|alt=|link=]]}}} Welcome to Wikipedia. Although everyone is welcome to contribute to Wikipedia, at least one of [[Special:Contributions/{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}|your recent edits]]{{<includeonly>safesubst:</includeonly>#if:{{{1|}}}|, such as the one you made to [[:{{{1}}}]] with <span class="plainlinks">[{{{2}}} this edit]</span>,}} did not appear to be constructive and has been [[Help:Reverting|reverted]] or removed. Please use [[Wikipedia:Sandbox|the sandbox]] for any test edits you would like to make, and read the [[Wikipedia:Welcoming committee/Welcome to Wikipedia|welcome page]] to learn more about contributing constructively to this encyclopedia. Thank you. ~~<noinclude></noinclude>~~<!-- Template:Huggle/warn-1 --><!-- Template:uw-vandalism1 --><noinclude> {{Huggle/TemplateNotice|series = uw-vandalism|max = 4im|s1=vw-n|s2 = uw-v1|s3 = uw-vand1|s4 = uw-vandal1}} [[pt:Predefinição:Huggle/warn-1]] </noinclude> suma5o8omy6j6yqbgeanxkfzrmrizon 739264 739263 2026-04-24T14:55:40Z Novem Linguae 49714 Replaced content with "{{subst:Huggle/warn-1}}" 739264 wikitext text/x-wiki [[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC) t9ewb5pzzvxls93je73x8z2wmcri08e 739265 739264 2026-04-24T14:56:19Z Novem Linguae 49714 739265 wikitext text/x-wiki [[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- This is a variant of Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC) auk8wx7u8i7u4fhqcbbc877x5scuuor 739266 739265 2026-04-24T14:57:19Z Novem Linguae 49714 739266 wikitext text/x-wiki [[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Novem Linguae|Novem Linguae]]. I wanted to let you know that I reverted one of [[Special:Contributions/NovemBot|your recent contributions]] because it did not appear constructive. If you would like to experiment, please use the [[Wikipedia:Sandbox|sandbox]]. If you think I made a mistake, or if you have any questions, you can leave me a message on [[User_talk:Novem Linguae|my talk page]]. Thanks. <!-- Template:Huggle/warn-1 --><!-- This is a variant of: --><!-- Template:uw-vandalism1 -->–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 14:55, 24 April 2026 (UTC) 1svm5kmgnwbtop51cw3uumo5dhwrzki User:Wooze/common.js 2 174857 739232 739231 2026-04-24T12:00:53Z Wooze 54732 739232 javascript text/javascript mw.loader.using(['mediawiki.api']).then(function () { var api = new mw.Api(); function createButton() { var btn = $('<button>') .text('⚡Mass Tool') .css({ position: 'fixed', bottom: '20px', right: '20px', zIndex: 9999, padding: '10px', background: '#222', color: '#fff', borderRadius: '8px', border: 'none' }) .on('click', openUI); $('body').append(btn); } function openUI() { var user = prompt("Kullanıcı adı:"); if (!user) return; // Tüm katkılar var contribReq = api.get({ action: 'query', list: 'usercontribs', ucuser: user, uclimit: 20, ucprop: 'ids|title' }); // Sadece oluşturulan sayfalar var newPagesReq = api.get({ action: 'query', list: 'usercontribs', ucuser: user, uclimit: 20, ucprop: 'ids|title', ucshow: 'new' }); $.when(contribReq, newPagesReq).done(function (cData, nData) { var contribs = cData[0].query.usercontribs; var newPages = nData[0].query.usercontribs; var box = $('<div>').css({ position: 'fixed', top: '5%', left: '5%', width: '90%', height: '85%', background: '#fff', overflow: 'auto', padding: '10px', zIndex: 9999 }); var log = $('<div>').css({ fontSize: '12px' }); // UNDO function undoOne(c, callback) { api.post({ action: 'edit', title: c.title, undo: c.revid, token: mw.user.tokens.get('csrfToken'), summary: 'Toplu geri alma (Mass Tool)' }).always(function () { setTimeout(callback, 1200); }); } function runUndoQueue(list) { var i = 0; function next() { if (i >= list.length) { log.append("<b>🎯 Undo bitti</b>"); return; } var c = list[i]; log.append("<div>⏳ Undo: " + c.title + "</div>"); undoOne(c, function () { log.append("<div>✅ " + c.title + "</div>"); i++; next(); }); } next(); } // DELETE function deletePage(title, callback) { api.post({ action: 'delete', title: title, token: mw.user.tokens.get('csrfToken'), reason: 'Toplu silme (Mass Tool)' }).always(function () { setTimeout(callback, 1500); }); } function runDeleteQueue(list) { var i = 0; function next() { if (i >= list.length) { log.append("<b>🗑️ Silme bitti</b>"); return; } var c = list[i]; log.append("<div>⏳ Siliniyor: " + c.title + "</div>"); deletePage(c.title, function () { log.append("<div>🗑️ " + c.title + "</div>"); i++; next(); }); } next(); } // Butonlar var undoBtn = $('<button>') .text("Hepsini Geri Al") .css({ background: '#d33', color: '#fff', margin: '5px' }) .on('click', function () { runUndoQueue(contribs); }); var deleteBtn = $('<button>') .text("Oluşturulanları Sil") .css({ background: '#000', color: '#fff', margin: '5px' }) .on('click', function () { runDeleteQueue(newPages); }); var closeBtn = $('<button>') .text("Kapat") .on('click', function () { box.remove(); }); box.append( "<h3>⚡ Mass Tool Panel</h3>", undoBtn, deleteBtn, closeBtn, "<hr>", "<b>Undo listesi:</b> " + contribs.length + "<br>", "<b>Silinecek sayfalar:</b> " + newPages.length + "<br><br>", log ); $('body').append(box); }); } $(createButton); }); 3e3lpoah42ky5egilmn2z9cgaevak3g 739252 739232 2026-04-24T13:03:00Z Wooze 54732 Testing tool 739252 javascript text/javascript mw.loader.using(['mediawiki.api']).then(function () { var api = new mw.Api(); function createButton() { var btn = $('<button>') .text('⚡Mass Control Panel') .css({ position: 'fixed', bottom: '20px', right: '20px', zIndex: 9999, padding: '10px', background: '#222', color: '#fff', borderRadius: '8px', border: 'none' }) .on('click', openUI); $('body').append(btn); } function openUI() { var user = prompt("Kullanıcı adı:"); if (!user) return; // Değişiklikler var contribReq = api.get({ action: 'query', list: 'usercontribs', ucuser: user, uclimit: 50, ucprop: 'ids|title' }); // Yeni sayfalar var newPagesReq = api.get({ action: 'query', list: 'usercontribs', ucuser: user, uclimit: 50, ucprop: 'ids|title', ucshow: 'new' }); $.when(contribReq, newPagesReq).done(function (cData, nData) { var contribs = cData[0].query.usercontribs; var newPages = nData[0].query.usercontribs; var box = $('<div>').css({ position: 'fixed', top: '5%', left: '5%', width: '90%', height: '85%', background: '#fff', overflow: 'auto', padding: '10px', zIndex: 9999, fontSize: '13px' }); var log = $('<div>').css({ marginTop: '10px' }); // ------------------------ // CHECKBOX LİST OLUŞTUR // ------------------------ function renderList(items, container, type) { items.forEach(function (c) { var row = $('<div>').css({ padding: '4px', borderBottom: '1px solid #eee' }); var cb = $('<input type="checkbox">') .data('title', c.title) .data('revid', c.revid); row.append(cb, " " + c.title); container.append(row); }); } // ------------------------ // UNDO // ------------------------ function undoOne(title, revid, callback) { api.post({ action: 'edit', title: title, undo: revid, token: mw.user.tokens.get('csrfToken'), summary: 'Seçimli geri alma (Mass Control Panel)' }).always(function () { setTimeout(callback, 1000); }); } function runUndo(selected) { var i = 0; function next() { if (i >= selected.length) { log.append("<b>✔️ Geri alma tamamlandı</b><br>"); return; } var item = selected[i]; log.append("<div>⏳ Undo: " + item.title + "</div>"); undoOne(item.title, item.revid, function () { i++; next(); }); } next(); } // ------------------------ // DELETE // ------------------------ function deletePage(title, callback) { api.post({ action: 'delete', title: title, token: mw.user.tokens.get('csrfToken'), reason: 'Seçimli silme (Mass Control Panel)' }).always(function () { setTimeout(callback, 1200); }); } function runDelete(selected) { var i = 0; function next() { if (i >= selected.length) { log.append("<b>🗑️ Silme tamamlandı</b><br>"); return; } var item = selected[i]; log.append("<div>⏳ Siliniyor: " + item.title + "</div>"); deletePage(item.title, function () { i++; next(); }); } next(); } // ------------------------ // UI // ------------------------ var createdBox = $('<div>').append("<h3>🟢 Oluşturulan Sayfalar</h3>"); var editedBox = $('<div>').append("<h3>🔵 Değişiklik Yapılan Sayfalar</h3>"); renderList(newPages, createdBox, "delete"); renderList(contribs, editedBox, "undo"); var undoBtn = $('<button>') .text("Seçilenleri Geri Al") .css({ background: '#2b6', color: '#fff', margin: '5px' }) .on('click', function () { var selected = []; editedBox.find('input:checked').each(function () { selected.push({ title: $(this).data('title'), revid: $(this).data('revid') }); }); runUndo(selected); }); var deleteBtn = $('<button>') .text("Seçilenleri Sil") .css({ background: '#c33', color: '#fff', margin: '5px' }) .on('click', function () { var selected = []; createdBox.find('input:checked').each(function () { selected.push({ title: $(this).data('title') }); }); runDelete(selected); }); var closeBtn = $('<button>') .text("Kapat") .on('click', function () { box.remove(); }); box.append( "<h2>⚡ Mass Control Panel</h2>", undoBtn, deleteBtn, closeBtn, "<hr>", createdBox, "<hr>", editedBox, "<hr>", log ); $('body').append(box); }); } $(createButton); }); en0tdn70m23apndhlzqmbz4amt3iu38 User:MrJaroslavik/CheckUserLogCount.js 2 174966 739277 739200 2026-04-24T18:28:31Z MrJaroslavik 44012 Enter test? 739277 javascript text/javascript // CheckUserLogCount.js // Based on countCUStats.js - https://meta.wikimedia.org/wiki/User:Glaisher/countCUStats.js // Features: // - Counts total CheckUser actions per user within a selected date range. // - Breakdown by Total actions, Months, Quarters (Q1–Q4), or Years // - Adds an interactive control panel directly to the [[Special:CheckUserLog]] page. // - Shows a sortable table with direct links to user CheckUsers log entries. // - Automatically loads all data even for very busy periods. // - Shows the date of the very last CheckUser action for every user in the results. // - Highlights current CheckUsers with green background and identifies inactive ones. // - Generates ready-to-copy Wikitext table. // - With help of Gemini 3 (function() { 'use strict'; // Only run on the CheckUserLog special page if (mw.config.get('wgCanonicalSpecialPageName') !== 'CheckUserLog') { return; } // Helper function to pause execution (useful for preventing API limits) const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { return await api.get(params); } catch (err) { if (err && err.status === 429) { retries++; const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); await sleep(waitTime * 1000); } else { throw err; } } } throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Load required MediaWiki modules for API and UI mw.loader.using(['mediawiki.api', 'mediawiki.util', 'jquery.tablesorter']).then(async function() { const api = new mw.Api(); const now = new Date(); const currYear = now.getUTCFullYear(); const currMonth = now.getUTCMonth() + 1; // Build the user interface panel function setupUI() { // Prevent duplicate panels on the page $('.cu-stats-wrapper').remove(); // Generate year options (from 2005 to present) let yearOpts = ''; for (let y = currYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate month selection menus let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { const label = String(m).padStart(2, '0'); // Default starting month to January monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${label}</option>`; // Default ending month to current month monthOptsTo += `<option value="${m}" ${m === currMonth ? 'selected' : ''}>${label}</option>`; } // Define the HTML structure for the control panel with mode selection const html = ` <div class="cu-stats-wrapper" style="border: 1px solid #a2a9b1; padding: 15px; margin-bottom: 20px; background: #f8f9fa; border-radius: 2px;"> <h3 style="margin-top:0; border-bottom: 1px solid #a2a9b1; padding-bottom: 5px;">CheckUserLogCount.js</h3> <div style="margin-bottom: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;"> <span>From:</span> <select id="cu-year-from" style="padding: 3px;">${yearOpts}</select> <select id="cu-month-from" style="padding: 3px;">${monthOptsFrom}</select> <span>To:</span> <select id="cu-year-to" style="padding: 3px;">${yearOpts}</select> <select id="cu-month-to" style="padding: 3px;">${monthOptsTo}</select> </div> <div style="margin-bottom: 10px; display: flex; gap: 15px; font-size: 0.9em; align-items: center;"> <span style="font-weight:bold;">View mode:</span> <label><input type="radio" name="cu-mode" value="total" checked> Total only</label> <label><input type="radio" name="cu-mode" value="months"> By Months</label> <label><input type="radio" name="cu-mode" value="quarters"> By Quarters</label> <label><input type="radio" name="cu-mode" value="years"> By Years</label> <button id="cu-stats-run" class="mw-ui-button mw-ui-progressive" style="margin-left: auto;">Run</button> </div> <div id="cu-stats-results" style="display:none; margin-top: 15px; border-top: 1px solid #c8ccd1; padding-top: 10px;"> <div id="cu-loader" style="font-weight:bold; margin-bottom: 5px;"></div> <div id="cu-table-container"></div> <div id="cu-wikitext-container" style="display:none; margin-top: 20px;"> <label style="font-weight:bold; display:block; margin-bottom:5px;">Report Wikitext:</label> <textarea id="cu-out" readonly style="width:100%; height:150px; font-family:monospace; font-size:11px; padding:5px; border:1px solid #c8ccd1;"></textarea> </div> </div> </div>`; // Add the interface to the top of the page content $('#mw-content-text').prepend(html); // Spuštění pomocí Enter kdekoli v panelu $('.cu-stats-wrapper').on('keypress', function(e) { if (e.which === 13) { e.preventDefault(); $('#cu-stats-run').click(); } }); // Handle the "Run" button click event $('#cu-stats-run').click(function() { const $btn = $(this); const params = { yf: parseInt($('#cu-year-from').val()), mf: parseInt($('#cu-month-from').val()), yt: parseInt($('#cu-year-to').val()), mt: parseInt($('#cu-month-to').val()), mode: $('input[name="cu-mode"]:checked').val() }; // Disable the button during processing $btn.prop('disabled', true).text('Running...'); // Run the audit with collected parameters runAudit(params).finally(() => { $btn.prop('disabled', false).text('Run'); }); }); } // Main logic to fetch and process CheckUser data with timeline support async function runAudit(cfg) { $('#cu-stats-results').show(); $('#cu-table-container').empty(); $('#cu-wikitext-container').hide(); // Skrytí wikitextu před novým během $('#cu-loader').text('Waiting for queue lock...').css("color", "orange").show(); // Request browser lock to prevent concurrent runs across tabs await navigator.locks.request('local_cu_count_lock', async () => { $('#cu-loader').text('Lock acquired. Connecting to API...').css("color", "#0056b3"); // Prepare dates and range limits const START = `${cfg.yf}-${String(cfg.mf).padStart(2, '0')}-01T00:00:00Z`; const lastDay = new Date(Date.UTC(cfg.yt, cfg.mt, 0)).getUTCDate(); const END = `${cfg.yt}-${String(cfg.mt).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}T23:59:59Z`; const userStats = {}; let totalActions = 0; let continueToken = ''; try { // Phase 1: Get current CheckUsers (even those with no activity) const currentCUs = []; const auRes = await robustCall(api, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); (auRes.query.allusers || []).forEach(u => { userStats[u.name] = { total: 0, timeline: {} }; currentCUs.push(u.name); }); // Phase 2: Fetch logs and build the activity timeline let finished = false; while (!finished) { const params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) params.culcontinue = continueToken; const res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; entries.forEach(e => { if (e.checkuser) { const u = e.checkuser; const monthKey = e.timestamp.substring(0, 7); // Extract YYYY-MM if (!userStats[u]) { userStats[u] = { total: 0, timeline: {} }; } userStats[u].total++; userStats[u].timeline[monthKey] = (userStats[u].timeline[monthKey] || 0) + 1; totalActions++; } }); if (res.continue && res.continue.culcontinue) { continueToken = res.continue.culcontinue; $('#cu-loader').text(`Processing logs... Found ${totalActions} actions.`); } else { finished = true; } } // Phase 3: Get the absolute last action date for each user const lastActions = {}; const usernames = Object.keys(userStats); for (let j = 0; j < usernames.length; j++) { const u = usernames[j]; $('#cu-loader').text(`Checking last activity for ${u} (${j + 1}/${usernames.length})...`); const lastRes = await robustCall(api, { action: 'query', list: 'checkuserlog', cullimit: 1, culuser: u, formatversion: 2 }); const lastEntry = (lastRes.query && lastRes.query.checkuserlog && lastRes.query.checkuserlog.entries && lastRes.query.checkuserlog.entries[0]) ? lastRes.query.checkuserlog.entries[0] : null; lastActions[u] = lastEntry ? lastEntry.timestamp.replace('T', ' ').substring(0, 16) : 'Never'; } $('#cu-loader').text('Rendering table...').css("color", "#0056b3"); renderTable(userStats, totalActions, lastActions, currentCUs, cfg); $('#cu-loader').text('Audit complete!').css("color", "green"); } catch (err) { console.error("CheckUser Stats Error:", err); const errorMsg = err.statusText || err.message || err; $('#cu-loader').text(`API Error: ${errorMsg}`).css("color", "#d33").show(); } }); } // Create the dynamic results table and Wikitext export function renderTable(userStats, total, lastActions, currentCUs, cfg) { // Sort users: highest total first, then alphabetical const sortedUsers = Object.keys(userStats).map(u => ({ name: u, ...userStats[u] })) .sort((a, b) => b.total - a.total || a.name.localeCompare(b.name)); // 1. Generate time columns based on the selected range and mode const timeCols = []; let startIter = new Date(Date.UTC(cfg.yf, cfg.mf - 1, 1)); const endIter = new Date(Date.UTC(cfg.yt, cfg.mt - 1, 1)); while (startIter <= endIter) { const k = startIter.toISOString().substring(0, 7); // "YYYY-MM" if (cfg.mode === 'months') { timeCols.push({ id: k, label: k }); } else if (cfg.mode === 'quarters') { const q = Math.floor(startIter.getUTCMonth() / 3) + 1; const qid = `${startIter.getUTCFullYear()}-Q${q}`; if (!timeCols.find(c => c.id === qid)) { timeCols.push({ id: qid, label: qid, months: [] }); } timeCols.find(c => c.id === qid).months.push(k); } else if (cfg.mode === 'years') { const yid = `${startIter.getUTCFullYear()}`; if (!timeCols.find(c => c.id === yid)) { timeCols.push({ id: yid, label: yid, months: [] }); } timeCols.find(c => c.id === yid).months.push(k); } startIter.setUTCMonth(startIter.getUTCMonth() + 1); } // 2. Build HTML table headers and rows const headerHtml = timeCols.map(c => `<th>${c.label}</th>`).join(''); const rows = sortedUsers.map(stat => { const isCurrent = currentCUs.includes(stat.name); const hasActions = stat.total > 0; let trStyle = ''; if (isCurrent) trStyle = 'style="background-color: #94e1cc;"'; else if (!hasActions) trStyle = 'style="color: #72777d;"'; // Generate cells for the timeline (HTML and Wikitext) const timeCells = timeCols.map(c => { let val = 0; if (cfg.mode === 'months') { val = stat.timeline[c.id] || 0; } else { // Works for both quarters and years c.months.forEach(m => val += (stat.timeline[m] || 0)); } return `<td style="font-size:0.9em; color:#54595d;">${val > 0 ? val : '-'}</td>`; }).join(''); return ` <tr ${trStyle}> <td style="text-align:left; white-space: nowrap;"> <a href="${mw.util.getUrl('Special:CheckUserLog', { cuInitiator: stat.name })}"> ${mw.html.escape(stat.name)} </a> <small style="color: #72777d;">Last: ${lastActions[stat.name]}</small> </td> <td><strong>${stat.total}</strong></td> ${timeCells} </tr>`; }).join(''); // 3. Prepare Wikitext for reports (cleaned structure) const dbName = mw.config.get('wgDBname'); let wikitext = `== CheckUser Stats for ${dbName} (${cfg.mf}/${cfg.yf} – ${cfg.mt}/${cfg.yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/CheckUserLogCount.js|CheckUserLogCount.js]]''\n`; wikitext += `\n'''Total actions in period: ${total}'''\n\n`; // Header logic: handle total only vs timeline columns const colHeader = timeCols.length ? ' !! ' + timeCols.map(c => c.label).join(' !! ') : ''; wikitext += `{| class="wikitable sortable" style="text-align:right;"\n! Initiator !! Total${colHeader}\n`; sortedUsers.forEach(stat => { let timelineStr = ''; if (timeCols.length) { timelineStr = ' || ' + timeCols.map(c => { let val = 0; if (cfg.mode === 'months') { val = stat.timeline[c.id] || 0; } else { c.months.forEach(m => val += (stat.timeline[m] || 0)); } return val; }).join(' || '); } wikitext += `|-\n| style="text-align:left; white-space:nowrap;" | [[User:${stat.name}|${stat.name}]] <small>(Last: ${lastActions[stat.name]})</small> || '''${stat.total}'''${timelineStr}\n`; }); wikitext += `|}\n`; // 4. Update the UI container $('#cu-table-container').html(` <p><strong>Total actions found: ${total}</strong></p> <div style="overflow-x: auto;"> <table class="wikitable sortable" style="width:100%; text-align:center;"> <thead> <tr> <th style="text-align:left">Initiator</th> <th>Total</th> ${headerHtml} </tr> </thead> <tbody>${rows}</tbody> </table> </div> `); $('#cu-out').val(wikitext); $('#cu-wikitext-container').show(); // Enable sorting $('table.sortable').tablesorter(); } // Initialize the UI on page load setupUI(); }); })(); 20u95c3gqv83bkkeghr9ukdxovkua3b 739287 739277 2026-04-24T19:00:33Z MrJaroslavik 44012 rv 739287 javascript text/javascript // CheckUserLogCount.js // Based on countCUStats.js - https://meta.wikimedia.org/wiki/User:Glaisher/countCUStats.js // Features: // - Counts total CheckUser actions per user within a selected date range. // - Breakdown by Total actions, Months, Quarters (Q1–Q4), or Years // - Adds an interactive control panel directly to the [[Special:CheckUserLog]] page. // - Shows a sortable table with direct links to user CheckUsers log entries. // - Automatically loads all data even for very busy periods. // - Shows the date of the very last CheckUser action for every user in the results. // - Highlights current CheckUsers with green background and identifies inactive ones. // - Generates ready-to-copy Wikitext table. // - With help of Gemini 3 (function() { 'use strict'; // Only run on the CheckUserLog special page if (mw.config.get('wgCanonicalSpecialPageName') !== 'CheckUserLog') { return; } // Helper function to pause execution (useful for preventing API limits) const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { return await api.get(params); } catch (err) { if (err && err.status === 429) { retries++; const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); await sleep(waitTime * 1000); } else { throw err; } } } throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Load required MediaWiki modules for API and UI mw.loader.using(['mediawiki.api', 'mediawiki.util', 'jquery.tablesorter']).then(async function() { const api = new mw.Api(); const now = new Date(); const currYear = now.getUTCFullYear(); const currMonth = now.getUTCMonth() + 1; // Build the user interface panel function setupUI() { // Prevent duplicate panels on the page $('.cu-stats-wrapper').remove(); // Generate year options (from 2005 to present) let yearOpts = ''; for (let y = currYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate month selection menus let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { const label = String(m).padStart(2, '0'); // Default starting month to January monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${label}</option>`; // Default ending month to current month monthOptsTo += `<option value="${m}" ${m === currMonth ? 'selected' : ''}>${label}</option>`; } // Define the HTML structure for the control panel with mode selection const html = ` <div class="cu-stats-wrapper" style="border: 1px solid #a2a9b1; padding: 15px; margin-bottom: 20px; background: #f8f9fa; border-radius: 2px;"> <h3 style="margin-top:0; border-bottom: 1px solid #a2a9b1; padding-bottom: 5px;">CheckUserLogCount.js</h3> <div style="margin-bottom: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;"> <span>From:</span> <select id="cu-year-from" style="padding: 3px;">${yearOpts}</select> <select id="cu-month-from" style="padding: 3px;">${monthOptsFrom}</select> <span>To:</span> <select id="cu-year-to" style="padding: 3px;">${yearOpts}</select> <select id="cu-month-to" style="padding: 3px;">${monthOptsTo}</select> </div> <div style="margin-bottom: 10px; display: flex; gap: 15px; font-size: 0.9em; align-items: center;"> <span style="font-weight:bold;">View mode:</span> <label><input type="radio" name="cu-mode" value="total" checked> Total only</label> <label><input type="radio" name="cu-mode" value="months"> By Months</label> <label><input type="radio" name="cu-mode" value="quarters"> By Quarters</label> <label><input type="radio" name="cu-mode" value="years"> By Years</label> <button id="cu-stats-run" class="mw-ui-button mw-ui-progressive" style="margin-left: auto;">Run</button> </div> <div id="cu-stats-results" style="display:none; margin-top: 15px; border-top: 1px solid #c8ccd1; padding-top: 10px;"> <div id="cu-loader" style="font-weight:bold; margin-bottom: 5px;"></div> <div id="cu-table-container"></div> <div id="cu-wikitext-container" style="display:none; margin-top: 20px;"> <label style="font-weight:bold; display:block; margin-bottom:5px;">Report Wikitext:</label> <textarea id="cu-out" readonly style="width:100%; height:150px; font-family:monospace; font-size:11px; padding:5px; border:1px solid #c8ccd1;"></textarea> </div> </div> </div>`; // Add the interface to the top of the page content $('#mw-content-text').prepend(html); // Handle the "Run" button click event $('#cu-stats-run').click(function() { const $btn = $(this); const params = { yf: parseInt($('#cu-year-from').val()), mf: parseInt($('#cu-month-from').val()), yt: parseInt($('#cu-year-to').val()), mt: parseInt($('#cu-month-to').val()), mode: $('input[name="cu-mode"]:checked').val() }; // Disable the button during processing $btn.prop('disabled', true).text('Running...'); // Run the audit with collected parameters runAudit(params).finally(() => { $btn.prop('disabled', false).text('Run'); }); }); } // Main logic to fetch and process CheckUser data with timeline support async function runAudit(cfg) { $('#cu-stats-results').show(); $('#cu-table-container').empty(); $('#cu-wikitext-container').hide(); // Skrytí wikitextu před novým během $('#cu-loader').text('Waiting for queue lock...').css("color", "orange").show(); // Request browser lock to prevent concurrent runs across tabs await navigator.locks.request('local_cu_count_lock', async () => { $('#cu-loader').text('Lock acquired. Connecting to API...').css("color", "#0056b3"); // Prepare dates and range limits const START = `${cfg.yf}-${String(cfg.mf).padStart(2, '0')}-01T00:00:00Z`; const lastDay = new Date(Date.UTC(cfg.yt, cfg.mt, 0)).getUTCDate(); const END = `${cfg.yt}-${String(cfg.mt).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}T23:59:59Z`; const userStats = {}; let totalActions = 0; let continueToken = ''; try { // Phase 1: Get current CheckUsers (even those with no activity) const currentCUs = []; const auRes = await robustCall(api, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); (auRes.query.allusers || []).forEach(u => { userStats[u.name] = { total: 0, timeline: {} }; currentCUs.push(u.name); }); // Phase 2: Fetch logs and build the activity timeline let finished = false; while (!finished) { const params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) params.culcontinue = continueToken; const res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; entries.forEach(e => { if (e.checkuser) { const u = e.checkuser; const monthKey = e.timestamp.substring(0, 7); // Extract YYYY-MM if (!userStats[u]) { userStats[u] = { total: 0, timeline: {} }; } userStats[u].total++; userStats[u].timeline[monthKey] = (userStats[u].timeline[monthKey] || 0) + 1; totalActions++; } }); if (res.continue && res.continue.culcontinue) { continueToken = res.continue.culcontinue; $('#cu-loader').text(`Processing logs... Found ${totalActions} actions.`); } else { finished = true; } } // Phase 3: Get the absolute last action date for each user const lastActions = {}; const usernames = Object.keys(userStats); for (let j = 0; j < usernames.length; j++) { const u = usernames[j]; $('#cu-loader').text(`Checking last activity for ${u} (${j + 1}/${usernames.length})...`); const lastRes = await robustCall(api, { action: 'query', list: 'checkuserlog', cullimit: 1, culuser: u, formatversion: 2 }); const lastEntry = (lastRes.query && lastRes.query.checkuserlog && lastRes.query.checkuserlog.entries && lastRes.query.checkuserlog.entries[0]) ? lastRes.query.checkuserlog.entries[0] : null; lastActions[u] = lastEntry ? lastEntry.timestamp.replace('T', ' ').substring(0, 16) : 'Never'; } $('#cu-loader').text('Rendering table...').css("color", "#0056b3"); renderTable(userStats, totalActions, lastActions, currentCUs, cfg); $('#cu-loader').text('Audit complete!').css("color", "green"); } catch (err) { console.error("CheckUser Stats Error:", err); const errorMsg = err.statusText || err.message || err; $('#cu-loader').text(`API Error: ${errorMsg}`).css("color", "#d33").show(); } }); } // Create the dynamic results table and Wikitext export function renderTable(userStats, total, lastActions, currentCUs, cfg) { // Sort users: highest total first, then alphabetical const sortedUsers = Object.keys(userStats).map(u => ({ name: u, ...userStats[u] })) .sort((a, b) => b.total - a.total || a.name.localeCompare(b.name)); // 1. Generate time columns based on the selected range and mode const timeCols = []; let startIter = new Date(Date.UTC(cfg.yf, cfg.mf - 1, 1)); const endIter = new Date(Date.UTC(cfg.yt, cfg.mt - 1, 1)); while (startIter <= endIter) { const k = startIter.toISOString().substring(0, 7); // "YYYY-MM" if (cfg.mode === 'months') { timeCols.push({ id: k, label: k }); } else if (cfg.mode === 'quarters') { const q = Math.floor(startIter.getUTCMonth() / 3) + 1; const qid = `${startIter.getUTCFullYear()}-Q${q}`; if (!timeCols.find(c => c.id === qid)) { timeCols.push({ id: qid, label: qid, months: [] }); } timeCols.find(c => c.id === qid).months.push(k); } else if (cfg.mode === 'years') { const yid = `${startIter.getUTCFullYear()}`; if (!timeCols.find(c => c.id === yid)) { timeCols.push({ id: yid, label: yid, months: [] }); } timeCols.find(c => c.id === yid).months.push(k); } startIter.setUTCMonth(startIter.getUTCMonth() + 1); } // 2. Build HTML table headers and rows const headerHtml = timeCols.map(c => `<th>${c.label}</th>`).join(''); const rows = sortedUsers.map(stat => { const isCurrent = currentCUs.includes(stat.name); const hasActions = stat.total > 0; let trStyle = ''; if (isCurrent) trStyle = 'style="background-color: #94e1cc;"'; else if (!hasActions) trStyle = 'style="color: #72777d;"'; // Generate cells for the timeline (HTML and Wikitext) const timeCells = timeCols.map(c => { let val = 0; if (cfg.mode === 'months') { val = stat.timeline[c.id] || 0; } else { // Works for both quarters and years c.months.forEach(m => val += (stat.timeline[m] || 0)); } return `<td style="font-size:0.9em; color:#54595d;">${val > 0 ? val : '-'}</td>`; }).join(''); return ` <tr ${trStyle}> <td style="text-align:left; white-space: nowrap;"> <a href="${mw.util.getUrl('Special:CheckUserLog', { cuInitiator: stat.name })}"> ${mw.html.escape(stat.name)} </a> <small style="color: #72777d;">Last: ${lastActions[stat.name]}</small> </td> <td><strong>${stat.total}</strong></td> ${timeCells} </tr>`; }).join(''); // 3. Prepare Wikitext for reports (cleaned structure) const dbName = mw.config.get('wgDBname'); let wikitext = `== CheckUser Stats for ${dbName} (${cfg.mf}/${cfg.yf} – ${cfg.mt}/${cfg.yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/CheckUserLogCount.js|CheckUserLogCount.js]]''\n`; wikitext += `\n'''Total actions in period: ${total}'''\n\n`; // Header logic: handle total only vs timeline columns const colHeader = timeCols.length ? ' !! ' + timeCols.map(c => c.label).join(' !! ') : ''; wikitext += `{| class="wikitable sortable" style="text-align:right;"\n! Initiator !! Total${colHeader}\n`; sortedUsers.forEach(stat => { let timelineStr = ''; if (timeCols.length) { timelineStr = ' || ' + timeCols.map(c => { let val = 0; if (cfg.mode === 'months') { val = stat.timeline[c.id] || 0; } else { c.months.forEach(m => val += (stat.timeline[m] || 0)); } return val; }).join(' || '); } wikitext += `|-\n| style="text-align:left; white-space:nowrap;" | [[User:${stat.name}|${stat.name}]] <small>(Last: ${lastActions[stat.name]})</small> || '''${stat.total}'''${timelineStr}\n`; }); wikitext += `|}\n`; // 4. Update the UI container $('#cu-table-container').html(` <p><strong>Total actions found: ${total}</strong></p> <div style="overflow-x: auto;"> <table class="wikitable sortable" style="width:100%; text-align:center;"> <thead> <tr> <th style="text-align:left">Initiator</th> <th>Total</th> ${headerHtml} </tr> </thead> <tbody>${rows}</tbody> </table> </div> `); $('#cu-out').val(wikitext); $('#cu-wikitext-container').show(); // Enable sorting $('table.sortable').tablesorter(); } // Initialize the UI on page load setupUI(); }); })(); es2dj74sshdcdh4g7njnkg63sqd8bfk User:MrJaroslavik/GlobalCheckUserStats.js 2 174967 739276 739199 2026-04-24T18:27:49Z MrJaroslavik 44012 + 739276 javascript text/javascript // GlobalCheckUserStats.js // ------------------------------------------------------- // Features: // - Global Stats: Scans logs across 800+ projects at once. // - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles. // - Deep Scan: Bypasses API limits to get full history (limit 500 entries). // - Stable: Retries 3x on rate limits so no wiki is missed. // - Error Logs: Lists failed projects (ex. because of 429) at the end of the report. // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]]. // - Output: sortable Wikitables and detailed rights change logs and CSV file // - With help of Gemini 3 // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserStats const isBlank = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isGCUS = mw.config.get('wgTitle').includes('GlobalCheckUserStats'); if (!isBlank || !isGCUS) { return; } // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Save data here so we don't have to download it twice let userCache = {}; let historyCache = {}; // This map of all wikis will be filled automatically let globalWikiMap = {}; // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known to have local CheckUsers const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Load required modules and start the script mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { // Display initial status message to the user $('#mw-content-text').html('<strong>GlobalCheckUserStats.js: Loading wiki list...</strong>'); // Download and cache the list of all Wikimedia wikis globalWikiMap = await loadGlobalWikiMap(); // Prepare the Meta-Wiki API and data containers const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); const now = new Date(); // Variables for storing audit results and script state let results = {}; let emptyWikis = []; let failedWikis = []; let scannedWikis = []; let isRunning = false; // Current UI filter settings let currentFilterMode = 'all'; let currentUserFilter = 'all'; // Prepare the year and month selection menus function setupUI() { const currentYear = now.getFullYear(); // Generate options for years from 2005 to today let yearOpts = ''; for (let y = currentYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate options for the month range (January to December) let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`; monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`; } // Set the page title and build the HTML interface $('#firstHeading').text('GlobalCheckUserStats.js'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <h3 style="margin-top:0;">Stats Range</h3> <div> From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select> &nbsp;&nbsp;&nbsp; To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-except"> All except</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-top:10px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="Example: enwiki, dewiki, commonswiki..."> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>User Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-local"> Local CheckUsers</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-steward"> Steward actions</label> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>View mode (Wikitext):</strong> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="total"> Total only</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="months" checked> Months</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="quarters"> Q1-Q4</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="years"> Years</label> </div> <div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-containers" style="display:none; margin-top:10px;"> <div style="margin-bottom:10px;"> <strong>Wikitext:</strong> <textarea id="out" style="width:100%; height:250px; font-family:monospace; font-size:12px;"></textarea> </div> <div> <strong>CSV:</strong> <textarea id="csv-out" style="width:100%; height:150px; font-family:monospace; font-size:12px;"></textarea> </div> </div> </div> `); // Listen for filter changes and button clicks $('#btn-all').on('click', () => { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').on('click', () => { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-except').on('click', () => { currentFilterMode = 'exclude'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); $('#btn-only').on('click', () => { currentFilterMode = 'include'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); // User type filters $('#u-all').on('click', () => { currentUserFilter = 'all'; }); $('#u-local').on('click', () => { currentUserFilter = 'local'; }); $('#u-steward').on('click', () => { currentUserFilter = 'steward'; }); // Help and Execution controls $('#wiki-help-trigger').on('click', () => $('#wiki-list-help').toggle()); $('#start').on('click', () => { runAudit( $('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val() ); }); $('#stop').on('click', () => { isRunning = false; }); } // Get the number of active users for a specific wiki async function fetchWikiMetrics(db) { try { // Find the correct server URL for this database const baseUrl = globalWikiMap[db]; const api = new mw.ForeignApi(baseUrl + '/w/api.php'); // Request site statistics from the API const res = await robustCall(api, { action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2 }); // Return the count of active users return { active: res.query.statistics.activeusers || 0 }; } catch (err) { // If the wiki is unreachable, return zero as a fallback return { active: 0 }; } } // Checks if the user held global roles (Steward/Staff/Ombuds) async function checkGlobalHistory(user, start, end) { try { const res = await robustCall(metaApi, { action: 'query', list: 'logevents', letype: 'gblrights', letitle: 'User:' + user, lelimit: 'max', formatversion: 2 }); const logs = res.query.logevents || []; const auditStart = new Date(start); const auditEnd = new Date(end); let status = { wasSteward: false, wasStaff: false, wasOmbuds: false }; logs.forEach(log => { const ts = new Date(log.timestamp); const p = log.params || {}; // Handle both new (named) and old (indexed) API formats const current = extractGroups(p.newGroups || p.add || p[1] || p["1"]); const old = extractGroups(p.oldGroups || p.remove || p[2] || p["0"]); if (ts >= auditStart && ts <= auditEnd) { if (current.includes('steward') || old.includes('steward')) status.wasSteward = true; if (current.includes('staff') || old.includes('staff')) status.wasStaff = true; if (current.some(g => g.includes('ombud')) || old.some(g => g.includes('ombud'))) status.wasOmbuds = true; } }); // If no changes during the period, check the state immediately before it started if (!status.wasSteward && !status.wasStaff && !status.wasOmbuds) { const priorLogs = logs .filter(log => new Date(log.timestamp) < auditStart) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (priorLogs.length > 0) { const p = priorLogs[0].params || {}; // Crucial: check both newGroups and legacy indexed params const groupsBefore = extractGroups(p.newGroups || p.add || p[1] || p["1"]); status.wasSteward = groupsBefore.includes('steward'); status.wasStaff = groupsBefore.includes('staff'); status.wasOmbuds = groupsBefore.some(g => g.includes('ombud')); } } return status; } catch (err) { return { wasSteward: false, wasStaff: false, wasOmbuds: false }; } } // Gather user information and fetch their rights logs async function fetchUserData(user, db, start, end) { const auditStart = new Date(start); const auditEnd = new Date(end); // Get current global groups from Meta-Wiki if not already cached if (!userCache[user]) { try { const globalRes = await robustCall(metaApi, { action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 }); userCache[user] = globalRes.query.globaluserinfo.groups || []; } catch (err) { userCache[user] = []; } } // Canonical variables for current global status const currentGlobalGroups = userCache[user]; const isGloballySteward = currentGlobalGroups.includes('steward'); const isGloballyStaff = currentGlobalGroups.includes('staff'); const isGloballyOmbuds = currentGlobalGroups.includes('ombuds') || currentGlobalGroups.includes('ombudsman'); // Check for global role history in cache or fetch new data if (!historyCache[user]) { historyCache[user] = await checkGlobalHistory(user, start, end); } const historyRes = historyCache[user]; let isCurrentLocal = false; // Check current local CheckUser rights on the target wiki try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Request user group information from the local API const localRes = await robustCall(localApi, { action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 }); // Parse response and verify if 'checkuser' group is present const localUserData = localRes.query.users[0]; const localGroups = (localUserData && localUserData.groups) || []; isCurrentLocal = localGroups.includes('checkuser'); } catch (err) { // Default to false if the local rights check fails isCurrentLocal = false; } // Define the target string for CentralAuth rights logs const target = 'User:' + user + '@' + db; let logText = '', isSelfAssign = false, maxDurationMins = -1, longestTimeStr = "", assignCount = 0, hasLocalRightsInPeriod = false; // Retrieve all rights change logs from Meta-Wiki let events = [], continueToken = null, finished = false; while (!finished) { let params = { action: 'query', list: 'logevents', letype: 'rights', letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2 }; // Handle API pagination with continue tokens if (continueToken) Object.assign(params, continueToken); // Execute the API call and merge the results const res = await robustCall(metaApi, params); const batch = res.query.logevents || []; events = events.concat(batch); // Check for more results to continue pagination if (res.continue) { continueToken = res.continue; } else { finished = true; } } // Initialize state variables for log processing let logEntries = [], pendingRemoved = null, lastPairedDate = null, capturedExpiry = null; // Iterate through collected events to process rights changes for (let i = 0; i < events.length; i++) { const e = events[i]; const p = e.params || {}; // Extract the performing user, stripping import prefixes if present const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user; // Determine CheckUser status before and after the event using various param keys const hadCU = extractGroups(p.oldgroups || p.oldGroups || p["0"]).includes('checkuser'); const hasCU = extractGroups(p.newgroups || p.newGroups || p["1"]).includes('checkuser'); // Extract expiry metadata if the user already has rights and they are being updated if (hasCU && hadCU) { const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') capturedExpiry = new Date(cuMeta.expiry); } } // Identify if CheckUser was added or removed in this event let cuAdded = (hasCU && !hadCU), cuRemoved = (hadCU && !hasCU); if (cuAdded || cuRemoved) { const eventDate = new Date(e.timestamp), exactTime = e.timestamp.replace('T', ' ').replace('Z', ''); const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd); if (isInPeriod) hasLocalRightsInPeriod = true; // Process events relevant to the audit timeframe or pairing logic if (isInPeriod || (eventDate > auditEnd) || (pendingRemoved && cuAdded)) { let expiryDate = capturedExpiry; capturedExpiry = null; // Check for metadata again to ensure current expiry is captured const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') expiryDate = new Date(cuMeta.expiry); } const eventBySelf = (actor === user || !actor); if (cuAdded) { const removalInPeriod = pendingRemoved && (pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd); const expiryInPeriod = expiryDate && (expiryDate >= auditStart && expiryDate <= auditEnd); // Pair adding event with its removal or expiry to calculate duration if ((isInPeriod || removalInPeriod || expiryInPeriod) && (pendingRemoved || expiryDate)) { const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate; const diffMs = Math.abs(checkDate - eventDate); const totalSecs = Math.floor(diffMs / 1000); // Intelligent rounding to handle short durations and edge cases let roundedMins = Math.round(totalSecs / 60); if (roundedMins === 0 && totalSecs > 0) roundedMins = 1; // Convert total minutes into days, hours, and remaining minutes let days = Math.floor(roundedMins / 1440); let hours = Math.floor((roundedMins % 1440) / 60); let mins = roundedMins % 60; // Build a human-readable duration string let dPart = days > 0 ? days + 'd ' : ''; let hPart = hours > 0 ? hours + 'h ' : ''; let mPart = (mins > 0 || (days === 0 && hours === 0)) ? mins + 'm' : ''; let durStr = totalSecs < 60 ? totalSecs + "s" : (dPart + hPart + mPart).trim(); // Capture the log comment for the ADDED event let reason = e.comment ? ` <small>''(${e.comment})''</small>` : ""; // Determine if this session was a Steward self-assignment if (pendingRemoved) { if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true; // Retrieve the removal comment if available let remReason = pendingRemoved.comment ? ` <small>''(${pendingRemoved.comment})''</small>` : ""; // Calculate discrepancy between manual removal and scheduled expiry let note = ""; if (expiryDate) { // Calculate planned duration in seconds and format it const pDiff = Math.abs(expiryDate - eventDate) / 1000; let rP = Math.round(pDiff / 60); let rd = Math.floor(rP / 1440), rh = Math.floor((rP % 1440) / 60), rm = rP % 60; let pD = pDiff < 60 ? Math.round(pDiff) + "s" : `${rd > 0 ? rd + 'd ' : ''}${rh > 0 ? rh + 'h ' : ''}${(rm > 0 || (rd === 0 && rh === 0)) ? rm + 'm' : ''}`.trim(); // Check if rights were removed manually earlier or later than planned let timeDiff = pendingRemoved.date - expiryDate; if (timeDiff < -60000) { note = ` - manual removal before scheduled expiry (set to ${pD})`; } else if (timeDiff > 600000) { note = ` - automatic removal not found (set to ${pD})`; } } // Finalize the paired log entry for manual removals logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}${remReason} (Duration: ${durStr}${note})`); } else if (expiryDate) { // Handle automatic expiration scenarios if (eventBySelf) isSelfAssign = true; logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | EXPIRED: ${expiryDate.toISOString().replace('T', ' ').substring(0, 19)} <small>''(Automatic)''</small> (Duration: ${durStr})`); } // Reset pending state and update audit metrics pendingRemoved = null; if (totalSecs / 60 > maxDurationMins) { maxDurationMins = totalSecs / 60; longestTimeStr = durStr.trim(); } assignCount++; lastPairedDate = eventDate; } else if (isInPeriod && !pendingRemoved && !expiryDate) { // Handle cases where rights are still active or removal log is missing if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) { logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`); lastPairedDate = eventDate; } pendingRemoved = null; } } else if (cuRemoved) { // Record standalone removal events for future pairing if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } // Store removal data to be paired with the next ADDED event pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf, comment: e.comment }; } } } } // Verify if the user held rights at the start of the period by checking prior logs if (!hasLocalRightsInPeriod && events.length > 0) { const firstBefore = events.find(ev => new Date(ev.timestamp) < auditStart); if (firstBefore) { const groupsBefore = extractGroups(firstBefore.params.newgroups || firstBefore.params.add || firstBefore.params[1] || firstBefore.params["1"]); if (groupsBefore.includes('checkuser')) hasLocalRightsInPeriod = true; } // If still not found, check the oldest available log entry for the initial state if (!hasLocalRightsInPeriod) { const chronologicallyFirst = events[events.length - 1]; const cp = chronologicallyFirst.params || {}; const oldGroups = extractGroups(cp.oldgroups || cp.oldGroups || cp.remove || cp[0] || cp["0"]); if (oldGroups.includes('checkuser')) hasLocalRightsInPeriod = true; } } // Finalize the log text assembly if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } if (logEntries.length) logText = '\n' + logEntries.join('\n'); // Logic for determining user roles and categories let roles = []; const isStewardInPeriod = isGloballySteward || historyRes.wasSteward; const isTemporaryMission = (longestTimeStr && isSelfAssign && isStewardInPeriod); // Process Global Staff and Ombudsman roles if (isGloballyStaff) roles.push("Current Staff"); else if (historyRes.wasStaff) roles.push("Former Staff"); if (isGloballyOmbuds) roles.push("Current Ombudsman"); else if (historyRes.wasOmbuds) roles.push("Former Ombudsman"); // Steward logic: Distinguish between temporary actions and permanent roles if (isTemporaryMission) { const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : ""; roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`); } else { if (isGloballySteward) roles.push("Current Steward"); else if (historyRes.wasSteward) roles.push("Former Steward"); } // Process Local CheckUser roles if (isCurrentLocal) roles.push("Current Local CheckUser"); if (hasLocalRightsInPeriod && !isCurrentLocal && !isTemporaryMission) { roles.push("Former Local CheckUser"); } // Consolidate unique roles into a formatted label let uniqueRoles = [...new Set(roles)]; let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role"; // Return final metadata object for project reporting return { role: roleLabel, log: logText, hasLocalRightsInPeriod: hasLocalRightsInPeriod, isStewardAction: isTemporaryMission, selfAssignCount: assignCount, maxDurationMins: maxDurationMins }; } // Main execution loop with Cross-Tab concurrency protection async function runAudit(yf, mf, yt, mt) { $('#status-msg').text('Waiting for other tabs to finish...').css("color", "orange"); $('#start').prop('disabled', true); // Utilize Web Locks API to prevent concurrent API requests from multiple tabs await navigator.locks.request('global_cu_audit', async () => { isRunning = true; // Clear cache and result containers for a fresh scan userCache = {}; historyCache = {}; results = {}; emptyWikis = []; failedWikis = []; scannedWikis = []; $('#status-msg').text('Lock acquired. Initializing...').css("color", "#0056b3"); $('#stop').prop('disabled', false); // Parse wiki filter input from the UI const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(s => s.trim()).filter(s => s !== "") : []; // Determine the set of wikis to process based on user filters const allAvailableWikis = Object.keys(globalWikiMap); let wikisToScan = allAvailableWikis; // Apply inclusion or exclusion logic based on current UI selection if (currentFilterMode === 'include') { wikisToScan = allAvailableWikis.filter(w => filterList.includes(w)); } else if (currentFilterMode === 'exclude') { wikisToScan = allAvailableWikis.filter(w => !filterList.includes(w)); } else if (currentFilterMode === 'localcu') { wikisToScan = allAvailableWikis.filter(w => localCUWikis.indexOf(w) !== -1); } // Validate that there are wikis to scan before proceeding if (wikisToScan.length === 0) { alert("No wikis selected for audit!"); isRunning = false; $('#start').prop('disabled', false); return; } // Initialize the progress bar UI $('#bar').attr('max', wikisToScan.length).val(0); // Define the standardized ISO timestamps for the MediaWiki API const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`; const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`; // Generate the chronological month columns for the report table const monthCols = []; let currY = parseInt(yf), currM = parseInt(mf), endY = parseInt(yt), endM = parseInt(mt); while (currY < endY || (currY === endY && currM <= endM)) { monthCols.push({ key: `${currY}-${String(currM).padStart(2, '0')}`, label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}` }); currM++; if (currM > 12) { currM = 1; currY++; } } // Iterate through each wiki project to collect CheckUser activity for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; // Allow for manual stop const db = wikisToScan[i]; scannedWikis.push(db); $('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`); let successLocal = false, continueToken = null; // Phase 1: Retrieve CheckUser logs and aggregate monthly stats while (!successLocal && isRunning) { try { const api = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); let params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) Object.assign(params, continueToken); let res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; if (entries.length) { if (!results[db]) results[db] = {}; entries.forEach(e => { const user = e.checkuser; if (!results[db][user]) results[db][user] = { total: 0, months: {} }; const mKey = e.timestamp.slice(0, 7); results[db][user].total++; results[db][user].months[mKey] = (results[db][user].months[mKey] || 0) + 1; }); } // Handle API pagination if (res.continue && isRunning) { continueToken = res.continue; } else { successLocal = true; } } catch (err) { failedWikis.push(db); successLocal = true; } } // Phase 2: Capture current CheckUsers (to include users with 0 actions) if (isRunning && !failedWikis.includes(db)) { try { const auApi = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); const auRes = await robustCall(auApi, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); const currentCUs = auRes.query.allusers || []; if (currentCUs.length > 0) { if (!results[db]) results[db] = {}; currentCUs.forEach(u => { if (!results[db][u.name]) results[db][u.name] = { total: 0, months: {} }; }); } } catch (e) { /* Fallback for local API failures */ } } // Record wikis with no CheckUser presence found if (!results[db] && !failedWikis.includes(db)) { emptyWikis.push(db); } // Update UI progress $('#bar').val(i + 1); await sleep(DELAY_MS); } // Finalize results and compile the audit report $('#status-msg').text(`Generating report...`); // Calculate global activity totals for each user across all wikis let userGlobalTotals = {}; Object.keys(results).forEach(db => { Object.keys(results[db]).forEach(user => { userGlobalTotals[user] = (userGlobalTotals[user] || 0) + results[db][user].total; }); }); // Setup report timestamps const reportDate = new Date().toISOString().split('T')[0]; const viewMode = $('input[name="cu-view-mode"]:checked').val(); const timeCols = []; if (viewMode === 'months') { monthCols.forEach(m => timeCols.push({ id: m.key, label: m.label })); } else if (viewMode !== 'total') { monthCols.forEach(m => { let label; if (viewMode === 'quarters') { const q = Math.floor((parseInt(m.key.split('-')[1]) - 1) / 3) + 1; label = `${m.key.split('-')[0]}-Q${q}`; } else { label = m.key.split('-')[0]; // Years } let col = timeCols.find(c => c.label === label); if (!col) { col = { label: label, months: [] }; timeCols.push(col); } col.months.push(m.key); }); } // Initialize Wikitext output with header information let wikitext = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/GlobalCheckUserStats.js|GlobalCheckUserStats.js]]''\n`; // Initialize CSV output with headers let csvData = "Project,User,Role,SelfAssignCount,MaxDuration_mins,TotalActions\n"; // Document applied filters in the wikitext report let filterSummary = []; // Only add wiki filter if the list is not empty, or if localcu is selected if (currentFilterMode === 'localcu') { filterSummary.push(`Wikis with local CheckUsers`); } else if (filterList.length > 0) { if (currentFilterMode === 'include') { filterSummary.push(`Only these wikis: ${filterList.join(', ')}`); } else if (currentFilterMode === 'exclude') { filterSummary.push(`All except wikis: ${filterList.join(', ')}`); } } if (currentUserFilter === 'local') filterSummary.push(`Only users: Local CheckUsers`); else if (currentUserFilter === 'steward') filterSummary.push(`Only users: Steward actions`); if (filterSummary.length > 0) { wikitext += `<br />\n''Used filters: ${filterSummary.join(' | ')}''\n`; } wikitext += `\n`; // Start building the main results table let rightsLogText = `\n== Rights Log (In Period) ==\n`; const per1kExpl = "Calculated as: (Total Actions in selected period / Current Active Users) * 1,000"; wikitext += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n! Wiki / User !! Category !! '''Total''' !! per 1k users<ref>${per1kExpl}</ref> ${timeCols.map(c => `!! ${c.label}`).join(' ')}\n`; // Process each database in alphabetical order const sortedDBs = Object.keys(results).sort(); for (const db of sortedDBs) { const metrics = await fetchWikiMetrics(db); const activeFormatted = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown'; const interwiki = getInterwikiPrefix(db); // Build wiki-specific header row let headerRow = `|-\n! colspan="${4 + timeCols.length}" style="background:#eaecf0; text-align:center;" | [[${interwiki}:|${db}]] <small style="font-weight:normal; color:#54595d;">(Active Users as of ${reportDate}: ${activeFormatted})</small> — [[${interwiki}:Special:CheckUserLog|CheckUserLog]] · [[${interwiki}:Special:ListUsers/checkuser|ListUsers/checkuser]]\n`; let projectRows = [], wikiTotal = 0, wikiMonthlyStats = {}; monthCols.forEach(col => wikiMonthlyStats[col.key] = 0); let projectUsers = Object.keys(results[db] || {}).sort(); for (const user of projectUsers) { const userData = results[db][user]; const meta = await fetchUserData(user, db, START, END); // Filter logic: Only show users active in period or holding rights if (userData.total === 0 && !meta.hasLocalRightsInPeriod) continue; const isStewardAction = meta.isStewardAction; const isLocalCU = meta.role.includes("Local CheckUser"); // Apply UI role filters if (currentUserFilter === 'local' && !isLocalCU) continue; if (currentUserFilter === 'steward' && !isStewardAction) continue; // Update totals wikiTotal += userData.total; monthCols.forEach(col => wikiMonthlyStats[col.key] += (userData.months[col.key] || 0)); // Calculate per 1k active users metric let per1k = metrics.active > 0 ? ((userData.total / metrics.active) * 1000).toFixed(1) : "0.0"; // Construct wikitext row with dynamic timeline let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${meta.role}</small> || '''${userData.total}''' || ${per1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = userData.months[col.id] || 0; else col.months.forEach(m => val += (userData.months[m] || 0)); row += ` || ${val}`; }); projectRows.push({ html: row + "\n", log: meta.log ? `'''${user}@${db}''':${meta.log}\n\n` : "" }); // Construct CSV row let baseRole = meta.role.split(' (')[0]; let sAssignCount = meta.isStewardAction ? meta.selfAssignCount : ""; let sMaxMins = meta.isStewardAction ? Math.round(meta.maxDurationMins) : ""; csvData += `${db},${user},"${baseRole}",${sAssignCount},${sMaxMins},${userData.total}\n`; } // Skip project if no users pass the filters if (projectRows.length === 0) { if (!emptyWikis.includes(db)) emptyWikis.push(db); continue; } // Construct project summary row let wikiPer1k = metrics.active > 0 ? ((wikiTotal / metrics.active) * 1000).toFixed(1) : "0.0"; let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wikiTotal} || ${wikiPer1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = wikiMonthlyStats[col.id] || 0; else col.months.forEach(m => val += (wikiMonthlyStats[m] || 0)); totalRow += ` || ${val}`; }); wikitext += headerRow + totalRow + "\n"; projectRows.forEach(r => { wikitext += r.html; if (r.log) rightsLogText += r.log; }); } // Close table and append metadata sections wikitext += `|}\n\n== Projects with 0 actions ==\n<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`; if (failedWikis.length > 0) { wikitext += `\n== API Errors / Skipped Projects ==\n<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`; } wikitext += rightsLogText + `\n\n<references />\n`; // Output results to UI and finalize state $('#out').val(wikitext); $('#csv-out').val(csvData); $('#output-containers').show(); $('#status-msg').text(`Done.`).css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } // Initialize the UI and finish script setup setupUI(); }); })(); luhmeihh5c63pbhzsirnw9qrq9j5gu5 739278 739276 2026-04-24T18:29:49Z MrJaroslavik 44012 + enter 739278 javascript text/javascript // GlobalCheckUserStats.js // ------------------------------------------------------- // Features: // - Global Stats: Scans logs across 800+ projects at once. // - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles. // - Deep Scan: Bypasses API limits to get full history (limit 500 entries). // - Stable: Retries 3x on rate limits so no wiki is missed. // - Error Logs: Lists failed projects (ex. because of 429) at the end of the report. // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]]. // - Output: sortable Wikitables and detailed rights change logs and CSV file // - With help of Gemini 3 // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserStats const isBlank = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isGCUS = mw.config.get('wgTitle').includes('GlobalCheckUserStats'); if (!isBlank || !isGCUS) { return; } // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Save data here so we don't have to download it twice let userCache = {}; let historyCache = {}; // This map of all wikis will be filled automatically let globalWikiMap = {}; // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known to have local CheckUsers const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Load required modules and start the script mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { // Display initial status message to the user $('#mw-content-text').html('<strong>GlobalCheckUserStats.js: Loading wiki list...</strong>'); // Download and cache the list of all Wikimedia wikis globalWikiMap = await loadGlobalWikiMap(); // Prepare the Meta-Wiki API and data containers const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); const now = new Date(); // Variables for storing audit results and script state let results = {}; let emptyWikis = []; let failedWikis = []; let scannedWikis = []; let isRunning = false; // Current UI filter settings let currentFilterMode = 'all'; let currentUserFilter = 'all'; // Prepare the year and month selection menus function setupUI() { const currentYear = now.getFullYear(); // Generate options for years from 2005 to today let yearOpts = ''; for (let y = currentYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate options for the month range (January to December) let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`; monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`; } // Set the page title and build the HTML interface $('#firstHeading').text('GlobalCheckUserStats.js'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <h3 style="margin-top:0;">Stats Range</h3> <div> From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select> &nbsp;&nbsp;&nbsp; To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-except"> All except</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-top:10px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="Example: enwiki, dewiki, commonswiki..."> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>User Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-local"> Local CheckUsers</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-steward"> Steward actions</label> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>View mode (Wikitext):</strong> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="total"> Total only</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="months" checked> Months</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="quarters"> Q1-Q4</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="years"> Years</label> </div> <div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-containers" style="display:none; margin-top:10px;"> <div style="margin-bottom:10px;"> <strong>Wikitext:</strong> <textarea id="out" style="width:100%; height:250px; font-family:monospace; font-size:12px;"></textarea> </div> <div> <strong>CSV:</strong> <textarea id="csv-out" style="width:100%; height:150px; font-family:monospace; font-size:12px;"></textarea> </div> </div> </div> `); // Listen for filter changes and button clicks $('#btn-all').on('click', () => { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').on('click', () => { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-except').on('click', () => { currentFilterMode = 'exclude'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); $('#btn-only').on('click', () => { currentFilterMode = 'include'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); // User type filters $('#u-all').on('click', () => { currentUserFilter = 'all'; }); $('#u-local').on('click', () => { currentUserFilter = 'local'; }); $('#u-steward').on('click', () => { currentUserFilter = 'steward'; }); // Help and Execution controls $('#wiki-help-trigger').on('click', () => $('#wiki-list-help').toggle()); // Enter $('#wiki-filter').on('keypress', function(e) { if (e.which === 13) { // 13 je kód klávesy Enter e.preventDefault(); $('#start').click(); } }); $('#start').on('click', () => { runAudit( $('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val() ); }); $('#stop').on('click', () => { isRunning = false; }); } // Get the number of active users for a specific wiki async function fetchWikiMetrics(db) { try { // Find the correct server URL for this database const baseUrl = globalWikiMap[db]; const api = new mw.ForeignApi(baseUrl + '/w/api.php'); // Request site statistics from the API const res = await robustCall(api, { action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2 }); // Return the count of active users return { active: res.query.statistics.activeusers || 0 }; } catch (err) { // If the wiki is unreachable, return zero as a fallback return { active: 0 }; } } // Checks if the user held global roles (Steward/Staff/Ombuds) async function checkGlobalHistory(user, start, end) { try { const res = await robustCall(metaApi, { action: 'query', list: 'logevents', letype: 'gblrights', letitle: 'User:' + user, lelimit: 'max', formatversion: 2 }); const logs = res.query.logevents || []; const auditStart = new Date(start); const auditEnd = new Date(end); let status = { wasSteward: false, wasStaff: false, wasOmbuds: false }; logs.forEach(log => { const ts = new Date(log.timestamp); const p = log.params || {}; // Handle both new (named) and old (indexed) API formats const current = extractGroups(p.newGroups || p.add || p[1] || p["1"]); const old = extractGroups(p.oldGroups || p.remove || p[2] || p["0"]); if (ts >= auditStart && ts <= auditEnd) { if (current.includes('steward') || old.includes('steward')) status.wasSteward = true; if (current.includes('staff') || old.includes('staff')) status.wasStaff = true; if (current.some(g => g.includes('ombud')) || old.some(g => g.includes('ombud'))) status.wasOmbuds = true; } }); // If no changes during the period, check the state immediately before it started if (!status.wasSteward && !status.wasStaff && !status.wasOmbuds) { const priorLogs = logs .filter(log => new Date(log.timestamp) < auditStart) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (priorLogs.length > 0) { const p = priorLogs[0].params || {}; // Crucial: check both newGroups and legacy indexed params const groupsBefore = extractGroups(p.newGroups || p.add || p[1] || p["1"]); status.wasSteward = groupsBefore.includes('steward'); status.wasStaff = groupsBefore.includes('staff'); status.wasOmbuds = groupsBefore.some(g => g.includes('ombud')); } } return status; } catch (err) { return { wasSteward: false, wasStaff: false, wasOmbuds: false }; } } // Gather user information and fetch their rights logs async function fetchUserData(user, db, start, end) { const auditStart = new Date(start); const auditEnd = new Date(end); // Get current global groups from Meta-Wiki if not already cached if (!userCache[user]) { try { const globalRes = await robustCall(metaApi, { action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 }); userCache[user] = globalRes.query.globaluserinfo.groups || []; } catch (err) { userCache[user] = []; } } // Canonical variables for current global status const currentGlobalGroups = userCache[user]; const isGloballySteward = currentGlobalGroups.includes('steward'); const isGloballyStaff = currentGlobalGroups.includes('staff'); const isGloballyOmbuds = currentGlobalGroups.includes('ombuds') || currentGlobalGroups.includes('ombudsman'); // Check for global role history in cache or fetch new data if (!historyCache[user]) { historyCache[user] = await checkGlobalHistory(user, start, end); } const historyRes = historyCache[user]; let isCurrentLocal = false; // Check current local CheckUser rights on the target wiki try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Request user group information from the local API const localRes = await robustCall(localApi, { action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 }); // Parse response and verify if 'checkuser' group is present const localUserData = localRes.query.users[0]; const localGroups = (localUserData && localUserData.groups) || []; isCurrentLocal = localGroups.includes('checkuser'); } catch (err) { // Default to false if the local rights check fails isCurrentLocal = false; } // Define the target string for CentralAuth rights logs const target = 'User:' + user + '@' + db; let logText = '', isSelfAssign = false, maxDurationMins = -1, longestTimeStr = "", assignCount = 0, hasLocalRightsInPeriod = false; // Retrieve all rights change logs from Meta-Wiki let events = [], continueToken = null, finished = false; while (!finished) { let params = { action: 'query', list: 'logevents', letype: 'rights', letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2 }; // Handle API pagination with continue tokens if (continueToken) Object.assign(params, continueToken); // Execute the API call and merge the results const res = await robustCall(metaApi, params); const batch = res.query.logevents || []; events = events.concat(batch); // Check for more results to continue pagination if (res.continue) { continueToken = res.continue; } else { finished = true; } } // Initialize state variables for log processing let logEntries = [], pendingRemoved = null, lastPairedDate = null, capturedExpiry = null; // Iterate through collected events to process rights changes for (let i = 0; i < events.length; i++) { const e = events[i]; const p = e.params || {}; // Extract the performing user, stripping import prefixes if present const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user; // Determine CheckUser status before and after the event using various param keys const hadCU = extractGroups(p.oldgroups || p.oldGroups || p["0"]).includes('checkuser'); const hasCU = extractGroups(p.newgroups || p.newGroups || p["1"]).includes('checkuser'); // Extract expiry metadata if the user already has rights and they are being updated if (hasCU && hadCU) { const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') capturedExpiry = new Date(cuMeta.expiry); } } // Identify if CheckUser was added or removed in this event let cuAdded = (hasCU && !hadCU), cuRemoved = (hadCU && !hasCU); if (cuAdded || cuRemoved) { const eventDate = new Date(e.timestamp), exactTime = e.timestamp.replace('T', ' ').replace('Z', ''); const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd); if (isInPeriod) hasLocalRightsInPeriod = true; // Process events relevant to the audit timeframe or pairing logic if (isInPeriod || (eventDate > auditEnd) || (pendingRemoved && cuAdded)) { let expiryDate = capturedExpiry; capturedExpiry = null; // Check for metadata again to ensure current expiry is captured const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') expiryDate = new Date(cuMeta.expiry); } const eventBySelf = (actor === user || !actor); if (cuAdded) { const removalInPeriod = pendingRemoved && (pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd); const expiryInPeriod = expiryDate && (expiryDate >= auditStart && expiryDate <= auditEnd); // Pair adding event with its removal or expiry to calculate duration if ((isInPeriod || removalInPeriod || expiryInPeriod) && (pendingRemoved || expiryDate)) { const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate; const diffMs = Math.abs(checkDate - eventDate); const totalSecs = Math.floor(diffMs / 1000); // Intelligent rounding to handle short durations and edge cases let roundedMins = Math.round(totalSecs / 60); if (roundedMins === 0 && totalSecs > 0) roundedMins = 1; // Convert total minutes into days, hours, and remaining minutes let days = Math.floor(roundedMins / 1440); let hours = Math.floor((roundedMins % 1440) / 60); let mins = roundedMins % 60; // Build a human-readable duration string let dPart = days > 0 ? days + 'd ' : ''; let hPart = hours > 0 ? hours + 'h ' : ''; let mPart = (mins > 0 || (days === 0 && hours === 0)) ? mins + 'm' : ''; let durStr = totalSecs < 60 ? totalSecs + "s" : (dPart + hPart + mPart).trim(); // Capture the log comment for the ADDED event let reason = e.comment ? ` <small>''(${e.comment})''</small>` : ""; // Determine if this session was a Steward self-assignment if (pendingRemoved) { if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true; // Retrieve the removal comment if available let remReason = pendingRemoved.comment ? ` <small>''(${pendingRemoved.comment})''</small>` : ""; // Calculate discrepancy between manual removal and scheduled expiry let note = ""; if (expiryDate) { // Calculate planned duration in seconds and format it const pDiff = Math.abs(expiryDate - eventDate) / 1000; let rP = Math.round(pDiff / 60); let rd = Math.floor(rP / 1440), rh = Math.floor((rP % 1440) / 60), rm = rP % 60; let pD = pDiff < 60 ? Math.round(pDiff) + "s" : `${rd > 0 ? rd + 'd ' : ''}${rh > 0 ? rh + 'h ' : ''}${(rm > 0 || (rd === 0 && rh === 0)) ? rm + 'm' : ''}`.trim(); // Check if rights were removed manually earlier or later than planned let timeDiff = pendingRemoved.date - expiryDate; if (timeDiff < -60000) { note = ` - manual removal before scheduled expiry (set to ${pD})`; } else if (timeDiff > 600000) { note = ` - automatic removal not found (set to ${pD})`; } } // Finalize the paired log entry for manual removals logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}${remReason} (Duration: ${durStr}${note})`); } else if (expiryDate) { // Handle automatic expiration scenarios if (eventBySelf) isSelfAssign = true; logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | EXPIRED: ${expiryDate.toISOString().replace('T', ' ').substring(0, 19)} <small>''(Automatic)''</small> (Duration: ${durStr})`); } // Reset pending state and update audit metrics pendingRemoved = null; if (totalSecs / 60 > maxDurationMins) { maxDurationMins = totalSecs / 60; longestTimeStr = durStr.trim(); } assignCount++; lastPairedDate = eventDate; } else if (isInPeriod && !pendingRemoved && !expiryDate) { // Handle cases where rights are still active or removal log is missing if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) { logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`); lastPairedDate = eventDate; } pendingRemoved = null; } } else if (cuRemoved) { // Record standalone removal events for future pairing if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } // Store removal data to be paired with the next ADDED event pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf, comment: e.comment }; } } } } // Verify if the user held rights at the start of the period by checking prior logs if (!hasLocalRightsInPeriod && events.length > 0) { const firstBefore = events.find(ev => new Date(ev.timestamp) < auditStart); if (firstBefore) { const groupsBefore = extractGroups(firstBefore.params.newgroups || firstBefore.params.add || firstBefore.params[1] || firstBefore.params["1"]); if (groupsBefore.includes('checkuser')) hasLocalRightsInPeriod = true; } // If still not found, check the oldest available log entry for the initial state if (!hasLocalRightsInPeriod) { const chronologicallyFirst = events[events.length - 1]; const cp = chronologicallyFirst.params || {}; const oldGroups = extractGroups(cp.oldgroups || cp.oldGroups || cp.remove || cp[0] || cp["0"]); if (oldGroups.includes('checkuser')) hasLocalRightsInPeriod = true; } } // Finalize the log text assembly if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } if (logEntries.length) logText = '\n' + logEntries.join('\n'); // Logic for determining user roles and categories let roles = []; const isStewardInPeriod = isGloballySteward || historyRes.wasSteward; const isTemporaryMission = (longestTimeStr && isSelfAssign && isStewardInPeriod); // Process Global Staff and Ombudsman roles if (isGloballyStaff) roles.push("Current Staff"); else if (historyRes.wasStaff) roles.push("Former Staff"); if (isGloballyOmbuds) roles.push("Current Ombudsman"); else if (historyRes.wasOmbuds) roles.push("Former Ombudsman"); // Steward logic: Distinguish between temporary actions and permanent roles if (isTemporaryMission) { const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : ""; roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`); } else { if (isGloballySteward) roles.push("Current Steward"); else if (historyRes.wasSteward) roles.push("Former Steward"); } // Process Local CheckUser roles if (isCurrentLocal) roles.push("Current Local CheckUser"); if (hasLocalRightsInPeriod && !isCurrentLocal && !isTemporaryMission) { roles.push("Former Local CheckUser"); } // Consolidate unique roles into a formatted label let uniqueRoles = [...new Set(roles)]; let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role"; // Return final metadata object for project reporting return { role: roleLabel, log: logText, hasLocalRightsInPeriod: hasLocalRightsInPeriod, isStewardAction: isTemporaryMission, selfAssignCount: assignCount, maxDurationMins: maxDurationMins }; } // Main execution loop with Cross-Tab concurrency protection async function runAudit(yf, mf, yt, mt) { $('#status-msg').text('Waiting for other tabs to finish...').css("color", "orange"); $('#start').prop('disabled', true); // Utilize Web Locks API to prevent concurrent API requests from multiple tabs await navigator.locks.request('global_cu_audit', async () => { isRunning = true; // Clear cache and result containers for a fresh scan userCache = {}; historyCache = {}; results = {}; emptyWikis = []; failedWikis = []; scannedWikis = []; $('#status-msg').text('Lock acquired. Initializing...').css("color", "#0056b3"); $('#stop').prop('disabled', false); // Parse wiki filter input from the UI const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(s => s.trim()).filter(s => s !== "") : []; // Determine the set of wikis to process based on user filters const allAvailableWikis = Object.keys(globalWikiMap); let wikisToScan = allAvailableWikis; // Apply inclusion or exclusion logic based on current UI selection if (currentFilterMode === 'include') { wikisToScan = allAvailableWikis.filter(w => filterList.includes(w)); } else if (currentFilterMode === 'exclude') { wikisToScan = allAvailableWikis.filter(w => !filterList.includes(w)); } else if (currentFilterMode === 'localcu') { wikisToScan = allAvailableWikis.filter(w => localCUWikis.indexOf(w) !== -1); } // Validate that there are wikis to scan before proceeding if (wikisToScan.length === 0) { alert("No wikis selected for audit!"); isRunning = false; $('#start').prop('disabled', false); return; } // Initialize the progress bar UI $('#bar').attr('max', wikisToScan.length).val(0); // Define the standardized ISO timestamps for the MediaWiki API const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`; const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`; // Generate the chronological month columns for the report table const monthCols = []; let currY = parseInt(yf), currM = parseInt(mf), endY = parseInt(yt), endM = parseInt(mt); while (currY < endY || (currY === endY && currM <= endM)) { monthCols.push({ key: `${currY}-${String(currM).padStart(2, '0')}`, label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}` }); currM++; if (currM > 12) { currM = 1; currY++; } } // Iterate through each wiki project to collect CheckUser activity for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; // Allow for manual stop const db = wikisToScan[i]; scannedWikis.push(db); $('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`); let successLocal = false, continueToken = null; // Phase 1: Retrieve CheckUser logs and aggregate monthly stats while (!successLocal && isRunning) { try { const api = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); let params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) Object.assign(params, continueToken); let res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; if (entries.length) { if (!results[db]) results[db] = {}; entries.forEach(e => { const user = e.checkuser; if (!results[db][user]) results[db][user] = { total: 0, months: {} }; const mKey = e.timestamp.slice(0, 7); results[db][user].total++; results[db][user].months[mKey] = (results[db][user].months[mKey] || 0) + 1; }); } // Handle API pagination if (res.continue && isRunning) { continueToken = res.continue; } else { successLocal = true; } } catch (err) { failedWikis.push(db); successLocal = true; } } // Phase 2: Capture current CheckUsers (to include users with 0 actions) if (isRunning && !failedWikis.includes(db)) { try { const auApi = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); const auRes = await robustCall(auApi, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); const currentCUs = auRes.query.allusers || []; if (currentCUs.length > 0) { if (!results[db]) results[db] = {}; currentCUs.forEach(u => { if (!results[db][u.name]) results[db][u.name] = { total: 0, months: {} }; }); } } catch (e) { /* Fallback for local API failures */ } } // Record wikis with no CheckUser presence found if (!results[db] && !failedWikis.includes(db)) { emptyWikis.push(db); } // Update UI progress $('#bar').val(i + 1); await sleep(DELAY_MS); } // Finalize results and compile the audit report $('#status-msg').text(`Generating report...`); // Calculate global activity totals for each user across all wikis let userGlobalTotals = {}; Object.keys(results).forEach(db => { Object.keys(results[db]).forEach(user => { userGlobalTotals[user] = (userGlobalTotals[user] || 0) + results[db][user].total; }); }); // Setup report timestamps const reportDate = new Date().toISOString().split('T')[0]; const viewMode = $('input[name="cu-view-mode"]:checked').val(); const timeCols = []; if (viewMode === 'months') { monthCols.forEach(m => timeCols.push({ id: m.key, label: m.label })); } else if (viewMode !== 'total') { monthCols.forEach(m => { let label; if (viewMode === 'quarters') { const q = Math.floor((parseInt(m.key.split('-')[1]) - 1) / 3) + 1; label = `${m.key.split('-')[0]}-Q${q}`; } else { label = m.key.split('-')[0]; // Years } let col = timeCols.find(c => c.label === label); if (!col) { col = { label: label, months: [] }; timeCols.push(col); } col.months.push(m.key); }); } // Initialize Wikitext output with header information let wikitext = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/GlobalCheckUserStats.js|GlobalCheckUserStats.js]]''\n`; // Initialize CSV output with headers let csvData = "Project,User,Role,SelfAssignCount,MaxDuration_mins,TotalActions\n"; // Document applied filters in the wikitext report let filterSummary = []; // Only add wiki filter if the list is not empty, or if localcu is selected if (currentFilterMode === 'localcu') { filterSummary.push(`Wikis with local CheckUsers`); } else if (filterList.length > 0) { if (currentFilterMode === 'include') { filterSummary.push(`Only these wikis: ${filterList.join(', ')}`); } else if (currentFilterMode === 'exclude') { filterSummary.push(`All except wikis: ${filterList.join(', ')}`); } } if (currentUserFilter === 'local') filterSummary.push(`Only users: Local CheckUsers`); else if (currentUserFilter === 'steward') filterSummary.push(`Only users: Steward actions`); if (filterSummary.length > 0) { wikitext += `<br />\n''Used filters: ${filterSummary.join(' | ')}''\n`; } wikitext += `\n`; // Start building the main results table let rightsLogText = `\n== Rights Log (In Period) ==\n`; const per1kExpl = "Calculated as: (Total Actions in selected period / Current Active Users) * 1,000"; wikitext += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n! Wiki / User !! Category !! '''Total''' !! per 1k users<ref>${per1kExpl}</ref> ${timeCols.map(c => `!! ${c.label}`).join(' ')}\n`; // Process each database in alphabetical order const sortedDBs = Object.keys(results).sort(); for (const db of sortedDBs) { const metrics = await fetchWikiMetrics(db); const activeFormatted = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown'; const interwiki = getInterwikiPrefix(db); // Build wiki-specific header row let headerRow = `|-\n! colspan="${4 + timeCols.length}" style="background:#eaecf0; text-align:center;" | [[${interwiki}:|${db}]] <small style="font-weight:normal; color:#54595d;">(Active Users as of ${reportDate}: ${activeFormatted})</small> — [[${interwiki}:Special:CheckUserLog|CheckUserLog]] · [[${interwiki}:Special:ListUsers/checkuser|ListUsers/checkuser]]\n`; let projectRows = [], wikiTotal = 0, wikiMonthlyStats = {}; monthCols.forEach(col => wikiMonthlyStats[col.key] = 0); let projectUsers = Object.keys(results[db] || {}).sort(); for (const user of projectUsers) { const userData = results[db][user]; const meta = await fetchUserData(user, db, START, END); // Filter logic: Only show users active in period or holding rights if (userData.total === 0 && !meta.hasLocalRightsInPeriod) continue; const isStewardAction = meta.isStewardAction; const isLocalCU = meta.role.includes("Local CheckUser"); // Apply UI role filters if (currentUserFilter === 'local' && !isLocalCU) continue; if (currentUserFilter === 'steward' && !isStewardAction) continue; // Update totals wikiTotal += userData.total; monthCols.forEach(col => wikiMonthlyStats[col.key] += (userData.months[col.key] || 0)); // Calculate per 1k active users metric let per1k = metrics.active > 0 ? ((userData.total / metrics.active) * 1000).toFixed(1) : "0.0"; // Construct wikitext row with dynamic timeline let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${meta.role}</small> || '''${userData.total}''' || ${per1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = userData.months[col.id] || 0; else col.months.forEach(m => val += (userData.months[m] || 0)); row += ` || ${val}`; }); projectRows.push({ html: row + "\n", log: meta.log ? `'''${user}@${db}''':${meta.log}\n\n` : "" }); // Construct CSV row let baseRole = meta.role.split(' (')[0]; let sAssignCount = meta.isStewardAction ? meta.selfAssignCount : ""; let sMaxMins = meta.isStewardAction ? Math.round(meta.maxDurationMins) : ""; csvData += `${db},${user},"${baseRole}",${sAssignCount},${sMaxMins},${userData.total}\n`; } // Skip project if no users pass the filters if (projectRows.length === 0) { if (!emptyWikis.includes(db)) emptyWikis.push(db); continue; } // Construct project summary row let wikiPer1k = metrics.active > 0 ? ((wikiTotal / metrics.active) * 1000).toFixed(1) : "0.0"; let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wikiTotal} || ${wikiPer1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = wikiMonthlyStats[col.id] || 0; else col.months.forEach(m => val += (wikiMonthlyStats[m] || 0)); totalRow += ` || ${val}`; }); wikitext += headerRow + totalRow + "\n"; projectRows.forEach(r => { wikitext += r.html; if (r.log) rightsLogText += r.log; }); } // Close table and append metadata sections wikitext += `|}\n\n== Projects with 0 actions ==\n<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`; if (failedWikis.length > 0) { wikitext += `\n== API Errors / Skipped Projects ==\n<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`; } wikitext += rightsLogText + `\n\n<references />\n`; // Output results to UI and finalize state $('#out').val(wikitext); $('#csv-out').val(csvData); $('#output-containers').show(); $('#status-msg').text(`Done.`).css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } // Initialize the UI and finish script setup setupUI(); }); })(); li8h92pxde447p1d5gdnxc0lfa81h0p 739283 739278 2026-04-24T18:57:35Z MrJaroslavik 44012 enter 739283 javascript text/javascript // GlobalCheckUserStats.js // ------------------------------------------------------- // Features: // - Global Stats: Scans logs across 800+ projects at once. // - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles. // - Deep Scan: Bypasses API limits to get full history (limit 500 entries). // - Stable: Retries 3x on rate limits so no wiki is missed. // - Error Logs: Lists failed projects (ex. because of 429) at the end of the report. // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]]. // - Output: sortable Wikitables and detailed rights change logs and CSV file // - With help of Gemini 3 // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserStats const isBlank = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isGCUS = mw.config.get('wgTitle').includes('GlobalCheckUserStats'); if (!isBlank || !isGCUS) { return; } // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Save data here so we don't have to download it twice let userCache = {}; let historyCache = {}; // This map of all wikis will be filled automatically let globalWikiMap = {}; // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known to have local CheckUsers const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Load required modules and start the script mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { // Display initial status message to the user $('#mw-content-text').html('<strong>GlobalCheckUserStats.js: Loading wiki list...</strong>'); // Download and cache the list of all Wikimedia wikis globalWikiMap = await loadGlobalWikiMap(); // Prepare the Meta-Wiki API and data containers const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); const now = new Date(); // Variables for storing audit results and script state let results = {}; let emptyWikis = []; let failedWikis = []; let scannedWikis = []; let isRunning = false; // Current UI filter settings let currentFilterMode = 'all'; let currentUserFilter = 'all'; // Prepare the year and month selection menus function setupUI() { const currentYear = now.getFullYear(); // Generate options for years from 2005 to today let yearOpts = ''; for (let y = currentYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate options for the month range (January to December) let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`; monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`; } // Set the page title and build the HTML interface $('#firstHeading').text('GlobalCheckUserStats.js'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <h3 style="margin-top:0;">Stats Range</h3> <div> From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select> &nbsp;&nbsp;&nbsp; To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-except"> All except</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-top:10px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="Example: enwiki, dewiki, commonswiki..."> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>User Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-local"> Local CheckUsers</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-steward"> Steward actions</label> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>View mode (Wikitext):</strong> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="total"> Total only</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="months" checked> Months</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="quarters"> Q1-Q4</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="years"> Years</label> </div> <div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-containers" style="display:none; margin-top:10px;"> <div style="margin-bottom:10px;"> <strong>Wikitext:</strong> <textarea id="out" style="width:100%; height:250px; font-family:monospace; font-size:12px;"></textarea> </div> <div> <strong>CSV:</strong> <textarea id="csv-out" style="width:100%; height:150px; font-family:monospace; font-size:12px;"></textarea> </div> </div> </div> `); // Listen for filter changes and button clicks $('#btn-all').on('click', () => { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').on('click', () => { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-except').on('click', () => { currentFilterMode = 'exclude'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); $('#btn-only').on('click', () => { currentFilterMode = 'include'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); // User type filters $('#u-all').on('click', () => { currentUserFilter = 'all'; }); $('#u-local').on('click', () => { currentUserFilter = 'local'; }); $('#u-steward').on('click', () => { currentUserFilter = 'steward'; }); // Help and Execution controls $('#wiki-help-trigger').on('click', () => $('#wiki-list-help').toggle()); // Enter $('#wiki-filter').on('keypress', function(e) { if (e.which === 13) { // 13 je kód klávesy Enter e.preventDefault(); $('#start').click(); } }); $('#start').on('click', () => { runAudit( $('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val() ); }); // Enter $(document).on('keypress', '#wiki-filter', function(e) { if (e.which === 13) { e.preventDefault(); $('#start').click(); } }); $('#stop').on('click', () => { isRunning = false; }); } // Get the number of active users for a specific wiki async function fetchWikiMetrics(db) { try { // Find the correct server URL for this database const baseUrl = globalWikiMap[db]; const api = new mw.ForeignApi(baseUrl + '/w/api.php'); // Request site statistics from the API const res = await robustCall(api, { action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2 }); // Return the count of active users return { active: res.query.statistics.activeusers || 0 }; } catch (err) { // If the wiki is unreachable, return zero as a fallback return { active: 0 }; } } // Checks if the user held global roles (Steward/Staff/Ombuds) async function checkGlobalHistory(user, start, end) { try { const res = await robustCall(metaApi, { action: 'query', list: 'logevents', letype: 'gblrights', letitle: 'User:' + user, lelimit: 'max', formatversion: 2 }); const logs = res.query.logevents || []; const auditStart = new Date(start); const auditEnd = new Date(end); let status = { wasSteward: false, wasStaff: false, wasOmbuds: false }; logs.forEach(log => { const ts = new Date(log.timestamp); const p = log.params || {}; // Handle both new (named) and old (indexed) API formats const current = extractGroups(p.newGroups || p.add || p[1] || p["1"]); const old = extractGroups(p.oldGroups || p.remove || p[2] || p["0"]); if (ts >= auditStart && ts <= auditEnd) { if (current.includes('steward') || old.includes('steward')) status.wasSteward = true; if (current.includes('staff') || old.includes('staff')) status.wasStaff = true; if (current.some(g => g.includes('ombud')) || old.some(g => g.includes('ombud'))) status.wasOmbuds = true; } }); // If no changes during the period, check the state immediately before it started if (!status.wasSteward && !status.wasStaff && !status.wasOmbuds) { const priorLogs = logs .filter(log => new Date(log.timestamp) < auditStart) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (priorLogs.length > 0) { const p = priorLogs[0].params || {}; // Crucial: check both newGroups and legacy indexed params const groupsBefore = extractGroups(p.newGroups || p.add || p[1] || p["1"]); status.wasSteward = groupsBefore.includes('steward'); status.wasStaff = groupsBefore.includes('staff'); status.wasOmbuds = groupsBefore.some(g => g.includes('ombud')); } } return status; } catch (err) { return { wasSteward: false, wasStaff: false, wasOmbuds: false }; } } // Gather user information and fetch their rights logs async function fetchUserData(user, db, start, end) { const auditStart = new Date(start); const auditEnd = new Date(end); // Get current global groups from Meta-Wiki if not already cached if (!userCache[user]) { try { const globalRes = await robustCall(metaApi, { action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 }); userCache[user] = globalRes.query.globaluserinfo.groups || []; } catch (err) { userCache[user] = []; } } // Canonical variables for current global status const currentGlobalGroups = userCache[user]; const isGloballySteward = currentGlobalGroups.includes('steward'); const isGloballyStaff = currentGlobalGroups.includes('staff'); const isGloballyOmbuds = currentGlobalGroups.includes('ombuds') || currentGlobalGroups.includes('ombudsman'); // Check for global role history in cache or fetch new data if (!historyCache[user]) { historyCache[user] = await checkGlobalHistory(user, start, end); } const historyRes = historyCache[user]; let isCurrentLocal = false; // Check current local CheckUser rights on the target wiki try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Request user group information from the local API const localRes = await robustCall(localApi, { action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 }); // Parse response and verify if 'checkuser' group is present const localUserData = localRes.query.users[0]; const localGroups = (localUserData && localUserData.groups) || []; isCurrentLocal = localGroups.includes('checkuser'); } catch (err) { // Default to false if the local rights check fails isCurrentLocal = false; } // Define the target string for CentralAuth rights logs const target = 'User:' + user + '@' + db; let logText = '', isSelfAssign = false, maxDurationMins = -1, longestTimeStr = "", assignCount = 0, hasLocalRightsInPeriod = false; // Retrieve all rights change logs from Meta-Wiki let events = [], continueToken = null, finished = false; while (!finished) { let params = { action: 'query', list: 'logevents', letype: 'rights', letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2 }; // Handle API pagination with continue tokens if (continueToken) Object.assign(params, continueToken); // Execute the API call and merge the results const res = await robustCall(metaApi, params); const batch = res.query.logevents || []; events = events.concat(batch); // Check for more results to continue pagination if (res.continue) { continueToken = res.continue; } else { finished = true; } } // Initialize state variables for log processing let logEntries = [], pendingRemoved = null, lastPairedDate = null, capturedExpiry = null; // Iterate through collected events to process rights changes for (let i = 0; i < events.length; i++) { const e = events[i]; const p = e.params || {}; // Extract the performing user, stripping import prefixes if present const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user; // Determine CheckUser status before and after the event using various param keys const hadCU = extractGroups(p.oldgroups || p.oldGroups || p["0"]).includes('checkuser'); const hasCU = extractGroups(p.newgroups || p.newGroups || p["1"]).includes('checkuser'); // Extract expiry metadata if the user already has rights and they are being updated if (hasCU && hadCU) { const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') capturedExpiry = new Date(cuMeta.expiry); } } // Identify if CheckUser was added or removed in this event let cuAdded = (hasCU && !hadCU), cuRemoved = (hadCU && !hasCU); if (cuAdded || cuRemoved) { const eventDate = new Date(e.timestamp), exactTime = e.timestamp.replace('T', ' ').replace('Z', ''); const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd); if (isInPeriod) hasLocalRightsInPeriod = true; // Process events relevant to the audit timeframe or pairing logic if (isInPeriod || (eventDate > auditEnd) || (pendingRemoved && cuAdded)) { let expiryDate = capturedExpiry; capturedExpiry = null; // Check for metadata again to ensure current expiry is captured const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') expiryDate = new Date(cuMeta.expiry); } const eventBySelf = (actor === user || !actor); if (cuAdded) { const removalInPeriod = pendingRemoved && (pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd); const expiryInPeriod = expiryDate && (expiryDate >= auditStart && expiryDate <= auditEnd); // Pair adding event with its removal or expiry to calculate duration if ((isInPeriod || removalInPeriod || expiryInPeriod) && (pendingRemoved || expiryDate)) { const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate; const diffMs = Math.abs(checkDate - eventDate); const totalSecs = Math.floor(diffMs / 1000); // Intelligent rounding to handle short durations and edge cases let roundedMins = Math.round(totalSecs / 60); if (roundedMins === 0 && totalSecs > 0) roundedMins = 1; // Convert total minutes into days, hours, and remaining minutes let days = Math.floor(roundedMins / 1440); let hours = Math.floor((roundedMins % 1440) / 60); let mins = roundedMins % 60; // Build a human-readable duration string let dPart = days > 0 ? days + 'd ' : ''; let hPart = hours > 0 ? hours + 'h ' : ''; let mPart = (mins > 0 || (days === 0 && hours === 0)) ? mins + 'm' : ''; let durStr = totalSecs < 60 ? totalSecs + "s" : (dPart + hPart + mPart).trim(); // Capture the log comment for the ADDED event let reason = e.comment ? ` <small>''(${e.comment})''</small>` : ""; // Determine if this session was a Steward self-assignment if (pendingRemoved) { if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true; // Retrieve the removal comment if available let remReason = pendingRemoved.comment ? ` <small>''(${pendingRemoved.comment})''</small>` : ""; // Calculate discrepancy between manual removal and scheduled expiry let note = ""; if (expiryDate) { // Calculate planned duration in seconds and format it const pDiff = Math.abs(expiryDate - eventDate) / 1000; let rP = Math.round(pDiff / 60); let rd = Math.floor(rP / 1440), rh = Math.floor((rP % 1440) / 60), rm = rP % 60; let pD = pDiff < 60 ? Math.round(pDiff) + "s" : `${rd > 0 ? rd + 'd ' : ''}${rh > 0 ? rh + 'h ' : ''}${(rm > 0 || (rd === 0 && rh === 0)) ? rm + 'm' : ''}`.trim(); // Check if rights were removed manually earlier or later than planned let timeDiff = pendingRemoved.date - expiryDate; if (timeDiff < -60000) { note = ` - manual removal before scheduled expiry (set to ${pD})`; } else if (timeDiff > 600000) { note = ` - automatic removal not found (set to ${pD})`; } } // Finalize the paired log entry for manual removals logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}${remReason} (Duration: ${durStr}${note})`); } else if (expiryDate) { // Handle automatic expiration scenarios if (eventBySelf) isSelfAssign = true; logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | EXPIRED: ${expiryDate.toISOString().replace('T', ' ').substring(0, 19)} <small>''(Automatic)''</small> (Duration: ${durStr})`); } // Reset pending state and update audit metrics pendingRemoved = null; if (totalSecs / 60 > maxDurationMins) { maxDurationMins = totalSecs / 60; longestTimeStr = durStr.trim(); } assignCount++; lastPairedDate = eventDate; } else if (isInPeriod && !pendingRemoved && !expiryDate) { // Handle cases where rights are still active or removal log is missing if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) { logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`); lastPairedDate = eventDate; } pendingRemoved = null; } } else if (cuRemoved) { // Record standalone removal events for future pairing if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } // Store removal data to be paired with the next ADDED event pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf, comment: e.comment }; } } } } // Verify if the user held rights at the start of the period by checking prior logs if (!hasLocalRightsInPeriod && events.length > 0) { const firstBefore = events.find(ev => new Date(ev.timestamp) < auditStart); if (firstBefore) { const groupsBefore = extractGroups(firstBefore.params.newgroups || firstBefore.params.add || firstBefore.params[1] || firstBefore.params["1"]); if (groupsBefore.includes('checkuser')) hasLocalRightsInPeriod = true; } // If still not found, check the oldest available log entry for the initial state if (!hasLocalRightsInPeriod) { const chronologicallyFirst = events[events.length - 1]; const cp = chronologicallyFirst.params || {}; const oldGroups = extractGroups(cp.oldgroups || cp.oldGroups || cp.remove || cp[0] || cp["0"]); if (oldGroups.includes('checkuser')) hasLocalRightsInPeriod = true; } } // Finalize the log text assembly if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } if (logEntries.length) logText = '\n' + logEntries.join('\n'); // Logic for determining user roles and categories let roles = []; const isStewardInPeriod = isGloballySteward || historyRes.wasSteward; const isTemporaryMission = (longestTimeStr && isSelfAssign && isStewardInPeriod); // Process Global Staff and Ombudsman roles if (isGloballyStaff) roles.push("Current Staff"); else if (historyRes.wasStaff) roles.push("Former Staff"); if (isGloballyOmbuds) roles.push("Current Ombudsman"); else if (historyRes.wasOmbuds) roles.push("Former Ombudsman"); // Steward logic: Distinguish between temporary actions and permanent roles if (isTemporaryMission) { const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : ""; roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`); } else { if (isGloballySteward) roles.push("Current Steward"); else if (historyRes.wasSteward) roles.push("Former Steward"); } // Process Local CheckUser roles if (isCurrentLocal) roles.push("Current Local CheckUser"); if (hasLocalRightsInPeriod && !isCurrentLocal && !isTemporaryMission) { roles.push("Former Local CheckUser"); } // Consolidate unique roles into a formatted label let uniqueRoles = [...new Set(roles)]; let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role"; // Return final metadata object for project reporting return { role: roleLabel, log: logText, hasLocalRightsInPeriod: hasLocalRightsInPeriod, isStewardAction: isTemporaryMission, selfAssignCount: assignCount, maxDurationMins: maxDurationMins }; } // Main execution loop with Cross-Tab concurrency protection async function runAudit(yf, mf, yt, mt) { $('#status-msg').text('Waiting for other tabs to finish...').css("color", "orange"); $('#start').prop('disabled', true); // Utilize Web Locks API to prevent concurrent API requests from multiple tabs await navigator.locks.request('global_cu_audit', async () => { isRunning = true; // Clear cache and result containers for a fresh scan userCache = {}; historyCache = {}; results = {}; emptyWikis = []; failedWikis = []; scannedWikis = []; $('#status-msg').text('Lock acquired. Initializing...').css("color", "#0056b3"); $('#stop').prop('disabled', false); // Parse wiki filter input from the UI const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(s => s.trim()).filter(s => s !== "") : []; // Determine the set of wikis to process based on user filters const allAvailableWikis = Object.keys(globalWikiMap); let wikisToScan = allAvailableWikis; // Apply inclusion or exclusion logic based on current UI selection if (currentFilterMode === 'include') { wikisToScan = allAvailableWikis.filter(w => filterList.includes(w)); } else if (currentFilterMode === 'exclude') { wikisToScan = allAvailableWikis.filter(w => !filterList.includes(w)); } else if (currentFilterMode === 'localcu') { wikisToScan = allAvailableWikis.filter(w => localCUWikis.indexOf(w) !== -1); } // Validate that there are wikis to scan before proceeding if (wikisToScan.length === 0) { alert("No wikis selected for audit!"); isRunning = false; $('#start').prop('disabled', false); return; } // Initialize the progress bar UI $('#bar').attr('max', wikisToScan.length).val(0); // Define the standardized ISO timestamps for the MediaWiki API const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`; const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`; // Generate the chronological month columns for the report table const monthCols = []; let currY = parseInt(yf), currM = parseInt(mf), endY = parseInt(yt), endM = parseInt(mt); while (currY < endY || (currY === endY && currM <= endM)) { monthCols.push({ key: `${currY}-${String(currM).padStart(2, '0')}`, label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}` }); currM++; if (currM > 12) { currM = 1; currY++; } } // Iterate through each wiki project to collect CheckUser activity for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; // Allow for manual stop const db = wikisToScan[i]; scannedWikis.push(db); $('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`); let successLocal = false, continueToken = null; // Phase 1: Retrieve CheckUser logs and aggregate monthly stats while (!successLocal && isRunning) { try { const api = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); let params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) Object.assign(params, continueToken); let res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; if (entries.length) { if (!results[db]) results[db] = {}; entries.forEach(e => { const user = e.checkuser; if (!results[db][user]) results[db][user] = { total: 0, months: {} }; const mKey = e.timestamp.slice(0, 7); results[db][user].total++; results[db][user].months[mKey] = (results[db][user].months[mKey] || 0) + 1; }); } // Handle API pagination if (res.continue && isRunning) { continueToken = res.continue; } else { successLocal = true; } } catch (err) { failedWikis.push(db); successLocal = true; } } // Phase 2: Capture current CheckUsers (to include users with 0 actions) if (isRunning && !failedWikis.includes(db)) { try { const auApi = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); const auRes = await robustCall(auApi, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); const currentCUs = auRes.query.allusers || []; if (currentCUs.length > 0) { if (!results[db]) results[db] = {}; currentCUs.forEach(u => { if (!results[db][u.name]) results[db][u.name] = { total: 0, months: {} }; }); } } catch (e) { /* Fallback for local API failures */ } } // Record wikis with no CheckUser presence found if (!results[db] && !failedWikis.includes(db)) { emptyWikis.push(db); } // Update UI progress $('#bar').val(i + 1); await sleep(DELAY_MS); } // Finalize results and compile the audit report $('#status-msg').text(`Generating report...`); // Calculate global activity totals for each user across all wikis let userGlobalTotals = {}; Object.keys(results).forEach(db => { Object.keys(results[db]).forEach(user => { userGlobalTotals[user] = (userGlobalTotals[user] || 0) + results[db][user].total; }); }); // Setup report timestamps const reportDate = new Date().toISOString().split('T')[0]; const viewMode = $('input[name="cu-view-mode"]:checked').val(); const timeCols = []; if (viewMode === 'months') { monthCols.forEach(m => timeCols.push({ id: m.key, label: m.label })); } else if (viewMode !== 'total') { monthCols.forEach(m => { let label; if (viewMode === 'quarters') { const q = Math.floor((parseInt(m.key.split('-')[1]) - 1) / 3) + 1; label = `${m.key.split('-')[0]}-Q${q}`; } else { label = m.key.split('-')[0]; // Years } let col = timeCols.find(c => c.label === label); if (!col) { col = { label: label, months: [] }; timeCols.push(col); } col.months.push(m.key); }); } // Initialize Wikitext output with header information let wikitext = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/GlobalCheckUserStats.js|GlobalCheckUserStats.js]]''\n`; // Initialize CSV output with headers let csvData = "Project,User,Role,SelfAssignCount,MaxDuration_mins,TotalActions\n"; // Document applied filters in the wikitext report let filterSummary = []; // Only add wiki filter if the list is not empty, or if localcu is selected if (currentFilterMode === 'localcu') { filterSummary.push(`Wikis with local CheckUsers`); } else if (filterList.length > 0) { if (currentFilterMode === 'include') { filterSummary.push(`Only these wikis: ${filterList.join(', ')}`); } else if (currentFilterMode === 'exclude') { filterSummary.push(`All except wikis: ${filterList.join(', ')}`); } } if (currentUserFilter === 'local') filterSummary.push(`Only users: Local CheckUsers`); else if (currentUserFilter === 'steward') filterSummary.push(`Only users: Steward actions`); if (filterSummary.length > 0) { wikitext += `<br />\n''Used filters: ${filterSummary.join(' | ')}''\n`; } wikitext += `\n`; // Start building the main results table let rightsLogText = `\n== Rights Log (In Period) ==\n`; const per1kExpl = "Calculated as: (Total Actions in selected period / Current Active Users) * 1,000"; wikitext += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n! Wiki / User !! Category !! '''Total''' !! per 1k users<ref>${per1kExpl}</ref> ${timeCols.map(c => `!! ${c.label}`).join(' ')}\n`; // Process each database in alphabetical order const sortedDBs = Object.keys(results).sort(); for (const db of sortedDBs) { const metrics = await fetchWikiMetrics(db); const activeFormatted = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown'; const interwiki = getInterwikiPrefix(db); // Build wiki-specific header row let headerRow = `|-\n! colspan="${4 + timeCols.length}" style="background:#eaecf0; text-align:center;" | [[${interwiki}:|${db}]] <small style="font-weight:normal; color:#54595d;">(Active Users as of ${reportDate}: ${activeFormatted})</small> — [[${interwiki}:Special:CheckUserLog|CheckUserLog]] · [[${interwiki}:Special:ListUsers/checkuser|ListUsers/checkuser]]\n`; let projectRows = [], wikiTotal = 0, wikiMonthlyStats = {}; monthCols.forEach(col => wikiMonthlyStats[col.key] = 0); let projectUsers = Object.keys(results[db] || {}).sort(); for (const user of projectUsers) { const userData = results[db][user]; const meta = await fetchUserData(user, db, START, END); // Filter logic: Only show users active in period or holding rights if (userData.total === 0 && !meta.hasLocalRightsInPeriod) continue; const isStewardAction = meta.isStewardAction; const isLocalCU = meta.role.includes("Local CheckUser"); // Apply UI role filters if (currentUserFilter === 'local' && !isLocalCU) continue; if (currentUserFilter === 'steward' && !isStewardAction) continue; // Update totals wikiTotal += userData.total; monthCols.forEach(col => wikiMonthlyStats[col.key] += (userData.months[col.key] || 0)); // Calculate per 1k active users metric let per1k = metrics.active > 0 ? ((userData.total / metrics.active) * 1000).toFixed(1) : "0.0"; // Construct wikitext row with dynamic timeline let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${meta.role}</small> || '''${userData.total}''' || ${per1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = userData.months[col.id] || 0; else col.months.forEach(m => val += (userData.months[m] || 0)); row += ` || ${val}`; }); projectRows.push({ html: row + "\n", log: meta.log ? `'''${user}@${db}''':${meta.log}\n\n` : "" }); // Construct CSV row let baseRole = meta.role.split(' (')[0]; let sAssignCount = meta.isStewardAction ? meta.selfAssignCount : ""; let sMaxMins = meta.isStewardAction ? Math.round(meta.maxDurationMins) : ""; csvData += `${db},${user},"${baseRole}",${sAssignCount},${sMaxMins},${userData.total}\n`; } // Skip project if no users pass the filters if (projectRows.length === 0) { if (!emptyWikis.includes(db)) emptyWikis.push(db); continue; } // Construct project summary row let wikiPer1k = metrics.active > 0 ? ((wikiTotal / metrics.active) * 1000).toFixed(1) : "0.0"; let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wikiTotal} || ${wikiPer1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = wikiMonthlyStats[col.id] || 0; else col.months.forEach(m => val += (wikiMonthlyStats[m] || 0)); totalRow += ` || ${val}`; }); wikitext += headerRow + totalRow + "\n"; projectRows.forEach(r => { wikitext += r.html; if (r.log) rightsLogText += r.log; }); } // Close table and append metadata sections wikitext += `|}\n\n== Projects with 0 actions ==\n<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`; if (failedWikis.length > 0) { wikitext += `\n== API Errors / Skipped Projects ==\n<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`; } wikitext += rightsLogText + `\n\n<references />\n`; // Output results to UI and finalize state $('#out').val(wikitext); $('#csv-out').val(csvData); $('#output-containers').show(); $('#status-msg').text(`Done.`).css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } // Initialize the UI and finish script setup setupUI(); }); })(); 8w98d7tfwlxqd55t54c51fy1ih1vjm9 739285 739283 2026-04-24T19:00:24Z MrJaroslavik 44012 rv 739285 javascript text/javascript // GlobalCheckUserStats.js // ------------------------------------------------------- // Features: // - Global Stats: Scans logs across 800+ projects at once. // - Smart Roles: Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles. // - Deep Scan: Bypasses API limits to get full history (limit 500 entries). // - Stable: Retries 3x on rate limits so no wiki is missed. // - Error Logs: Lists failed projects (ex. because of 429) at the end of the report. // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserStats]]. // - Output: sortable Wikitables and detailed rights change logs and CSV file // - With help of Gemini 3 // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserStats const isBlank = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isGCUS = mw.config.get('wgTitle').includes('GlobalCheckUserStats'); if (!isBlank || !isGCUS) { return; } // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Save data here so we don't have to download it twice let userCache = {}; let historyCache = {}; // This map of all wikis will be filled automatically let globalWikiMap = {}; // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known to have local CheckUsers const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Load required modules and start the script mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { // Display initial status message to the user $('#mw-content-text').html('<strong>GlobalCheckUserStats.js: Loading wiki list...</strong>'); // Download and cache the list of all Wikimedia wikis globalWikiMap = await loadGlobalWikiMap(); // Prepare the Meta-Wiki API and data containers const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); const now = new Date(); // Variables for storing audit results and script state let results = {}; let emptyWikis = []; let failedWikis = []; let scannedWikis = []; let isRunning = false; // Current UI filter settings let currentFilterMode = 'all'; let currentUserFilter = 'all'; // Prepare the year and month selection menus function setupUI() { const currentYear = now.getFullYear(); // Generate options for years from 2005 to today let yearOpts = ''; for (let y = currentYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate options for the month range (January to December) let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`; monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`; } // Set the page title and build the HTML interface $('#firstHeading').text('GlobalCheckUserStats.js'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <h3 style="margin-top:0;">Stats Range</h3> <div> From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select> &nbsp;&nbsp;&nbsp; To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-except"> All except</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-top:10px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="Example: enwiki, dewiki, commonswiki..."> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>User Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-local"> Local CheckUsers</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-steward"> Steward actions</label> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>View mode (Wikitext):</strong> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="total"> Total only</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="months" checked> Months</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="quarters"> Q1-Q4</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="years"> Years</label> </div> <div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-containers" style="display:none; margin-top:10px;"> <div style="margin-bottom:10px;"> <strong>Wikitext:</strong> <textarea id="out" style="width:100%; height:250px; font-family:monospace; font-size:12px;"></textarea> </div> <div> <strong>CSV:</strong> <textarea id="csv-out" style="width:100%; height:150px; font-family:monospace; font-size:12px;"></textarea> </div> </div> </div> `); // Listen for filter changes and button clicks $('#btn-all').on('click', () => { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').on('click', () => { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-except').on('click', () => { currentFilterMode = 'exclude'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); $('#btn-only').on('click', () => { currentFilterMode = 'include'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); // User type filters $('#u-all').on('click', () => { currentUserFilter = 'all'; }); $('#u-local').on('click', () => { currentUserFilter = 'local'; }); $('#u-steward').on('click', () => { currentUserFilter = 'steward'; }); // Help and Execution controls $('#wiki-help-trigger').on('click', () => $('#wiki-list-help').toggle()); $('#start').on('click', () => { runAudit( $('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val() ); }); $('#stop').on('click', () => { isRunning = false; }); } // Get the number of active users for a specific wiki async function fetchWikiMetrics(db) { try { // Find the correct server URL for this database const baseUrl = globalWikiMap[db]; const api = new mw.ForeignApi(baseUrl + '/w/api.php'); // Request site statistics from the API const res = await robustCall(api, { action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2 }); // Return the count of active users return { active: res.query.statistics.activeusers || 0 }; } catch (err) { // If the wiki is unreachable, return zero as a fallback return { active: 0 }; } } // Checks if the user held global roles (Steward/Staff/Ombuds) async function checkGlobalHistory(user, start, end) { try { const res = await robustCall(metaApi, { action: 'query', list: 'logevents', letype: 'gblrights', letitle: 'User:' + user, lelimit: 'max', formatversion: 2 }); const logs = res.query.logevents || []; const auditStart = new Date(start); const auditEnd = new Date(end); let status = { wasSteward: false, wasStaff: false, wasOmbuds: false }; logs.forEach(log => { const ts = new Date(log.timestamp); const p = log.params || {}; // Handle both new (named) and old (indexed) API formats const current = extractGroups(p.newGroups || p.add || p[1] || p["1"]); const old = extractGroups(p.oldGroups || p.remove || p[2] || p["0"]); if (ts >= auditStart && ts <= auditEnd) { if (current.includes('steward') || old.includes('steward')) status.wasSteward = true; if (current.includes('staff') || old.includes('staff')) status.wasStaff = true; if (current.some(g => g.includes('ombud')) || old.some(g => g.includes('ombud'))) status.wasOmbuds = true; } }); // If no changes during the period, check the state immediately before it started if (!status.wasSteward && !status.wasStaff && !status.wasOmbuds) { const priorLogs = logs .filter(log => new Date(log.timestamp) < auditStart) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (priorLogs.length > 0) { const p = priorLogs[0].params || {}; // Crucial: check both newGroups and legacy indexed params const groupsBefore = extractGroups(p.newGroups || p.add || p[1] || p["1"]); status.wasSteward = groupsBefore.includes('steward'); status.wasStaff = groupsBefore.includes('staff'); status.wasOmbuds = groupsBefore.some(g => g.includes('ombud')); } } return status; } catch (err) { return { wasSteward: false, wasStaff: false, wasOmbuds: false }; } } // Gather user information and fetch their rights logs async function fetchUserData(user, db, start, end) { const auditStart = new Date(start); const auditEnd = new Date(end); // Get current global groups from Meta-Wiki if not already cached if (!userCache[user]) { try { const globalRes = await robustCall(metaApi, { action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 }); userCache[user] = globalRes.query.globaluserinfo.groups || []; } catch (err) { userCache[user] = []; } } // Canonical variables for current global status const currentGlobalGroups = userCache[user]; const isGloballySteward = currentGlobalGroups.includes('steward'); const isGloballyStaff = currentGlobalGroups.includes('staff'); const isGloballyOmbuds = currentGlobalGroups.includes('ombuds') || currentGlobalGroups.includes('ombudsman'); // Check for global role history in cache or fetch new data if (!historyCache[user]) { historyCache[user] = await checkGlobalHistory(user, start, end); } const historyRes = historyCache[user]; let isCurrentLocal = false; // Check current local CheckUser rights on the target wiki try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Request user group information from the local API const localRes = await robustCall(localApi, { action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 }); // Parse response and verify if 'checkuser' group is present const localUserData = localRes.query.users[0]; const localGroups = (localUserData && localUserData.groups) || []; isCurrentLocal = localGroups.includes('checkuser'); } catch (err) { // Default to false if the local rights check fails isCurrentLocal = false; } // Define the target string for CentralAuth rights logs const target = 'User:' + user + '@' + db; let logText = '', isSelfAssign = false, maxDurationMins = -1, longestTimeStr = "", assignCount = 0, hasLocalRightsInPeriod = false; // Retrieve all rights change logs from Meta-Wiki let events = [], continueToken = null, finished = false; while (!finished) { let params = { action: 'query', list: 'logevents', letype: 'rights', letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2 }; // Handle API pagination with continue tokens if (continueToken) Object.assign(params, continueToken); // Execute the API call and merge the results const res = await robustCall(metaApi, params); const batch = res.query.logevents || []; events = events.concat(batch); // Check for more results to continue pagination if (res.continue) { continueToken = res.continue; } else { finished = true; } } // Initialize state variables for log processing let logEntries = [], pendingRemoved = null, lastPairedDate = null, capturedExpiry = null; // Iterate through collected events to process rights changes for (let i = 0; i < events.length; i++) { const e = events[i]; const p = e.params || {}; // Extract the performing user, stripping import prefixes if present const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user; // Determine CheckUser status before and after the event using various param keys const hadCU = extractGroups(p.oldgroups || p.oldGroups || p["0"]).includes('checkuser'); const hasCU = extractGroups(p.newgroups || p.newGroups || p["1"]).includes('checkuser'); // Extract expiry metadata if the user already has rights and they are being updated if (hasCU && hadCU) { const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') capturedExpiry = new Date(cuMeta.expiry); } } // Identify if CheckUser was added or removed in this event let cuAdded = (hasCU && !hadCU), cuRemoved = (hadCU && !hasCU); if (cuAdded || cuRemoved) { const eventDate = new Date(e.timestamp), exactTime = e.timestamp.replace('T', ' ').replace('Z', ''); const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd); if (isInPeriod) hasLocalRightsInPeriod = true; // Process events relevant to the audit timeframe or pairing logic if (isInPeriod || (eventDate > auditEnd) || (pendingRemoved && cuAdded)) { let expiryDate = capturedExpiry; capturedExpiry = null; // Check for metadata again to ensure current expiry is captured const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') expiryDate = new Date(cuMeta.expiry); } const eventBySelf = (actor === user || !actor); if (cuAdded) { const removalInPeriod = pendingRemoved && (pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd); const expiryInPeriod = expiryDate && (expiryDate >= auditStart && expiryDate <= auditEnd); // Pair adding event with its removal or expiry to calculate duration if ((isInPeriod || removalInPeriod || expiryInPeriod) && (pendingRemoved || expiryDate)) { const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate; const diffMs = Math.abs(checkDate - eventDate); const totalSecs = Math.floor(diffMs / 1000); // Intelligent rounding to handle short durations and edge cases let roundedMins = Math.round(totalSecs / 60); if (roundedMins === 0 && totalSecs > 0) roundedMins = 1; // Convert total minutes into days, hours, and remaining minutes let days = Math.floor(roundedMins / 1440); let hours = Math.floor((roundedMins % 1440) / 60); let mins = roundedMins % 60; // Build a human-readable duration string let dPart = days > 0 ? days + 'd ' : ''; let hPart = hours > 0 ? hours + 'h ' : ''; let mPart = (mins > 0 || (days === 0 && hours === 0)) ? mins + 'm' : ''; let durStr = totalSecs < 60 ? totalSecs + "s" : (dPart + hPart + mPart).trim(); // Capture the log comment for the ADDED event let reason = e.comment ? ` <small>''(${e.comment})''</small>` : ""; // Determine if this session was a Steward self-assignment if (pendingRemoved) { if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true; // Retrieve the removal comment if available let remReason = pendingRemoved.comment ? ` <small>''(${pendingRemoved.comment})''</small>` : ""; // Calculate discrepancy between manual removal and scheduled expiry let note = ""; if (expiryDate) { // Calculate planned duration in seconds and format it const pDiff = Math.abs(expiryDate - eventDate) / 1000; let rP = Math.round(pDiff / 60); let rd = Math.floor(rP / 1440), rh = Math.floor((rP % 1440) / 60), rm = rP % 60; let pD = pDiff < 60 ? Math.round(pDiff) + "s" : `${rd > 0 ? rd + 'd ' : ''}${rh > 0 ? rh + 'h ' : ''}${(rm > 0 || (rd === 0 && rh === 0)) ? rm + 'm' : ''}`.trim(); // Check if rights were removed manually earlier or later than planned let timeDiff = pendingRemoved.date - expiryDate; if (timeDiff < -60000) { note = ` - manual removal before scheduled expiry (set to ${pD})`; } else if (timeDiff > 600000) { note = ` - automatic removal not found (set to ${pD})`; } } // Finalize the paired log entry for manual removals logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}${remReason} (Duration: ${durStr}${note})`); } else if (expiryDate) { // Handle automatic expiration scenarios if (eventBySelf) isSelfAssign = true; logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | EXPIRED: ${expiryDate.toISOString().replace('T', ' ').substring(0, 19)} <small>''(Automatic)''</small> (Duration: ${durStr})`); } // Reset pending state and update audit metrics pendingRemoved = null; if (totalSecs / 60 > maxDurationMins) { maxDurationMins = totalSecs / 60; longestTimeStr = durStr.trim(); } assignCount++; lastPairedDate = eventDate; } else if (isInPeriod && !pendingRemoved && !expiryDate) { // Handle cases where rights are still active or removal log is missing if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) { logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`); lastPairedDate = eventDate; } pendingRemoved = null; } } else if (cuRemoved) { // Record standalone removal events for future pairing if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } // Store removal data to be paired with the next ADDED event pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf, comment: e.comment }; } } } } // Verify if the user held rights at the start of the period by checking prior logs if (!hasLocalRightsInPeriod && events.length > 0) { const firstBefore = events.find(ev => new Date(ev.timestamp) < auditStart); if (firstBefore) { const groupsBefore = extractGroups(firstBefore.params.newgroups || firstBefore.params.add || firstBefore.params[1] || firstBefore.params["1"]); if (groupsBefore.includes('checkuser')) hasLocalRightsInPeriod = true; } // If still not found, check the oldest available log entry for the initial state if (!hasLocalRightsInPeriod) { const chronologicallyFirst = events[events.length - 1]; const cp = chronologicallyFirst.params || {}; const oldGroups = extractGroups(cp.oldgroups || cp.oldGroups || cp.remove || cp[0] || cp["0"]); if (oldGroups.includes('checkuser')) hasLocalRightsInPeriod = true; } } // Finalize the log text assembly if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } if (logEntries.length) logText = '\n' + logEntries.join('\n'); // Logic for determining user roles and categories let roles = []; const isStewardInPeriod = isGloballySteward || historyRes.wasSteward; const isTemporaryMission = (longestTimeStr && isSelfAssign && isStewardInPeriod); // Process Global Staff and Ombudsman roles if (isGloballyStaff) roles.push("Current Staff"); else if (historyRes.wasStaff) roles.push("Former Staff"); if (isGloballyOmbuds) roles.push("Current Ombudsman"); else if (historyRes.wasOmbuds) roles.push("Former Ombudsman"); // Steward logic: Distinguish between temporary actions and permanent roles if (isTemporaryMission) { const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : ""; roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`); } else { if (isGloballySteward) roles.push("Current Steward"); else if (historyRes.wasSteward) roles.push("Former Steward"); } // Process Local CheckUser roles if (isCurrentLocal) roles.push("Current Local CheckUser"); if (hasLocalRightsInPeriod && !isCurrentLocal && !isTemporaryMission) { roles.push("Former Local CheckUser"); } // Consolidate unique roles into a formatted label let uniqueRoles = [...new Set(roles)]; let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role"; // Return final metadata object for project reporting return { role: roleLabel, log: logText, hasLocalRightsInPeriod: hasLocalRightsInPeriod, isStewardAction: isTemporaryMission, selfAssignCount: assignCount, maxDurationMins: maxDurationMins }; } // Main execution loop with Cross-Tab concurrency protection async function runAudit(yf, mf, yt, mt) { $('#status-msg').text('Waiting for other tabs to finish...').css("color", "orange"); $('#start').prop('disabled', true); // Utilize Web Locks API to prevent concurrent API requests from multiple tabs await navigator.locks.request('global_cu_audit', async () => { isRunning = true; // Clear cache and result containers for a fresh scan userCache = {}; historyCache = {}; results = {}; emptyWikis = []; failedWikis = []; scannedWikis = []; $('#status-msg').text('Lock acquired. Initializing...').css("color", "#0056b3"); $('#stop').prop('disabled', false); // Parse wiki filter input from the UI const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(s => s.trim()).filter(s => s !== "") : []; // Determine the set of wikis to process based on user filters const allAvailableWikis = Object.keys(globalWikiMap); let wikisToScan = allAvailableWikis; // Apply inclusion or exclusion logic based on current UI selection if (currentFilterMode === 'include') { wikisToScan = allAvailableWikis.filter(w => filterList.includes(w)); } else if (currentFilterMode === 'exclude') { wikisToScan = allAvailableWikis.filter(w => !filterList.includes(w)); } else if (currentFilterMode === 'localcu') { wikisToScan = allAvailableWikis.filter(w => localCUWikis.indexOf(w) !== -1); } // Validate that there are wikis to scan before proceeding if (wikisToScan.length === 0) { alert("No wikis selected for audit!"); isRunning = false; $('#start').prop('disabled', false); return; } // Initialize the progress bar UI $('#bar').attr('max', wikisToScan.length).val(0); // Define the standardized ISO timestamps for the MediaWiki API const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`; const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`; // Generate the chronological month columns for the report table const monthCols = []; let currY = parseInt(yf), currM = parseInt(mf), endY = parseInt(yt), endM = parseInt(mt); while (currY < endY || (currY === endY && currM <= endM)) { monthCols.push({ key: `${currY}-${String(currM).padStart(2, '0')}`, label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}` }); currM++; if (currM > 12) { currM = 1; currY++; } } // Iterate through each wiki project to collect CheckUser activity for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; // Allow for manual stop const db = wikisToScan[i]; scannedWikis.push(db); $('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`); let successLocal = false, continueToken = null; // Phase 1: Retrieve CheckUser logs and aggregate monthly stats while (!successLocal && isRunning) { try { const api = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); let params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) Object.assign(params, continueToken); let res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; if (entries.length) { if (!results[db]) results[db] = {}; entries.forEach(e => { const user = e.checkuser; if (!results[db][user]) results[db][user] = { total: 0, months: {} }; const mKey = e.timestamp.slice(0, 7); results[db][user].total++; results[db][user].months[mKey] = (results[db][user].months[mKey] || 0) + 1; }); } // Handle API pagination if (res.continue && isRunning) { continueToken = res.continue; } else { successLocal = true; } } catch (err) { failedWikis.push(db); successLocal = true; } } // Phase 2: Capture current CheckUsers (to include users with 0 actions) if (isRunning && !failedWikis.includes(db)) { try { const auApi = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); const auRes = await robustCall(auApi, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); const currentCUs = auRes.query.allusers || []; if (currentCUs.length > 0) { if (!results[db]) results[db] = {}; currentCUs.forEach(u => { if (!results[db][u.name]) results[db][u.name] = { total: 0, months: {} }; }); } } catch (e) { /* Fallback for local API failures */ } } // Record wikis with no CheckUser presence found if (!results[db] && !failedWikis.includes(db)) { emptyWikis.push(db); } // Update UI progress $('#bar').val(i + 1); await sleep(DELAY_MS); } // Finalize results and compile the audit report $('#status-msg').text(`Generating report...`); // Calculate global activity totals for each user across all wikis let userGlobalTotals = {}; Object.keys(results).forEach(db => { Object.keys(results[db]).forEach(user => { userGlobalTotals[user] = (userGlobalTotals[user] || 0) + results[db][user].total; }); }); // Setup report timestamps const reportDate = new Date().toISOString().split('T')[0]; const viewMode = $('input[name="cu-view-mode"]:checked').val(); const timeCols = []; if (viewMode === 'months') { monthCols.forEach(m => timeCols.push({ id: m.key, label: m.label })); } else if (viewMode !== 'total') { monthCols.forEach(m => { let label; if (viewMode === 'quarters') { const q = Math.floor((parseInt(m.key.split('-')[1]) - 1) / 3) + 1; label = `${m.key.split('-')[0]}-Q${q}`; } else { label = m.key.split('-')[0]; // Years } let col = timeCols.find(c => c.label === label); if (!col) { col = { label: label, months: [] }; timeCols.push(col); } col.months.push(m.key); }); } // Initialize Wikitext output with header information let wikitext = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/GlobalCheckUserStats.js|GlobalCheckUserStats.js]]''\n`; // Initialize CSV output with headers let csvData = "Project,User,Role,SelfAssignCount,MaxDuration_mins,TotalActions\n"; // Document applied filters in the wikitext report let filterSummary = []; // Only add wiki filter if the list is not empty, or if localcu is selected if (currentFilterMode === 'localcu') { filterSummary.push(`Wikis with local CheckUsers`); } else if (filterList.length > 0) { if (currentFilterMode === 'include') { filterSummary.push(`Only these wikis: ${filterList.join(', ')}`); } else if (currentFilterMode === 'exclude') { filterSummary.push(`All except wikis: ${filterList.join(', ')}`); } } if (currentUserFilter === 'local') filterSummary.push(`Only users: Local CheckUsers`); else if (currentUserFilter === 'steward') filterSummary.push(`Only users: Steward actions`); if (filterSummary.length > 0) { wikitext += `<br />\n''Used filters: ${filterSummary.join(' | ')}''\n`; } wikitext += `\n`; // Start building the main results table let rightsLogText = `\n== Rights Log (In Period) ==\n`; const per1kExpl = "Calculated as: (Total Actions in selected period / Current Active Users) * 1,000"; wikitext += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n! Wiki / User !! Category !! '''Total''' !! per 1k users<ref>${per1kExpl}</ref> ${timeCols.map(c => `!! ${c.label}`).join(' ')}\n`; // Process each database in alphabetical order const sortedDBs = Object.keys(results).sort(); for (const db of sortedDBs) { const metrics = await fetchWikiMetrics(db); const activeFormatted = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown'; const interwiki = getInterwikiPrefix(db); // Build wiki-specific header row let headerRow = `|-\n! colspan="${4 + timeCols.length}" style="background:#eaecf0; text-align:center;" | [[${interwiki}:|${db}]] <small style="font-weight:normal; color:#54595d;">(Active Users as of ${reportDate}: ${activeFormatted})</small> — [[${interwiki}:Special:CheckUserLog|CheckUserLog]] · [[${interwiki}:Special:ListUsers/checkuser|ListUsers/checkuser]]\n`; let projectRows = [], wikiTotal = 0, wikiMonthlyStats = {}; monthCols.forEach(col => wikiMonthlyStats[col.key] = 0); let projectUsers = Object.keys(results[db] || {}).sort(); for (const user of projectUsers) { const userData = results[db][user]; const meta = await fetchUserData(user, db, START, END); // Filter logic: Only show users active in period or holding rights if (userData.total === 0 && !meta.hasLocalRightsInPeriod) continue; const isStewardAction = meta.isStewardAction; const isLocalCU = meta.role.includes("Local CheckUser"); // Apply UI role filters if (currentUserFilter === 'local' && !isLocalCU) continue; if (currentUserFilter === 'steward' && !isStewardAction) continue; // Update totals wikiTotal += userData.total; monthCols.forEach(col => wikiMonthlyStats[col.key] += (userData.months[col.key] || 0)); // Calculate per 1k active users metric let per1k = metrics.active > 0 ? ((userData.total / metrics.active) * 1000).toFixed(1) : "0.0"; // Construct wikitext row with dynamic timeline let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${meta.role}</small> || '''${userData.total}''' || ${per1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = userData.months[col.id] || 0; else col.months.forEach(m => val += (userData.months[m] || 0)); row += ` || ${val}`; }); projectRows.push({ html: row + "\n", log: meta.log ? `'''${user}@${db}''':${meta.log}\n\n` : "" }); // Construct CSV row let baseRole = meta.role.split(' (')[0]; let sAssignCount = meta.isStewardAction ? meta.selfAssignCount : ""; let sMaxMins = meta.isStewardAction ? Math.round(meta.maxDurationMins) : ""; csvData += `${db},${user},"${baseRole}",${sAssignCount},${sMaxMins},${userData.total}\n`; } // Skip project if no users pass the filters if (projectRows.length === 0) { if (!emptyWikis.includes(db)) emptyWikis.push(db); continue; } // Construct project summary row let wikiPer1k = metrics.active > 0 ? ((wikiTotal / metrics.active) * 1000).toFixed(1) : "0.0"; let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wikiTotal} || ${wikiPer1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = wikiMonthlyStats[col.id] || 0; else col.months.forEach(m => val += (wikiMonthlyStats[m] || 0)); totalRow += ` || ${val}`; }); wikitext += headerRow + totalRow + "\n"; projectRows.forEach(r => { wikitext += r.html; if (r.log) rightsLogText += r.log; }); } // Close table and append metadata sections wikitext += `|}\n\n== Projects with 0 actions ==\n<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`; if (failedWikis.length > 0) { wikitext += `\n== API Errors / Skipped Projects ==\n<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`; } wikitext += rightsLogText + `\n\n<references />\n`; // Output results to UI and finalize state $('#out').val(wikitext); $('#csv-out').val(csvData); $('#output-containers').show(); $('#status-msg').text(`Done.`).css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } // Initialize the UI and finish script setup setupUI(); }); })(); luhmeihh5c63pbhzsirnw9qrq9j5gu5 739308 739285 2026-04-25T09:58:10Z MrJaroslavik 44012 e 739308 javascript text/javascript // GlobalCheckUserStats.js // ------------------------------------------------------- // Features: // - Scans logs across 800+ projects at once. With filters "All except these wikis", "Only these wikis" and "Only wikis with local CheckUsers". // - Identifies Local CheckUsers, Stewards, Staff, and Ombuds - and distinguishes between Former/Current roles. // - Bypasses API limits to get full history (limit 500 entries). // - If run in multiple tabs, it waits for the previous scan to finish. // - Lists projects with 0 actions and also failed projects (ex. because of 429) at the end of the report. // - Integrated UI at [[meta:Special:BlankPage/GlobalCheckUserStats]]. // - Outputs sortable Wikitable with detailed rights change logs and CSV file. // - With help of Gemini 3. // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserStats const isBlank = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isGCUS = mw.config.get('wgTitle').includes('GlobalCheckUserStats'); if (!isBlank || !isGCUS) { return; } // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Save data here so we don't have to download it twice let userCache = {}; let historyCache = {}; // This map of all wikis will be filled automatically let globalWikiMap = {}; // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known to have local CheckUsers const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Load required modules and start the script mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { // Display initial status message to the user $('#mw-content-text').html('<strong>GlobalCheckUserStats.js: Loading wiki list...</strong>'); // Download and cache the list of all Wikimedia wikis globalWikiMap = await loadGlobalWikiMap(); // Prepare the Meta-Wiki API and data containers const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); const now = new Date(); // Variables for storing audit results and script state let results = {}; let emptyWikis = []; let failedWikis = []; let scannedWikis = []; let isRunning = false; // Current UI filter settings let currentFilterMode = 'all'; let currentUserFilter = 'all'; // Prepare the year and month selection menus function setupUI() { const currentYear = now.getFullYear(); // Generate options for years from 2005 to today let yearOpts = ''; for (let y = currentYear; y >= 2005; y--) { yearOpts += `<option value="${y}">${y}</option>`; } // Generate options for the month range (January to December) let monthOptsFrom = ''; let monthOptsTo = ''; for (let m = 1; m <= 12; m++) { monthOptsFrom += `<option value="${m}" ${m === 1 ? 'selected' : ''}>${m}</option>`; monthOptsTo += `<option value="${m}" ${m === 12 ? 'selected' : ''}>${m}</option>`; } // Set the page title and build the HTML interface $('#firstHeading').text('GlobalCheckUserStats.js'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <h3 style="margin-top:0;">Stats Range</h3> <div> From: <select id="y-f" style="width:70px;">${yearOpts}</select> <select id="m-f" style="width:50px;">${monthOptsFrom}</select> &nbsp;&nbsp;&nbsp; To: <select id="y-t" style="width:70px;">${yearOpts}</select> <select id="m-t" style="width:50px;">${monthOptsTo}</select> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-localcu"> Only wikis with local CheckUsers</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-except"> All except these wikis</label> <label style="cursor:pointer;"><input type="radio" name="wiki-mode" id="btn-only"> Only these wikis</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-top:10px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="Example: enwiki, dewiki, commonswiki..."> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>User Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-local"> Local CheckUsers</label> <label style="cursor:pointer;"><input type="radio" name="user-mode" id="u-steward"> Steward actions</label> </div> <div style="margin-top:15px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wikitext output:</strong> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="total"> Total only</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="months" checked> Months</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="quarters"> Q1-Q4</label> <label style="cursor:pointer;"><input type="radio" name="cu-view-mode" value="years"> Years</label> </div> <div id="wiki-list-help" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-containers" style="display:none; margin-top:10px;"> <div style="margin-bottom:10px;"> <strong>Wikitext:</strong> <textarea id="out" style="width:100%; height:250px; font-family:monospace; font-size:12px;"></textarea> </div> <div> <strong>CSV:</strong> <textarea id="csv-out" style="width:100%; height:150px; font-family:monospace; font-size:12px;"></textarea> </div> </div> </div> `); // Listen for filter changes and button clicks $('#btn-all').on('click', () => { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').on('click', () => { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-except').on('click', () => { currentFilterMode = 'exclude'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); $('#btn-only').on('click', () => { currentFilterMode = 'include'; $('#filter-input-container').show(); $('#wiki-filter').focus(); }); // User type filters $('#u-all').on('click', () => { currentUserFilter = 'all'; }); $('#u-local').on('click', () => { currentUserFilter = 'local'; }); $('#u-steward').on('click', () => { currentUserFilter = 'steward'; }); // Help and Execution controls $('#wiki-help-trigger').on('click', () => $('#wiki-list-help').toggle()); $('#start').on('click', () => { runAudit( $('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val() ); }); $('#stop').on('click', () => { isRunning = false; }); } // Get the number of active users for a specific wiki async function fetchWikiMetrics(db) { try { // Find the correct server URL for this database const baseUrl = globalWikiMap[db]; const api = new mw.ForeignApi(baseUrl + '/w/api.php'); // Request site statistics from the API const res = await robustCall(api, { action: 'query', meta: 'siteinfo', siprop: 'statistics', formatversion: 2 }); // Return the count of active users return { active: res.query.statistics.activeusers || 0 }; } catch (err) { // If the wiki is unreachable, return zero as a fallback return { active: 0 }; } } // Checks if the user held global roles (Steward/Staff/Ombuds) async function checkGlobalHistory(user, start, end) { try { const res = await robustCall(metaApi, { action: 'query', list: 'logevents', letype: 'gblrights', letitle: 'User:' + user, lelimit: 'max', formatversion: 2 }); const logs = res.query.logevents || []; const auditStart = new Date(start); const auditEnd = new Date(end); let status = { wasSteward: false, wasStaff: false, wasOmbuds: false }; logs.forEach(log => { const ts = new Date(log.timestamp); const p = log.params || {}; // Handle both new (named) and old (indexed) API formats const current = extractGroups(p.newGroups || p.add || p[1] || p["1"]); const old = extractGroups(p.oldGroups || p.remove || p[2] || p["0"]); if (ts >= auditStart && ts <= auditEnd) { if (current.includes('steward') || old.includes('steward')) status.wasSteward = true; if (current.includes('staff') || old.includes('staff')) status.wasStaff = true; if (current.some(g => g.includes('ombud')) || old.some(g => g.includes('ombud'))) status.wasOmbuds = true; } }); // If no changes during the period, check the state immediately before it started if (!status.wasSteward && !status.wasStaff && !status.wasOmbuds) { const priorLogs = logs .filter(log => new Date(log.timestamp) < auditStart) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); if (priorLogs.length > 0) { const p = priorLogs[0].params || {}; // Crucial: check both newGroups and legacy indexed params const groupsBefore = extractGroups(p.newGroups || p.add || p[1] || p["1"]); status.wasSteward = groupsBefore.includes('steward'); status.wasStaff = groupsBefore.includes('staff'); status.wasOmbuds = groupsBefore.some(g => g.includes('ombud')); } } return status; } catch (err) { return { wasSteward: false, wasStaff: false, wasOmbuds: false }; } } // Gather user information and fetch their rights logs async function fetchUserData(user, db, start, end) { const auditStart = new Date(start); const auditEnd = new Date(end); // Get current global groups from Meta-Wiki if not already cached if (!userCache[user]) { try { const globalRes = await robustCall(metaApi, { action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 }); userCache[user] = globalRes.query.globaluserinfo.groups || []; } catch (err) { userCache[user] = []; } } // Canonical variables for current global status const currentGlobalGroups = userCache[user]; const isGloballySteward = currentGlobalGroups.includes('steward'); const isGloballyStaff = currentGlobalGroups.includes('staff'); const isGloballyOmbuds = currentGlobalGroups.includes('ombuds') || currentGlobalGroups.includes('ombudsman'); // Check for global role history in cache or fetch new data if (!historyCache[user]) { historyCache[user] = await checkGlobalHistory(user, start, end); } const historyRes = historyCache[user]; let isCurrentLocal = false; // Check current local CheckUser rights on the target wiki try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Request user group information from the local API const localRes = await robustCall(localApi, { action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 }); // Parse response and verify if 'checkuser' group is present const localUserData = localRes.query.users[0]; const localGroups = (localUserData && localUserData.groups) || []; isCurrentLocal = localGroups.includes('checkuser'); } catch (err) { // Default to false if the local rights check fails isCurrentLocal = false; } // Define the target string for CentralAuth rights logs const target = 'User:' + user + '@' + db; let logText = '', isSelfAssign = false, maxDurationMins = -1, longestTimeStr = "", assignCount = 0, hasLocalRightsInPeriod = false; // Retrieve all rights change logs from Meta-Wiki let events = [], continueToken = null, finished = false; while (!finished) { let params = { action: 'query', list: 'logevents', letype: 'rights', letitle: target, ledir: 'older', lelimit: 'max', formatversion: 2 }; // Handle API pagination with continue tokens if (continueToken) Object.assign(params, continueToken); // Execute the API call and merge the results const res = await robustCall(metaApi, params); const batch = res.query.logevents || []; events = events.concat(batch); // Check for more results to continue pagination if (res.continue) { continueToken = res.continue; } else { finished = true; } } // Initialize state variables for log processing let logEntries = [], pendingRemoved = null, lastPairedDate = null, capturedExpiry = null; // Iterate through collected events to process rights changes for (let i = 0; i < events.length; i++) { const e = events[i]; const p = e.params || {}; // Extract the performing user, stripping import prefixes if present const actor = e.user && e.user.includes('>') ? e.user.split('>')[1] : e.user; // Determine CheckUser status before and after the event using various param keys const hadCU = extractGroups(p.oldgroups || p.oldGroups || p["0"]).includes('checkuser'); const hasCU = extractGroups(p.newgroups || p.newGroups || p["1"]).includes('checkuser'); // Extract expiry metadata if the user already has rights and they are being updated if (hasCU && hadCU) { const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') capturedExpiry = new Date(cuMeta.expiry); } } // Identify if CheckUser was added or removed in this event let cuAdded = (hasCU && !hadCU), cuRemoved = (hadCU && !hasCU); if (cuAdded || cuRemoved) { const eventDate = new Date(e.timestamp), exactTime = e.timestamp.replace('T', ' ').replace('Z', ''); const isInPeriod = (eventDate >= auditStart && eventDate <= auditEnd); if (isInPeriod) hasLocalRightsInPeriod = true; // Process events relevant to the audit timeframe or pairing logic if (isInPeriod || (eventDate > auditEnd) || (pendingRemoved && cuAdded)) { let expiryDate = capturedExpiry; capturedExpiry = null; // Check for metadata again to ensure current expiry is captured const rawMeta = p.newmetadata || p.newMetadata; if (rawMeta) { const metaArray = Array.isArray(rawMeta) ? rawMeta : Object.values(rawMeta); const cuMeta = metaArray.find(m => m && (m.group === 'checkuser' || m.group === 'check user')); if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity') expiryDate = new Date(cuMeta.expiry); } const eventBySelf = (actor === user || !actor); if (cuAdded) { const removalInPeriod = pendingRemoved && (pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd); const expiryInPeriod = expiryDate && (expiryDate >= auditStart && expiryDate <= auditEnd); // Pair adding event with its removal or expiry to calculate duration if ((isInPeriod || removalInPeriod || expiryInPeriod) && (pendingRemoved || expiryDate)) { const checkDate = pendingRemoved ? pendingRemoved.date : expiryDate; const diffMs = Math.abs(checkDate - eventDate); const totalSecs = Math.floor(diffMs / 1000); // Intelligent rounding to handle short durations and edge cases let roundedMins = Math.round(totalSecs / 60); if (roundedMins === 0 && totalSecs > 0) roundedMins = 1; // Convert total minutes into days, hours, and remaining minutes let days = Math.floor(roundedMins / 1440); let hours = Math.floor((roundedMins % 1440) / 60); let mins = roundedMins % 60; // Build a human-readable duration string let dPart = days > 0 ? days + 'd ' : ''; let hPart = hours > 0 ? hours + 'h ' : ''; let mPart = (mins > 0 || (days === 0 && hours === 0)) ? mins + 'm' : ''; let durStr = totalSecs < 60 ? totalSecs + "s" : (dPart + hPart + mPart).trim(); // Capture the log comment for the ADDED event let reason = e.comment ? ` <small>''(${e.comment})''</small>` : ""; // Determine if this session was a Steward self-assignment if (pendingRemoved) { if (eventBySelf && pendingRemoved.isSelf) isSelfAssign = true; // Retrieve the removal comment if available let remReason = pendingRemoved.comment ? ` <small>''(${pendingRemoved.comment})''</small>` : ""; // Calculate discrepancy between manual removal and scheduled expiry let note = ""; if (expiryDate) { // Calculate planned duration in seconds and format it const pDiff = Math.abs(expiryDate - eventDate) / 1000; let rP = Math.round(pDiff / 60); let rd = Math.floor(rP / 1440), rh = Math.floor((rP % 1440) / 60), rm = rP % 60; let pD = pDiff < 60 ? Math.round(pDiff) + "s" : `${rd > 0 ? rd + 'd ' : ''}${rh > 0 ? rh + 'h ' : ''}${(rm > 0 || (rd === 0 && rh === 0)) ? rm + 'm' : ''}`.trim(); // Check if rights were removed manually earlier or later than planned let timeDiff = pendingRemoved.date - expiryDate; if (timeDiff < -60000) { note = ` - manual removal before scheduled expiry (set to ${pD})`; } else if (timeDiff > 600000) { note = ` - automatic removal not found (set to ${pD})`; } } // Finalize the paired log entry for manual removals logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}${remReason} (Duration: ${durStr}${note})`); } else if (expiryDate) { // Handle automatic expiration scenarios if (eventBySelf) isSelfAssign = true; logEntries.unshift(`* ADDED: ${exactTime} by ${actor}${reason} | EXPIRED: ${expiryDate.toISOString().replace('T', ' ').substring(0, 19)} <small>''(Automatic)''</small> (Duration: ${durStr})`); } // Reset pending state and update audit metrics pendingRemoved = null; if (totalSecs / 60 > maxDurationMins) { maxDurationMins = totalSecs / 60; longestTimeStr = durStr.trim(); } assignCount++; lastPairedDate = eventDate; } else if (isInPeriod && !pendingRemoved && !expiryDate) { // Handle cases where rights are still active or removal log is missing if (!(lastPairedDate && Math.abs(lastPairedDate - eventDate) < 86400000)) { logEntries.unshift(`* ADDED: ${exactTime} by ${actor} | REMOVED: (Active/Not removed)`); lastPairedDate = eventDate; } pendingRemoved = null; } } else if (cuRemoved) { // Record standalone removal events for future pairing if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } // Store removal data to be paired with the next ADDED event pendingRemoved = { time: exactTime, date: eventDate, user: actor, isSelf: eventBySelf, comment: e.comment }; } } } } // Verify if the user held rights at the start of the period by checking prior logs if (!hasLocalRightsInPeriod && events.length > 0) { const firstBefore = events.find(ev => new Date(ev.timestamp) < auditStart); if (firstBefore) { const groupsBefore = extractGroups(firstBefore.params.newgroups || firstBefore.params.add || firstBefore.params[1] || firstBefore.params["1"]); if (groupsBefore.includes('checkuser')) hasLocalRightsInPeriod = true; } // If still not found, check the oldest available log entry for the initial state if (!hasLocalRightsInPeriod) { const chronologicallyFirst = events[events.length - 1]; const cp = chronologicallyFirst.params || {}; const oldGroups = extractGroups(cp.oldgroups || cp.oldGroups || cp.remove || cp[0] || cp["0"]); if (oldGroups.includes('checkuser')) hasLocalRightsInPeriod = true; } } // Finalize the log text assembly if (pendingRemoved && pendingRemoved.date >= auditStart && pendingRemoved.date <= auditEnd) { logEntries.unshift(`* REMOVED: ${pendingRemoved.time} by ${pendingRemoved.user}`); } if (logEntries.length) logText = '\n' + logEntries.join('\n'); // Logic for determining user roles and categories let roles = []; const isStewardInPeriod = isGloballySteward || historyRes.wasSteward; const isTemporaryMission = (longestTimeStr && isSelfAssign && isStewardInPeriod); // Process Global Staff and Ombudsman roles if (isGloballyStaff) roles.push("Current Staff"); else if (historyRes.wasStaff) roles.push("Former Staff"); if (isGloballyOmbuds) roles.push("Current Ombudsman"); else if (historyRes.wasOmbuds) roles.push("Former Ombudsman"); // Steward logic: Distinguish between temporary actions and permanent roles if (isTemporaryMission) { const countLabel = assignCount > 1 ? `${assignCount}x, longest ` : ""; roles.push(`Steward action (Self-assign: ${countLabel}${longestTimeStr})`); } else { if (isGloballySteward) roles.push("Current Steward"); else if (historyRes.wasSteward) roles.push("Former Steward"); } // Determine local CheckUser status (Current and Former) if (isCurrentLocal) roles.push("Current Local CheckUser"); if (hasLocalRightsInPeriod && !isCurrentLocal && !isTemporaryMission) { roles.push("Former Local CheckUser"); } // Consolidate unique roles into a formatted label let uniqueRoles = [...new Set(roles)]; let roleLabel = uniqueRoles.length > 0 ? uniqueRoles.join(' & ') : "Unknown role"; // Return final metadata object for project reporting return { role: roleLabel, log: logText, hasLocalRightsInPeriod: hasLocalRightsInPeriod, isStewardAction: isTemporaryMission, selfAssignCount: assignCount, maxDurationMins: maxDurationMins }; } // Main execution loop with Cross-Tab concurrency protection async function runAudit(yf, mf, yt, mt) { $('#status-msg').text('Waiting for other tabs to finish...').css("color", "orange"); $('#start').prop('disabled', true); // Utilize Web Locks API to prevent concurrent API requests from multiple tabs await navigator.locks.request('global_cu_audit', async () => { isRunning = true; // Clear cache and result containers for a fresh scan userCache = {}; historyCache = {}; results = {}; emptyWikis = []; failedWikis = []; scannedWikis = []; $('#status-msg').text('Lock acquired. Initializing...').css("color", "#0056b3"); $('#stop').prop('disabled', false); // Parse wiki filter input from the UI const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(s => s.trim()).filter(s => s !== "") : []; // Determine the set of wikis to process based on user filters const allAvailableWikis = Object.keys(globalWikiMap); let wikisToScan = allAvailableWikis; // Apply inclusion or exclusion logic based on current UI selection if (currentFilterMode === 'include') { wikisToScan = allAvailableWikis.filter(w => filterList.includes(w)); } else if (currentFilterMode === 'exclude') { wikisToScan = allAvailableWikis.filter(w => !filterList.includes(w)); } else if (currentFilterMode === 'localcu') { wikisToScan = allAvailableWikis.filter(w => localCUWikis.indexOf(w) !== -1); } // Validate that there are wikis to scan before proceeding if (wikisToScan.length === 0) { alert("No wikis selected for audit!"); isRunning = false; $('#start').prop('disabled', false); return; } // Initialize the progress bar UI $('#bar').attr('max', wikisToScan.length).val(0); // Define the standardized ISO timestamps for the MediaWiki API const START = `${yf}-${String(mf).padStart(2, '0')}-01T00:00:00Z`; const END = `${yt}-${String(mt).padStart(2, '0')}-${new Date(Date.UTC(yt, mt, 0)).getUTCDate().toString().padStart(2, '0')}T23:59:59Z`; // Generate the chronological month columns for the report table const monthCols = []; let currY = parseInt(yf), currM = parseInt(mf), endY = parseInt(yt), endM = parseInt(mt); while (currY < endY || (currY === endY && currM <= endM)) { monthCols.push({ key: `${currY}-${String(currM).padStart(2, '0')}`, label: `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][currM - 1]} ${currY}` }); currM++; if (currM > 12) { currM = 1; currY++; } } // Iterate through each wiki project to collect CheckUser activity for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; // Allow for manual stop const db = wikisToScan[i]; scannedWikis.push(db); $('#status-msg').text(`Scanning ${db} (${i + 1}/${wikisToScan.length})...`); let successLocal = false, continueToken = null; // Phase 1: Retrieve CheckUser logs and aggregate monthly stats while (!successLocal && isRunning) { try { const api = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); let params = { action: 'query', list: 'checkuserlog', culfrom: START, culto: END, culdir: 'newer', cullimit: 'max', formatversion: 2 }; if (continueToken) Object.assign(params, continueToken); let res = await robustCall(api, params); const entries = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) ? res.query.checkuserlog.entries : []; if (entries.length) { if (!results[db]) results[db] = {}; entries.forEach(e => { const user = e.checkuser; if (!results[db][user]) results[db][user] = { total: 0, months: {} }; const mKey = e.timestamp.slice(0, 7); results[db][user].total++; results[db][user].months[mKey] = (results[db][user].months[mKey] || 0) + 1; }); } // Handle API pagination if (res.continue && isRunning) { continueToken = res.continue; } else { successLocal = true; } } catch (err) { failedWikis.push(db); successLocal = true; } } // Phase 2: Capture current CheckUsers (to include users with 0 actions) if (isRunning && !failedWikis.includes(db)) { try { const auApi = new mw.ForeignApi(globalWikiMap[db] + '/w/api.php'); const auRes = await robustCall(auApi, { action: 'query', list: 'allusers', augroup: 'checkuser', aulimit: 'max', formatversion: 2 }); const currentCUs = auRes.query.allusers || []; if (currentCUs.length > 0) { if (!results[db]) results[db] = {}; currentCUs.forEach(u => { if (!results[db][u.name]) results[db][u.name] = { total: 0, months: {} }; }); } } catch (e) { /* Fallback for local API failures */ } } // Record wikis with no CheckUser presence found if (!results[db] && !failedWikis.includes(db)) { emptyWikis.push(db); } // Update UI progress $('#bar').val(i + 1); await sleep(DELAY_MS); } // Finalize results and compile the audit report $('#status-msg').text(`Generating report...`); // Calculate global activity totals for each user across all wikis let userGlobalTotals = {}; Object.keys(results).forEach(db => { Object.keys(results[db]).forEach(user => { userGlobalTotals[user] = (userGlobalTotals[user] || 0) + results[db][user].total; }); }); // Setup report timestamps const reportDate = new Date().toISOString().split('T')[0]; const viewMode = $('input[name="cu-view-mode"]:checked').val(); const timeCols = []; if (viewMode === 'months') { monthCols.forEach(m => timeCols.push({ id: m.key, label: m.label })); } else if (viewMode !== 'total') { monthCols.forEach(m => { let label; if (viewMode === 'quarters') { const q = Math.floor((parseInt(m.key.split('-')[1]) - 1) / 3) + 1; label = `${m.key.split('-')[0]}-Q${q}`; } else { label = m.key.split('-')[0]; // Years } let col = timeCols.find(c => c.label === label); if (!col) { col = { label: label, months: [] }; timeCols.push(col); } col.months.push(m.key); }); } // Initialize Wikitext output with header information let wikitext = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`; wikitext += `''Report generated on: ${getReportTimestamp()}<br />\nGenerated with [[testwiki:User:MrJaroslavik/GlobalCheckUserStats.js|GlobalCheckUserStats.js]]''\n`; // Initialize CSV output with headers let csvData = "Project,User,Role,SelfAssignCount,MaxDuration_mins,TotalActions\n"; // Document applied filters in the wikitext report let filterSummary = []; // Only add wiki filter if the list is not empty, or if localcu is selected if (currentFilterMode === 'localcu') { filterSummary.push(`Wikis with local CheckUsers`); } else if (filterList.length > 0) { if (currentFilterMode === 'include') { filterSummary.push(`Only these wikis: ${filterList.join(', ')}`); } else if (currentFilterMode === 'exclude') { filterSummary.push(`All except wikis: ${filterList.join(', ')}`); } } if (currentUserFilter === 'local') filterSummary.push(`Only users: Local CheckUsers`); else if (currentUserFilter === 'steward') filterSummary.push(`Only users: Steward actions`); if (filterSummary.length > 0) { wikitext += `<br />\n''Applied Filters: ${filterSummary.join(' | ')}''\n`; } wikitext += `\n`; // Start building the main results table let rightsLogText = `\n== Rights Log (In Period) ==\n`; const per1kExpl = "Calculated as: (Total Actions in selected period / Current Active Users) * 1,000"; wikitext += `{| class="wikitable sortable" style="font-size:90%; text-align:right;"\n! Wiki / User !! Category !! '''Total''' !! per 1k users<ref>${per1kExpl}</ref> ${timeCols.map(c => `!! ${c.label}`).join(' ')}\n`; // Process each database in alphabetical order const sortedDBs = Object.keys(results).sort(); for (const db of sortedDBs) { const metrics = await fetchWikiMetrics(db); const activeFormatted = metrics.active > 0 ? metrics.active.toLocaleString('en-US') : 'Unknown'; const interwiki = getInterwikiPrefix(db); // Build wiki-specific header row let headerRow = `|-\n! colspan="${4 + timeCols.length}" style="background:#eaecf0; text-align:center;" | [[${interwiki}:|${db}]] <small style="font-weight:normal; color:#54595d;">(Active Users as of ${reportDate}: ${activeFormatted})</small> — [[${interwiki}:Special:CheckUserLog|CheckUserLog]] · [[${interwiki}:Special:ListUsers/checkuser|ListUsers/checkuser]]\n`; let projectRows = [], wikiTotal = 0, wikiMonthlyStats = {}; monthCols.forEach(col => wikiMonthlyStats[col.key] = 0); let projectUsers = Object.keys(results[db] || {}).sort(); for (const user of projectUsers) { const userData = results[db][user]; const meta = await fetchUserData(user, db, START, END); // Filter logic: Only show users active in period or holding rights if (userData.total === 0 && !meta.hasLocalRightsInPeriod) continue; const isStewardAction = meta.isStewardAction; const isLocalCU = meta.role.includes("Local CheckUser"); // Apply UI role filters if (currentUserFilter === 'local' && !isLocalCU) continue; if (currentUserFilter === 'steward' && !isStewardAction) continue; // Update totals wikiTotal += userData.total; monthCols.forEach(col => wikiMonthlyStats[col.key] += (userData.months[col.key] || 0)); // Calculate per 1k active users metric let per1k = metrics.active > 0 ? ((userData.total / metrics.active) * 1000).toFixed(1) : "0.0"; // Construct wikitext row with dynamic timeline let row = `|-\n| style="text-align:left;" | ${user}@${db} || <small>${meta.role}</small> || '''${userData.total}''' || ${per1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = userData.months[col.id] || 0; else col.months.forEach(m => val += (userData.months[m] || 0)); row += ` || ${val}`; }); projectRows.push({ html: row + "\n", log: meta.log ? `'''${user}@${db}''':${meta.log}\n\n` : "" }); // Construct CSV row let baseRole = meta.role.split(' (')[0]; let sAssignCount = meta.isStewardAction ? meta.selfAssignCount : ""; let sMaxMins = meta.isStewardAction ? Math.round(meta.maxDurationMins) : ""; csvData += `${db},${user},"${baseRole}",${sAssignCount},${sMaxMins},${userData.total}\n`; } // Skip project if no users pass the filters if (projectRows.length === 0) { if (!emptyWikis.includes(db)) emptyWikis.push(db); continue; } // Construct project summary row let wikiPer1k = metrics.active > 0 ? ((wikiTotal / metrics.active) * 1000).toFixed(1) : "0.0"; let totalRow = `|- style="background:#f8f9fa; font-weight:bold;"\n| style="text-align:left;" | TOTAL ${db} || — || ${wikiTotal} || ${wikiPer1k}`; timeCols.forEach(col => { let val = 0; if (viewMode === 'months') val = wikiMonthlyStats[col.id] || 0; else col.months.forEach(m => val += (wikiMonthlyStats[m] || 0)); totalRow += ` || ${val}`; }); wikitext += headerRow + totalRow + "\n"; projectRows.forEach(r => { wikitext += r.html; if (r.log) rightsLogText += r.log; }); } // Close table and append metadata sections wikitext += `|}\n\n== Projects with 0 actions ==\n<div style="font-size:85%; color:#54595d;">${emptyWikis.sort().join(', ')}</div>\n`; if (failedWikis.length > 0) { wikitext += `\n== API Errors / Skipped Projects ==\n<div style="font-size:85%; color:#d33;">${failedWikis.sort().join(', ')}</div>\n`; } wikitext += rightsLogText + `\n\n<references />\n`; // Output results to UI and finalize state $('#out').val(wikitext); $('#csv-out').val(csvData); $('#output-containers').show(); $('#status-msg').text(`Done.`).css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } // Initialize the UI and finish script setup setupUI(); }); })(); nad2eoe1ykj2x0i3gskrrqlqn6fv56i User:MrJaroslavik/GlobalCheckUserList.js 2 174968 739275 739202 2026-04-24T18:24:32Z MrJaroslavik 44012 + 739275 javascript text/javascript // GlobalCheckUserList.js // ------------------------------------------------------- // Features: // - Scans current CheckUsers across 800+ projects. With filters "only these" and "Only with local CheckUsers" // - Checks Checks Meta and Local logs for the grant date. // - Wikitext output in one sortable table. // - Nicknames link to local logs; headers link to project, CheckUsers list, and CheckUserLog. // - Shows last logged actions - CheckUser, Admin, General and if User is OS, shows last OS action // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserList]]. // - Created with Gemini 3. // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserList const isBlankPage = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isOurScript = mw.config.get('wgTitle').includes('GlobalCheckUserList'); if (!isBlankPage || !isOurScript) return; let globalWikiMap = {}; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known that have local CheckUsers - https://meta.wikimedia.org/wiki/CheckUser_policy/Users_with_CheckUser_access const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Manual overrides for missing grant dates // Format: "Username@dbname": "YYYY-MM-DD" const customGrantDates = { "RiazACU@bnwiki": "2022-05-13", "KnudW@dawiki": "2017-03-23", "Dbeef@enwiki": "2025-03-07", "MarcGarver@enwikibooks": "2012-03-26", "Jake Park@eswiki": "2021-07-03", "Nohirara@idwiki": "2016-06-03", "Superspritz@itwiki": "2012-12-06", "Uncitoyen@trwiki": "2020-03-31" }; // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { $('#mw-content-text').html('<strong>GlobalCheckUserList.js: Mapping wikis...</strong>'); globalWikiMap = await loadGlobalWikiMap(); const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); let isRunning = false; let currentFilterMode = 'all'; function setupUI() { $('#firstHeading').text('GlobalCheckUserList'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <p>Audits current CheckUsers across Wikimedia projects. Reports are grouped into one single table.</p> <div style="margin-bottom:10px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-bottom:15px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="dbname1, dbname2..."> </div> <div id="wiki-list-help" style="display:none; margin-bottom:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run Audit</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-container" style="display:none; margin-top:15px;"> <strong>Wikitext Report:</strong> <textarea id="out" style="width:100%; height:450px; font-family:monospace; font-size:11px; padding:5px; border:1px solid #c8ccd1;"></textarea> </div> </div> `); $('#btn-all').click(function() { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').click(function() { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-only').click(function() { currentFilterMode = 'include'; $('#filter-input-container').show().focus(); }); $('#wiki-help-trigger').click(function() { $('#wiki-list-help').toggle(); }); $('#start').click(function() { runAudit(); }); $('#stop').click(function() { isRunning = false; }); } async function findGrantDate(user, db) { // Check manual list first const key = user + "@" + db; if (customGrantDates[key]) return customGrantDates[key]; // Search Meta-Wiki for steward actions (User:Name@dbname) // We use User:Name for Meta itself, and User:Name@dbname for other wikis const title = (db === 'metawiki') ? 'User:' + user : 'User:' + user + '@' + db; let mPars = { action: 'query', list: 'logevents', letype: 'rights', letitle: title, ledir: 'newer', lelimit: 'max', formatversion: 2 }; let resM = await robustCall(metaApi, mPars); let logsM = (resM.query && resM.query.logevents) || []; for (let ev of logsM) { let p = ev.params || {}; let nG = extractGroups(p.newgroups || p.add || p[1] || p["1"]); let oG = extractGroups(p.oldgroups || p.remove || p[0] || p["0"]); if (nG.indexOf('checkuser') !== -1 && oG.indexOf('checkuser') === -1) { return ev.timestamp.substring(0, 10); } } return "Unknown"; } async function runAudit() { isRunning = true; let wt = ""; const adminTypes = [ 'block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'contentmodel', 'import', 'managetags', 'massmessage', 'checkuser-temporary-account', 'ipinfo', 'pagelang', 'renameuser', 'stable', 'gblblock', 'abusefilter-protected-vars' ]; const cuInPublicTypes = ['abusefilterprivatedetails']; $('#start').prop('disabled', true); $('#stop').prop('disabled', false); $('#status-msg').text('Waiting for queue lock...').css("color", "orange"); // Request browser lock to prevent concurrent runs await navigator.locks.request('global_cu_list_lock', async () => { wt = "== Global CheckUser List ==\n"; wt += "''Report generated on: " + getReportTimestamp() + "<br />\n"; wt += "Generated with [[testwiki:User:MrJaroslavik/GlobalCheckUserList.js|GlobalCheckUserList.js]]''\n"; const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ""; }) : []; if (currentFilterMode === 'localcu') { wt += "<br />''Filter applied: Wikis with local CheckUsers''<ref>[[meta:CheckUser policy/Users with CheckUser access]]</ref>\n"; } else if (currentFilterMode === 'include' && filterList.length > 0) { wt += "<br />''Filter applied: Only these wikis (" + filterList.join(', ') + ")''\n"; } wt += "\n"; wt += '{| class="wikitable sortable" style="font-size:90%; width:100%;"\n' + '! User !! CU Since !! Duration !! Last CU Action<ref>Logs: checkuserlog, checkuser-temporary-account, abusefilterprivatedetails</ref> !! ' + 'Last Admin Action<ref>Logs: ' + adminTypes.join(', ') + '</ref> !! ' + 'Last OS Action<ref>Logs: suppress</ref> !! ' + 'Last Logged Action<ref>Logs: all public logged actions</ref> !! Last Edit\n'; const allWikis = Object.keys(globalWikiMap); let wikisToScan; if (currentFilterMode === 'include' && filterList.length > 0) { wikisToScan = allWikis.filter(function(w) { return filterList.indexOf(w) !== -1; }); } else if (currentFilterMode === 'localcu') { wikisToScan = allWikis.filter(function(w) { return localCUWikis.indexOf(w) !== -1; }); } else { wikisToScan = allWikis; } $('#bar').attr('max', wikisToScan.length).val(0); for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; const db = wikisToScan[i]; $('#status-msg').text('Auditing ' + db + ' (' + (i + 1) + '/' + wikisToScan.length + ')...').css("color", "#0056b3"); try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Find all users currently in the checkuser group let users = [], auDone = false, auCont = null; while (!auDone && isRunning) { let auPars = { action: 'query', list: 'allusers', augroup: 'checkuser', auprop: 'groups', aulimit: 'max', formatversion: 2 }; if (auCont) Object.assign(auPars, auCont); let res = await robustCall(localApi, auPars); users = users.concat(res.query.allusers || []); if (res.continue) auCont = res.continue; else auDone = true; } if (users.length > 0) { const iw = getInterwikiPrefix(db); wt += '|-\n! colspan="8" style="background:#eaecf0; text-align:center;" | ' + '[[' + iw + ':|' + db + ']] — [[' + iw + ':Special:ListUsers/checkuser|(list)]] — [[' + iw + ':Special:CheckUserLog|(log)]]\n'; for (const u of users) { const username = u.name; const sinceDate = await findGrantDate(username, db, localApi); // Calculate how many days user has held rights let days = "-"; if (sinceDate && sinceDate !== "Unknown") { const cleanDate = sinceDate.substring(0, 10); const diff = new Date() - new Date(cleanDate); days = Math.floor(diff / 86400000) + "d"; } // Check private CheckUser log const cuLog = await robustCall(localApi, { action: 'query', list: 'checkuserlog', culuser: username, cullimit: 1, formatversion: 2 }); let lastCU = (cuLog.query && cuLog.query.checkuserlog && cuLog.query.checkuserlog.entries && cuLog.query.checkuserlog.entries[0]) ? cuLog.query.checkuserlog.entries[0].timestamp.substring(0, 10) : "Never"; // Fetch public log history const allUserLogsReq = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, lelimit: 'max', formatversion: 2 }); const allUserLogs = (allUserLogsReq.query && allUserLogsReq.query.logevents) || []; // Cross-check for public logs that indicate CU activity const pubCUEvt = allUserLogs.find(function(ev) { return cuInPublicTypes.indexOf(ev.type) !== -1; }); if (pubCUEvt) { const pubCUTs = pubCUEvt.timestamp.substring(0, 10); if (lastCU === "Never" || pubCUTs > lastCU) lastCU = pubCUTs; } // Identify latest admin action const admEvent = allUserLogs.find(function(ev) { return adminTypes.indexOf(ev.type) !== -1; }); const lastAdm = admEvent ? admEvent.timestamp.substring(0, 10) : "Never"; // Get most recent log entry of any type const lastGen = allUserLogs.length > 0 ? allUserLogs[0].timestamp.substring(0, 10) : "Never"; // Look for Suppression actions (filtering for setstatus) const osLog = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, letype: 'suppress', lelimit: 50, // Scan more entries to find setstatus formatversion: 2 }); let lastOS = "-"; var uGroups = extractGroups(u.groups); var hasCurrentOSGroups = (uGroups.indexOf('suppress') !== -1 || uGroups.indexOf('oversight') !== -1); if (hasCurrentOSGroups) { lastOS = "Never"; if (osLog.query && osLog.query.logevents) { // Find first entry that is NOT 'setstatus' const realOSEvent = osLog.query.logevents.find(function(e) { // Check if params exists and if subtype is NOT setstatus return !e.params || (e.params && e.params.subtype !== 'setstatus'); }); if (realOSEvent) { lastOS = realOSEvent.timestamp.substring(0, 10); } } } // Check last manual edit date const edLog = await robustCall(localApi, { action: 'query', list: 'usercontribs', ucuser: username, uclimit: 1, formatversion: 2 }); const lastEdit = (edLog.query && edLog.query.usercontribs && edLog.query.usercontribs[0]) ? edLog.query.usercontribs[0].timestamp.substring(0, 10) : "Never"; wt += '|-\n| style="text-align:left; white-space:nowrap;" | [[' + iw + ':Special:Log/' + username + '|' + username + '@' + db + ']] || ' + sinceDate + ' || ' + days + ' || ' + lastCU + ' || ' + lastAdm + ' || ' + lastOS + ' || ' + lastGen + ' || ' + lastEdit + '\n'; } } } catch (err) { console.error('Audit failed for ' + db, err); } $('#bar').val(i + 1); await sleep(DELAY_MS); } wt += '|}\n\n=== References ===\n<references />\n'; $('#out').val(wt); $('#output-container').show(); $('#status-msg').text('Audit complete!').css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } setupUI(); }); })(); gs9uu5q95n132gekuk96o5jths321w2 739279 739275 2026-04-24T18:32:01Z MrJaroslavik 44012 enter 739279 javascript text/javascript // GlobalCheckUserList.js // ------------------------------------------------------- // Features: // - Scans current CheckUsers across 800+ projects. With filters "only these" and "Only with local CheckUsers" // - Checks Checks Meta and Local logs for the grant date. // - Wikitext output in one sortable table. // - Nicknames link to local logs; headers link to project, CheckUsers list, and CheckUserLog. // - Shows last logged actions - CheckUser, Admin, General and if User is OS, shows last OS action // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserList]]. // - Created with Gemini 3. // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserList const isBlankPage = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isOurScript = mw.config.get('wgTitle').includes('GlobalCheckUserList'); if (!isBlankPage || !isOurScript) return; let globalWikiMap = {}; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known that have local CheckUsers - https://meta.wikimedia.org/wiki/CheckUser_policy/Users_with_CheckUser_access const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Manual overrides for missing grant dates // Format: "Username@dbname": "YYYY-MM-DD" const customGrantDates = { "RiazACU@bnwiki": "2022-05-13", "KnudW@dawiki": "2017-03-23", "Dbeef@enwiki": "2025-03-07", "MarcGarver@enwikibooks": "2012-03-26", "Jake Park@eswiki": "2021-07-03", "Nohirara@idwiki": "2016-06-03", "Superspritz@itwiki": "2012-12-06", "Uncitoyen@trwiki": "2020-03-31" }; // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { $('#mw-content-text').html('<strong>GlobalCheckUserList.js: Mapping wikis...</strong>'); globalWikiMap = await loadGlobalWikiMap(); const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); let isRunning = false; let currentFilterMode = 'all'; function setupUI() { $('#firstHeading').text('GlobalCheckUserList'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <p>Audits current CheckUsers across Wikimedia projects. Reports are grouped into one single table.</p> <div style="margin-bottom:10px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-bottom:15px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="dbname1, dbname2..."> </div> <div id="wiki-list-help" style="display:none; margin-bottom:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run Audit</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-container" style="display:none; margin-top:15px;"> <strong>Wikitext Report:</strong> <textarea id="out" style="width:100%; height:450px; font-family:monospace; font-size:11px; padding:5px; border:1px solid #c8ccd1;"></textarea> </div> </div> `); $('#btn-all').click(function() { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').click(function() { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-only').click(function() { currentFilterMode = 'include'; $('#filter-input-container').show().focus(); }); $('#wiki-help-trigger').click(function() { $('#wiki-list-help').toggle(); }); // Enter $('#wiki-filter').keypress(function(e) { if (e.which === 13) { // 13 is Enter e.preventDefault(); $('#start').click(); } }); $('#start').click(function() { runAudit(); }); $('#stop').click(function() { isRunning = false; }); } async function findGrantDate(user, db) { // Check manual list first const key = user + "@" + db; if (customGrantDates[key]) return customGrantDates[key]; // Search Meta-Wiki for steward actions (User:Name@dbname) // We use User:Name for Meta itself, and User:Name@dbname for other wikis const title = (db === 'metawiki') ? 'User:' + user : 'User:' + user + '@' + db; let mPars = { action: 'query', list: 'logevents', letype: 'rights', letitle: title, ledir: 'newer', lelimit: 'max', formatversion: 2 }; let resM = await robustCall(metaApi, mPars); let logsM = (resM.query && resM.query.logevents) || []; for (let ev of logsM) { let p = ev.params || {}; let nG = extractGroups(p.newgroups || p.add || p[1] || p["1"]); let oG = extractGroups(p.oldgroups || p.remove || p[0] || p["0"]); if (nG.indexOf('checkuser') !== -1 && oG.indexOf('checkuser') === -1) { return ev.timestamp.substring(0, 10); } } return "Unknown"; } async function runAudit() { isRunning = true; let wt = ""; const adminTypes = [ 'block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'contentmodel', 'import', 'managetags', 'massmessage', 'checkuser-temporary-account', 'ipinfo', 'pagelang', 'renameuser', 'stable', 'gblblock', 'abusefilter-protected-vars' ]; const cuInPublicTypes = ['abusefilterprivatedetails']; $('#start').prop('disabled', true); $('#stop').prop('disabled', false); $('#status-msg').text('Waiting for queue lock...').css("color", "orange"); // Request browser lock to prevent concurrent runs await navigator.locks.request('global_cu_list_lock', async () => { wt = "== Global CheckUser List ==\n"; wt += "''Report generated on: " + getReportTimestamp() + "<br />\n"; wt += "Generated with [[testwiki:User:MrJaroslavik/GlobalCheckUserList.js|GlobalCheckUserList.js]]''\n"; const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ""; }) : []; if (currentFilterMode === 'localcu') { wt += "<br />''Filter applied: Wikis with local CheckUsers''<ref>[[meta:CheckUser policy/Users with CheckUser access]]</ref>\n"; } else if (currentFilterMode === 'include' && filterList.length > 0) { wt += "<br />''Filter applied: Only these wikis (" + filterList.join(', ') + ")''\n"; } wt += "\n"; wt += '{| class="wikitable sortable" style="font-size:90%; width:100%;"\n' + '! User !! CU Since !! Duration !! Last CU Action<ref>Logs: checkuserlog, checkuser-temporary-account, abusefilterprivatedetails</ref> !! ' + 'Last Admin Action<ref>Logs: ' + adminTypes.join(', ') + '</ref> !! ' + 'Last OS Action<ref>Logs: suppress</ref> !! ' + 'Last Logged Action<ref>Logs: all public logged actions</ref> !! Last Edit\n'; const allWikis = Object.keys(globalWikiMap); let wikisToScan; if (currentFilterMode === 'include' && filterList.length > 0) { wikisToScan = allWikis.filter(function(w) { return filterList.indexOf(w) !== -1; }); } else if (currentFilterMode === 'localcu') { wikisToScan = allWikis.filter(function(w) { return localCUWikis.indexOf(w) !== -1; }); } else { wikisToScan = allWikis; } $('#bar').attr('max', wikisToScan.length).val(0); for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; const db = wikisToScan[i]; $('#status-msg').text('Auditing ' + db + ' (' + (i + 1) + '/' + wikisToScan.length + ')...').css("color", "#0056b3"); try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Find all users currently in the checkuser group let users = [], auDone = false, auCont = null; while (!auDone && isRunning) { let auPars = { action: 'query', list: 'allusers', augroup: 'checkuser', auprop: 'groups', aulimit: 'max', formatversion: 2 }; if (auCont) Object.assign(auPars, auCont); let res = await robustCall(localApi, auPars); users = users.concat(res.query.allusers || []); if (res.continue) auCont = res.continue; else auDone = true; } if (users.length > 0) { const iw = getInterwikiPrefix(db); wt += '|-\n! colspan="8" style="background:#eaecf0; text-align:center;" | ' + '[[' + iw + ':|' + db + ']] — [[' + iw + ':Special:ListUsers/checkuser|(list)]] — [[' + iw + ':Special:CheckUserLog|(log)]]\n'; for (const u of users) { const username = u.name; const sinceDate = await findGrantDate(username, db, localApi); // Calculate how many days user has held rights let days = "-"; if (sinceDate && sinceDate !== "Unknown") { const cleanDate = sinceDate.substring(0, 10); const diff = new Date() - new Date(cleanDate); days = Math.floor(diff / 86400000) + "d"; } // Check private CheckUser log const cuLog = await robustCall(localApi, { action: 'query', list: 'checkuserlog', culuser: username, cullimit: 1, formatversion: 2 }); let lastCU = (cuLog.query && cuLog.query.checkuserlog && cuLog.query.checkuserlog.entries && cuLog.query.checkuserlog.entries[0]) ? cuLog.query.checkuserlog.entries[0].timestamp.substring(0, 10) : "Never"; // Fetch public log history const allUserLogsReq = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, lelimit: 'max', formatversion: 2 }); const allUserLogs = (allUserLogsReq.query && allUserLogsReq.query.logevents) || []; // Cross-check for public logs that indicate CU activity const pubCUEvt = allUserLogs.find(function(ev) { return cuInPublicTypes.indexOf(ev.type) !== -1; }); if (pubCUEvt) { const pubCUTs = pubCUEvt.timestamp.substring(0, 10); if (lastCU === "Never" || pubCUTs > lastCU) lastCU = pubCUTs; } // Identify latest admin action const admEvent = allUserLogs.find(function(ev) { return adminTypes.indexOf(ev.type) !== -1; }); const lastAdm = admEvent ? admEvent.timestamp.substring(0, 10) : "Never"; // Get most recent log entry of any type const lastGen = allUserLogs.length > 0 ? allUserLogs[0].timestamp.substring(0, 10) : "Never"; // Look for Suppression actions (filtering for setstatus) const osLog = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, letype: 'suppress', lelimit: 50, // Scan more entries to find setstatus formatversion: 2 }); let lastOS = "-"; var uGroups = extractGroups(u.groups); var hasCurrentOSGroups = (uGroups.indexOf('suppress') !== -1 || uGroups.indexOf('oversight') !== -1); if (hasCurrentOSGroups) { lastOS = "Never"; if (osLog.query && osLog.query.logevents) { // Find first entry that is NOT 'setstatus' const realOSEvent = osLog.query.logevents.find(function(e) { // Check if params exists and if subtype is NOT setstatus return !e.params || (e.params && e.params.subtype !== 'setstatus'); }); if (realOSEvent) { lastOS = realOSEvent.timestamp.substring(0, 10); } } } // Check last manual edit date const edLog = await robustCall(localApi, { action: 'query', list: 'usercontribs', ucuser: username, uclimit: 1, formatversion: 2 }); const lastEdit = (edLog.query && edLog.query.usercontribs && edLog.query.usercontribs[0]) ? edLog.query.usercontribs[0].timestamp.substring(0, 10) : "Never"; wt += '|-\n| style="text-align:left; white-space:nowrap;" | [[' + iw + ':Special:Log/' + username + '|' + username + '@' + db + ']] || ' + sinceDate + ' || ' + days + ' || ' + lastCU + ' || ' + lastAdm + ' || ' + lastOS + ' || ' + lastGen + ' || ' + lastEdit + '\n'; } } } catch (err) { console.error('Audit failed for ' + db, err); } $('#bar').val(i + 1); await sleep(DELAY_MS); } wt += '|}\n\n=== References ===\n<references />\n'; $('#out').val(wt); $('#output-container').show(); $('#status-msg').text('Audit complete!').css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } setupUI(); }); })(); joe7ka3qtnn6c9mrl952tbgmwwcwikm 739282 739279 2026-04-24T18:57:06Z MrJaroslavik 44012 enter 739282 javascript text/javascript // GlobalCheckUserList.js // ------------------------------------------------------- // Features: // - Scans current CheckUsers across 800+ projects. With filters "only these" and "Only with local CheckUsers" // - Checks Checks Meta and Local logs for the grant date. // - Wikitext output in one sortable table. // - Nicknames link to local logs; headers link to project, CheckUsers list, and CheckUserLog. // - Shows last logged actions - CheckUser, Admin, General and if User is OS, shows last OS action // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserList]]. // - Created with Gemini 3. // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserList const isBlankPage = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isOurScript = mw.config.get('wgTitle').includes('GlobalCheckUserList'); if (!isBlankPage || !isOurScript) return; let globalWikiMap = {}; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known that have local CheckUsers - https://meta.wikimedia.org/wiki/CheckUser_policy/Users_with_CheckUser_access const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Manual overrides for missing grant dates // Format: "Username@dbname": "YYYY-MM-DD" const customGrantDates = { "RiazACU@bnwiki": "2022-05-13", "KnudW@dawiki": "2017-03-23", "Dbeef@enwiki": "2025-03-07", "MarcGarver@enwikibooks": "2012-03-26", "Jake Park@eswiki": "2021-07-03", "Nohirara@idwiki": "2016-06-03", "Superspritz@itwiki": "2012-12-06", "Uncitoyen@trwiki": "2020-03-31" }; // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { $('#mw-content-text').html('<strong>GlobalCheckUserList.js: Mapping wikis...</strong>'); globalWikiMap = await loadGlobalWikiMap(); const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); let isRunning = false; let currentFilterMode = 'all'; function setupUI() { $('#firstHeading').text('GlobalCheckUserList'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <p>Audits current CheckUsers across Wikimedia projects. Reports are grouped into one single table.</p> <div style="margin-bottom:10px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-bottom:15px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="dbname1, dbname2..."> </div> <div id="wiki-list-help" style="display:none; margin-bottom:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run Audit</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-container" style="display:none; margin-top:15px;"> <strong>Wikitext Report:</strong> <textarea id="out" style="width:100%; height:450px; font-family:monospace; font-size:11px; padding:5px; border:1px solid #c8ccd1;"></textarea> </div> </div> `); $('#btn-all').click(function() { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').click(function() { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-only').click(function() { currentFilterMode = 'include'; $('#filter-input-container').show().focus(); }); $('#wiki-help-trigger').click(function() { $('#wiki-list-help').toggle(); }); // Enter $('#wiki-filter').keypress(function(e) { if (e.which === 13) { // 13 is Enter e.preventDefault(); $('#start').click(); } }); $('#start').click(function() { runAudit(); }); // Enter $(document).on('keypress', '#wiki-filter', function(e) { if (e.which === 13) { e.preventDefault(); $('#start').click(); } }); $('#stop').click(function() { isRunning = false; }); } async function findGrantDate(user, db) { // Check manual list first const key = user + "@" + db; if (customGrantDates[key]) return customGrantDates[key]; // Search Meta-Wiki for steward actions (User:Name@dbname) // We use User:Name for Meta itself, and User:Name@dbname for other wikis const title = (db === 'metawiki') ? 'User:' + user : 'User:' + user + '@' + db; let mPars = { action: 'query', list: 'logevents', letype: 'rights', letitle: title, ledir: 'newer', lelimit: 'max', formatversion: 2 }; let resM = await robustCall(metaApi, mPars); let logsM = (resM.query && resM.query.logevents) || []; for (let ev of logsM) { let p = ev.params || {}; let nG = extractGroups(p.newgroups || p.add || p[1] || p["1"]); let oG = extractGroups(p.oldgroups || p.remove || p[0] || p["0"]); if (nG.indexOf('checkuser') !== -1 && oG.indexOf('checkuser') === -1) { return ev.timestamp.substring(0, 10); } } return "Unknown"; } async function runAudit() { isRunning = true; let wt = ""; const adminTypes = [ 'block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'contentmodel', 'import', 'managetags', 'massmessage', 'checkuser-temporary-account', 'ipinfo', 'pagelang', 'renameuser', 'stable', 'gblblock', 'abusefilter-protected-vars' ]; const cuInPublicTypes = ['abusefilterprivatedetails']; $('#start').prop('disabled', true); $('#stop').prop('disabled', false); $('#status-msg').text('Waiting for queue lock...').css("color", "orange"); // Request browser lock to prevent concurrent runs await navigator.locks.request('global_cu_list_lock', async () => { wt = "== Global CheckUser List ==\n"; wt += "''Report generated on: " + getReportTimestamp() + "<br />\n"; wt += "Generated with [[testwiki:User:MrJaroslavik/GlobalCheckUserList.js|GlobalCheckUserList.js]]''\n"; const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ""; }) : []; if (currentFilterMode === 'localcu') { wt += "<br />''Filter applied: Wikis with local CheckUsers''<ref>[[meta:CheckUser policy/Users with CheckUser access]]</ref>\n"; } else if (currentFilterMode === 'include' && filterList.length > 0) { wt += "<br />''Filter applied: Only these wikis (" + filterList.join(', ') + ")''\n"; } wt += "\n"; wt += '{| class="wikitable sortable" style="font-size:90%; width:100%;"\n' + '! User !! CU Since !! Duration !! Last CU Action<ref>Logs: checkuserlog, checkuser-temporary-account, abusefilterprivatedetails</ref> !! ' + 'Last Admin Action<ref>Logs: ' + adminTypes.join(', ') + '</ref> !! ' + 'Last OS Action<ref>Logs: suppress</ref> !! ' + 'Last Logged Action<ref>Logs: all public logged actions</ref> !! Last Edit\n'; const allWikis = Object.keys(globalWikiMap); let wikisToScan; if (currentFilterMode === 'include' && filterList.length > 0) { wikisToScan = allWikis.filter(function(w) { return filterList.indexOf(w) !== -1; }); } else if (currentFilterMode === 'localcu') { wikisToScan = allWikis.filter(function(w) { return localCUWikis.indexOf(w) !== -1; }); } else { wikisToScan = allWikis; } $('#bar').attr('max', wikisToScan.length).val(0); for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; const db = wikisToScan[i]; $('#status-msg').text('Auditing ' + db + ' (' + (i + 1) + '/' + wikisToScan.length + ')...').css("color", "#0056b3"); try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Find all users currently in the checkuser group let users = [], auDone = false, auCont = null; while (!auDone && isRunning) { let auPars = { action: 'query', list: 'allusers', augroup: 'checkuser', auprop: 'groups', aulimit: 'max', formatversion: 2 }; if (auCont) Object.assign(auPars, auCont); let res = await robustCall(localApi, auPars); users = users.concat(res.query.allusers || []); if (res.continue) auCont = res.continue; else auDone = true; } if (users.length > 0) { const iw = getInterwikiPrefix(db); wt += '|-\n! colspan="8" style="background:#eaecf0; text-align:center;" | ' + '[[' + iw + ':|' + db + ']] — [[' + iw + ':Special:ListUsers/checkuser|(list)]] — [[' + iw + ':Special:CheckUserLog|(log)]]\n'; for (const u of users) { const username = u.name; const sinceDate = await findGrantDate(username, db, localApi); // Calculate how many days user has held rights let days = "-"; if (sinceDate && sinceDate !== "Unknown") { const cleanDate = sinceDate.substring(0, 10); const diff = new Date() - new Date(cleanDate); days = Math.floor(diff / 86400000) + "d"; } // Check private CheckUser log const cuLog = await robustCall(localApi, { action: 'query', list: 'checkuserlog', culuser: username, cullimit: 1, formatversion: 2 }); let lastCU = (cuLog.query && cuLog.query.checkuserlog && cuLog.query.checkuserlog.entries && cuLog.query.checkuserlog.entries[0]) ? cuLog.query.checkuserlog.entries[0].timestamp.substring(0, 10) : "Never"; // Fetch public log history const allUserLogsReq = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, lelimit: 'max', formatversion: 2 }); const allUserLogs = (allUserLogsReq.query && allUserLogsReq.query.logevents) || []; // Cross-check for public logs that indicate CU activity const pubCUEvt = allUserLogs.find(function(ev) { return cuInPublicTypes.indexOf(ev.type) !== -1; }); if (pubCUEvt) { const pubCUTs = pubCUEvt.timestamp.substring(0, 10); if (lastCU === "Never" || pubCUTs > lastCU) lastCU = pubCUTs; } // Identify latest admin action const admEvent = allUserLogs.find(function(ev) { return adminTypes.indexOf(ev.type) !== -1; }); const lastAdm = admEvent ? admEvent.timestamp.substring(0, 10) : "Never"; // Get most recent log entry of any type const lastGen = allUserLogs.length > 0 ? allUserLogs[0].timestamp.substring(0, 10) : "Never"; // Look for Suppression actions (filtering for setstatus) const osLog = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, letype: 'suppress', lelimit: 50, // Scan more entries to find setstatus formatversion: 2 }); let lastOS = "-"; var uGroups = extractGroups(u.groups); var hasCurrentOSGroups = (uGroups.indexOf('suppress') !== -1 || uGroups.indexOf('oversight') !== -1); if (hasCurrentOSGroups) { lastOS = "Never"; if (osLog.query && osLog.query.logevents) { // Find first entry that is NOT 'setstatus' const realOSEvent = osLog.query.logevents.find(function(e) { // Check if params exists and if subtype is NOT setstatus return !e.params || (e.params && e.params.subtype !== 'setstatus'); }); if (realOSEvent) { lastOS = realOSEvent.timestamp.substring(0, 10); } } } // Check last manual edit date const edLog = await robustCall(localApi, { action: 'query', list: 'usercontribs', ucuser: username, uclimit: 1, formatversion: 2 }); const lastEdit = (edLog.query && edLog.query.usercontribs && edLog.query.usercontribs[0]) ? edLog.query.usercontribs[0].timestamp.substring(0, 10) : "Never"; wt += '|-\n| style="text-align:left; white-space:nowrap;" | [[' + iw + ':Special:Log/' + username + '|' + username + '@' + db + ']] || ' + sinceDate + ' || ' + days + ' || ' + lastCU + ' || ' + lastAdm + ' || ' + lastOS + ' || ' + lastGen + ' || ' + lastEdit + '\n'; } } } catch (err) { console.error('Audit failed for ' + db, err); } $('#bar').val(i + 1); await sleep(DELAY_MS); } wt += '|}\n\n=== References ===\n<references />\n'; $('#out').val(wt); $('#output-container').show(); $('#status-msg').text('Audit complete!').css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } setupUI(); }); })(); 6qgawo3hpjm5b4uedoy4bien3dgvkor 739286 739282 2026-04-24T19:00:29Z MrJaroslavik 44012 rv 739286 javascript text/javascript // GlobalCheckUserList.js // ------------------------------------------------------- // Features: // - Scans current CheckUsers across 800+ projects. With filters "only these" and "Only with local CheckUsers" // - Checks Checks Meta and Local logs for the grant date. // - Wikitext output in one sortable table. // - Nicknames link to local logs; headers link to project, CheckUsers list, and CheckUserLog. // - Shows last logged actions - CheckUser, Admin, General and if User is OS, shows last OS action // - UI: Integrated at [[meta:Special:BlankPage/GlobalCheckUserList]]. // - Created with Gemini 3. // ------------------------------------------------------- (function() { 'use strict'; // Run only on Special:BlankPage/GlobalCheckUserList const isBlankPage = mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage'; const isOurScript = mw.config.get('wgTitle').includes('GlobalCheckUserList'); if (!isBlankPage || !isOurScript) return; let globalWikiMap = {}; // List of wikis where CheckUser extension is disabled - https://noc.wikimedia.org/conf/highlight.php?file=dblists/checkuser-disabled.dblist const disabledCUWikis = [ 'aawikibooks', 'abwiktionary', 'advisorywiki', 'akwiktionary', 'angwikiquote', 'angwikisource', 'astwikibooks', 'astwikiquote', 'aswikibooks', 'aswiktionary', 'avwiktionary', 'aywikibooks', 'bhwiktionary', 'biwikibooks', 'biwiktionary', 'bmwikibooks', 'bmwikiquote', 'bmwiktionary', 'bowiktionary', 'chwikibooks', 'chwiktionary', 'cnwikimedia', 'crwikiquote', 'crwiktionary', 'dzwiktionary', 'gawikibooks', 'gnwikibooks', 'gotwikibooks', 'guwikibooks', 'htwikisource', 'huwikinews', 'hzwiki', 'iewikibooks', 'iiwiki', 'internalwiki', 'kjwiki', 'kkwikiquote', 'knwikibooks', 'krwiki', 'krwikiquote', 'kswikibooks', 'kswikiquote', 'kwwikiquote', 'lbwikibooks', 'lbwikiquote', 'lnwikibooks', 'lvwikibooks', 'mhwiktionary', 'mnwikibooks', 'muswiki', 'nahwikibooks', 'nawikibooks', 'nawikiquote', 'ndswikibooks', 'ndswikiquote', 'piwiktionary', 'pswikibooks', 'quwikibooks', 'quwikiquote', 'rmwikibooks', 'rmwiktionary', 'rnwiktionary', 'scwiktionary', 'searchcomwiki', 'snwiktionary', 'spcomwiki', 'swwikibooks', 'thwikinews', 'tkwikibooks', 'tkwikiquote', 'towiktionary', 'transitionteamwiki', 'trwikinews', 'ttwikiquote', 'twwiktionary', 'ugwikibooks', 'ugwikiquote', 'vowikibooks', 'vowikiquote', 'wawikibooks', 'wikimania2005wiki', 'wikimania2006wiki', 'wikimania2007wiki', 'wikimania2009wiki', 'wikimania2015wiki', 'wowikiquote', 'xhwikibooks', 'xhwiktionary', 'yowikibooks', 'yowiktionary', 'zawikibooks', 'zawikiquote', 'zawiktionary', 'zh_min_nanwikibooks', 'zuwikibooks', // Closed/Unavailable projects (added manually) 'aawiktionary', 'akwikibooks', 'amwikiquote', 'angwikibooks', 'bgwikinews', 'bowikibooks', 'chowiki', 'cowikibooks', 'cowikiquote', 'gawikiquote', 'howiki', 'ikwiktionary', 'kywikibooks', 'mhwiki', 'miwikibooks', 'mywikibooks', 'nawiki', 'nzwikimedia', 'pa_uswikimedia', 'qualitywiki', 'ruwikimedia', 'sdwikinews', 'sewikibooks', 'simplewikibooks', 'strategywiki', 'suwikibooks', 'tenwiki', 'usabilitywiki', 'uzwikibooks', 'wikimania2010wiki', 'wikimania2011wiki', 'wikimania2012wiki', 'wikimania2013wiki', 'wikimania2014wiki', 'wikimania2016wiki', 'wikimania2017wiki', 'wikimania2018wiki', 'zh_min_nanwikiquote' ]; // Projects known that have local CheckUsers - https://meta.wikimedia.org/wiki/CheckUser_policy/Users_with_CheckUser_access const localCUWikis = [ 'arwiki', 'bnwiki', 'cawiki', 'cswiki', 'dawiki', 'nlwiki', 'enwikibooks', 'enwiki', 'enwikivoyage', 'enwiktionary', 'fiwiki', 'frwiki', 'dewiki', 'hewiki', 'huwiki', 'idwiki', 'itwiki', 'jawiki', 'kowiki', 'fawiki', 'plwiki', 'ptwiki', 'ruwiki', 'srwiki', 'simplewiki', 'slwiki', 'eswiki', 'svwiki', 'thwiki', 'trwiki', 'ukwiki', 'viwiki', 'commonswiki', 'specieswiki', 'metawiki', 'wikidatawiki' ]; // Fetch all available wikis and filter out restricted ones function loadGlobalWikiMap() { const api = new mw.Api(); return api.get({ action: 'sitematrix', format: 'json', smtype: 'language|special', smlangprop: 'site', smsiteprop: 'dbname|url', formatversion: 2, origin: '*' }).then(function(data) { const matrix = data.sitematrix; const wikiMap = {}; Object.keys(matrix).forEach(function(key) { const entry = matrix[key]; if (entry.site && Array.isArray(entry.site)) { entry.site.forEach(function(site) { if (site.private || site.fishbowl || disabledCUWikis.indexOf(site.dbname) !== -1) return; wikiMap[site.dbname] = site.url; }); } }); if (matrix.specials && Array.isArray(matrix.specials)) { matrix.specials.forEach(function(special) { if (special.private || special.fishbowl || disabledCUWikis.indexOf(special.dbname) !== -1) return; wikiMap[special.dbname] = special.url; }); } return wikiMap; }); } // Convert database names to interwiki prefixes for table links function getInterwikiPrefix(db) { const specialMap = { 'commonswiki': 'c', 'metawiki': 'm', 'wikidatawiki': 'd', 'wikifunctionswiki': 'f', 'mediawikiwiki': 'mw', 'specieswiki': 'species', 'sourceswiki': 'oldwikisource', 'foundationwiki': 'foundation', 'incubatorwiki': 'incubator', 'outreachwiki': 'outreach', 'betawikiversity': 'betawikiversity', 'be_x_oldwiki': 'be-tarask', 'zh_classicalwiki': 'lzh', 'zh_min_nanwiki': 'nan', 'zh_yuewiki': 'yue' }; if (specialMap[db]) return specialMap[db]; const name = db.replace(/_/g, '-'); if (name.endsWith('wikisource')) return 's:' + name.slice(0, -10); if (name.endsWith('wikiversity')) return 'v:' + name.slice(0, -11); if (name.endsWith('wiktionary')) return 'wikt:' + name.slice(0, -10); if (name.endsWith('wikivoyage')) return 'voy:' + name.slice(0, -10); if (name.endsWith('wikibooks')) return 'b:' + name.slice(0, -9); if (name.endsWith('wikiquote')) return 'q:' + name.slice(0, -9); if (name.endsWith('wikinews')) return 'n:' + name.slice(0, -8); if (name.endsWith('wikimedia')) return 'wm' + name.slice(0, -9); if (name.endsWith('wiki')) return 'w:' + name.slice(0, -4); return 'w:' + name; } // Manual overrides for missing grant dates // Format: "Username@dbname": "YYYY-MM-DD" const customGrantDates = { "RiazACU@bnwiki": "2022-05-13", "KnudW@dawiki": "2017-03-23", "Dbeef@enwiki": "2025-03-07", "MarcGarver@enwikibooks": "2012-03-26", "Jake Park@eswiki": "2021-07-03", "Nohirara@idwiki": "2016-06-03", "Superspritz@itwiki": "2012-12-06", "Uncitoyen@trwiki": "2020-03-31" }; // Helper function to pause execution (useful for preventing API limits) const DELAY_MS = 500; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // Makes API calls with automatic retries for "429 Too Many Requests" errors async function robustCall(api, params) { let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Attempt to fetch data from the MediaWiki API return await api.get(params); } catch (err) { // Check if the server is actively rejecting requests due to high load (HTTP 429) // Note: Optional chaining (?.) is avoided for broad MediaWiki editor compatibility if (err && err.status === 429) { retries++; // Read the 'Retry-After' header provided by the server, or default to a scaling wait time const waitTime = parseInt(err.xhr && err.xhr.getResponseHeader('Retry-After')) || (30 * retries); // Notify the user via the UI that the script is pausing (supports both UI container types) $('#status-msg, #cu-loader').html( `<span style="color:orange; font-weight:bold;">Server is busy! Pausing for ${waitTime} seconds...</span>` ).show(); // Pause execution for the requested duration before trying again await sleep(waitTime * 1000); } else { // If it is a different error (network failure, 500 Internal Error, etc.), stop and throw throw err; } } } // Fail completely if max retries are exceeded throw new Error("Failed to reach API after multiple attempts due to server limits."); } // Returns a standardized UTC timestamp for report headers (YYYY-MM-DD HH:MM) function getReportTimestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 16) + ' (UTC)'; } // Process various API response formats into a clean array of group names const extractGroups = function(data) { if (!data) return []; var res = []; if (Array.isArray(data)) { res = data; } else if (typeof data === 'object') { res = Object.values(data); } else if (typeof data === 'string') { res = data.split(',').map(function(s) { return s.trim(); }); } return res.map(function(g) { if (typeof g !== 'string') return g; return g.replace(/\s+/g, '').toLowerCase(); }); }; mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(async function() { $('#mw-content-text').html('<strong>GlobalCheckUserList.js: Mapping wikis...</strong>'); globalWikiMap = await loadGlobalWikiMap(); const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); let isRunning = false; let currentFilterMode = 'all'; function setupUI() { $('#firstHeading').text('GlobalCheckUserList'); $('#mw-content-text').empty().append(` <div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;"> <p>Audits current CheckUsers across Wikimedia projects. Reports are grouped into one single table.</p> <div style="margin-bottom:10px; display:flex; align-items:center; gap:20px; font-size:13px;"> <strong>Wiki Filter:</strong> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-all" checked> All</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-localcu"> Local CU Wikis</label> <label style="cursor:pointer;"><input type="radio" name="w-mode" id="btn-only"> Only these</label> <span id="wiki-help-trigger" style="cursor:help; background:#36c; color:#fff; border-radius:50%; width:18px; height:18px; display:inline-block; text-align:center; font-weight:bold;" title="Show available DB names">?</span> </div> <div id="filter-input-container" style="display:none; margin-bottom:15px;"> <input id="wiki-filter" type="text" style="width:100%;" placeholder="dbname1, dbname2..."> </div> <div id="wiki-list-help" style="display:none; margin-bottom:10px; padding:10px; background:#fff; border:1px solid #a2a9b1; font-size:11px; max-height:120px; overflow-y:auto; font-family:monospace;"> <strong>Available database names:</strong><br>${Object.keys(globalWikiMap).sort().join(', ')} </div> <div style="margin-top:20px;"> <button id="start" class="mw-ui-button mw-ui-progressive">Run Audit</button> <button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button> </div> <div id="status-msg" style="margin-top:10px; font-weight:bold; color:#0056b3;">Ready.</div> <div style="margin-top:5px;"> <progress id="bar" value="0" max="${Object.keys(globalWikiMap).length}" style="width:100%"></progress> </div> <div id="output-container" style="display:none; margin-top:15px;"> <strong>Wikitext Report:</strong> <textarea id="out" style="width:100%; height:450px; font-family:monospace; font-size:11px; padding:5px; border:1px solid #c8ccd1;"></textarea> </div> </div> `); $('#btn-all').click(function() { currentFilterMode = 'all'; $('#filter-input-container').hide(); }); $('#btn-localcu').click(function() { currentFilterMode = 'localcu'; $('#filter-input-container').hide(); }); $('#btn-only').click(function() { currentFilterMode = 'include'; $('#filter-input-container').show().focus(); }); $('#wiki-help-trigger').click(function() { $('#wiki-list-help').toggle(); }); $('#start').click(function() { runAudit(); }); $('#stop').click(function() { isRunning = false; }); } async function findGrantDate(user, db) { // Check manual list first const key = user + "@" + db; if (customGrantDates[key]) return customGrantDates[key]; // Search Meta-Wiki for steward actions (User:Name@dbname) // We use User:Name for Meta itself, and User:Name@dbname for other wikis const title = (db === 'metawiki') ? 'User:' + user : 'User:' + user + '@' + db; let mPars = { action: 'query', list: 'logevents', letype: 'rights', letitle: title, ledir: 'newer', lelimit: 'max', formatversion: 2 }; let resM = await robustCall(metaApi, mPars); let logsM = (resM.query && resM.query.logevents) || []; for (let ev of logsM) { let p = ev.params || {}; let nG = extractGroups(p.newgroups || p.add || p[1] || p["1"]); let oG = extractGroups(p.oldgroups || p.remove || p[0] || p["0"]); if (nG.indexOf('checkuser') !== -1 && oG.indexOf('checkuser') === -1) { return ev.timestamp.substring(0, 10); } } return "Unknown"; } async function runAudit() { isRunning = true; let wt = ""; const adminTypes = [ 'block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'contentmodel', 'import', 'managetags', 'massmessage', 'checkuser-temporary-account', 'ipinfo', 'pagelang', 'renameuser', 'stable', 'gblblock', 'abusefilter-protected-vars' ]; const cuInPublicTypes = ['abusefilterprivatedetails']; $('#start').prop('disabled', true); $('#stop').prop('disabled', false); $('#status-msg').text('Waiting for queue lock...').css("color", "orange"); // Request browser lock to prevent concurrent runs await navigator.locks.request('global_cu_list_lock', async () => { wt = "== Global CheckUser List ==\n"; wt += "''Report generated on: " + getReportTimestamp() + "<br />\n"; wt += "Generated with [[testwiki:User:MrJaroslavik/GlobalCheckUserList.js|GlobalCheckUserList.js]]''\n"; const filterText = $('#wiki-filter').val().trim().toLowerCase(); const filterList = filterText ? filterText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ""; }) : []; if (currentFilterMode === 'localcu') { wt += "<br />''Filter applied: Wikis with local CheckUsers''<ref>[[meta:CheckUser policy/Users with CheckUser access]]</ref>\n"; } else if (currentFilterMode === 'include' && filterList.length > 0) { wt += "<br />''Filter applied: Only these wikis (" + filterList.join(', ') + ")''\n"; } wt += "\n"; wt += '{| class="wikitable sortable" style="font-size:90%; width:100%;"\n' + '! User !! CU Since !! Duration !! Last CU Action<ref>Logs: checkuserlog, checkuser-temporary-account, abusefilterprivatedetails</ref> !! ' + 'Last Admin Action<ref>Logs: ' + adminTypes.join(', ') + '</ref> !! ' + 'Last OS Action<ref>Logs: suppress</ref> !! ' + 'Last Logged Action<ref>Logs: all public logged actions</ref> !! Last Edit\n'; const allWikis = Object.keys(globalWikiMap); let wikisToScan; if (currentFilterMode === 'include' && filterList.length > 0) { wikisToScan = allWikis.filter(function(w) { return filterList.indexOf(w) !== -1; }); } else if (currentFilterMode === 'localcu') { wikisToScan = allWikis.filter(function(w) { return localCUWikis.indexOf(w) !== -1; }); } else { wikisToScan = allWikis; } $('#bar').attr('max', wikisToScan.length).val(0); for (let i = 0; i < wikisToScan.length; i++) { if (!isRunning) break; const db = wikisToScan[i]; $('#status-msg').text('Auditing ' + db + ' (' + (i + 1) + '/' + wikisToScan.length + ')...').css("color", "#0056b3"); try { const wikiUrl = globalWikiMap[db]; const localApi = new mw.ForeignApi(wikiUrl + '/w/api.php'); // Find all users currently in the checkuser group let users = [], auDone = false, auCont = null; while (!auDone && isRunning) { let auPars = { action: 'query', list: 'allusers', augroup: 'checkuser', auprop: 'groups', aulimit: 'max', formatversion: 2 }; if (auCont) Object.assign(auPars, auCont); let res = await robustCall(localApi, auPars); users = users.concat(res.query.allusers || []); if (res.continue) auCont = res.continue; else auDone = true; } if (users.length > 0) { const iw = getInterwikiPrefix(db); wt += '|-\n! colspan="8" style="background:#eaecf0; text-align:center;" | ' + '[[' + iw + ':|' + db + ']] — [[' + iw + ':Special:ListUsers/checkuser|(list)]] — [[' + iw + ':Special:CheckUserLog|(log)]]\n'; for (const u of users) { const username = u.name; const sinceDate = await findGrantDate(username, db, localApi); // Calculate how many days user has held rights let days = "-"; if (sinceDate && sinceDate !== "Unknown") { const cleanDate = sinceDate.substring(0, 10); const diff = new Date() - new Date(cleanDate); days = Math.floor(diff / 86400000) + "d"; } // Check private CheckUser log const cuLog = await robustCall(localApi, { action: 'query', list: 'checkuserlog', culuser: username, cullimit: 1, formatversion: 2 }); let lastCU = (cuLog.query && cuLog.query.checkuserlog && cuLog.query.checkuserlog.entries && cuLog.query.checkuserlog.entries[0]) ? cuLog.query.checkuserlog.entries[0].timestamp.substring(0, 10) : "Never"; // Fetch public log history const allUserLogsReq = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, lelimit: 'max', formatversion: 2 }); const allUserLogs = (allUserLogsReq.query && allUserLogsReq.query.logevents) || []; // Cross-check for public logs that indicate CU activity const pubCUEvt = allUserLogs.find(function(ev) { return cuInPublicTypes.indexOf(ev.type) !== -1; }); if (pubCUEvt) { const pubCUTs = pubCUEvt.timestamp.substring(0, 10); if (lastCU === "Never" || pubCUTs > lastCU) lastCU = pubCUTs; } // Identify latest admin action const admEvent = allUserLogs.find(function(ev) { return adminTypes.indexOf(ev.type) !== -1; }); const lastAdm = admEvent ? admEvent.timestamp.substring(0, 10) : "Never"; // Get most recent log entry of any type const lastGen = allUserLogs.length > 0 ? allUserLogs[0].timestamp.substring(0, 10) : "Never"; // Look for Suppression actions (filtering for setstatus) const osLog = await robustCall(localApi, { action: 'query', list: 'logevents', leuser: username, letype: 'suppress', lelimit: 50, // Scan more entries to find setstatus formatversion: 2 }); let lastOS = "-"; var uGroups = extractGroups(u.groups); var hasCurrentOSGroups = (uGroups.indexOf('suppress') !== -1 || uGroups.indexOf('oversight') !== -1); if (hasCurrentOSGroups) { lastOS = "Never"; if (osLog.query && osLog.query.logevents) { // Find first entry that is NOT 'setstatus' const realOSEvent = osLog.query.logevents.find(function(e) { // Check if params exists and if subtype is NOT setstatus return !e.params || (e.params && e.params.subtype !== 'setstatus'); }); if (realOSEvent) { lastOS = realOSEvent.timestamp.substring(0, 10); } } } // Check last manual edit date const edLog = await robustCall(localApi, { action: 'query', list: 'usercontribs', ucuser: username, uclimit: 1, formatversion: 2 }); const lastEdit = (edLog.query && edLog.query.usercontribs && edLog.query.usercontribs[0]) ? edLog.query.usercontribs[0].timestamp.substring(0, 10) : "Never"; wt += '|-\n| style="text-align:left; white-space:nowrap;" | [[' + iw + ':Special:Log/' + username + '|' + username + '@' + db + ']] || ' + sinceDate + ' || ' + days + ' || ' + lastCU + ' || ' + lastAdm + ' || ' + lastOS + ' || ' + lastGen + ' || ' + lastEdit + '\n'; } } } catch (err) { console.error('Audit failed for ' + db, err); } $('#bar').val(i + 1); await sleep(DELAY_MS); } wt += '|}\n\n=== References ===\n<references />\n'; $('#out').val(wt); $('#output-container').show(); $('#status-msg').text('Audit complete!').css("color", "green"); $('#start').prop('disabled', false); $('#stop').prop('disabled', true); }); } setupUI(); }); })(); gs9uu5q95n132gekuk96o5jths321w2 Wikipedia:Requests/Permissions/Wooze 4 175008 739235 2026-04-24T12:09:58Z Wooze 54732 Created page with "<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.--> === [[User:{{subst:REVISIONUSER}}|{{subst:REVISIONUSER}}]] === * {{User3|{{subst:REVISIONUSER}}}}, [[Special:CentralAuth/{{subst:REVISIONUSER}}|global contribs]] ~~~~~ * '''Motive for request:''' Hello, I’m Wooze,..." 739235 wikitext text/x-wiki <!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.--> === [[User:Wooze|Wooze]] === * {{User3|Wooze}}, [[Special:CentralAuth/Wooze|global contribs]] 12:09, 24 April 2026 (UTC) * '''Motive for request:''' Hello, I’m Wooze, a sysop on the Turkish Wikipedia. I would like to request sysop access on TestWiki in order to test a tool I am developing. Thanks. [[User:Wooze|Wooze]] ([[User talk:Wooze|talk]]) 12:09, 24 April 2026 (UTC) * '''Requested rights:''' Sysop * '''Comments:''' <!-- Comments of other users --> <!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! --> [[Category:!Requests]] [[Category:Really big category]] __NOINDEX__ 5nx3p5jkgauc405wuqw748mnpv0vred 739250 739235 2026-04-24T12:35:38Z EPIC 44279 done 739250 wikitext text/x-wiki {{Request-done|1=[[User:EPIC|EPIC]] ([[User talk:EPIC|talk]]) 12:35, 24 April 2026 (UTC)|2=Valid use case, sysop on a production wiki.}} === [[User:Wooze|Wooze]] === * {{User3|Wooze}}, [[Special:CentralAuth/Wooze|global contribs]] 12:09, 24 April 2026 (UTC) * '''Motive for request:''' Hello, I’m Wooze, a sysop on the Turkish Wikipedia. I would like to request sysop access on TestWiki in order to test a tool I am developing. Thanks. [[User:Wooze|Wooze]] ([[User talk:Wooze|talk]]) 12:09, 24 April 2026 (UTC) * '''Requested rights:''' Sysop * '''Comments:''' <!-- Comments of other users --> <!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! --> [[Category:!Requests]] [[Category:Really big category]] __NOINDEX__ {{Request closed}} fc5hgmdezps8xh6nact7a9liaolmtq1 User:Cryptocurrency777 2 175011 739268 2026-04-24T15:39:59Z Cryptocurrency777 73698 Created page with "{{DISPLAYTITLE:User:<span style="color:Lavender;">'''Cryptocurrency777'''</span>}}" 739268 wikitext text/x-wiki {{DISPLAYTITLE:User:<span style="color:Lavender;">'''Cryptocurrency777'''</span>}} fpda5poxzi9aca8t8jpixkkpcw2sd1g 739269 739268 2026-04-24T15:40:56Z Cryptocurrency777 73698 739269 wikitext text/x-wiki {{DISPLAYTITLE:User:<span style="color:Lavender;">'''Cryptocurrency777'''</span>}} 😤 pya3mnzpmdee2urik2s8lrbuctz46mw User:MMunyoki (WMF)/Starter kit/Welcome banner 2 175012 739270 2026-04-24T16:12:12Z MMunyoki (WMF) 53374 Initialised by StarterKit tool — ready for translation 739270 wikitext text/x-wiki <div style="text-align: center; font-family: 'Linux Libertine', Georgia, Times, serif; margin: 1.5em 0;"> <span style="font-size: 2.3em; line-height: 1.2;">Welcome to {{#language:{{PAGELANGUAGE}}}} Wikipedia</span><br/> <span style="font-size: 1.1em; color: #54595d;">The free encyclopedia that anyone can edit</span><br/> <span style="font-size: 0.95em; color: #72777d; margin-top: 0.5em; display: inline-block;">{{NUMBEROFACTIVEUSERS}} active editors • '''{{NUMBEROFARTICLES}}''' articles in this {{SITENAME}}</span> </div> <noinclude>[[Category:Starter kit templates]]</noinclude> 2gaz0l1265uxned195lmw3a6r0oocxa Castries 0 175013 739273 2026-04-24T17:30:35Z Cryptocurrency777 73698 test 739273 wikitext text/x-wiki '''Castries''' (/kəˈstriːz/) is the capital and largest city of [[Saint Lucia]], an island country in the [[Caribbean]]. The urban area has a population of approximately 20,000, while the eponymous district has a population of just under 70,000, as of May 2013. The city covers 80 km2 (31 sq mi). Castries is on a flood plain and is built on reclaimed land. It houses the seat of government and the head offices of many foreign and local businesses. The city is laid out in a grid pattern. Its sheltered harbour receives cargo vessels, ferries and cruise ships. It houses duty-free shopping facilities such as Point Seraphine and La Place Carenage. altmr6ygnypkg2wxby01mfap2w11n29 Event:Testdulu 1728 175014 739296 2026-04-25T06:51:24Z Annidafattiya 73701 Created page with "ini demo untuk bikin event page" 739296 wikitext text/x-wiki ini demo untuk bikin event page 516qgza7kzd2oc4e0ir997i53op57xc