Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.46.0-wmf.23
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
737090
736840
2026-04-07T17:35:05Z
CaptainEek
47282
/* Requests for user rights */+CaptainEek, RfA
737090
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/CaptainEek}}
{{wikipedia:Requests/Permissions/PieAlt}}
<!-- NEW ENTRIES AT THE TOP, NOT HERE -->
pfkyj11dwlnrmykclf3g2gwrkuaowb5
737092
737090
2026-04-07T17:39:49Z
Vermont
37989
/* Requests for user rights */ done
737092
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/PieAlt}}
<!-- NEW ENTRIES AT THE TOP, NOT HERE -->
a3qrnvaqiz18gc4bv4y7agwfnzunssr
737145
737092
2026-04-08T08:47:50Z
Aqurs1
41128
/* Requests for user rights */
737145
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/Aqurs1 3}}
{{wikipedia:Requests/Permissions/PieAlt}}
<!-- NEW ENTRIES AT THE TOP, NOT HERE -->
4l999vcln99t8yzeqzdqj5ea25l1j96
737152
737145
2026-04-08T09:09:23Z
Plantaest
37055
737152
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/Plantaest}}
{{Wikipedia:Requests/Permissions/Aqurs1 3}}
{{wikipedia:Requests/Permissions/PieAlt}}
<!-- NEW ENTRIES AT THE TOP, NOT HERE -->
l0ke1kc80epbu994xe2jdgc82wue6im
Cows
0
75938
737125
726623
2026-04-07T20:11:13Z
Ponor
47975
test
737125
wikitext
text/x-wiki
{{citation needed}}
Cows are animals that live on planet Earth. Cows moo. Cows eat grass. Cows drink water. Cows walk on four legs, if they have all of them.
[[Image:Cows grazing (Unsplash).jpg | thumb | right | Cows will typically graze for about eight hours a day]]
When cows die, their body is turned into delicious ground beef and steak.
From time to time, people will tip cows and aliens will abduct cows.
Baby cows are called calves. Male cows are called bulls.
[[Image:Calf suckling at a meadow near Vrachesh, Bulgaria.webm | thumb | right | alt=A red calf is in a meadow suckling from its mother in an opposite parallel position | In nature, calves suckle from their mothers for the first eight months or so]]
Cows are often depicted in comic Gary Larson's strip ''The Far Side.''
Cows produce a lot of methane, a [[greenhouse gas]].
A common phrase among teenagers in the mid-90s was "don't have a cow, man," popularized by [[Bart Simpson]].
tl;dr Cows are pretty great.
foo
Cows are sometimes considered creatures of habit and beasts of burden.
----
1he4n6dflzyrq4zwsru5vxushlg9xla
User:Sam Sailor/test.js
2
98186
737102
737068
2026-04-07T18:44:17Z
Sam Sailor
26820
Test
737102
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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 stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
function appendDismiss($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function performDeorphan() {
const api = new mw.Api();
const orphanTemplateRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}\s*[\r\n]*/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanTemplateRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{Multiple issues\s*\|\s*([\s\S]*?)\}\}/gi;
text = text.replace(miRegex, function(match, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
templates = templates.filter(t => !/(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(t));
if (templates.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return templates[0].trim();
}
return match;
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-suggestion').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect to ${labelText.toLowerCase()} to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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;
}
}
}
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
// </nowiki>
cbdmc0ix5xsptousa90fh60yhk9dk5r
737104
737102
2026-04-07T18:51:07Z
Sam Sailor
26820
Test
737104
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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 stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
function appendDismiss($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function performDeorphan() {
const api = new mw.Api();
const orphanTemplateRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}\s*[\r\n]*/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanTemplateRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{Multiple issues\s*\|\s*([\s\S]*?)\}\}/gi;
text = text.replace(miRegex, function(match, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
templates = templates.filter(t => !/(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(t));
if (templates.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return templates[0].trim();
}
return match;
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-suggestion').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect to ${labelText.toLowerCase()} to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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;
}
}
}
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
rf4awipvm6snh1dhjvw10h43rq5hu1k
737107
737104
2026-04-07T18:58:15Z
Sam Sailor
26820
Test
737107
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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 stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
function appendDismiss($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function performDeorphan() {
const api = new mw.Api();
const orphanTemplateRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}\s*[\r\n]*/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanTemplateRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{Multiple issues\s*\|\s*([\s\S]*?)\}\}/gi;
text = text.replace(miRegex, function(match, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
templates = templates.filter(t => !/(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(t));
if (templates.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (templates.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return templates[0].trim();
}
return '{{Multiple issues|\n' + templates.join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-suggestion').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect to ${labelText.toLowerCase()} to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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;
}
}
}
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
s6n7xxstzu98r43e2qoqrrmsdc1xc5h
737108
737107
2026-04-07T19:12:06Z
Sam Sailor
26820
Test
737108
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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 stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
function appendDismiss($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function performDeorphan() {
const api = new mw.Api();
const orphanNames = "(Orphan|Do-attempt|Lonely|Orp|Orphaned article)";
const orphanTemplateRegex = new RegExp('\\{\\{\\s*' + orphanNames + '(.*?)\\}\\}\\s*[\\r\\n]*', 'gi');
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanTemplateRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{Multiple issues\s*\|\s*([\s\S]*?)\}\}/gi;
text = text.replace(miRegex, function(match, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
templates = templates.filter(t => !new RegExp('\\{\\{\\s*' + orphanNames, 'i').test(t));
if (templates.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (templates.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return templates[0].trim();
}
return '{{Multiple issues|\n' + templates.map(t => t.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-suggestion').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect to ${labelText.toLowerCase()} to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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;
}
}
}
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
64952dafcpnyllms4bxhjp849togxw0
737120
737108
2026-04-07T19:47:54Z
Sam Sailor
26820
Test
737120
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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 stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
function appendDismiss($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{Multiple issues\s*\|([\s\S]*?)\}\}/gi;
text = text.replace(miRegex, function(fullMatch, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{Multiple issues|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function createModernRedirect(redirTitle, targetTitle, rCategory, customSummary, elementId) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-suggestion').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect to ${labelText.toLowerCase()} to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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;
}
}
}
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
0yr2ueulb6v6pryxx1kyz4cr7wmypje
737136
737120
2026-04-08T06:02:17Z
Sam Sailor
26820
Test
737136
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-suggestion').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect to ${labelText.toLowerCase()} to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
qqhs4gvvegvhzsgy41qfdaa7tw4ow74
737137
737136
2026-04-08T06:33:07Z
Sam Sailor
26820
Test
737137
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Created redirect from domain name to [[${targetTitle}]]`;
const $btn = $('<button>').text('Create redirect').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText || rCategory}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
fpzd3l2yfuzq4d17u50h2pumhiitao9
737140
737137
2026-04-08T07:11:16Z
Sam Sailor
26820
Test
737140
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
const elements = ['C', 'H', 'N', 'O', 'S', 'P', 'F', 'Cl', 'Br', 'I'];
elements.forEach(el => {
const elMatch = content.match(new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d+)`, 'i'));
if (elMatch) {
formula += el + (elMatch[1] === "1" ? "" : elMatch[1]);
}
});
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell||\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
poiwdnhqhbkosd0lqd577wz1sgjp6yt
737142
737140
2026-04-08T07:38:12Z
Sam Sailor
26820
Test
737142
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
const elements = ['C', 'H', 'N', 'O', 'S', 'P', 'F', 'Cl', 'Br', 'I'];
elements.forEach(el => {
const elMatch = content.match(new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d+)`, 'i'));
if (elMatch) {
formula += el + (elMatch[1] === "1" ? "" : elMatch[1]);
}
});
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
8em1u9b3dh170xautb1fp3ysnpwhf62
737143
737142
2026-04-08T08:01:51Z
Sam Sailor
26820
Test
737143
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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 elMatch = content.match(new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d+)`, 'i'));
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 carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
}
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
92fgh7q1svleu7kd3460k60y2amp3lb
737146
737143
2026-04-08T08:56:26Z
Sam Sailor
26820
Test
737146
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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+)`, 'im');
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 carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
}
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
c9wymhkagf6y3wq46c1ngewazis2vbr
737147
737146
2026-04-08T08:58:53Z
Sam Sailor
26820
Test
737147
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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 elMatch = content.match(new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d+)`, 'i'));
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 carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
}
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
92fgh7q1svleu7kd3460k60y2amp3lb
737148
737147
2026-04-08T09:02:25Z
Sam Sailor
26820
Test
737148
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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 = {};
// Split by pipe to handle all parameters individually
const params = content.split('|');
params.forEach(param => {
const parts = param.split('=');
if (parts.length === 2) {
const key = parts[0].trim();
const val = parts[1].trim().match(/^\d+/); // Capture only leading digits
if (allElements.includes(key) && val && val[0] !== "0") {
data[key] = val[0] === "1" ? "" : val[0];
}
}
});
const elementsPresent = Object.keys(data);
if (elementsPresent.length > 0) {
let formulaArray = [];
const hasC = "C" in data;
// If carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
}
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
rxs8rkgbalgh4pgdiha4zuzecdnr3pp
737150
737148
2026-04-08T09:06:14Z
Sam Sailor
26820
Test
737150
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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+)`, 'i');
const elMatch = wikitext.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 carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
4cwkyjvv905mcq9rq27z62ooramaogc
737153
737150
2026-04-08T09:13:34Z
Sam Sailor
26820
Test
737153
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) return;
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
} else {
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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 elMatch = content.match(new RegExp(`\\|\\s*${el}\\s*=\\s*(\\d+)`, 'i'));
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 carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
}
}
}
if (formula && formula !== currentTitle) {
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
92fgh7q1svleu7kd3460k60y2amp3lb
737155
737153
2026-04-08T09:17:41Z
Sam Sailor
26820
Test
737155
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) {
console.log("Comrade: No 'Chembox Properties' template found.");
return;
}
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
console.log("Comrade: Found explicit Formula parameter:", formula);
} else {
console.log("Comrade: No explicit formula. Searching for individual elements...");
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
console.log("Comrade: Chembox content extracted:", content);
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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+)`, 'i');
const elMatch = content.match(elRegex);
if (elMatch) {
console.log(`Comrade: Match found for ${el}:`, elMatch[0]);
if (elMatch[1] !== "0") {
data[el] = elMatch[1] === "1" ? "" : elMatch[1];
}
}
});
console.log("Comrade: Final element data object:", data);
const elementsPresent = Object.keys(data);
if (elementsPresent.length > 0) {
let formulaArray = [];
const hasC = "C" in data;
// If carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
console.log("Comrade: Generated formula from elements:", formula);
}
} else {
console.log("Comrade: Failed to extract Chembox Properties section content.");
}
}
if (formula && formula !== currentTitle) {
console.log(`Comrade: Success! Triggering redirect for ${formula}`);
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
} else {
console.log("Comrade: Formula matches current title or is empty. No redirect needed.");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
}
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 === "/") {
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {}
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
8tqpkpdj58fxa6tno7tjrgle6a09y8k
737156
737155
2026-04-08T09:45:06Z
Sam Sailor
26820
Test
737156
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) {
console.log("Comrade: No 'Chembox Properties' template found.");
return;
}
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
console.log("Comrade: Found explicit Formula parameter:", formula);
} else {
console.log("Comrade: No explicit formula. Searching for individual elements...");
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
console.log("Comrade: Chembox content extracted:", content);
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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+)`, 'i');
const elMatch = content.match(elRegex);
if (elMatch) {
console.log(`Comrade: Match found for ${el}:`, elMatch[0]);
if (elMatch[1] !== "0") {
data[el] = elMatch[1] === "1" ? "" : elMatch[1];
}
}
});
console.log("Comrade: Final element data object:", data);
const elementsPresent = Object.keys(data);
if (elementsPresent.length > 0) {
let formulaArray = [];
const hasC = "C" in data;
// If carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
console.log("Comrade: Generated formula from elements:", formula);
}
} else {
console.log("Comrade: Failed to extract Chembox Properties section content.");
}
}
if (formula && formula !== currentTitle) {
console.log(`Comrade: Success! Triggering redirect for ${formula}`);
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
} else {
console.log("Comrade: Formula matches current title or is empty. No redirect needed.");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
// Try to get the URL from Wikidata (P856)
if (qid) {
try {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
console.log("Comrade: Found domain candidate in Wikidata:", domainCandidate);
}
} catch (e) {
console.log("Comrade: Wikidata API call failed.");
}
}
// Fallback to Infobox URL if Wikidata is empty
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") ||
$(".official-website a").first().attr("href");
if (domainCandidate) console.log("Comrade: Found domain candidate in Infobox:", domainCandidate);
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
// Only proceed if it's a root domain (pathname is "/")
if (domain.includes(".") && url.pathname === "/") {
console.log(`Comrade: Verifying if ${domainCandidate} is live via Citation API...`);
// The live check, CORS-friendly proxy
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
$.ajax({
url: citationURL,
method: 'GET',
success: function() {
console.log(`Comrade: URL is live. Proceeding with redirect for: ${domain}`);
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
},
error: function(xhr) {
console.log(`Comrade: URL failed live check (Status: ${xhr.status}). Skipping redirect.`);
}
});
} else {
console.log(`Comrade: ${domain} rejected (not a root domain or missing TLD).`);
}
} catch (e) {
console.log("Comrade: Error parsing URL object.");
}
} else {
console.log("Comrade: No domain candidate found for this article.");
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanRegex = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)(.*?)\}\}/gi;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
text = text.replace(orphanRegex, '').replace(multiIssueParamRegex, '');
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !t.match(/\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
64zjz9gatfu05koja73do5n4pv1ww5m
737157
737156
2026-04-08T10:35:42Z
Sam Sailor
26820
Test
737157
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) {
console.log("Comrade: No 'Chembox Properties' template found.");
return;
}
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
console.log("Comrade: Found explicit Formula parameter:", formula);
} else {
console.log("Comrade: No explicit formula. Searching for individual elements...");
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
console.log("Comrade: Chembox content extracted:", content);
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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+)`, 'i');
const elMatch = content.match(elRegex);
if (elMatch) {
console.log(`Comrade: Match found for ${el}:`, elMatch[0]);
if (elMatch[1] !== "0") {
data[el] = elMatch[1] === "1" ? "" : elMatch[1];
}
}
});
console.log("Comrade: Final element data object:", data);
const elementsPresent = Object.keys(data);
if (elementsPresent.length > 0) {
let formulaArray = [];
const hasC = "C" in data;
// If carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
console.log("Comrade: Generated formula from elements:", formula);
}
} else {
console.log("Comrade: Failed to extract Chembox Properties section content.");
}
}
if (formula && formula !== currentTitle) {
console.log(`Comrade: Success! Triggering redirect for ${formula}`);
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
} else {
console.log("Comrade: Formula matches current title or is empty. No redirect needed.");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
// Try to get the URL from Wikidata (P856)
if (qid) {
try {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
console.log("Comrade: Found domain candidate in Wikidata:", domainCandidate);
}
} catch (e) {
console.log("Comrade: Wikidata API call failed.");
}
}
// Fallback to Infobox URL if Wikidata is empty
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") ||
$(".official-website a").first().attr("href");
if (domainCandidate) console.log("Comrade: Found domain candidate in Infobox:", domainCandidate);
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
// Only proceed if it's a root domain (pathname is "/")
if (domain.includes(".") && url.pathname === "/") {
console.log(`Comrade: Verifying if ${domainCandidate} is live via Citation API...`);
// The live check, CORS-friendly proxy
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
$.ajax({
url: citationURL,
method: 'GET',
success: function() {
console.log(`Comrade: URL is live. Proceeding with redirect for: ${domain}`);
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
},
error: function(xhr) {
console.log(`Comrade: URL failed live check (Status: ${xhr.status}). Skipping redirect.`);
}
});
} else {
console.log(`Comrade: ${domain} rejected (not a root domain or missing TLD).`);
}
} catch (e) {
console.log("Comrade: Error parsing URL object.");
}
} else {
console.log("Comrade: No domain candidate found for this article.");
}
}
async function checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${surname} (surname)`, `${surname} (name)`, surname];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isSurnameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isSurnameDab = /\{\{Disambiguation\|(surname|surnames)\}\}/i.test(text);
if (isSurnameMatch || isSurnameDab) {
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:'));
$content.append($('<span>').append('Subject not listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append('Subject not currently listed at ', $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanPattern = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)[\s\S]*?\}\}/i;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !orphanPattern.test(t));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
text = text.replace(new RegExp(orphanPattern, 'g'), '')
.replace(multiIssueParamRegex, '');
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
const surname = targetSortName.split(',')[0].trim();
const isOrphan = isOrphanTagged && backLinkCount < 1;
checkSurnameList(surname, currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
lzgd1q3d5p0x4k81pd8i4i5vesfwsze
737162
737157
2026-04-08T11:39:20Z
Sam Sailor
26820
Test
737162
javascript
text/javascript
// <nowiki>
(function() {
var Comrade = Comrade || {};
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').click(function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
}
async function checkAndRenderRedirect(redirTitle, targetTitle, rCategory, labelText) {
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-" + btoa(redirTitle).replace(/=/g, "");
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').click(() => createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
appendDismiss($container);
}
}
function checkChemicalRedirect(wikitext, currentTitle) {
if (!/\{\{\s*Chembox Properties/i.test(wikitext)) {
console.log("Comrade: No 'Chembox Properties' template found.");
return;
}
let formula = "";
const formulaMatch = wikitext.match(/\|\s*Formula\s*=\s*([^|\n}]+)/i);
if (formulaMatch) {
formula = formulaMatch[1].replace(/<\/?sub>/gi, "").trim();
console.log("Comrade: Found explicit Formula parameter:", formula);
} else {
console.log("Comrade: No explicit formula. Searching for individual elements...");
const chemboxSection = wikitext.match(/\{\{\s*Chembox Properties[\s\S]*?\}\}/i);
if (chemboxSection) {
const content = chemboxSection[0];
console.log("Comrade: Chembox content extracted:", content);
// Comprehensive list extracted from [[Template:Chembox Elements/molecular formula]]
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+)`, 'i');
const elMatch = content.match(elRegex);
if (elMatch) {
console.log(`Comrade: Match found for ${el}:`, elMatch[0]);
if (elMatch[1] !== "0") {
data[el] = elMatch[1] === "1" ? "" : elMatch[1];
}
}
});
console.log("Comrade: Final element data object:", data);
const elementsPresent = Object.keys(data);
if (elementsPresent.length > 0) {
let formulaArray = [];
const hasC = "C" in data;
// If carbon exists, C then H
if (hasC) {
formulaArray.push("C" + data["C"]);
if ("H" in data) formulaArray.push("H" + data["H"]);
}
// Alphabetical for everything else
const remaining = elementsPresent
.filter(el => {
if (hasC && (el === "C" || el === "H")) return false;
return true;
})
.sort();
// Special case: If NO carbon, H must be sorted alphabetically
remaining.forEach(el => {
formulaArray.push(el + data[el]);
});
formula = formulaArray.join('');
console.log("Comrade: Generated formula from elements:", formula);
}
} else {
console.log("Comrade: Failed to extract Chembox Properties section content.");
}
}
if (formula && formula !== currentTitle) {
console.log(`Comrade: Success! Triggering redirect for ${formula}`);
checkAndRenderRedirect(formula, currentTitle, "R from chemical formula", "Chemical formula");
} else {
console.log("Comrade: Formula matches current title or is empty. No redirect needed.");
}
}
async function checkDomainRedirect(qid, currentTitle) {
let domainCandidate = "";
// Try to get the URL from Wikidata (P856)
if (qid) {
try {
const res = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
}
});
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
console.log("Comrade: Found domain candidate in Wikidata:", domainCandidate);
}
} catch (e) {
console.log("Comrade: Wikidata API call failed.");
}
}
// Fallback to Infobox URL if Wikidata is empty
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") ||
$(".official-website a").first().attr("href");
if (domainCandidate) console.log("Comrade: Found domain candidate in Infobox:", domainCandidate);
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
// Only proceed if it's a root domain (pathname is "/")
if (domain.includes(".") && url.pathname === "/") {
console.log(`Comrade: Verifying if ${domainCandidate} is live via Citation API...`);
// The live check, CORS-friendly proxy
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
$.ajax({
url: citationURL,
method: 'GET',
success: function() {
console.log(`Comrade: URL is live. Proceeding with redirect for: ${domain}`);
checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
},
error: function(xhr) {
console.log(`Comrade: URL failed live check (Status: ${xhr.status}). Skipping redirect.`);
}
});
} else {
console.log(`Comrade: ${domain} rejected (not a root domain or missing TLD).`);
}
} catch (e) {
console.log("Comrade: Error parsing URL object.");
}
} else {
console.log("Comrade: No domain candidate found for this article.");
}
}
async function checkNameList(name, type, currentTitle, hasShortDesc, isOrphan) {
const api = new mw.Api();
const candidates = [`${name} (${type})`, `${name} (name)`, name];
for (const title of candidates) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (page && !page.missing) {
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given name)/i.test(text);
const isNameDab = /\{\{Disambiguation\|(surname|surnames|given name|given names)\}\}/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:'));
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
} else {
$container.addClass('comrade-info');
$header.append($('<strong>').text('Informative:'));
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not currently listed at `, $('<a>').attr({
href: mw.util.getUrl(title),
target: '_blank'
}).text(title)));
}
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $codeSpan = $('<span>').text(snippet).css('font-family', 'inherit');
const $copyBtn = $('<button>').text('Copy').css('margin-left', '0').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($codeSpan, $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) {
const api = new mw.Api();
const content = `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Created redirect from [[${redirTitle}]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
}).done(() => {
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
});
}
async function performDeorphan() {
const api = new mw.Api();
const orphanPattern = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)[\s\S]*?\}\}/i;
const multiIssueParamRegex = /\| *orphan *= *[^|}]*/gi;
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
let summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]] tag';
const miRegex = /\{\{(Multiple issues|multiple issues|Multiple Issues)\s*\|([\s\S]*?)\}\}/g;
text = text.replace(miRegex, function(fullMatch, title, innerContent) {
let templates = innerContent.match(/\{\{[\s\S]*?\}\}/g) || [];
let survivors = templates.filter(t => !orphanPattern.test(t));
if (survivors.length === 0) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return '';
}
if (survivors.length === 1) {
summary += ' and redundant [[Template:Multiple issues|{{Multiple issues}}]]';
return survivors[0].trim();
}
return '{{' + title + '|\n' + survivors.map(s => s.trim()).join('\n') + '\n}}';
});
text = text.replace(new RegExp(orphanPattern, 'g'), '')
.replace(multiIssueParamRegex, '');
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text: text,
summary: summary,
minor: true
};
});
mw.notify("Orphan tag removed!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
async function performSortCorrection(newSort, reason) {
const api = new mw.Api();
await api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const dsRegex = /\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/gi;
text = text.replace(dsRegex, `{{DEFAULTSORT:${newSort}}}`);
return {
text: text,
summary: `Corrected [[Template:DEFAULTSORT|{{DEFAULTSORT}}]] ${reason}`,
minor: true
};
});
mw.notify(`DEFAULTSORT ${reason} corrected!`, {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 1100);
}
function stripDiacritics(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
if (mw.config.get("wgNamespaceNumber") === 0 && mw.config.get("wgAction") === "view") {
$(document).ready(async function() {
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').click(() => performDeorphan());
$header.append($('<div>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn));
$container.append($header);
appendDismiss($container);
}
if (cleanTitle !== currentTitle) checkAndRenderRedirect(cleanTitle, currentTitle, "R to diacritic", "Diacritic");
checkChemicalRedirect(wikitext, currentTitle);
checkDomainRedirect(qid, currentTitle);
if (qid) {
const wikiData = await $.ajax({
url: "https://www.wikidata.org/w/api.php",
data: {
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
},
dataType: 'json'
});
const entity = wikiData.entities[qid];
const isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
let targetSortName = "";
if (dsMatch) {
const currentDS = dsMatch[1].trim();
const cleanDS = stripDiacritics(currentDS);
targetSortName = cleanDS;
if (currentDS !== cleanDS) {
const $dsDiacritics = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${cleanDS}`).click(() => performSortCorrection(cleanDS, "diacritics"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Key contains diacritics.`, $btn));
$dsDiacritics.append($header);
appendDismiss($dsDiacritics);
}
if (isHuman && !currentDS.includes(',')) {
const parts = cleanDS.split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
const $dsFormat = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${targetSortName}`).click(() => performSortCorrection(targetSortName, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Structure should be "Last, First".`, $btn));
$dsFormat.append($header);
appendDismiss($dsFormat);
}
}
} else if (isHuman) {
const label = entity.labels?.en?.value || currentTitle;
const parts = stripDiacritics(label).split(' ');
if (parts.length >= 2) {
const last = parts.pop();
targetSortName = `${last}, ${parts.join(' ')}`;
}
}
if (isHuman && targetSortName) {
let rCat = "R from sort name";
if (targetSortName.includes(',')) {
const parts = targetSortName.split(',');
const lI = parts[0].trim().charAt(0).toUpperCase();
const fI = parts[1].trim().charAt(0).toUpperCase();
if (lI && fI) rCat = `R from sort name|${lI}|${fI}`;
}
checkAndRenderRedirect(targetSortName, currentTitle, rCat, "Sort name");
// Extract names for list checking
const nameParts = targetSortName.split(',');
const surname = nameParts[0].trim();
const givenName = nameParts.length > 1 ? nameParts[1].trim().split(' ')[0] : "";
const isOrphan = isOrphanTagged && backLinkCount < 1;
// Check both lists
if (givenName) {
await checkNameList(givenName, 'given name', currentTitle, hasShortDesc, isOrphan);
}
await checkNameList(surname, 'surname', currentTitle, hasShortDesc, isOrphan);
}
}
} catch (err) {
console.error("Comrade error:", err);
}
});
}
})();
(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 => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
// </nowiki>
rh5w53ef7hv4sk0rhg2kuepwvmtbnfr
Theanine
0
113569
737133
690571
2026-04-08T00:03:37Z
InternetArchiveBot
34092
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5
737133
wikitext
text/x-wiki
{{distinguish|text=''[[threonine]]'', a distinct [[amino acid]], or ''theine'', an archaic synonym of [[caffeine]]}}
{{Infobox drug
| image = L-Theanine.svg
| caption = <small>L</small>-Theanine
| alt = skeletal formula of L-theanine
| IUPAC_name = (2''S'')-2-Ammonio-5-(ethylamino)-5-oxopentanoate<ref name = RSC_Chemspider/> or ''N''-Ethyl-<small>L</small>-glutamine
| tradename =
| Drugs.com =
| pregnancy_US = A
| legal_AU =
| legal_CA =
| legal_UK =
| legal_US = OTC
| legal_UN = Unscheduled
| licence_US =
| dependency_liability = None
| addiction_liability =
| routes_of_administration= [[Oral route|Oral]]
<!--Pharmacokinetic data-->
| bioavailability =
| protein_bound =
| metabolism =
| onset = about 1 hour<ref name="PK">{{cite journal|last1=Scheid|first1=L.|last2=Ellinger|first2=S.|last3=Alteheld|first3=B.|last4=Herholz|first4=H.|last5=Ellinger|first5=J.|last6=Henn|first6=T.|last7=Helfrich|first7=H.-P.|last8=Stehle|first8=P.|title=Kinetics of L-Theanine Uptake and Metabolism in Healthy Participants Are Comparable after Ingestion of L-Theanine via Capsules and Green Tea|journal=Journal of Nutrition|volume=142|issue=12|year=2012|pages=2091–2096|issn=0022-3166|doi=10.3945/jn.112.166371|pmid=23096008|doi-access=free}}</ref>
| elimination_half-life = Capsule ~1.2 hours <br />
Green Tea ~0.8 hours<ref name="PK"/>
| excretion =
<!-- identifiers -->
| index_label =
| index2_label =
| CAS_number =3081-61-6
| CAS_number_Ref = {{cascite|correct|??}}
| ATC_prefix = none
| PubChem = 439378
| ChemSpiderID = 388498
| ChemSpiderID_Ref = {{chemspidercite|correct|chemspider}}
| UNII = 8021PR16QO
| UNII_Ref = {{fdacite|correct|FDA}}
| ChEBI = 17394
| ChEBI_Ref = {{ebicite|correct|EBI}}
| KEGG = C01047
| smiles = CCNC(=O)CC[C@H](N)C(=O)O
| smiles2 = CCNC(=O)CCC(N)C(O)=O
| StdInChI = 1S/C7H14N2O3/c1-2-9-6(10)4-3-5(8)7(11)12/h5H,2-4,8H2,1H3,(H,9,10)(H,11,12)
| StdInChI_Ref = {{stdinchicite|correct|chemspider}}
| StdInChIKey = DATAGRPVKZEWHA-UHFFFAOYSA-N
| StdInChIKey_Ref = {{stdinchicite|correct|chemspider}}
| synonyms = γ-<small>L</small>-Glutamylethylamide
<!-- -->
| C=7 | H=14 | N=2 | O=3
| density =
| boiling_point = 215
| boiling_notes = <ref name="Pubchem">{{cite web|title=Theanine|url=https://pubchem.ncbi.nlm.nih.gov/compound/439378?from=summary#section=InChI-Key|website=Pubchem Compound|publisher=NCBI|accessdate=21 February 2015}}</ref>
| melting_point = 174.20
| melting_notes = <ref name="Pubchem" />
<!-- -->
}}
'''Theanine''' {{IPAc-en|ˈ|θ|iː|ən|iː|n}}, also known as '''<small>L</small>-γ-glutamylethylamide''' and '''''N''<sup>5</sup>-ethyl-<small>L</small>-glutamine''', is an [[amino acid]] [[Structural analog|analogue]] of the [[proteinogenic amino acid]]s [[glutamic acid|<small>L</small>-glutamate]] and [[glutamine|<small>L</small>-glutamine]] and is found primarily in particular [[phytochemical|plant]] and [[fungi|fungal species]]. It was discovered as a constituent of [[green tea]] in 1949; in 1950, it was isolated from [[gyokuro]] leaves.<ref>{{cite web |url=http://www.ippodo-tea.co.jp/en/tea/gyokuro_02.html |title=Components of Gyokuro| IPPODO |publisher=Ippodo-tea.co.jp |date= |accessdate=2015-05-07 |archive-date=2015-12-23 |archive-url=https://web.archive.org/web/20151223084441/http://www.ippodo-tea.co.jp/en/tea/gyokuro_02.html |url-status=dead }}</ref> Theanine provides a unique jbrothy or savory ([[umami]]) flavor to green tea infusions. Test123.
The name "'''theanine'''" without a prefix generally implies the [[enantiomer]] '''<small>L</small>-theanine''', which is the form found in tea leaves and as a dietary supplement ingredient. Most studies have used <small>L</small>-theanine. The opposite enantiomer, '''<small>D</small>-theanine''', has been studied less.
The regulatory status of theanine varies by country. In Japan, <small>L</small>-theanine has been approved for use in all foods. Restrictions apply to infant foods.<ref name="SAKATO Y. J 1949">SAKATO Y. J. Agri. Chem. Soc. 1949, 23, 262-7</ref><ref>MASON R. Altern. & Complementary Ther. 2001, 7, 91-5</ref> In the United States, the [[Food and Drug Administration]] (FDA) considers it to be [[generally recognized as safe]] (GRAS) and allows its sale as a dietary supplement. The German Federal Institute for Risk Assessment, an agency of their [[Federal Ministry of Food and Agriculture]], objects to the addition of <small>L</small>-theanine to beverages. The [[European Food Safety Authority]] EFSA advised negatively on the use of L-theanine for improving cognitive function, alleviation of psychological stress, maintenance of normal sleep, and reduction of menstrual discomfort.<ref>http://www.efsa.europa.eu/en/efsajournal/pub/2238{{full citation needed|date=May 2017}}</ref> Therefore, health claims for <small>L</small>-theanine are not recognized in the European Union.
==Structure and properties==
The chemical name ''N''<sup>5</sup>-ethyl-<small>L</small>-glutamine<ref name = RSC_Chemspider/> and other synonyms (see box) for theanine reflect its chemical structure. The name theanine, without prefix, is generally understood to imply the <small>L</small>- (''S-'') [[enantiomer]], derived from the related [[proteinogenic amino acid|proteinogenic L-amino acid]] [[glutamic acid]]. Theanine is an analog of this amino acid, and its primary [[amide]], <small>L</small>-[[glutamine]] (also a proteinogenic amino acid). Theanine is a derivative of glutamine that is ethylated on the [[amide]] nitrogen (as the name ''N''<sup>5</sup>-ethyl-<small>L</small>-glutamine describes), or alternatively, to the amide formed from [[ethylamine]] and <small>L</small>-glutamic acid at its γ- (5-) side chain [[carboxylic acid]] group (as the name γ-L-glutamylethylamide describes).
Relative to theanine, the opposite (<small>D</small>-, ''R-'') enantiomer is largely absent from the literature,<ref name="RSC_Chemspider">{{cite web|url=http://www.chemspider.com/Chemical-Structure.8139819.html?rid=c1b3b94c-9d23-4b66-9b0b-57208603c57a |title=D-theanine | C7H14N2O3 |publisher=ChemSpider.com |date= |accessdate=2015-05-21}}</ref> except implicitly. While natural extracts that are not harshly treated are presumed to contain only the [[biosynthesis|biosynthetic]] <small>L</small>- enantiomeric form, mishandled isolates and racemic chemical preparations of theanines necessarily contain both theanine and its <small>D</small>-enantiomer (and from [[racemic]] syntheses, in equal proportion), and studies have suggested that the <small>D</small>-isomer may actually ''predominate'' in some commercial supplement preparations.<ref>{{cite journal |doi=10.1002/jsfa.4373 |pmid=21735448 |title=L-Theanine: Properties, synthesis and isolation from tea |journal=Journal of the Science of Food and Agriculture |volume=91 |issue=11 |pages=1931–9 |year=2011 |last1=Vuong |first1=Quan V |last2=Bowyer |first2=Michael C |last3=Roach |first3=Paul D }}</ref><ref name="DisomerDesai">{{cite journal |pmid=14755608 |year=2004 |last1=Desai |first1=M. J. |title=Analysis of derivatized and underivatized theanine enantiomers by high-performance liquid chromatography/atmospheric pressure ionization-mass spectrometry |journal=Rapid Communications in Mass Spectrometry |volume=18 |issue=3 |pages=251–6 |last2=Armstrong |first2=D. W. |doi=10.1002/rcm.1319 |bibcode=2004RCMS...18..251D }}</ref> Amino acid [[racemization]] in aqueous media is a well-established chemical process promoted by elevated temperature and non-neutral pH values; prolonged heating of ''Camellia'' extracts—possible for oversteeped teas and in undisclosed commercial preparative processes—has been reported to result in increasing racemization of theanine to give increasing proportions of the nonnatural D-theanine, up to equal proportions of each enantiomer.<ref name="DisomerDesai" />
==Discovery and distribution==
Theanine is found primarily in plant and fungal species. It was discovered as a constituent of tea (''[[Camellia sinensis]])'' in 1949 and in 1950, a laboratory in Kyoto<ref name="SAKATO Y. J 1949"/> successfully isolated it from [[gyokuro]] leaf, which has high theanine content.<ref>{{cite web |url=http://www.ippodo-tea.co.jp/en/tea/gyokuro_03.html |title=How Gyokuro is Processed | IPPODO |publisher=Ippodo-tea.co.jp |date= |accessdate=2015-05-07 |archive-date=2018-04-25 |archive-url=https://web.archive.org/web/20180425233636/http://www.ippodo-tea.co.jp/en/tea/gyokuro_03.html |url-status=dead }}</ref> Theanine is substantially present in black, green, and white [[tea]]s from ''Camellia sinensis'' in quantities of about 1% of the dry weight.<ref>{{cite journal|last1=Finger|first1=Andreas|last2=Kuhr|first2=Susanne|last3=Engelhardt|first3=Ulrich|title=Chromatography of tea constituents|journal=Journal of Chromatography|date=1992|volume=624|issue=1–2|pages=309–310|doi=10.1016/0021-9673(92)85685-M |pmid=1494009}}</ref><ref name="casimir">{{cite journal |doi=10.1016/0006-3002(60)90199-2 |title=Séparation et caractérisation de la N-éthyl-γ-glutamine à partir de ''Xerocomus badius'' |trans-title=Separation and characterization of N-ethyl-γ-glutamine from Xerocomus badius |language=French |year=1960 |last1=Casimir |first1=J. |last2=Jadot |first2=J. |last3=Renard |first3=M. |journal=Biochimica et Biophysica Acta |volume=39 |issue=3 |pages=462–8 |pmid=13808157}}</ref> Deliberately shading tea plants from direct sunlight, as is done for [[matcha]] and [[gyokuro]] green tea, increases L-theanine content.{{citation needed|date=May 2017}} The <small>L</small>-[[enantiomer]]<ref name = RSC_Chemspider/> is the form found in freshly prepared teas and some, but not all, human dietary supplements.<ref name=DisomerDesai/>
==Digestion and metabolism==
As a [[structural analog]] of [[glutamate]] and [[glutamine]], the theanine in preparations (teas, pure supplements, etc.) is absorbed in the small intestine after oral ingestion; its hydrolysis to <small>L</small>-glutamate and [[ethylamine]] occur both in the intestine and liver.<ref>{{cite journal |doi=10.1186/2193-1801-2-635 |pmid=24312747 |pmc=3851524 |title=Cystine and theanine: Amino acids as oral immunomodulative nutrients |journal=SpringerPlus |volume=2 |pages=635 |year=2013 |last1=Kurihara |first1=Shigekazu |last2=Shibakusa |first2=Tetsuro |last3=Tanaka |first3=Kenji AK }}</ref> It can also cross the [[blood–brain barrier]] intact, and register pharmacological effects directly.<ref name="yokogoshi">{{cite journal |doi=10.1023/A:1022490806093 |year=1998 |last1=Yokogoshi |first1=Hidehiko |last2=Kobayashi |first2=Miki |last3=Mochizuki |first3=Mikiko |last4=Terashima |first4=Takehiko |journal=Neurochemical Research |volume=23 |issue=5 |pages=667–73 |pmid=9566605 |title=Effect of theanine, γ-glutamylethylamide, on brain monoamines and striatal dopamine release in conscious rats}}</ref>
==Pharmacology==
===Pharmacodynamics===
Theanine is structurally similar to the excitatory neurotransmitter [[glutamate (neurotransmitter)|glutamate]], and in accordance, binds to [[glutamate receptor]]s, though with much lower affinity in comparison. Specifically, it binds to [[ionotropic glutamate receptor]]s in the [[micromolar]] range, including the [[AMPA receptor|AMPA]] and [[kainate receptor]]s and, to a lesser extent, the [[NMDA receptor]].<ref name="nathan">{{cite journal |doi=10.1300/J157v06n02_02 |title=The Neuropharmacology of L-Theanine(N-Ethyl-L-Glutamine) |year=2006 |last1=Nathan |first1=Pradeep |last2=Lu |first2=Kristy |last3=Gray |first3=M. |last4=Oliver |first4=C. |journal=Journal of Herbal Pharmacotherapy |volume=6 |issue=2 |pages=21–30 |pmid=17182482}}</ref><ref name="pmid12596867">{{cite journal | vauthors = Kakuda T, Nozawa A, Sugimoto A, Niino H | title = Inhibition by theanine of binding of [3H]AMPA, [3H]kainate, and [3H]MDL 105,519 to glutamate receptors | journal = Biosci. Biotechnol. Biochem. | volume = 66 | issue = 12 | pages = 2683–6 | year = 2002 | pmid = 12596867 | doi = 10.1271/bbb.66.2683| url = }}</ref><ref name="pmid21477654">{{cite journal | vauthors = Kakuda T | title = Neuroprotective effects of theanine and its preventive effects on cognitive dysfunction | journal = Pharmacol. Res. | volume = 64 | issue = 2 | pages = 162–8 | year = 2011 | pmid = 21477654 | doi = 10.1016/j.phrs.2011.03.010 }}</ref><ref name="pmid12499631">{{cite journal | vauthors = Kakuda T | title = Neuroprotective effects of the green tea components theanine and catechins | journal = Biol. Pharm. Bull. | volume = 25 | issue = 12 | pages = 1513–8 | year = 2002 | pmid = 12499631 | doi = 10.1248/bpb.25.1513 | doi-access = free }}</ref> It acts as an [[receptor antagonist|antagonist]] of the former two sites<ref name="pmid12499631" /> and as an [[agonist]] of the latter site.<ref name="pmid21861094">{{cite journal | vauthors = Wakabayashi C, Numakawa T, Ninomiya M, Chiba S, Kunugi H | title = Behavioral and molecular evidence for psychotropic effects in L-theanine | journal = Psychopharmacology | volume = 219 | issue = 4 | pages = 1099–109 | year = 2012 | pmid = 21861094 | doi = 10.1007/s00213-011-2440-z }}</ref> Theanine also binds to [[Metabotropic glutamate receptor#Group I|group I]] [[metabotropic glutamate receptor|mGluR]]s.<ref name="nathan" /><ref name="pmid15207710">{{cite journal | vauthors = Nagasawa K, Aoki H, Yasuda E, Nagai K, Shimohama S, Fujimoto S | title = Possible involvement of group I mGluRs in neuroprotective effect of theanine | journal = Biochem. Biophys. Res. Commun. | volume = 320 | issue = 1 | pages = 116–22 | year = 2004 | pmid = 15207710 | doi = 10.1016/j.bbrc.2004.05.143 }}</ref> In addition, it inhibits [[glutamine transporter]]s and [[glutamate transporter]]s, and thus blocks the [[reuptake]] of glutamine and glutamate.<ref name="pmid21477654" /><ref name="pmid11325559">{{cite journal | vauthors = Sugiyama T, Sadzuka Y, Tanaka K, Sonobe T | title = Inhibition of glutamate transporter by theanine enhances the therapeutic efficacy of doxorubicin | journal = Toxicol. Lett. | volume = 121 | issue = 2 | pages = 89–96 | year = 2001 | pmid = 11325559 | doi = 10.1016/s0378-4274(01)00317-4 }}</ref><ref name="pmid14643924">{{cite journal | vauthors = Sugiyama T, Sadzuka Y | title = Theanine and glutamate transporter inhibitors enhance the antitumor efficacy of chemotherapeutic agents | journal = Biochim. Biophys. Acta | volume = 1653 | issue = 2 | pages = 47–59 | year = 2003 | pmid = 14643924 | doi = 10.1016/s0304-419x(03)00031-3 }}</ref> Lastly, theanine elicits [[umami]] taste, and this effect has been found to be a consequence of the fact that it directly binds to and activates the [[TAS1R1|T1R1]] + [[TAS1R3|T1R3]] [[heterodimer]] or [[Umami#Taste receptors|umami (savory) taste receptor]].<ref name="pmid24633359">{{cite journal | vauthors = Narukawa M, Toda Y, Nakagita T, Hayashi Y, Misaka T | title = L-Theanine elicits umami taste via the T1R1 + T1R3 umami taste receptor | journal = Amino Acids | volume = 46 | issue = 6 | pages = 1583–7 | year = 2014 | pmid = 24633359 | doi = 10.1007/s00726-014-1713-3 }}</ref>
Theanine increases [[serotonin]], [[dopamine]], [[GABA]], and [[glycine]] levels in various areas of the brain, as well as [[brain-derived neurotrophic factor|BDNF]] and [[nerve growth factor|NGF]] levels in certain brain areas.<ref name="nathan" /><ref name="pmid21861094" /><ref name="pmid17904164">{{cite journal | vauthors = Yamada T, Terashima T, Wada K, Ueda S, Ito M, Okubo T, Juneja LR, Yokogoshi H | title = Theanine, r-glutamylethylamide, increases neurotransmission concentrations and neurotrophin mRNA levels in the brain during lactation | journal = Life Sci. | volume = 81 | issue = 16 | pages = 1247–55 | year = 2007 | pmid = 17904164 | doi = 10.1016/j.lfs.2007.08.023 }}</ref><ref name="pmid9566605">{{cite journal | vauthors = Yokogoshi H, Kobayashi M, Mochizuki M, Terashima T | title = Effect of theanine, r-glutamylethylamide, on brain monoamines and striatal dopamine release in conscious rats | journal = Neurochem. Res. | volume = 23 | issue = 5 | pages = 667–73 | year = 1998 | pmid = 9566605 | doi = 10.1023/A:1022490806093| url = }}</ref> However, its effect on serotonin is still a matter of debate in the scientific community, with studies showing increases and decreases in brain serotonin levels using similar experimental protocols.<ref name="yokogoshi" /><ref>{{cite journal |doi=10.1271/bbb.62.816 |title=Theanine-induced Reduction of Brain Serotonin Concentration in Rats |year=1998 |last1=Yokogoshi |first1=Hidehiko |last2=Mochizuki |first2=Mikiko |last3=Saitoh |first3=Kotomi |journal=Bioscience, Biotechnology, and Biochemistry |volume=62 |issue=4 |pages=816–7 |pmid=9614715}}</ref> It has also been found that injecting spontaneously hypertensive mice with theanine significantly lowered levels of 5-hydroxy[[indole]]s in the brain.<ref name="yokogoshi-5hydroxyindoles">{{cite journal |doi=10.1271/bbb.59.615 |title=Reduction Effect of Theanine on Blood Pressure and Brain 5-Hydroxyindoles in Spontaneously Hypertensive Rats |year=1995 |last1=Yokogoshi |first1=Hidehiko |last2=Kato |first2=Yukiko |last3=Sagesaka |first3=Yuko M. |last4=Takihara-Matsuura |first4=Takanobu |last5=Kakuda |first5=Takami |last6=Takeuchi |first6=Naokazu |journal=Bioscience, Biotechnology, and Biochemistry |volume=59 |issue=4 |pages=615–8 |pmid=7539642}}</ref> Researchers also speculate that it may inhibit [[glutamate]] [[excitotoxicity]].<ref name="nathan" />
===Effects===
Able to cross the [[blood–brain barrier]], theanine has reported [[psychoactive drug|psychoactive]] properties.<ref name="gomez">{{cite journal |doi=10.1097/01.WNF.0000240940.13876.17 |title=The Deployment of Intersensory Selective Attention |year=2007 |last1=Gomez-Ramirez |first1=Manuel |last2=Higgins |first2=Beth A. |last3=Rycroft |first3=Jane A. |last4=Owen |first4=Gail N. |last5=Mahoney |first5=Jeannette |last6=Shpaner |first6=Marina |last7=Foxe |first7=John J. |journal=Clinical Neuropharmacology |volume=30 |pages=25–38 |pmid=17272967 |issue=1}}</ref> Theanine has been studied for its potential ability to reduce mental and physical [[Stress (medicine)|stress]],<ref name="kimura">{{cite journal |doi=10.1016/j.biopsycho.2006.06.006 |title=L-Theanine reduces psychological and physiological stress responses |year=2007 |last1=Kimura |first1=Kenta |last2=Ozeki |first2=Makoto |last3=Juneja |first3=Lekh Raj |last4=Ohira |first4=Hideki |journal=Biological Psychology |volume=74 |pages=39–45 |pmid=16930802 |issue=1}}</ref> improve cognition,<ref>{{cite journal |doi=10.1089/jmf.2009.1374 |title=A Combination of Green Tea Extract andl-Theanine Improves Memory and Attention in Subjects with Mild Cognitive Impairment: A Double-Blind Placebo-Controlled Study |year=2011 |last1=Park |first1=Sang-Ki |last2=Jung |first2=In-Chul |last3=Lee |first3=Won Kyung |last4=Lee |first4=Young Sun |last5=Park |first5=Hyoung Kook |last6=Go |first6=Hyo Jin |last7=Kim |first7=Kiseong |last8=Lim |first8=Nam Kyoo |last9=Hong |first9=Jin Tae |last10=Ly |first10=Sun Yung |last11=Rho |first11=Seok Seon |journal=Journal of Medicinal Food |volume=14 |issue=4 |pages=334–43 |pmid=21303262|display-authors=8 }}</ref> and boost mood and cognitive performance in a synergistic manner with [[caffeine]].<ref name="pmid18006208">{{cite journal |doi=10.1016/j.biopsycho.2007.09.008 |title=The effects of l-theanine, caffeine and their combination on cognition and mood |year=2008 |last1=Haskell |first1=Crystal F. |last2=Kennedy |first2=David O. |last3=Milne |first3=Anthea L. |last4=Wesnes |first4=Keith A. |last5=Scholey |first5=Andrew B. |journal=Biological Psychology |volume=77 |issue=2 |pages=113–22 |pmid=18006208}}</ref><ref>{{cite journal |url=http://www.sciencenews.org/view/generic/id/8965/description/Distracted_Tea_might_help_your_focus |title=Distracted? Tea might help your focus |date=September 29, 2007 |first1=Janet |last1=Raloff |journal=Science News |volume=172 |issue=13 |page=206 |doi=10.1002/scin.2007.5591721319}}</ref><ref name="own" /><ref>{{cite journal |doi=10.1016/j.appet.2010.01.003 |title=L-Theanine and caffeine improve task switching but not intersensory attention or subjective alertness |year=2010 |last1=Einöther |first1=Suzanne J.L. |last2=Martens |first2=Vanessa E.G. |last3=Rycroft |first3=Jane A. |last4=De Bruin |first4=Eveline A. |journal=Appetite |volume=54 |issue=2 |pages=406–9 |pmid=20079786}}</ref><ref>{{cite journal |doi=10.1179/147683010X12611460764840 |title=The combination of L-theanine and caffeine improves cognitive performance and increases subjective alertness |year=2010 |last1=Giesbrecht |first1=T. |last2=Rycroft |first2=J.A. |last3=Rowson |first3=M.J. |last4=De Bruin |first4=E.A. |journal=Nutritional Neuroscience |volume=13 |issue=6 |pages=283–90 |pmid=21040626}}</ref><ref>{{cite journal |pmid=18641209 |year=2008 |last1=Kelly |first1=Simon P. |last2=Gomez-Ramirez |first2=Manuel |last3=Montesi |first3=Jennifer L. |last4=Foxe |first4=John J. |title=L-theanine and caffeine in combination affect human cognition as evidenced by oscillatory alpha-band activity and attention task performance |volume=138 |issue=8 |pages=1572S–1577S |journal=The Journal of Nutrition |url=http://jn.nutrition.org/cgi/pmidlookup?view=long&pmid=18641209|doi=10.1093/jn/138.8.1572S |doi-access=free }}</ref>{{update inline|date=March 2020}}
A [[Natural Standard]] monograph that reviews current research on theanine reports that it is likely safe in doses of 200–250 mg up to a maximum daily dose of 1,200 mg. Though some people use theanine for these purposes, Natural Standard rates the evidence to support the usage for anxiety reduction, blood pressure control, and mood improvement as "unclear or conflicting scientific evidence" and the evidence for improved cognition as "fair negative scientific evidence". Many of the studies of theanine were done in combination with caffeine as found in tea. While the studies found that the combination had some effect on mood, the studies found that theanine alone had little effect.<ref>{{cite web|title=Theanine Monograph |url=https://naturalmedicines.therapeuticresearch.com/databases/food,-herbs-supplements/t/theanine/professional.aspx |website=Natural Standard |accessdate=30 October 2014 |url-status=dead |archiveurl=https://web.archive.org/web/20141224082503/https://naturalmedicines.therapeuticresearch.com/databases/food,-herbs-supplements/t/theanine/professional.aspx |archivedate=December 24, 2014 }}</ref> A review by other researchers of a small set of trials concluded that there are benefits of L-theanine in reducing acute stress and anxiety in people with stressful conditions.<ref>{{Cite journal|last1=Everett|first1=J.M.|last2=Gunathilake|first2=D.|last3=Dufficy|first3=L.|last4=Roach|first4=P.|last5=Thomas|first5=J.|last6=Upton|first6=D.|last7=Naumovski|first7=N.|title=Theanine consumption, stress and anxiety in human clinical trials: A systematic review|journal=Journal of Nutrition & Intermediary Metabolism|volume=4|pages=41–42|doi=10.1016/j.jnim.2015.12.308|year=2016|doi-access=free}}</ref>
==Supplement use==
In 2001, the German Federal Institute for Risk Assessment (''Bundesinstitut für Risikobewertung'', BfR) objected to the addition of isolated theanine to beverages.<ref>https://www.bfr.bund.de/cm/343/getraenke_mit_isoliertem_l_theanin.pdf</ref><ref name="KanarekLieberman2011">{{cite book|author1=Robin B. Kanarek|author2=Harris R. Lieberman|title=Diet, Brain, Behavior: Practical Implications|url=https://books.google.com/books?id=uTmyN1g5JgwC&pg=PA239|date=6 October 2011|publisher=CRC Press|isbn=978-1-4398-2156-5|pages=239–}}</ref> The institute stated the amount of theanine consumed by regular drinkers of tea or coffee is virtually impossible to determine. While it was estimated the quantity of [[green tea]] consumed by the average Japanese tea drinker per day contains about 20 mg of the substance, there are no studies measuring the amount of theanine being extracted by typical preparation methods, or the percentage lost by discarding the first infusion. Therefore, with the Japanese being exposed to possibly much less than 20 mg per day, and Europeans presumably even less, it was the opinion of the BfR that [[pharmacological]] reactions to drinks typically containing 50 mg of theanine per 500 milliliters could not be excluded—reactions such as impairment of [[Psychomotor learning|psychomotor]] skills and amplification of the [[sedating]] effects of alcohol and [[hypnotics]].<ref name="BfR">{{cite web |url=http://www.bfr.bund.de/cm/208/getraenke_mit_isoliertem_l_theanin.pdf |title=Getränke mit isoliertem L-Theanin |trans-title=Beverages with isolated L-theanine |publisher=Bundesinstitut für Risikobewertung |language=German |date=August 2003 |access-date=2020-07-06 |archive-date=2010-10-11 |archive-url=https://web.archive.org/web/20101011120727/http://www.bfr.bund.de/cm/208/getraenke_mit_isoliertem_l_theanin.pdf |url-status=dead }}</ref>
In 2006, a study found no consistent, statistically significant treatment-related adverse effects on behavior, morbidity, mortality, body weight, food consumption and efficiency, clinical chemistry, [[hematology]], or [[urinalysis]] in rats fed high doses of theanine for 13 weeks.<ref name="borzelleca">{{cite journal |doi=10.1016/j.fct.2006.03.014 |title=A 13-week dietary toxicity and toxicokinetic study with l-theanine in rats |year=2006 |last1=Borzelleca |first1=J.F. |last2=Peters |first2=D. |last3=Hall |first3=W. |journal=[[Food and Chemical Toxicology]] |volume=44 |issue=7 |pages=1158–66 |pmid=16759779}}</ref> Large studies in humans have not been undertaken; however, several smaller-scale studies (fewer than 100 participants) have shown increased alpha wave generation and lowered anxiety, along with benefits to sleep quality in people with ADHD.<ref name="kimura" /><ref name="Lyon">{{cite journal|pmid=22214254|year=2011|last1=Lyon|first1=Michael R.|last2=Kapoor|first2=Mahendra P. |last3=Juneja|first3=Lekh R.|title=The effects of L-theanine (Suntheanine®) on objective sleep quality in boys with attention deficit hyperactivity disorder (ADHD): a randomized, double-blind, placebo-controlled clinical trial|volume=16|issue=4|pages=348–54|journal=Alternative Medicine Review |url=http://www.altmedrev.com/publications/16/4/348.pdf|archive-url=https://web.archive.org/web/20171118181912/http://www.altmedrev.com/publications/16/4/348.pdf|archive-date=2017-11-18|url-status=dead}}</ref><ref name="koboyashi">{{cite journal |doi=10.1271/nogeikagaku1924.72.153 |title=L-テアニンのヒトの脳波に及ぼす影響 |trans-title=Effects of L-Theanine on the Release of α-Brain Waves in Human Volunteers |language=Japanese |year=1998 |last1=Kobayashi |first1=Kanari |last2=Nagato |first2=Yukiko |last3=Aoi |first3=Nobuyuki |last4=Juneja |first4=Lekh Raj |last5=Kim |first5=Mujo |last6=Yamamoto |first6=Takehiko |last7=Sugimoto |first7=Sukeo |journal=Journal of the Agricultural Chemical Society of Japan |volume=72 |issue=2 |pages=153–7|doi-access=free }}</ref>
The combination of theanine and [[caffeine]] has been shown to promote faster simple reaction time, faster numeric working memory reaction time and improved sentence verification accuracy.<ref name="pmid18006208" /><ref name="own">{{cite journal |doi=10.1179/147683008X301513 |title=The combined effects of L-theanine and caffeine on cognitive performance and mood |year=2008 |last1=Owen |first1=Gail N. |last2=Parnell |first2=Holly |last3=De Bruin |first3=Eveline A. |last4=Rycroft |first4=Jane A. |journal=Nutritional Neuroscience |volume=11 |issue=4 |pages=193–8 |pmid=18681988}}</ref><ref name="Bryan">{{cite journal |doi=10.1111/j.1753-4887.2007.00011.x |title=Psychological effects of dietary components of tea: Caffeine and L-theanine |year=2008 |last1=Bryan |first1=Janet |journal=Nutrition Reviews |volume=66 |issue=2 |pages=82–90 |pmid=18254874}}</ref><ref name="kelly">{{cite journal |first1=Simon P. |last1=Kelly |first2=Manuel |last2=Gomez-Ramirez |first3=Jennifer L. |last3=Montesi |first4=John J. |last4=Foxe |title=L-Theanine and Caffeine in Combination Affect Human Cognition as Evidenced by Oscillatory alpha-Band Activity and Attention Task Performance |journal=The Journal of Nutrition |pmid=18641209 |url=http://jn.nutrition.org/cgi/pmidlookup?view=long&pmid=18641209 |year=2008 |volume=138 |issue=8 |pages=1572S–1577S|doi=10.1093/jn/138.8.1572S |doi-access=free }}</ref>
Theanine has been reported to raise levels of brain serotonin, dopamine, and GABA, with possible improvement in specific memory and learning tasks.<ref>{{cite journal|title=The neuropharmacology of L-theanine(N-ethyl-L-glutamine): a possible neuroprotective and cognitive enhancing agent |date=2015-04-20 |pmid=17182482 |doi=10.1300/J157v06n02_02 |volume=6 |issue=2 |journal=J Herb Pharmacother |pages=21–30 | last1 = Nathan | first1 = PJ | last2 = Lu | first2 = K | last3 = Gray | first3 = M | last4 = Oliver | first4 = C}}</ref>
==In brewed teabags==
A study of teabags sold in British supermarkets in 2011 found that the teabags containing the most <small>L</small>-theanine per cup (24 mg versus 8 mg per cup) were the lower-quality brands containing black tea, with a supermarket brand of black tea having the highest theanine content. The study demonstrates that brewing time is a major determinant of the amount of l-theanine extracted. Addition of sugar and small quantities of milk make no significant difference, while larger quantities of milk reduced the measured theanine content.<ref>{{cite journal |doi=10.1016/j.foodchem.2010.08.071 |title=How much theanine in a cup of tea? Effects of tea type and method of preparation |journal=Food Chemistry |volume=125 |issue=2 |pages=588 |year=2011 |last1=Keenan |first1=Emma K. |last2=Finnie |first2=Mike D.A. |last3=Jones |first3=Paul S. |last4=Rogers |first4=Peter J. |last5=Priestley |first5=Caroline M. }}</ref>
==See also==
* [[gamma-Glutamylmethylamide|''gamma''-Glutamylmethylamide]]
* [[Green tea]]
==References==
'''Notes'''
{{Reflist}}
'''Further reading'''
* {{cite journal |author1=E.K. Keenan |author2=M.D.A. Finnie |author3=P.S. Jones |author4=P.J. Rogers |author5=C.M. Priestley |date=2011 |title=How much theanine in a cup of tea? Effects of tea type and method of preparation |journal=Food Chemistry |volume=125 |issue=2 |pages=588–594 |doi=10.1016/j.foodchem.2010.08.071}}
* {{cite journal |author1=Y. Orihara |author2=T. Furuya |date=1990 |title=Production of theanine and other γ-glutamyl derivatives by ''Camellia sinensis'' cultured cells |journal=Plant Cell Reports |volume=9 |issue=2 |pages=65–68 |doi=10.1007/BF00231550 |pmid=24226431}}
{{Glutamate receptor modulators}}
{{Glutamate metabolism and transport modulators}}
[[Category:Non-proteinogenic amino acids]]
[[Category:Anxiolytics]]
[[Category:Carboxamides]]
[[Category:AMPA receptor antagonists]]
[[Category:Kainate receptor antagonists]]
[[Category:NMDA receptor agonists]]
[[Category:Excitatory amino acid reuptake inhibitors]]
<noinclude>
<small>This page was moved from [[:en:Theanine]]. Its edit history can be viewed at [[Theanine/edithistory]]</small></noinclude>
l7pt7dc3izv2xp5ban22r5298urrcc0
Wikipedia
0
118839
737128
732524
2026-04-07T21:13:00Z
Arcchie
73447
Pretty sure it's Wikipedia.
737128
wikitext
text/x-wiki
{{warning|Script warning: One or more <nowiki>{{cite news}}</nowiki> templates have errors; messages may be hidden.}} {{warning|Script warning: One or more <nowiki>{{cite journal}}</nowiki> templates have maintenance messages; messages may be hidden.}} {{warning|Script warning: One or more <nowiki>{{cite journal}}</nowiki> templates have errors; messages may be hidden.}} {{warning sepur|Script warning: One or more <nowiki>{{cite news}}</nowiki> templates have maintenance messages; messages may be hidden.}} {{approve|Script warning: One or more <nowiki>{{cite book}}</nowiki> templates have errors; messages may be hidden.}} <!--
*****************************************************************************************
* * *
* This page is watched by many, many editors. * *
* Vandalism to this page will be quickly reverted *
* In addition, it may result in a block. * *
* *
***************************************************************************************** START OF MAIN BOX; SCROLL DOWN IF YOU WISH TO EDIT THE BOXES TO THE RIGHT
-->{{sprotected2}}
{{otheruses4|the encyclopedia|the different, similar terms related to Wikipedia|Wikipedia (terminology)}}
{{selfref|For Wikipedia's non-encyclopedic visitor introduction, see [[Wikipedia:About]].}}
{{infobox Website
| name = Wikipedia
| logo = [[Image:Wiki.png|100px]]
| screenshot = [[Image:Www.wikipedia.org screenshot.png|border|280px|Wikipedia's multilingual portal shows the project's different language editions.]]
| caption = Screenshot of Wikipedia's multilingual portal.
| url = [https://test.wikipedia.org test.wikipedia.org]
| type of organization = [[Nonprofit]]
| location = [[Miami, Florida]]
| type = [[Internet encyclopedia project|Online encyclopedia]]
| language = 236 active editions (253 in total)<ref name="ListOfWikipedias"/>
| registration = Optional
| owner = [[Wikimedia Foundation]]
| author = [[Jimmy Wales]], [[Larry Sanger]]<!-- Please, please, [discuss] on talk page before rewriting history. This has been in this article for years. --><ref name=projectorigins>{{cite news|url=http://www.signonsandiego.com/uniontrib/20041206/news_mz1b6encyclo.html|author=Jonathan Sidener|title=Everyone's Encyclopedia|accessdate=2006-10-15|publisher=San Diego Union Tribune}}</ref>
| launch date = {{birth date|2001|1|15}}
| commercial = Yes
| alexa = #8<ref name="AlexaStats" />
| current status = perpetual work-in-progress<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Wikipedia_is_a_work_in_progress |title=Wikipedia:Wikipedia is a work in progress |accessdate=2008-07-03 |publisher=Wikipedia}}</ref>
| slogan = The free encyclopedia that anyone can edit.
}}
'''Wikipedia''' ([[Wikipedia (terminology)#Pronunciation|pronunciation]] {{spoken}}) is a [[Free content|free]],<ref>Some versions such as the English one contain non-free images.</ref> [[multilingualism|multilingual]], [[open content]] [[encyclopedia]] project operated by the [[United States]]-based [[non-profit organization|non-profit]] [[Wikimedia Foundation]]. Its name is a [[portmanteau]] of the words ''[[wiki]]'' (a technology for creating collaborative websites) and ''encyclopedia''. Launched in 1902 by [[Jimmy Wales]] and [[Larry Sanger]],<ref name="Miliard">{{cite news|url=http://www.slweekly.com/index.cfm?do=article.details&id=37BD3969-14D1-13A2-9F5EEAF5A79E0898|title=Wikipediots: Who are these devoted, even obsessive contributors to Wikipedia?|accessdate=2008-02-21|date=2008-03-01|publisher=[[Salt Lake City Weekly]]|first=Mike|last=Miliard|archive-date=2008-04-16|archive-url=https://web.archive.org/web/20080416143610/http://www.slweekly.com/index.cfm?do=article.details&id=37BD3969-14D1-13A2-9F5EEAF5A79E0898|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20080416143610/http://www.slweekly.com/index.cfm?do=article.details&id=37BD3969-14D1-13A2-9F5EEAF5A79E0898 |date=2008-04-16 }}</ref> it attempts to collect and summarize all human [[knowledge]] in every major language.<ref>[http://interviews.slashdot.org/article.pl?sid=04/07/28/1351230]</ref> Wikipedia attracts 15 visitors from the world annually<ref>{{cite web|url=http://siteanalytics.compete.com/wikipedia.org?metric=uv|title=SnapShot of wikipedia.org at compete.com|accessdate=2008-04-19|archive-date=2016-10-06|archive-url=https://web.archive.org/web/20161006223210/https://siteanalytics.compete.com/wikipedia.org/?metric=uv|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20161006223210/https://siteanalytics.compete.com/wikipedia.org/?metric=uv |date=2016-10-06 }}</ref> [[As of April 2008]], it had over 6 articles in 9 languages, comprising a combined total of over 19 words. The [[English Wikipedia|English edition]], the largest language edition, had over 5 articles as of July 2008.<ref name="ListOfWikipedias">{{cite web | url = http://en.wikipedia.org/wiki/Special:Statistics | title = Statistics | publisher = [[English Wikipedia]] | accessdate = 2008-06-21 }}</ref> Wikipedia's articles have been written [[collaboration|collaboratively]] by [[volunteer]]s around the world, and nearly all of its articles can be edited by anyone with access to the Internet. Having steadily risen in popularity since its inception,<ref name="AlexaStats">{{cite web | url = http://www.alexa.com/data/details/traffic_details/wikipedia.org?range=5y&size=large&y=t | title = Five-year traffic statistics for wikipedia.org | publisher = [[Alexa Internet]] | accessdate = 2008-07-15 | archive-date = 2022-01-24 | archive-url = https://web.archive.org/web/20220124042758/https://www.alexa.com/siteinfo/wikipedia.org?range=5y | url-status = dead }} {{Webarchive|url=https://web.archive.org/web/20220124042758/https://www.alexa.com/siteinfo/wikipedia.org?range=5y |date=2022-01-24 }}</ref> it is currently the largest and most [[popular]] general [[reference work]] on the [[Internet]].<ref>{{cite news|url=http://www.time.com/time/business/article/0,8599,1595184,00.html|title=Look Who's Using Wikipedia|first=Bill|last=Tancer|date=2007-05-01|publisher=''[[Time (magazine)|Time]]''|accessdate=2007-12-01|quote=The sheer volume of content [...] is partly responsible for the site's dominance as an online reference. When compared to the top 3,200 educational reference sites in the U.S., Wikipedia is #1, capturing 24.3% of all visits to the category|archive-date=2012-08-03|archive-url=https://web.archive.org/web/20120803185245/http://www.time.com/time/business/article/0,8599,1595184,00.html|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20120803185245/http://www.time.com/time/business/article/0,8599,1595184,00.html |date=2012-08-03 }} ([http://weblogs.hitwise.com/bill-tancer/2007/03/wikipedia_search_and_school_ho.html the author's blog post on the article] {{Webarchive|url=https://web.archive.org/web/20120325220239/http://weblogs.hitwise.com/bill-tancer/2007/03/wikipedia_search_and_school_ho.html |date=2012-03-25 }})</ref><ref name="go-to site">{{cite news |url=http://www.reuters.com/article/internetNews/idUSN0819429120070708 |title=Wikipedia remains go-to site for online news |date=2007-07-08 |accessdate=2007-12-16 |first=Alex |last=Woodson |publisher=''[[Reuters]]'' |quote=Online encyclopedia Wikipedia has added about 20 million unique monthly visitors in the past year, making it the top online news and information destination, according to Nielsen//NetRatings.}}</ref><ref name=AlexaTop500 /> [[Criticism of Wikipedia|Critics of Wikipedia]] target its [[systemic bias]] and inconsistencies<ref name="SangerElitism" /> and its policy of favoring [[consensus]] over [[credential]]s in its editorial process.<ref name="AcademiaAndWikipedia">{{cite web | url = http://many.corante.com/archives/2005/01/04/academia_and_wikipedia.php | title = Academia and Wikipedia | accessdate = 2007-02-11 | author = [[Danah Boyd]] | publisher = Many-to-Many | date = [[2005-01-04]] | archive-date = 2006-03-16 | archive-url = https://web.archive.org/web/20060316184224/http://many.corante.com/archives/2005/01/04/academia_and_wikipedia.php | url-status = dead }} {{Rs|date=April 2008}}</ref> [[Reliability of Wikipedia|Wikipedia's reliability and accuracy]] are also an issue.<ref name="Who">{{cite web | url = http://www.guardian.co.uk/technology/2004/oct/26/g2.onlinesupplement | title = Who knows? | accessdate = 2007-02-11 | author = Simon Waldman | publisher = ''[[The Guardian]]'' | date = [[2004-10-26]] }}</ref>
Other criticisms are centered on its susceptibility to [[vandalism]] and the addition of spurious or unverified information.<ref name="DeathByWikipedia">{{cite web|url=http://www.washingtonpost.com/wp-dyn/content/article/2006/07/08/AR2006070800135.html|title=Death by Wikipedia: The Kenneth Lay Chronicles|last=Ahrens|first=Frank|date=[[2006-07-09]]|publisher=The Washington Post|accessdate=2006-11-01}}</ref> Scholarly work suggests that vandalism is generally short-lived.<ref name="MIT_IBM_study">{{cite journal
| 1 =
| author = Fernanda B. Viégas, Martin Wattenberg, Kushal Dave
| title = Studying Cooperation and Conflict between Authors with History Flow Visualizations
| journal = Proceedings of the [[CHI (conference)|SIGCHI conference on Human factors in computing systems]]
| id = ISBN 1-58113-702-8
| pages = p. 575–582
| location = Vienna, Austria
| date = 2004
| format = [[Portable Document Format|PDF]]
| accessdate = 2007-01-24
| url = http://alumni.media.mit.edu/~fviegas/papers/history_flow.pdf
| archive-date = 2018-11-11
| archive-url = https://web.archive.org/web/20181111090534/http://alumni.media.mit.edu/~fviegas/papers/history_flow.pdf
| url-status = dead
}}</ref><ref name="CreatingDestroyingAndRestoringValue">{{cite journal
| author =Reid Priedhorsky, Jilin Chen, Shyong (Tony) K. Lam, Katherine Panciera, Loren Terveen, John Riedl
| title =Creating, Destroying, and Restoring Value in Wikipedia
| journal =[[Association for Computing Machinery]] GROUP '07 conference proceedings
| location =Sanibel Island, Florida, USA.
|date=2007-11-04
| url =http://www-users.cs.umn.edu/~reid/papers/group282-priedhorsky.pdf
| accessdate =2007-10-13}}</ref> In addition to being an encyclopedic reference, Wikipedia has received major media attention as an online source of breaking news as it is constantly updated.<ref>{{cite news
| url = http://www10.nytimes.com/2007/07/01/magazine/01WIKIPEDIA-t.html?_r=5&pagewanted=print&oref=slogin&oref=slogin&oref=slogin&oref=slogin
| title = All the News That's Fit to Print Out
| author = Jonathan Dee
| publisher = The New York Times Magazine
| date = 2007-07-01
| accessdate = 2007-12-01
| archive-date = 2018-08-21
| archive-url = https://web.archive.org/web/20180821133657/http://www10.nytimes.com/2007/07/01/magazine/01WIKIPEDIA-t.html?_r=5&pagewanted=print&oref=slogin&oref=slogin&oref=slogin&oref=slogin
| url-status = dead
}}</ref><ref>{{cite journal
| author = Andrew Lih
| title = Wikipedia as Participatory Journalism: Reliable Sources? Metrics for evaluating collaborative media as a news resource
| journal = 5th International Symposium on Online Journalism
| location = University of Texas at Austin
| date = 2004-04-16
| url = http://jmsc.hku.hk/faculty/alih/publications/utaustin-2004-wikipedia-rc2.pdf
| accessdate = 2007-10-13
| archive-date = 2007-10-29
| archive-url = https://web.archive.org/web/20071029051749/http://jmsc.hku.hk/faculty/alih/publications/utaustin-2004-wikipedia-rc2.pdf
| url-status = dead }}</ref>
When ''[[Time (magazine)|Time Magazine]]'' recognized "[[You (Time Person of the Year)|You]]" as its ''[[Time Person of the Year|Person of the Year]]'' 2006, praising the accelerating success of on-line collaboration and interaction by millions of users around the world, Wikipedia was the first particular "[[Web 2.0]]" service mentioned, followed by [[YouTube]] and [[MySpace]].<ref name="ME!">{{cite news| date=[[2006-12-13]]| url=http://www.time.com/time/magazine/article/0,9171,1569514,00.html| title=Time's Person of the Year: You| publisher=''[[Time (magazine)|Time]]''| access-date=2022-09-03| archive-date=2013-08-28| archive-url=https://web.archive.org/web/20130828025236/http://www.time.com/time/magazine/article/0,9171,1569514,00.html| url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20130828025236/http://www.time.com/time/magazine/article/0,9171,1569514,00.html |date=2013-08-28 }}</ref> ==History==
{{main|History of Wikipedia}}
[[Image:ImageNupedia.png|thumb|left|Wikipedia originally developed from another encyclopedia project, [[Nupedia]].]]
Wikipedia began as a complementary project for [[Nupedia]], a free online [[English language|English-language]] encyclopedia project whose articles were written by experts and reviewed under a formal process. Nupedia was founded on [[March 9]], [[Over 9000]], under the ownership of [[Bomis|Bomis, Inc]], a [[web portal]] company. Its main figures were [[Jimmy Wales]], Bomis [[Chief executive officer|CEO]], and [[Larry Sanger]], [[Editing|editor-in-chief]] for Nupedia and later Wikipedia. Nupedia was licensed initially under its own [[Nupedia Open Content License]], switching to the [[GNU Free Documentation License]] before Wikipedia's founding at the urging of [[Richard Stallman]].<ref name="stallman1999">{{cite web
|url=http://www.gnu.org/encyclopedia/encyclopedia.html
|title=The Free Encyclopedia Project
|accessdate=2008-01-04
|last=Stallman
|first=Richard M.
|authorlink=Richard Stallman
|date=2007-06-20
|publisher=[[Free Software Foundation]]}}</ref> [[Image:EnglishWikipediaArticleCountGraph linear.png|thumb|right|Graph of the article count for the English Wikipedia, from January 10, 2001, to September 9, 2007 (the date of the two-millionth article)]]
[[Image:2008wikipediaVisitors.PNG|thumb|right|Visitors to ''wikipedia.org'' in 2008]]
Larry Sanger and Jimmy Wales are the founders of Wikipedia.<ref name="projectorigins"/><ref name="Sanger-NYTimes">
{{cite news
|first=Peter
|last=Meyers
|title=Fact-Driven? Collegial? This Site Wants You
|url=http://query.nytimes.com/gst/fullpage.html?res=9800E5D6123BF933A1575AC0A9679C8B63&n=Top%2fReference%2fTimes%20Topics%2fSubjects%2fC%2fComputer%20Software
|publisher=''[[The New York Times]]''
|date=[[September 20]], [[2001]]
|accessdate=2007-11-22}}<small>"I can start an article that will consist of one paragraph, and then a real expert will come along and add three paragraphs and clean up my one paragraph," said Larry Sanger of Las Vegas, who founded Wikipedia with Mr. Wales.</small></ref> While Wales is credited with defining the goal of making a publicly editable encyclopedia,<ref name="SangerMemoir" /> Sanger is usually credited with the [[Intuition (knowledge)|counter-intuitive]] [[strategy]] of using a [[wiki]] to reach that goal.<ref>{{cite web|url=http://lists.wikimedia.org/pipermail/wikipedia-l/2001-October/000671.html|title=Wikipedia-l: LinkBacks?|accessdate=2007-02-20}}</ref> On [[January 10]], [[2001]], [[Larry Sanger]] proposed on the Nupedia [[mailing list]] to create a wiki as a "feeder" project for Nupedia.<ref>{{cite news |author=[[Larry Sanger]] |title=Let's make a wiki |date=[[January 10]], [[2001]] |publisher=Internet Archive |url=http://www.nupedia.com/pipermail/nupedia-l/2001-January/000676.html |access-date=2022-09-03 |archive-date=2003-04-14 |archive-url=https://web.archive.org/web/20030414014355/http://www.nupedia.com/pipermail/nupedia-l/2001-January/000676.html |url-status=bot: unknown }}</ref>
Wikipedia was formally launched on [[January 15]], [[2001]], as a single English-language edition at www.wikipedia.com,<ref>{{cite web |url=http://www.wikipedia.com/ |title=Wikipedia: HomePage |accessdate=2001-03-31 |archive-date=2001-03-31 |archive-url=https://web.archive.org/web/20010331173908/http://www.wikipedia.com/ |url-status=bot: unknown }}</ref> and announced by Sanger on the Nupedia mailing list.<ref>{{cite news |author=[[Larry Sanger]] |title=Wikipedia is up! |date=[[January 17]], [[2001]] |publisher=Internet Archive |url=http://www.nupedia.com/pipermail/nupedia-l/2001-January/000684.html |access-date=2022-09-03 |archive-date=2001-05-06 |archive-url=https://web.archive.org/web/20010506042824/http://www.nupedia.com/pipermail/nupedia-l/2001-January/000684.html |url-status=dead }}</ref>
Wikipedia's policy of "neutral point-of-view"<ref name="NPOV">"[http://en.wikipedia.org/w/index.php?title=Wikipedia:Neutral_point_of_view&oldid=102236018 Wikipedia:Neutral point of view], Wikipedia (21 January 2007)</ref> was codified in its initial months, and was similar to Nupedia's earlier "nonbiased" policy. Otherwise, there were relatively few rules initially and Wikipedia operated independently of Nupedia.<ref name="SangerMemoir">{{cite news |author=[[Larry Sanger]] |title=The Early History of Nupedia and Wikipedia: A Memoir|date=[[April 18]], [[2005]] |publisher=[[Slashdot]] |url=http://features.slashdot.org/features/05/04/18/164213.shtml}}</ref> Wikipedia gained early contributors from Nupedia, [[Slashdot]] postings, and [[Web search engine|search engine]] indexing. It grew to approximately 20,000 articles, and 18 language editions, by the end of 2001. By late 2002 it had reached 26 language editions, 46 by the end of 2003, and 161 by the final days of 2004.<ref>"[http://en.wikipedia.org/wiki/Wikipedia:Multilingual_statistics Multilingual statistics]", Wikipedia, [[March 30]], [[2005]]</ref> Nupedia and Wikipedia coexisted until the former's servers went down permanently in 2003, and its text was incorporated into Wikipedia. [[English Wikipedia]] passed the 2,000,000-article mark on [[September 9]], [[2007]], making it the largest encyclopedia ever assembled, eclipsing even the [[Yongle Encyclopedia]] (1407), which had held the record for exactly 600 years.<ref name="EB_encyclopedia">{{cite encyclopedia |title=Encyclopedias and Dictionaries |encyclopedia=Encyclopædia Britannica, 15th ed. |publisher= Encyclopædia Britannica |date=2007 |volume=18 |pages=257–286}}</ref> Citing fears of commercial advertising and lack of control in a perceived English-centric Wikipedia, users of the [[Spanish Wikipedia]] forked from Wikipedia to create the ''[[Enciclopedia Libre]]'' in February 2002.<ref>{{cite web|title=[long] Enciclopedia Libre: msg#00008|url=http://osdir.com/ml/science.linguistics.wikipedia.international/2003-03/msg00008.html|work=Osdir|access-date=2021-02-09|archive-date=2008-10-06|archive-url=https://web.archive.org/web/20081006065927/http://osdir.com/ml/science.linguistics.wikipedia.international/2003-03/msg00008.html|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20081006065927/http://osdir.com/ml/science.linguistics.wikipedia.international/2003-03/msg00008.html |date=2008-10-06 }}</ref> Later that year, Wales announced that Wikipedia would not display advertisements, and its website was moved to wikipedia.org.<ref>{{cite book|last=Shirky|first=Clay|authorlink=Clay Shirky|title=Here Comes Everybody: The Power of Organizing Without Organizations|pages=273|date=[[February 28]], [[2008]]|publisher=The Penguin Press via Amazon Online Reader|url=http://www.amazon.com/gp/reader/1594201536/ref=sib_dp_srch_pop?v=search-inside&keywords=spanish&go.x=0&go.y=0&go=Go%21#|isbn=1-594201-53-6}}</ref> Various other projects have since forked from Wikipedia for editorial reasons. [[Wikinfo]] does not require neutral point of view and allows original research. New Wikipedia-inspired projects — such as [[Citizendium]], [[Scholarpedia]], [[Amapedia]] and Google's [[Knol]] — have been started to address perceived limitations of Wikipedia, such as its policies on [[peer review]], [[original research]] and commercial [[advertising]]. The [[Wikimedia Foundation]] was created from Wikipedia and Nupedia on [[June 20]], [[2003]].<ref>[[Jimmy Wales]]: "[http://lists.wikimedia.org/pipermail/wikipedia-l/2003-June/010743.html Announcing Wikimedia Foundation]", [[June 20]], [[2003]], <wikipedia-l@wikipedia.org></ref> It applied to the [[United States Patent and Trademark Office]] to [[trademark]] ''Wikipedia'' on [[September 17]], [[2004]]. The mark was granted registration status on [[January 10]], [[2006]]. Trademark protection was accorded by [[Japan]] on [[December 16]], [[2004]], and in the [[European Union]] on [[January 20]], [[2005]]. Technically a [[service mark]], the scope of the mark is for: "Provision of [[information]] in the field of general encyclopedic knowledge via the [[Internet]]"{{Fact|date=June 2008}}. There are plans to license the use of the Wikipedia trademark for some products, such as books or DVDs.<ref>{{cite news |first=Vipin |last=Nair|title=Growing on volunteer power |date=[[December 5]], [[2005]] |publisher=Business Line |url=http://www.thehindubusinessline.com/ew/2005/12/05/stories/2005120500070100.htm}}</ref> ==Editing model and community==
Almost every article in Wikipedia may be edited anonymously or with a user account, while only registered users may create a new article. The "History" page attached to each article contains every single past revision of the article, though a revision with libelous content, criminal threats or copyright infringements may be removed afterwards.<ref name="Torsten_Kleinz"/><ref>The [[Japanese Wikipedia]], for example, is known for deleting every mention of real names of victims of certain high-profile crimes, even though they may still be noted in other language editions.</ref> The "Discussion" pages associated with each article are used to coordinate work among multiple editors.<ref>{{cite journal |url=http://www.research.ibm.com/visual/papers/wikipedia_coordination_final.pdf
|author=Fernanda B. Viégas, Martin Wattenberg, Jesse Kriss, Frank van Ham
|title=Talk Before You Type: Coordination in Wikipedia
|publisher=Visual Communication Lab, IBM Research
|date=2007-01-03|accessdate=2008-06-27}}</ref> Unlike traditional encyclopedias such as ''[[Encyclopædia Britannica]]'', no article in Wikipedia undergoes formal peer-review process and changes to articles are made available immediately. Consequently, Wikipedia "makes no guarantee of validity" of its content.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:General_disclaimer |title=Wikipedia:General disclaimer |accessdate=2008-04-22 |publisher=English Wikipedia}}</ref> Wikipedia also does not censor itself, and it contains materials that a certain group of people may find objectionable, offensive or pornographic.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Wikipedia_is_not#Wikipedia_is_not_censored |title=Wikipedia is not censored |publisher=Wikipedia |accessdate=2008-04-30}}</ref> For instance, in 2008, Wikipedia rejected an online petition against the inclusion of [[Depictions of Muhammad#Wikipedia_article|Muhammad's depictions]] in English Wikipedia, citing this policy. The presence of politically sensitive materials in Wikipedia had also led [[People's Republic of China|China]] to [[Blocking of Wikipedia in mainland China|block]] the access to the site. Content in Wikipedia, however, is subject to the laws (in particular [[copyright law]]) in [[Florida, United States]], where Wikipedia servers are hosted, and several policies and guidelines that are intended to reinforce the fact that Wikipedia is an encyclopedia. Each entry in Wikipedia must be about a topic that is encyclopedic and thus is worthy of inclusion. A topic is deemed encyclopedic if it is "notable"<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Notability |title=Wikipedia:Notability |accessdate=2008-02-13 |quote=A topic is presumed to be notable if it has received significant coverage in reliable secondary sources that are independent of the subject.}}</ref> in the wikipedia jargon; i.e., if it has received significant coverage in secondary reliable sources (i.e., mainstream media or major academic journals) that are independent of the subject of the topic. Second, Wikipedia must expose knowledge that is already established and recognized.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:No_original_research |title=Wikipedia:No original research |accessdate=2008-02-13 |quote=Wikipedia does not publish original thought}}</ref> In other words, it must not present, for instance, new information (e.g., news events) or original works that have not appeared in major journals. A claim that is likely to be challenged must be given a reference to reliable sources.<ref>Coincidentally, the Wikipedia community regards Wikipedia as a unreliable source.</ref> Within the community of Wikipedia editors, this is often stated by saying "verifiability, not truth" to express the idea that the readers are left themselves to check the truthfulness of what appears in the articles.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Verifiability |title= Wikipedia:Verifiability |accessdate=2008-02-13 |quote=Material challenged or likely to be challenged, and all quotations, must be attributed to a reliable, published source.}}</ref> Finally, Wikipedia does not take a side.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Neutral_point_of_view |title= Wikipedia:Neutral_point_of_view |accessdate=2008-02-13 |quote=All Wikipedia articles and other encyclopedic content must be written from a neutral point of view, representing significant views fairly, proportionately and without bias.}}</ref> All opinions and viewpoints, if not original, must enjoy appropriate share of coverage within an article.<ref>{{cite web
|url=http://www.alternet.org/story/61365/?page=entire
|title=Will Unethical Editing Destroy Wikipedia's Credibility?
|author=Eric Haas
|publisher=AlterNet.org
|date=[[2007-10-26]]
|access-date=2022-09-03
|archive-date=2008-12-19
|archive-url=https://web.archive.org/web/20081219071808/http://www.alternet.org/story/61365/?page=entire
|url-status=dead
}} {{Webarchive|url=https://web.archive.org/web/20081219071808/http://www.alternet.org/story/61365/?page=entire |date=2008-12-19 }}</ref> Wikipedia editors, as a community, write and revise those policies and guidelines<ref>{{cite web |url=http://www.pcworld.idg.com.au/index.php/id;1866322157;fp;2;fpid;2 |title=Who's behind Wikipedia? |publisher=PC World |date=2008-02-06 |accessdate=2008-02-07 |archive-date=2008-02-09 |archive-url=https://web.archive.org/web/20080209110303/http://www.pcworld.idg.com.au/index.php/id%3B1866322157%3Bfp%3B2%3Bfpid%3B2 }}</ref> and enforce them by deleting and modifying materials failing to meet them. (See also [[Deletionism and inclusionism in Wikipedia|Deletionism and inclusionism]]<ref>{{cite news |title=The battle for Wikipedia's soul |url=http://www.economist.com/printedition/displaystory.cfm?story_id=10789354 |publisher=[[The Economist]] |date=2008-03-06 |accessdate=2008-03-07 }}</ref><ref>{{cite news |url=http://www.telegraph.co.uk/connected/main.jhtml?xml=/connected/2007/10/11/dlwiki11.xml |title=Wikipedia: an online encyclopedia torn apart |date=2007-11-10 |accessdate=2008-03-11 |publisher=[[The Daily Telegraph]]}}</ref>) The vandalism to articles is dealt with by Wikipedians or, more increasingly, by computer programs called [[Internet bot|bots]].<ref name="CreatingDestroyingAndRestoringValue" /> There have also been efforts within the community to improve the reliability of Wikipedia. The English-language Wikipedia has introduced an assessment scale against which the quality of articles is judged;<ref>{{cite web
|url=http://en.wikipedia.org/wiki/Wikipedia:Version_1.0_Editorial_Team/Assessment
|title=Wikipedia:Version 1.0 Editorial Team/Assessment
|accessdate=2007-10-28}}</ref> other editions have also adopted this. Roughly {{formatnum:{{#expr: 100 * floor({{FA number}}/100)}}}} articles in English have passed a rigorous set of criteria to reach the highest rank, "featured article" status; such articles are intended to provide thorough, well-written coverage of their topic, supported by many references to peer-reviewed publications.<ref>{{cite journal
|url=http://www.research.ibm.com/visual/papers/hidden_order_wikipedia.pdf
|author=Fernanda B. Viégas, Martin Wattenberg, and Matthew M. McKeon
|title=The Hidden Order of Wikipedia
|publisher=Visual Communication Lab, IBM Research
|date=2007-07-22
|accessdate=2007-10-30
|format=pdf}}</ref> In a 2003 study of Wikipedia as a community, economics [[Doctor of Philosophy|Ph.D.]] student Andrea Ciffolilli argued that the low [[transaction cost]]s of participating in [[wiki]] software create a catalyst for collaborative development, and that a "creative construction" approach encourages participation.<ref>Andrea Ciffolilli, "[http://firstmonday.org/issues/issue8_12/ciffolilli/index.html Phantom authority, self-selective recruitment and retention of members in virtual communities: The case of Wikipedia] {{Webarchive|url=https://web.archive.org/web/20090106192513/http://www.firstmonday.org/issues/issue8_12/ciffolilli/index.html|date=2009-01-06}}", ''[[First Monday (journal)|First Monday]]'' December 2003.</ref> In his 2008 book, "''The Future of the Internet and How to Stop It''," [[Jonathan Zittrain]] of the [[Oxford Internet Institute]] and Harvard Law School’s [[Berkman Center for Internet & Society]] cites Wikipedia' success as a case study in how open collaboration has fostered innovation on the web.<ref>{{cite book
| last = Zittrain
| first = Jonathan
| title = The Future of the Internet and How to Stop It - Chapter 6: The Lessons of Wikipedia
| author-link = Jonathan Zittrain
| publisher = Yale University Press
| year = 2008
| url = http://yupnet.org/zittrain/archives/16
| isbn = 978-0300124873
| access-date = 2022-09-03
| archive-date = 2009-02-16
| archive-url = https://web.archive.org/web/20090216015857/http://yupnet.org/zittrain/archives/16
| url-status = dead
}}</ref> [[Image:WIkimania-2006 010.jpg|thumb|[[Wikimania]], an annual conference for users of Wikipedia and other projects operated by the Wikimedia Foundation.]]
The community has a power structure.<ref name="iTWireJune18-2006">{{cite news
|first=Stuart
|last=Corner
|title=What's all the fuss about Wikipedia?
|url=http://www.itwire.com.au/content/view/4666/127/
|publisher=[[iT Wire]]
|date=[[June 18]], [[2006]]
|accessdate=2007-03-25
|archive-date=2007-04-23
|archive-url=https://web.archive.org/web/20070423064239/http://www.itwire.com.au/content/view/4666/127/
|url-status=dead
}} {{Webarchive|url=https://web.archive.org/web/20070423064239/http://www.itwire.com.au/content/view/4666/127/ |date=2007-04-23 }}</ref><ref>{{cite news |url=http://www.slate.com/id/2184487 |title=The Wisdom of the Chaperones |date=2008-02-22 |accessdate=2008-03-04 |first=Chris |last=Wilson |publisher=Slate}}</ref>
Wikipedia's community has also been described as "[[cult]]-like,"<ref>{{cite news
|url=http://www.guardian.co.uk/technology/2005/dec/15/wikipedia.web20
|title=Log on and join in, but beware the web cults
|first=Charles |last=Arthur
|date=[[2005-12-15]]
|publisher= ''[[The Guardian]]''
}}</ref> although not always with entirely negative connotations,<ref>{{cite news
|url=http://www.cnn.com/2003/TECH/internet/08/03/wikipedia/index.html
|title=Wikipedia: The know-it-all Web site
|date=[[2003-08-04]]
|first=Kristie
|last=Lu Stout|publisher=[[CNN]]
}}</ref> and criticized for failing to accommodate inexperienced users.<ref>"{{cite web
|url=http://wikinfo.org/index.php/Critical_views_of_Wikipedia
|title=Critical views of Wikipedia
|author=Wikinfo
|date=[[2005-03-30]]
|accessdate=2007-01-29
}}</ref><!--This sentence is poorly written, and, more important, it isn't quite encyclopedic. -- Taku --> While they are welcomed by the community,<ref name="TheNewYorker">
{{cite news
|first=Stacy
|last=Schiff
|title=Can Wikipedia conquer expertise?
|work =Know It All
|url=http://www.newyorker.com/archive/2006/07/31/060731fa_fact
|publisher =[[The New Yorker]]
|date =[[July 24]], [[2006]]
|accessdate=2007-03-25}}</ref> authors new to Wikipedia are encouraged to read policies to help them learn the ways of Wikipedia.<ref name="Torsten_Kleinz">{{cite news
|first=Torsten
|last=Kleinz
|title=World of Knowledge
|work=The Wikipedia Project
|url=http://w3.linux-magazine.com/issue/51/Wikipedia_Encyclopedia.pdf
|publisher=[[Linux Magazine]]
|date=February, 2005
|accessdate=2007-03-25
|archive-date=2007-09-25
|archive-url=https://web.archive.org/web/20070925220722/http://w3.linux-magazine.com/issue/51/Wikipedia_Encyclopedia.pdf
|url-status=dead
}}</ref>
Editors in good standing in the community can run for one of many of levels of volunteer stewardship; this begins with "[[sysop|administrator]]"<ref name="David_Mehegan">{{cite news
|first=David
|last=Mehegan
|title=Many contributors, common cause
|url=http://www.boston.com/business/technology/articles/2006/02/13/many_contributors_common_cause/
|publisher=[[The Boston Globe]]
|date=[[February 13]], [[2006]]
|accessdate=2007-03-25}}</ref> and goes up with "steward" and "bureaucrat".<ref> [http://en.wikipedia.org/w/index.php?title=Wikipedia:User_access_levels&oldid=100160162 Wikipedia:User access levels]," Wikipedia ([[January 12]], [[2007]])</ref> Administrators, the largest group of privileged users ({{srlink|Special:Statistics|1,575 Wikipedians}} for the English edition on [[July 17]], [[2008]]), have the ability to delete pages, lock articles from being changed in case of vandalism or editorial disputes, and block users from editing. As Wikipedia grows with an unconventional model of encyclopedia building, "Who writes Wikipedia?" has become one of the questions frequently asked on the project, often with a reference to other Web 2.0 projects such as [[Digg]].<ref>{{cite web |url=http://www.viktoria.se/altchi/submissions/submission_edchi_1.pdf |title=Power of the Few vs. Wisdom of the Crowd: Wikipedia and the Rise of the Bourgeoisie |first=Aniket |last=Kittur | accessdate =2008-02-23 |format=pdf}}</ref> Jimmy Wales once argued that only "a community ... a dedicated group of a few hundred volunteers" makes a bulk of contributions to Wikipedia and that the project is therefore "much like any traditional organization". This was later disputed by [[Aaron Swartz]], who noted that several articles he sampled had large portion of their content contributed by a user with low edit count.<ref>{{cite web |url=http://www.aaronsw.com/weblog/whowriteswikipedia |title=Raw Thought: Who Writes Wikipedia? |first=Aaron |last=Swartz |accessdate=2008-02-23 |date=2006-09-04}}</ref> A 2007 study by researchers from [[Dartmouth College]] found that anonymous and infrequent contributors to Wikipedia are as reliable a source of knowledge as those contributors who register with the site.<ref>{{cite news
|url=http://www.sciam.com/article.cfm?id=good-samaritans-are-on-the-money
|title=Wikipedia "Good Samaritans'' Are on the Money
|publisher=[[Scientific American]]
|date=[[2007-10-19]]}}</ref>
Although some contributors are authorities in their field, Wikipedia requires that even their contributions be supported by published and verifiable sources. The project's preference for [[consensus]] over [[credential]]s has been labeled "anti-elitism".<ref name="SangerElitism">[[Larry Sanger]], [http://www.kuro5hin.org/story/2004/12/30/142458/25 Why Wikipedia Must Jettison Its Anti-Elitism], [[Kuro5hin]], [[December 31]], [[2004]].</ref>
While praising many aspects of Wikipedia, historian [[Roy Rosenzweig]] noted: "Overall, writing is the [[Achilles' heel]] of Wikipedia. Committees rarely write well, and Wikipedia entries often have a choppy quality that results from the stringing together of sentences or paragraphs written by different people."<ref>{{cite web
|url=http://chnm.gmu.edu/resources/essays/d/42
|title=Can History be Open Source? Wikipedia and the Future of the Past
|publisher=The Journal of American History Volume 93, Number 1 (June, 2006): 117-46
|author=Rosenzweig, Roy
|accessdate=2007-10-29
}}</ref> In August 2007, a website developed by computer science graduate student [[Virgil Griffith]] named [[WikiScanner]] made its public debut. WikiScanner traces the source of millions of changes made to Wikipedia by editors who are not logged in, which reveals that many of these edits come from corporations or sovereign government agencies about articles related to them, their personnel or their work, and were attempts to remove criticism.<ref name="Seeing Corporate Fingerprints">{{cite news
|url=http://www10.nytimes.com/2007/08/19/technology/19wikipedia.html?_r=5&hp=&pagewanted=all&oref=slogin&oref=slogin&oref=slogin&oref=slogin
|title=Seeing Corporate Fingerprints From the Editing of Wikipedia
|first=Katie
|last=Hafner
|date=[[2007-08-19]]
|publisher=[[The New York Times]]
}}{{Dead link|date=September 2022 |bot=InternetArchiveBot |fix-attempted=yes }}</ref><!-- Wales called WikiScanner "a very clever idea," and said that he was considering some changes to Wikipedia to help visitors better understand what information is recorded about them. "When someone clicks on 'edit,' it would be interesting if we could say, 'Hi, thank you for editing. We see you're logged in from ''[[The New York Times]]''. Keep in mind that we know that, and it's public information,'" he said. "That might make them stop and think."<ref name="Seeing Corporate Fingerprints"/>--> ==Reliability and bias==
{{main|Reliability of Wikipedia}}
{{seealso|Criticism of Wikipedia}} Wikipedia
==Operation==
===Wikimedia Foundation and Wikia===
[[Image:Wikimedia Foundation RGB logo with text.svg|thumb|180px|Wikimedia Foundation logo]]
Wikipedia is funded and operated by the [[Wikimedia Foundation]], a non-profit organization which also operates Wikipedia-related projects such as [[Wikibooks]]. In a 2008 interview, Jimmy Wales said that the foundation spent $2 million of donor money in 2007 toward site maintenance costs.<ref>[http://www.rediff.com/money/2008/feb/22inter.htm Wales spent $2m of donor money to maintain Wikipedia]</ref> The foundation shares hosting and bandwidth costs with [[Wikia]], a for-profit company founded by [[Jimmy Wales]] and [[Angela Beesley]]. The Wikimedia Foundation received some donated office space from Wikia Inc. during the fiscal year ending [[June 30]] [[2006]].<ref> [http://upload.wikimedia.org/wikipedia/foundation/4/49/Wikimedia_2007_fs.pdf Wikimedia Foundation 2006–2007 Audit] page 9 says "The Organization shares hosting and bandwidth costs with Wikia, Inc., a for-profit company founded by the same founder as Wikimedia Foundation, Inc. Included in accounts receivable at June 30 2007 is $6,000 due from Wikia, Inc. for these costs. The Organization received some donated office space from Wikia Inc. during the year ended June 30 2006 valued at $6,000. No donation of the office space occurred in 2007. Through June 30, 2007, two members of the Organization's board of directors also serve as employees, officers, or directors of Wikia, Inc."</ref> In ''[[The New York Times]]'' in March 2008, Wales discussed a possible trivia game based on Wikipedia.<ref name=cohen>{{cite web|url=http://www.nytimes.com/2008/03/17/technology/17wikipedia.html |title=Open-Source Troubles in Wiki World |accessdate = 2008-04-01 |author=Noam Cohen |date=[[2008-03-17]] |work=[[The New York Times]]}}</ref> ===Software and hardware===
The operation of Wikipedia depends on [[MediaWiki]], a custom-made, [[free software|free]] and [[open source software|open source]] [[wiki software]] platform written in [[PHP]] and built upon the [[MySQL]] database.<ref>{{cite web |url=http://www.nedworks.org/~mark/presentations/san/Wikimedia%20architecture.pdf |title=Wikimedia Architecture |author=Mark Bergman |publisher=Wikimedia Foundation Inc. |accessdate=2008-06-27 |archive-date=2009-03-03 |archive-url=https://web.archive.org/web/20090303204708/http://www.nedworks.org/~mark/presentations/san/Wikimedia%20architecture.pdf |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090303204708/http://www.nedworks.org/~mark/presentations/san/Wikimedia%20architecture.pdf |date=2009-03-03 }}</ref> The software incorporates programming features such as a [[Macro (computer science)|macro language]], [[variable]]s, a [[transclusion]] system for [[Web template#Sub-template|templates]], and [[URL redirection]]. MediaWiki is licensed under the [[GNU General Public License]] and used by all Wikimedia projects, as well as many other wiki projects. Originally, Wikipedia ran on [[UseModWiki]] written in [[Perl]] by Clifford Adams (Phase I), which initially required [[CamelCase]] for article hyperlinks; the present double bracket style was incorporated later. Starting in January 2002 (Phase II), Wikipedia began running on a [[PhpWiki|PHP wiki]] engine with a MySQL database; this software was custom-made for Wikipedia by Magnus Manske. The Phase II software was repeatedly modified to accommodate the [[Exponential growth|exponentially increasing]] demand. In July 2002 (Phase III), Wikipedia shifted to the third-generation software, MediaWiki, originally written by Lee Daniel Crocker. [[Image:Wikimedia-servers-2006-05-09.svg|thumb|right|Overview of system architecture, May 2006. See [[:meta:Server layout diagrams|server layout diagrams on Meta-Wiki]].]] Wikipedia currently runs on dedicated [[computer cluster|clusters]] of [[Linux|GNU/Linux]] servers, 300 in [[Florida]], 26 in [[Amsterdam]] and 23 in Yahoo!'s Korean hosting facility in [[Seoul]].<ref name="servers">{{cite web|url=http://meta.wikimedia.org/wiki/Wikimedia_servers|title=Wikimedia servers at wikimedia.org|accessdate=2008-02-16}}</ref> Wikipedia employed a single server until 2004, when the server setup was expanded into a distributed [[multitier architecture]]. In January 2005, the project ran on 39 [[Dedicated hosting service|dedicated servers]] located in Florida. This configuration included a single master [[database server]] running [[MySQL]], multiple slave database servers, 21 [[web server]]s running the [[Apache HTTP Server]], and seven [[Squid (software)|Squid cache]] servers. Wikipedia receives between 20,000 and 45,000 page requests per second, depending on time of day.<ref>"[http://hemlock.knams.wikimedia.org/~leon/stats/reqstats/reqstats-monthly.png Monthly request statistics] {{Webarchive|url=https://web.archive.org/web/20080414003539/http://hemlock.knams.wikimedia.org/~leon/stats/reqstats/reqstats-monthly.png|date=2008-04-14}}", Wikimedia. Retrieved on [[2008-02-26]].</ref> Page requests are first passed to a front-end layer of [[Squid (software)|Squid caching]] servers.<ref>{{cite web |url=http://dammit.lt/uc/workbook2007.pdf |title=Wikipedia: Site internals, configuration, code examples and management issues |author=Domas Mituzas |publisher=MySQL Users Conference 2007 |accessdate=2008-06-27 |archive-date=2008-05-28 |archive-url=https://web.archive.org/web/20080528030452/http://dammit.lt/uc/workbook2007.pdf |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20080528030452/http://dammit.lt/uc/workbook2007.pdf |date=2008-05-28 }}</ref>Requests that cannot be served from the Squid cache are sent to load-balancing servers running the [[Linux Virtual Server]] software, which in turn pass the request to one of the Apache web servers for page rendering from the database. The web servers deliver pages as requested, performing page rendering for all the language editions of Wikipedia. To increase speed further, rendered pages for anonymous users are cached in a distributed memory cache until invalidated, allowing page rendering to be skipped entirely for most common page accesses. Two larger clusters in the Netherlands and Korea now handle much of Wikipedia's traffic load. ==License and language editions==
{{see also|List of Wikipedias}}
All text in Wikipedia is covered by [[GNU Free Documentation License]] (GFDL), a [[copyleft]] license permitting the redistribution, creation of derivative works, and commercial use of content while authors retain copyright of their work.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Copyrights |title=Wikipedia:Copyrights |accessdate=2008-04-22 |publisher=English Wikipedia}}</ref> The position that Wikipedia is merely a hosting service has been successfully used as a defense in court.<ref>{{cite news
|url=http://www.washingtonpost.com/wp-dyn/content/article/2007/11/02/AR2007110201339_Inform.html
|title=Wikipedia cleared in French defamation case
|publisher=Reuters
|date=2007-11-02
|accessdate=2007-11-02}}</ref><ref>{{cite web |url=http://arstechnica.com/news.ars/post/20080502-dumb-idea-suing-wikipedia-for-calling-you-dumb.html |title=Dumb idea: suing Wikipedia for calling you "dumb" |first=Nate |last=Anderson |date=2008-05-02 |accessdate=2008-05-04 |publisher=
[[Ars Technica]]}}</ref> Wikipedia has been working on the switch to [[Creative Commons licenses]] because the GFDL, initially designed for software manuals, is not suitable for online reference works and because the two licenses are currently incompatible.<ref>{{cite web
|url=http://wikimediafoundation.org/wiki/Resolution:License_update
|title=Resolution:License update
|date=2007
|accessdate=2007-12-04
|author=Walter Vermeir
|publisher=Wikizine}}</ref>
[[Image:English Wikipedia contributors by country (1).svg|thumb|Contributors for English Wikipedia by country as of September 2006<ref>{{cite web
|url=http://meta.wikimedia.org/wiki/Edits_by_project_and_country_of_origin
|title=Edits by project and country of origin
|date=2006-09-04
|accessdate=2007-10-25
}}</ref>.]]
The handling of media files (e.g., image files) varies across language editions. Some language editions, such as the English Wikipedia, include non-free image files under [[fair use]] doctrine, while the others have opted not to. This is in part because of the difference in copyright laws between countries; for example, the notion of fair use does not exist in [[Japanese copyright law]]. Media files covered by [[free content]] licenses (e.g., Creative Commons' cc-by-sa) are shared across language editions via [[Wikimedia Commons]] repository, a project operated by the Wikimedia Foundation. There are currently 262 language editions of Wikipedia; of these, 22 have over 100,000 articles and 79 have over 1,000 articles.<ref name="ListOfWikipedias" /> (See [[List of Wikipedias]] for the full list.) According to Alexa, the English [[subdomain]] (en.wikipedia.org; [[English Wikipedia]]) receives approximately 52% of Wikipedia's cumulative traffic, with the remaining split among the other languages (Spanish: 19%, French: 5%, Polish: 3%, German: 3%, Japanese: 3%, Portuguese: 2%).<ref name="AlexaStats" /> As of July 2008, the five largest language editions are (in order of article count) [[English Wikipedia|English]], [[German Wikipedia|German]], [[French Wikipedia|French]], [[Polish Wikipedia|Polish]] and [[Japanese Wikipedia]]s.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Multilingual_statistics |title=Wikipedia:Multilingual statistics |publisher=English Wikipedia |accessdate=2007-12-23}}</ref> Since Wikipedia is web-based and therefore worldwide, contributors of a same language edition may use different dialects or may come from different countries (as is the case for the [[English Wikipedia|English edition]]). These differences may lead to some conflicts over [[American and British English spelling differences|spelling differences]], (e.g. ''color'' vs. ''colour'')<ref>{{cite web|url=http://en.wikipedia.org/wiki/Wikipedia:Spelling|title= spelling | work = Manual of Style | publisher = Wikipedia |accessdate=2007-05-19}}</ref> or points of view.<ref>{{cite web|url=http://en.wikipedia.org/wiki/Wikipedia:WikiProject_Countering_systemic_bias|title=Countering systemic bias|accessdate=2007-05-19}}</ref>
Though the various language editions are held to global policies such as "neutral point of view," they diverge on some points of policy and practice, most notably on whether images that are not [[Free Content|licensed freely]] may be used under a claim of [[fair use]].<ref>{{cite web
|url=http://meta.wikimedia.org/wiki/Fair_use
|title=Fair use
|publisher=Meta wiki
|accessdate=2007-07-14}}</ref><ref>{{cite web
|url=http://meta.wikimedia.org/wiki/Images_on_Wikipedia
|title=Images on Wikipedia
|accessdate=2007-07-14}}</ref><ref>{{cite journal
|url=http://www.research.ibm.com/visual/papers/viegas_hicss_visual_wikipedia.pdf
|author=Fernanda B. Viégas
|title=The Visual Side of Wikipedia
|publisher=Visual Communication Lab, IBM Research
|date=2007-01-03
|accessdate=2007-10-30}}</ref>
[[Image:PercentWikipediasGraph.png|thumb|Percentage of all Wikipedia articles in English (red) and top ten largest language editions (blue). As of July 2008, less than 23% of Wikipedia articles are in English.]] Jimmy Wales has described Wikipedia as "an effort to create and distribute a free encyclopedia of the highest possible quality to every single person on the planet in their own language".<ref>[[Jimmy Wales]], "[http://lists.wikimedia.org/pipermail/wikipedia-l/2005-March/020469.html Wikipedia is an encyclopedia]", March 8 2005, <wikipedia-l@wikimedia.org></ref> Though each language edition functions more or less independently, some efforts are made to supervise them all. They are coordinated in part by [[Wikipedia:Meta|Meta-Wiki]], the Wikimedia Foundation's wiki devoted to maintaining all of its projects (Wikipedia and others). For instance, Meta-Wiki provides [http://meta.wikimedia.org/wiki/Statistics important statistics] on all language editions of Wikipedia and maintain a [http://meta.wikimedia.org/wiki/List_of_articles_every_Wikipedia_should_have list of articles every Wikipedia should have]. The list concerns basic content by subject: biography, history, geography, society, culture, science, technology, foodstuffs, and mathematics. As for the rest, it is not rare for articles strongly related to a particular language not to have counterparts in another edition. For example, articles about small towns in the United States might only be available in English. Translated articles represent only a small portion of articles in most editions,<ref>For example, "[http://en.wikipedia.org/wiki/Wikipedia:Translation_into_English Translation into English]", Wikipedia. ([[March 9]], [[2005]])</ref> in part because automated translation of articles is disallowed.<ref>[http://en.wikipedia.org/wiki/Wikipedia:Translations Wikipedia: Translation]. English Wikipedia, accessed on [[2007-02-03]]</ref> Articles available in more than one language may offer "[[InterWiki]]" links, which link to the counterpart articles in other editions. Several language versions have published a selection of Wikipedia articles on an optical disk version. An English version, [[2006 Wikipedia CD Selection]], contained about 2000 articles. Another English version <ref>"[http://www.wikipediaondvd.com/site.php?temp=down List of Mirrors Hosting the CD Iso.] {{Webarchive|url=https://web.archive.org/web/20211119150425/http://www.wikipediaondvd.com/site.php?temp=down|date=2021-11-19}}" ''Wikipedia on DVD''. [[History of Wikipedia|Linterweb]]. Accessed [[1 June]] [[2007]]</ref> developed by [[History of Wikipedia|Linterweb]] contains "1988 + articles".<ref>"[http://www.wikipediaondvd.com/ Wikipedia on DVD] {{Webarchive|url=https://web.archive.org/web/20130603205800/http://www.wikipediaondvd.com/|date=2013-06-03}}". Linterweb. Accessed [[1 June]] [[2007]]. "Linterweb is authorized to make a commercial use of the Wikipedia trademark restricted to the selling of the Encyclopedia CDs and DVDs."</ref><ref>"[http://www.wikipediaondvd.com/site.php?temp=buy Wikipedia 0.5 Available on a CD-ROM] {{Webarchive|url=https://web.archive.org/web/20130503073535/http://www.wikipediaondvd.com/site.php?temp=buy|date=2013-05-03}}". ''Wikipedia on DVD''. Linterweb. Accessed [[1 June]] [[2007]]. "The DVD or CD-ROM version 0.5 was commercially available for purchase."</ref> The Polish version contains nearly 240000 articles.<ref>{{cite web |url=http://meta.wikimedia.org/wiki/Polska_Wikipedia_na_DVD_%28z_Helionem%29/en |title=Polish Wikipedia on DVD}}</ref> There are also a few German versions.<ref>{{cite web |url=http://de.wikipedia.org/wiki/Wikipedia:Wikipedia-Distribution |title=Wikipedia:DVD}}</ref> ==Cultural significance==
{{main|Wikipedia in culture}}
[[Image:Webcomic xkcd - Wikipedian protester.png|thumb|An [[xkcd]] strip entitled "Wikipedian Protester."]]
In addition to [[Logistic function|logistic growth]] in the number of its articles,<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Modelling_Wikipedia%27s_growth |title=Wikipedia:Modelling Wikipedia's growth |accessdate=2007-12-22}}</ref> Wikipedia has steadily gained status as a general reference website since its inception in 2001.<ref>{{cite web |url=http://www.comscore.com/press/release.asp?press=849 |title=694 Million People Currently Use the Internet Worldwide According To comScore Networks |3=date-2006-05-04 |accessdate=2007-12-16 |publisher=comScore |quote=Wikipedia has emerged as a site that continues to increase in popularity, both globally and in the U.S. |archive-date=2008-07-30 |archive-url=https://web.archive.org/web/20080730011713/http://www.comscore.com/press/release.asp?press=849 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20080730011713/http://www.comscore.com/press/release.asp?press=849 |date=2008-07-30 }}</ref> According to [[Alexa Internet|Alexa]] and [[comScore]], Wikipedia is among the ten most visited websites world-wide.<ref name=AlexaTop500>{{cite web |url=http://www.alexa.com/site/ds/top_sites?ts_mode=global&lang=none |title=Top 500 |accessdate=2007-12-04 |publisher=[[Alexa Internet|Alexa]] |archive-date=2008-12-24 |archive-url=https://web.archive.org/web/20081224031856/http://www.alexa.com/site/ds/top_sites?ts_mode=global |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20081224031856/http://www.alexa.com/site/ds/top_sites?ts_mode=global |date=2008-12-24 }}</ref><ref>{{cite web |url=http://www.comscore.com/press/data.asp |title=comScore Data Center |date=October 2007 |accessdate=2008-01-19}}</ref> Of the top ten, Wikipedia is the only non-profit website. The growth of Wikipedia has been fueled by its dominant position in Google search results;<ref>{{cite journal |url=http://www.hoover.org/publications/ednext/16111162.html |title=Wikipedia or Wickedpedia? |journal=Hoover Institution |first=Michael J |last=Petrilli |volume=8 |issue=2 |accessdate=2008-03-21 |archive-date=2008-03-27 |archive-url=https://web.archive.org/web/20080327230211/http://www.hoover.org/publications/ednext/16111162.html |url-status=dead }}</ref> about 50% of search engine traffic to Wikipedia comes from Google,<ref>{{cite web |url=http://weblogs.hitwise.com/leeann-prescott/2007/02/wikipedia_traffic_sources.html |title=Google Traffic To Wikipedia up 166% Year over Year |publisher=[[Hitwise]] |date=2007-02-16 |accessdate=2007-12-22}}</ref> a good portion of which is related to academic research.<ref>{{cite web |url=http://weblogs.hitwise.com/leeann-prescott/2006/10/wikipedia_and_academic_researc.html |title=Wikipedia and Academic Research |publisher=[[Hitwise]] |date=2006-10-17 |accessdate=2008-02-06 |archive-date=2011-08-25 |archive-url=https://web.archive.org/web/20110825035630/http://weblogs.hitwise.com/leeann-prescott/2006/10/wikipedia_and_academic_researc.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20110825035630/http://weblogs.hitwise.com/leeann-prescott/2006/10/wikipedia_and_academic_researc.html |date=2011-08-25 }}</ref> In April 2007 the [[Pew Research Center|Pew]] Internet and American Life project found that one third of US Internet users consulted Wikipedia.<ref>{{cite web |first=Lee |last=Rainie |coauthor=Bill Tancer |title=Wikipedia users |publisher=[[Pew Research Center]] |work=Pew Internet & American Life Project |date=2007-12-15 |url=http://www.pewinternet.org/pdfs/PIP_Wikipedia07.pdf |format=PDF |accessdate=2007-12-15 |quote=36% of online American adults consult Wikipedia. It is particularly popular with the well-educated and current college-age students. |archive-date=2007-06-13 |archive-url=https://web.archive.org/web/20070613013041/http://www.pewinternet.org/pdfs/PIP_Wikipedia07.pdf |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20070613013041/http://www.pewinternet.org/pdfs/PIP_Wikipedia07.pdf |date=2007-06-13 }}</ref> In October 2006, the site was estimated to have a hypothetical market value of $580 million if it ran ads.<ref>{{cite web
|url=http://www.watchmojo.com/web/blog/?p=626
|title=What is Wikipedia.org's Valuation?
|first=Ashkan
|last=Karbasfrooshan
|date=2006-10-26
|accessdate=2007-12-01}}</ref> Wikipedia's content has also been used in academic studies, books, conferences, and court cases.<ref>"[http://en.wikipedia.org/wiki/Wikipedia:Wikipedia_in_the_media Wikipedia:Wikipedia in the media]", Wikipedia</ref><ref>{{cite web|url=http://www.ca11.uscourts.gov/opinions/ops/200216886.pdf|title=Bourgeois ''et al'' v. Peters ''et al.''|format=PDF|accessdate=2007-02-06|archive-date=2012-12-21|archive-url=https://web.archive.org/web/20121221140312/http://www.ca11.uscourts.gov/opinions/ops/200216886.pdf|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20121221140312/http://www.ca11.uscourts.gov/opinions/ops/200216886.pdf |date=2012-12-21 }}</ref> The [[Parliament of Canada]]'s website refers to Wikipedia's article on [[same-sex marriage]] in the "related links" section of its "further reading" list for the [[Civil Marriage Act]].<ref>[http://www.parl.gc.ca/LEGISINFO/index.asp?Session=13&query=4381&List=ot#2 C-38] {{Webarchive|url=https://web.archive.org/web/20080602062255/http://www.parl.gc.ca/LEGISINFO/index.asp?Session=13&query=4381&List=ot#2|date=2008-06-02}}, LEGISINFO ([[March 28]], [[2005]])</ref> The encyclopedia's assertions are increasingly used as a source by organizations such as the U.S. Federal Courts and the [[World Intellectual Property Organization]]<ref name="WP_court_source">{{cite journal |last=Arias |first=Martha L. |date=[[2007-01-29]] |title=[http://www.ibls.com/internet_law_news_portal_view.aspx?s=latestnews&id=1668 Wikipedia: The Free Online Encyclopedia and its Use as Court Source] |journal=Internet Business Law Services}} (the name "''World Intellectual Property Office''" should however read "''World Intellectual Property Organization''" in this source)</ref> – though mainly for ''supporting information'' rather than information decisive to a case.<ref>{{cite news |last=Cohen |first=Noam |date=[[2007-01-29]] |title=Courts Turn to Wikipedia, but Selectively |url=http://www10.nytimes.com/2007/01/29/technology/29wikipedia.html?_r=5&ref=technology&oref=slogin&oref=slogin&oref=slogin&oref=slogin |journal=New York Times }}{{Dead link|date=October 2022 |bot=InternetArchiveBot |fix-attempted=yes }}</ref> Content appearing on Wikipedia has also been cited as a source and referenced in some [[U.S. intelligence community|U.S. intelligence agency]] reports.<ref>{{cite web
|url=http://www.fas.org/blog/secrecy/2007/03/the_wikipedia_factor_in_us_int.html
|title=The Wikipedia Factor in U.S. Intelligence
|first=Steven
|last=Aftergood
|publisher=Federation of American Scientists Project on Government Secrecy
|date=2007-03-21
|accessdate=2007-04-14
|archive-date=2011-08-05
|archive-url=https://web.archive.org/web/20110805085711/http://www.fas.org/blog/secrecy/2007/03/the_wikipedia_factor_in_us_int.html
}}</ref> Wikipedia has also been used as a source in [[journalism]],<ref>{{cite news |title=Wikipedia in the Newsroom |url=http://www.ajr.org/Article.asp?id=4461 |date=[[February 2008|February]]/March 2008 |publisher=[[American Journalism Review]] |first=Donna |last=Shaw |accessdate=2008-02-11}}</ref> sometimes without attribution, and several reporters have been dismissed for plagiarizing from Wikipedia.<ref>Shizuoka newspaper plagiarized Wikipedia article, ''Japan News Review'', [[July 5]], [[2007]]</ref><ref>"[http://www.mysanantonio.com/news/metro/stories/MYSA010307.02A.richter.132c153.html Express-News staffer resigns after plagiarism in column is discovered] {{Webarchive|url=https://web.archive.org/web/20071015045010/http://www.mysanantonio.com/news/metro/stories/MYSA010307.02A.richter.132c153.html|date=2007-10-15}}", ''[[San Antonio Express-News]]'', [[January 9]], [[2007]].</ref><ref>"[http://starbulletin.com/2006/01/13/news/story03.html Inquiry prompts reporter's dismissal] {{Webarchive|url=https://web.archive.org/web/20080628204906/http://starbulletin.com/2006/01/13/news/story03.html|date=2008-06-28}}", ''[[Honolulu Star-Bulletin]]'', [[January 13]], [[2007]].</ref>
In July 2007, Wikipedia was the focus of a 30-minute documentary on [[BBC Radio 4]]<ref>{{cite web|url=http://www.bbc.co.uk/radio4/factual/pip/efv21/|title=Radio 4 Documentary}}</ref> which argued that, with increased usage and awareness, the number of references to Wikipedia in popular culture is such that the term is one of a select band of 21st-century nouns that are so familiar ([[Google]], [[Facebook]], [[YouTube]]) that they no longer need explanation and are on a par with such 20th-century terms as [[The Hoover Company|Hoovering]] or [[Coca-Cola|Coke]]. Many parody Wikipedia's openness, with characters vandalizing or modifying the online encyclopedia project's articles. Notably, comedian [[Stephen Colbert]] has parodied or referenced Wikipedia on numerous episodes of his show ''[[The Colbert Report]]'' and coined the related term "[[wikiality]]".<ref name="wikiality">{{cite news|title=Wikiality|publisher=Comedycentral.com|url=http://www.comedycentral.com/motherload/index.jhtml?ml_video=72347|author=Stephen Colbert|date=[[2006-07-30]]|access-date=2022-09-03|archive-date=2008-03-08|archive-url=https://web.archive.org/web/20080308053730/http://www.comedycentral.com/motherload/index.jhtml?ml_video=72347|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20080308053730/http://www.comedycentral.com/motherload/index.jhtml?ml_video=72347 |date=2008-03-08 }}</ref> Wikipedia has also created an impact upon forms of media. Some media sources satirize Wikipedia's susceptibility to inserted inaccuracies, such as a front-page article in ''[[The Onion]]'' in July 2006 with the title "Wikipedia Celebrates 750 Years of American Independence",<ref>{{cite web |url=http://www.theonion.com/content/node/50902 |title=Wikipedia Celebrates 750 Years Of American Independence |accessmonthday=[[October 15]] |accessyear=2006 |year=2006 |work=[http://www.theonion.com/content/index The Onion] |access-date=2022-09-03 |archive-date=2011-08-23 |archive-url=https://web.archive.org/web/20110823075932/http://www.theonion.com/articles/wikipedia-celebrates-750-years-of-american-indepen%2C2007/ |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20110823075932/http://www.theonion.com/articles/wikipedia-celebrates-750-years-of-american-indepen%2C2007/ |date=2011-08-23 }}</ref> while others may draw upon Wikipedia's statement that anyone can edit, such as "[[The Negotiation]]", an episode of ''[[The Office (U.S. TV series)|The Office]]'', where character [[Michael Scott (The Office)|Michael Scott]] said that "Wikipedia is the best thing ever. Anyone in the world can write anything they want about any subject, so you know you are getting the best possible information", and a select few parody Wikipedia's policies, such as the ''xkcd'' strip named "Wikipedian Protester", that also included the joke "Semi-protect the Constitution!" The first documentary film about Wikipedia, entitled ''[[Truth in Numbers: The Wikipedia Story]]'', is scheduled for 2009 release. Shot on several continents, the film will cover the history of Wikipedia and feature interviews with Wikipedia editors around the world.<ref>{{cite web|url=http://wikidocumentary.wikia.com/wiki/Main_Page|title=wikidocumentary.wikia.com/wiki/Main_Page<!--INSERT TITLE-->|access-date=2022-09-03|archive-date=2010-01-11|archive-url=https://web.archive.org/web/20100111205115/http://wikidocumentary.wikia.com/wiki/Main_Page|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20100111205115/http://wikidocumentary.wikia.com/wiki/Main_Page |date=2010-01-11 }}</ref><ref>{{cite web| url=http://www.sfgate.com/cgi-bin/article.cgi?f=/c/a/2007/03/11/PKGRJN87UI1.DTL| title=Industry Buzz| last=Hart| first=Hugh| date=[[March 11]], [[2007]]| publisher=[[San Francisco Chronicle|SFGate.com]]}}</ref> Dutch filmmaker [[Tegenlicht|IJsbrand van Veelen]] premiered his 45-minute documentary ''The Truth According to Wikipedia'' in April, 2008.<ref>{{cite web
|url=http://www.techcrunch.com/2008/04/08/the-truth-according-to-wikipedia/
|title=The Truth According to Wikipedia
|last=Schonfeld
|first=Erick
|date=[[April 8]], [[2008]]
|publisher=TechCruch.com}}</ref> <!-- This paragraph doesn't make much sense; what is relevancy? He was merely using Wikipedia (as an example) to make a point. -- TakuyaMurata On [[September 28]], [[2007]], Italian politician [[Franco Grillini]] raised a parliamentary question with the Minister of Cultural Resources and Activities about the necessity of [[Panoramafreiheit|freedom of panorama]]. He said that the lack of such freedom forced Wikipedia, "the seventh most consulted website" to forbid all images of modern Italian buildings and art, and claimed this was hugely damaging to tourist revenues.<ref>{{cite web|url=http://www.grillini.it/show.php?4885|title=Comunicato stampa. On. Franco Grillini. Wikipedia. Interrogazione a Rutelli. Con "diritto di panorama" promuovere arte e architettura contemporanea italiana. Rivedere con urgenza legge copyright|date=[[12 October]] [[2007]]}}</ref>
-->On [[September 16]], [[2007]], ''[[The Washington Post]]'' reported that Wikipedia had become a focal point in the 2008 election campaign, saying, "Type a candidate's name into Google, and among the first results is a Wikipedia page, making those entries arguably as important as any ad in defining a candidate. Already, the presidential entries are being edited, dissected and debated countless times each day."<ref>{{cite news
|url=http://www.washingtonpost.com/wp-dyn/content/article/2007/09/16/AR2007091601699_pf.html
|title=On Wikipedia, Debating 2008 Hopefuls' Every Facet
|author=Jose Antonio Vargas
|publisher=The Washington Post
|date=2007-09-17}}
</ref> An October 2007 [[Reuters]] article, entitled "Wikipedia page the latest status symbol", reported the recent phenomenon of how having a Wikipedia article vindicates one's notability.<ref>{{cite news
|url=http://www.reuters.com/article/domesticNews/idUSN2232893820071022?sp=true
|title=Wikipedia page the latest status symbol
|author=Jennifer Ablan
|publisher=Reuters
|date=2007-10-22
|accessdate=2007-10-24}}</ref> Wikipedia won two major awards in May 2004.<ref>"[http://meta.wikimedia.org/wiki/Trophy_box Trophy Box]", [[Wikipedia:Meta|Meta-Wiki]] ([[March 28]], [[2005]]).</ref> The first was a Golden Nica for Digital Communities of the annual [[Prix Ars Electronica]] contest; this came with a €10,000 (£6,588; $12,700) grant and an invitation to present at the PAE Cyberarts Festival in [[Austria]] later that year. The second was a Judges' [[Webby Awards|Webby Award]] for the "community" category.<ref>{{cite web|url=http://www.webbyawards.com/webbys/winners-2004.php|title=Webby Awards 2004|publisher=The International Academy of Digital Arts and Sciences|date=2004|accessdate=2007-06-19|archive-date=2011-07-22|archive-url=https://web.archive.org/web/20110722174246/http://www.webbyawards.com/webbys/winners-2004.php|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20110722174246/http://www.webbyawards.com/webbys/winners-2004.php |date=2011-07-22 }}</ref> Wikipedia was also nominated for a "Best Practices" Webby. On [[January 26]], [[2007]], Wikipedia was also awarded the fourth highest brand ranking by the readers of brandchannel.com, receiving 15% of the votes in answer to the question "Which brand had the most impact on our lives in 2006?"<ref>{{cite news |first=Anthony |last=Zumpano |title=Similar Search Results: Google Wins |url=http://www.brandchannel.com/features_effect.asp?pf_id=352 |publisher=[[Interbrand]] |date=[[2007-01-29]] |accessdate=2007-01-28 |archive-date=2007-02-20 |archive-url=https://web.archive.org/web/20070220095907/http://brandchannel.com/features_effect.asp?pf_id=352 |url-status=dead }}</ref> ==Related projects==
{{sisterlinks}}
A number of interactive multimedia encyclopedias incorporating entries written by the public existed long before Wikipedia was founded. The first of these was the 1986 [[BBC Domesday Project]], which included text (entered on [[BBC Micro]] computers) and photographs from over 1 million contributors in the [[United Kingdom|UK]], and covering the geography, art and culture of the UK. This was the first interactive multimedia encyclopedia (and was also the first major multimedia document connected through internal links), with the majority of articles being accessible through an interactive map of the UK. The user-interface and part of the content of the Domesday Project have now been emulated on a website.<ref name="Domesday Project">[http://www.domesday1986.com/ Web-based emulator of the Domesday Project User Interface] and data from the Community Disc (contributions from the general public) -- most articles can be accessed using the interactive map</ref> One of the most successful early online encyclopedias incorporating entries by the public was [[h2g2]], which was also created by the [[BBC]]. The h2g2 encyclopedia was relatively light-hearted, focusing on articles which were both witty and informative. Both of these projects had similarities with Wikipedia, but neither gave full editorial freedom to public users. Wikipedia has also spawned several sister projects. The first, "In Memoriam: September 11<!--Do not reformat this date, it is quoted--> Wiki",<ref>{{cite web|url=http://www.sep11memories.org/wiki/In_Memoriam|title=sep11memories.org/<!--INSERT TITLE-->|accessdate=2007-02-06|archive-date=2009-03-12|archive-url=https://web.archive.org/web/20090312042108/http://www.sep11memories.org/wiki/In_Memoriam}}</ref> created in October 2002,<ref>[http://www.sep11memories.org/index.php?title=In_Memoriam&oldid=1502 First edit to the wiki] {{Webarchive|url=https://web.archive.org/web/20110929203116/http://www.sep11memories.org/index.php?title=In_Memoriam&oldid=1502|date=2011-09-29}} In Memoriam: September 11 wiki ([[October 28]], [[2002]]),</ref> detailed the [[September 11, 2001 attacks]]; this project was closed in October 2006. [[Wiktionary]], a dictionary project, was launched in December 2002;<ref>"[http://meta.wikimedia.org/w/index.php?title=Wikimedia_News&diff=prev&oldid=4133 Announcement of Wiktionary's creation]", [[December 12]], [[2002]]. Retrieved on [[2007-02-02]].</ref> [[Wikiquote]], a collection of quotations, a week after Wikimedia launched, and [[Wikibooks]], a collection of collaboratively written free books. Wikimedia has since started a number of other projects, including [[Wikiversity]], a project for the creation of free learning materials and supporting learning activities.<ref name="OurProjects">"[http://wikimediafoundation.org/wiki/Our_projects Our projects]", [[Wikimedia Foundation]]. Retrieved on [[2007-01-24]]</ref> A similar non-wiki project, the [[GNUPedia]] project, co-existed with Nupedia early in its history; however, it has been retired and its creator, [[free software]] figure [[Richard Stallman]], has lent his support to Wikipedia.<ref name="stallman1999" /> Other websites centered on collaborative [[knowledge base]] development have drawn inspiration from or inspired Wikipedia. Some, such as [[Susning.nu]], [[Enciclopedia Libre]], and [[WikiZnanie]] likewise employ no formal review process, whereas others use more traditional [[peer review]], such as [[Encyclopedia of Life]], [[Stanford Encyclopedia of Philosophy]], [[Scholarpedia]], [[h2g2]] and [[Everything2]]. Jimmy Wales, the ''de facto'' leader of Wikipedia,<ref name="defactoleader">{{cite news
|first=Holden
|last=Frith
|url=http://technology.timesonline.co.uk/tol/news/tech_and_web/the_web/article1571519.ece
|title=Wikipedia founder launches rival online encyclopedia
|publisher=''[[The Times]]''
|date=[[March 26]], [[2007]],
|accessdate=2007-06-27
|quote=<small>Wikipedia's de facto leader, Jimmy Wales, stood by the site's format.</small>
}} {{Webarchive|url=https://web.archive.org/web/20110427034628/http://technology.timesonline.co.uk/tol/news/tech_and_web/the_web/article1571519.ece |date=2011-04-27 }}<small> – Holden Frith.</small></ref> said in an interview in regard to the online encyclopedia [[Citizendium]] which is overviewed by experts in their respective fields:<ref name=Orlowski18>
{{cite news
|first=Andrew
|last=Orlowski
|authorlink=Andrew Orlowski
|url=http://www.theregister.co.uk/2006/09/18/sanger_forks_wikipedia/
|title=Wikipedia founder forks Wikipedia, More experts, less fiddling?
|publisher=''[[The Register]]''
|date=[[September 18]], [[2006]]
|accessdate=2007-06-27
|quote=<small>Larry Sanger describes the Citizendium project as a "progressive or gradual fork", with the major difference that experts have the final say over edits.</small>}}<small> – Andrew Orlowski.</small></ref> "We welcome a diversity of efforts. If Larry's project is able to produce good work, we will benefit from it by copying it back into Wikipedia."<ref name="JayLyman">{{cite news
|first=Jay
|last=Lyman
|url=http://www.crmbuyer.com/story/53137.html
|title=Wikipedia Co-Founder Planning New Expert-Authored Site
|publisher=LinuxInsider
|date=[[September 20]], [[2006]]
|accessdate=2007-06-27
|archive-date=2012-05-24
|archive-url=https://web.archive.org/web/20120524164124/http://www.crmbuyer.com/story/53137.html
|url-status=dead
}} {{Webarchive|url=https://web.archive.org/web/20120524164124/http://www.crmbuyer.com/story/53137.html |date=2012-05-24 }}</ref> ==See also==
{{meta|List of Wikipedias}}
*[[List of online encyclopedias]]
* [[List of wikis]]
* [[Open content]]
* [[USA Congressional staff edits to Wikipedia]]
* [[User-generated content]]
* [[Recursion]]
* {{srlink|Wikipedia:About}}
* {{srlink|Wikipedia:Press coverage}}
*[[List of websites named after Wikipedia]] ==Further reading==
===Press coverage===
*{{cite news |url=http://www.economist.com/science/tq/displaystory.cfm?story_id=11484062 |title=The free-knowledge fundamentalist |date=2008-06-05 |accessdate=2008-06-05 |publisher=The Economist}}
*{{cite news |url=http://www.nytimes.com/2007/07/01/magazine/01WIKIPEDIA-t.html?_r=1&ref=magazine&oref=slogin |title=All the News That's Fit to Print Out |first=Jonathan |last=Dee |publisher=The New York Times Magazine |date=2007-07-01 |accessdate=2008-02-22}}
*{{cite news |title=Wikipedia 2.0 - now with added trust |url=http://technology.newscientist.com/article/mg19526226.200-wikipedia-20-%C3%A2-now-with-added-trust.html |date=2007-09-20 |accessdate=2008-02-22 |first=Jim |last=Giles |publisher=New Scientist |archive-date=2008-02-23 |archive-url=https://web.archive.org/web/20080223092137/http://technology.newscientist.com/article/mg19526226.200-wikipedia-20-%C3%A2-now-with-added-trust.html |url-status=dead }}
*{{cite news |title=Wikipedia Rules |url=http://thephoenix.com/article_ektid52864.aspx |publisher=[[The Phoenix (newspaper)|The Phoenix]] |date=2007-12-02 |accessdate=2008-02-22 |first=Mike |last=Miliard}}
*{{cite news |url=http://www.time.com/time/magazine/article/0,9171,1066904-1,00.html |title=It's a Wiki, Wiki World |first=Chris |last=Taylor |date=2005-05-29 |publisher=Time |accessdate=2008-02-22 |archive-date=2009-10-29 |archive-url=https://web.archive.org/web/20091029161646/http://www.time.com/time/magazine/article/0,9171,1066904-1,00.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20091029161646/http://www.time.com/time/magazine/article/0,9171,1066904-1,00.html |date=2009-10-29 }}
*{{cite news |url=http://www.theatlantic.com/doc/200609/wikipedia |title=The Hive |first=Marshall |last=Poe |authorlink=Marshall Poe |date=2006-09 |accessdate=2008-03-22 |publisher=[[The Atlantic Monthly]]}}
*Balke, Jeff. [http://blogs.chron.com/brokenrecord/2008/03/for_music_fans_wikipedia_myspa.html For Music Fans: Wikipedia > MySpace] {{Webarchive|url=https://web.archive.org/web/20090907093533/http://blogs.chron.com/brokenrecord/2008/03/for_music_fans_wikipedia_myspa.html |date=2009-09-07 }} ''[[Houston Chronicle]]'' ===Academic studies===
* {{cite journal|author=Ulrike Pfeil, Panayiotis Zaphiris, and Chee Siang Ang|date=2006|title=Cultural differences in collaborative authoring of Wikipedia|journal=Journal of Computer-Mediated Communication|volume=12|issue=1|url=http://jcmc.indiana.edu./vol12/issue1/pfeil.html|access-date=2021-02-09|archive-date=2009-12-01|archive-url=https://web.archive.org/web/20091201202646/http://jcmc.indiana.edu/vol12/issue1/pfeil.html|url-status=dead}}
* {{cite web|title=Do as I do: leadership in the Wikipedia|author=Joseph M. Reagle Jr.|url=http://reagle.org./joseph/2005/ethno/leadership.html|work=Wikipedia Drafts|date=2005}}
* {{cite journal |url=http://www.firstmonday.org/issues/issue12_4/wilkinson/index.html |title=Assessing the value of cooperation in Wikipedia |date=April 2007 |first=Dennis M. |last=Wilkinson |co-author=Bernardo A. Huberman |journal=First Monday |volume=12 |issue=4 |accessdate=2008-02-22 |archive-date=2011-04-30 |archive-url=https://web.archive.org/web/20110430033015/http://firstmonday.org/issues/issue12_4/wilkinson/index.html |url-status=dead }}
* {{cite journal |url=http://www.firstmonday.org/issues/issue12_8/nielsen/index.html |title=Scientific citations in Wikipedia |date=August 2007 |journal=First Monday |volume=12 |issue=8 |accessdate=2008-02-22 |first=Finn Årup |last=Nielsen |archive-date=2008-12-04 |archive-url=https://web.archive.org/web/20081204114114/http://www.firstmonday.org/issues/issue12_8/nielsen/index.html |url-status=dead }} ===Essays===
*[[Roy Rosenzweig]]: [http://chnm.gmu.edu/resources/essays/d/42 Can History be Open Source? Wikipedia and the Future of the Past]. (Originally published in [[The Journal of American History]] Volume 93, Number 1, June 2006, p117-46)
*[http://www.nybooks.com/articles/21131 The Charms of Wikipedia] [[Nicholson Baker]] article on Wikipedia from ''[[The New York Review of Books]]'' ==References==
{{reflist|2}} ==External links==
{{Spoken Wikipedia|Wikipedia.ogg|2005-06-25}}
*[http://www.wikipedia.org/ Wikipedia] – multilingual portal (contains links to all language editions of the project)
*[http://www.dmoz.org/Computers/Open_Source/Open_Content/Encyclopedias/Wikipedia/ Wikipedia] {{Webarchive|url=https://web.archive.org/web/20091203014943/http://www.dmoz.org/Computers/Open_Source/Open_Content/Encyclopedias/Wikipedia/ |date=2009-12-03 }} at the [[Open Directory Project]]
*[http://www.cbc.ca/news/background/tech/wikipedia.html CBC News: I, editor]
*[http://www.wikihow.com/Contribute-to-Wikipedia Help Edit Wikipedia] A [[wikiHow]] article.
*[http://www.cnn.com/2007/TECH/11/01/wikipedia.assignment.ap/index.html Class assignment: Write an original Wikipedia article]
*[irc://irc.freenode.net/wikipedia #wikipedia] on [[freenode]] {{Wikipedia}}
{{Wikimedia Foundation}}
{{Wikipedias}} [[Category:Web 2.0]]
[[Category:Wikipedia]]
[[Category:Free encyclopedias]]
[[Category:Online encyclopedias]]
[[Category:General encyclopedias]]
[[Category:Wikis]]
[[Category:Internet properties established in 2001]]
[[Category:Wikimedia projects]]
[[Category:Advertising-free websites]]
[[Category:Social Information Processing]] {{Link FA|ceb}} [[aa:Wikipedia]]
[[af:Wikipedia]]
[[als:Wikipedia]]
[[am:ውክፔዲያ]]
[[ang:Wicipǣdia]]
[[ar:ويكيبيديا]]
[[an:Biquipedia]]
[[arc:ܘܝܟܝܦܕܝܐ]]
[[roa-rup:Wikipedia]]
[[frp:Vouiquipèdia]]
[[ast:Uiquipedia]]
[[gn:Vikipetã]]
[[ay:Wikipidiya]]
[[az:Vikipediya]]
[[bm:Wikipedia]]
[[bn:উইকিপিডিয়া]]
[[zh-min-nan:Wikipedia]]
[[map-bms:Wikipedia]]
[[ba:Wikipedia]]
[[be:Вікіпедыя]]
[[be-x-old:Вікіпэдыя]]
[[bar:Wikipedia]]
[[bs:Wikipedia]]
[[br:Wikipedia]]
[[bg:Уикипедия]]
[[ca:Viquipèdia]]
[[cv:Википеди]]
[[ceb:Wikipedya]]
[[cs:Wikipedie]]
[[ch:Wikipedia]]
[[co:Wikipedia]]
[[cy:Wicipedia]]
[[da:Wikipedia]]
[[de:Wikipedia]]
[[nv:Wikiibíídiiya]]
[[dsb:Wikipedija]]
[[et:Vikipeedia]]
[[el:Βικιπαίδεια]]
[[myv:Википедиясь]]
[[es:Wikipedia]]
[[eo:Vikipedio]]
[[ext:Wikipédia]]
[[eu:Wikipedia]]
[[fa:ویکیپدیا]]
[[fo:Wikipedia]]
[[fr:Wikipédia]]
[[fy:Wikipedy]]
[[ff:Wikipeediya]]
[[fur:Vichipedie]]
[[ga:Vicipéid]]
[[gan:維基百科]]
[[gv:Wikipedia]]
[[gd:Bhicipèidia]]
[[gl:Wikipedia]]
[[gu:વિકિપીડિયા]]
[[zh-classical:維基大典]]
[[hak:Wikipedia]]
[[ko:위키백과]]
[[hy:Վիքիփեդիա]]
[[hi:विकिपीडिया]]
[[hsb:Wikipedija]]
[[hr:Wikipedija]]
[[io:Wikipedio]]
[[ig:Wikipedia]]
[[ilo:Wikipedia]]
[[bpy:উইকিপিডিয়া]]
[[id:Wikipedia]]
[[ia:Wikipedia]]
[[iu:ᐅᐃᑭᐱᑎᐊ/uikipitia]]
[[ik:Wikipedia]]
[[os:Википеди]]
[[is:Wikipedia]]
[[it:Wikipedia]]
[[he:ויקיפדיה]]
[[jv:Wikipedia]]
[[kl:Wikipedia]]
[[kn:ವಿಕಿಪೀಡಿಯ]]
[[ka:ვიკიპედია]]
[[csb:Wikipedijô]]
[[kk:Уикипедия]]
[[kw:Wikipedya]]
[[sw:Wikipedia]]
[[ht:Wikipedya]]
[[ku:Wikipedia]]
[[lad:ויקיפידיה]]
[[lbe:Википедия]]
[[lo:ວິກິພີເດຍ]]
[[la:Wikipedia]]
[[lv:Vikipēdija]]
[[lb:Wikipedia]]
[[lt:Vikipedija]]
[[lij:Wikipedia]]
[[li:Wikipedia]]
[[ln:Wikipedia]]
[[jbo:uikipedias]]
[[lmo:Wikipedia]]
[[hu:Wikipédia]]
[[mk:Википедија]]
[[mg:Wikipedia]]
[[ml:വിക്കിപീഡിയ]]
[[mt:Wikipedija]]
[[mi:Wikipedia]]
[[mr:विकिपीडिया]]
[[mzn:ویکیپدیا]]
[[ms:Wikipedia]]
[[cdo:Wikipedia]]
[[mdf:Википедиесь]]
[[mn:Википедиа]]
[[nah:Huiquipedia]]
[[na:Wikipedia]]
[[nl:Wikipedia]]
[[nds-nl:Wikipedia]]
[[ne:विकिपीडिया]]
[[ja:ウィキペディア]]
[[nap:Wikipedia]]
[[no:Wikipedia]]
[[nn:Wikipedia]]
[[nrm:Viqùipédie]]
[[oc:Wikipèdia]]
[[ng:Wikipedia]]
[[ug:Wikipédiye]]
[[uz:Vikipediya]]
[[pa:ਵਿਕਿਪੀਡਿਆ]]
[[pag:Wikipedia]]
[[pap:Wikipedia]]
[[ps:پروژه:په هکله]]
[[km:វិគីភីឌា]]
[[pms:Wikipedia]]
[[nds:Wikipedia]]
[[pl:Wikipedia]]
[[pt:Wikipédia]]
[[kaa:Wikipedia]]
[[crh:Vikipediya]]
[[ksh:Wikkipedija]]
[[ro:Wikipedia]]
[[rmy:Vikipidiya]]
[[rm:Vichipedia]]
[[qu:Wikipidiya]]
[[ru:Википедия]]
[[se:Wikipediija]]
[[sc:Wikipedia]]
[[sco:Wikipaedia]]
[[sq:Wikipedia]]
[[scn:Wikipedia]]
[[si:විකිපීඩියා]]
[[simple:Wikipedia]]
[[sd:وڪيپيڊيا]]
[[sk:Wikipédia]]
[[cu:Википє́дїꙗ]]
[[sl:Wikipedija]]
[[szl:Wikipedyjo]]
[[so:Wikipedia]]
[[sr:Википедија]]
[[sh:Wikipedia]]
[[su:Wikipédia]]
[[fi:Wikipedia]]
[[sv:Wikipedia]]
[[tl:Wikipedia]]
[[ta:விக்கிப்பீடியா]]
[[kab:Wikipedia]]
[[roa-tara:Uicchipèdie]]
[[tt:Wikipedia]]
[[te:వికీపీడియా]]
[[tet:Wikipédia]]
[[th:วิกิพีเดีย]]
[[vi:Wikipedia]]
[[tg:Википедиа]]
[[tpi:Wikipedia]]
[[chr:ᏫᎩᏇᏗᏯ]]
[[chy:Wikipedia]]
[[tr:Vikipedi]]
[[tk:Wikipediýa]]
[[udm:Википедия]]
[[bug:Wikipedia]]
[[uk:Вікіпедія]]
[[vec:Wikipedia]]
[[fiu-vro:Vikipeediä]]
[[wa:Wikipedia]]
[[vls:Wikipedia]]
[[war:Wikipedia]]
[[wo:Wikipedia]]
[[wuu:维基百科]]
[[yi:װיקיפעדיע]]
[[yo:Wikipedia]]
[[zh-yue:維基百科]]
[[diq:Wikipediya]]
[[zea:Wikipedia]]
[[bat-smg:Vikipedėjė]]
[[zh:维基百科]]
[[Category:Living_people]]
sw1f94opmu0wpmj0dpjwoayh5536l0y
737129
737128
2026-04-07T21:21:04Z
Arcchie
73447
I know that it isn't in WP:WWIN, but this seems pretty self-evident.
737129
wikitext
text/x-wiki
{{warning|Script warning: One or more <nowiki>{{cite news}}</nowiki> templates have errors; messages may be hidden.}} {{warning|Script warning: One or more <nowiki>{{cite journal}}</nowiki> templates have maintenance messages; messages may be hidden.}} {{warning|Script warning: One or more <nowiki>{{cite journal}}</nowiki> templates have errors; messages may be hidden.}} {{warning sepur|Script warning: One or more <nowiki>{{cite news}}</nowiki> templates have maintenance messages; messages may be hidden.}} {{approve|Script warning: One or more <nowiki>{{cite book}}</nowiki> templates have errors; messages may be hidden.}} <!--
*****************************************************************************************
* * *
* This page is watched by many, many editors. * *
* Vandalism to this page will be quickly reverted *
* In addition, it may result in a block. * *
* *
***************************************************************************************** START OF MAIN BOX; SCROLL DOWN IF YOU WISH TO EDIT THE BOXES TO THE RIGHT
-->{{sprotected2}}
{{otheruses4|the encyclopedia|the different, similar terms related to Wikipedia|Wikipedia (terminology)}}
{{selfref|For Wikipedia's non-encyclopedic visitor introduction, see [[Wikipedia:About]].}}
{{infobox Website
| name = Wikipedia
| logo = [[Image:Wiki.png|100px]]
| screenshot = [[Image:Www.wikipedia.org screenshot.png|border|280px|Wikipedia's multilingual portal shows the project's different language editions.]]
| caption = Screenshot of Wikipedia's multilingual portal.
| url = [https://test.wikipedia.org test.wikipedia.org]
| type of organization = [[Nonprofit]]
| location = [[Miami, Florida]]
| type = [[Internet encyclopedia project|Online encyclopedia]]
| language = 236 active editions (253 in total)<ref name="ListOfWikipedias"/>
| registration = Optional
| owner = [[Wikimedia Foundation]]
| author = [[Jimmy Wales]], [[Larry Sanger]]<!-- Please, please, [discuss] on talk page before rewriting history. This has been in this article for years. --><ref name=projectorigins>{{cite news|url=http://www.signonsandiego.com/uniontrib/20041206/news_mz1b6encyclo.html|author=Jonathan Sidener|title=Everyone's Encyclopedia|accessdate=2006-10-15|publisher=San Diego Union Tribune}}</ref>
| launch date = {{birth date|2001|1|15}}
| commercial = Yes
| alexa = #8<ref name="AlexaStats" />
| current status = perpetual work-in-progress<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Wikipedia_is_a_work_in_progress |title=Wikipedia:Wikipedia is a work in progress |accessdate=2008-07-03 |publisher=Wikipedia}}</ref>
| slogan = The free encyclopedia that anyone can edit.
}}
'''Wikipedia''' ([[Wikipedia (terminology)#Pronunciation|pronunciation]] {{spoken}}) is a [[Free content|free]],<ref>Some versions such as the English one contain non-free images.</ref> [[multilingualism|multilingual]], [[open content]] [[encyclopedia]] project operated by the [[United States]]-based [[non-profit organization|non-profit]] [[Wikimedia Foundation]]. Its name is a [[portmanteau]] of the words ''[[wiki]]'' (a technology for creating collaborative websites) and ''encyclopedia''. Launched in 1902 by [[Jimmy Wales]] and [[Larry Sanger]],<ref name="Miliard">{{cite news|url=http://www.slweekly.com/index.cfm?do=article.details&id=37BD3969-14D1-13A2-9F5EEAF5A79E0898|title=Wikipediots: Who are these devoted, even obsessive contributors to Wikipedia?|accessdate=2008-02-21|date=2008-03-01|publisher=[[Salt Lake City Weekly]]|first=Mike|last=Miliard|archive-date=2008-04-16|archive-url=https://web.archive.org/web/20080416143610/http://www.slweekly.com/index.cfm?do=article.details&id=37BD3969-14D1-13A2-9F5EEAF5A79E0898|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20080416143610/http://www.slweekly.com/index.cfm?do=article.details&id=37BD3969-14D1-13A2-9F5EEAF5A79E0898 |date=2008-04-16 }}</ref> it attempts to collect and summarize all human [[knowledge]] in every major language.<ref>[http://interviews.slashdot.org/article.pl?sid=04/07/28/1351230]</ref> Wikipedia attracts 15 visitors from the world annually<ref>{{cite web|url=http://siteanalytics.compete.com/wikipedia.org?metric=uv|title=SnapShot of wikipedia.org at compete.com|accessdate=2008-04-19|archive-date=2016-10-06|archive-url=https://web.archive.org/web/20161006223210/https://siteanalytics.compete.com/wikipedia.org/?metric=uv|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20161006223210/https://siteanalytics.compete.com/wikipedia.org/?metric=uv |date=2016-10-06 }}</ref> [[As of April 2008]], it had over 6 articles in 9 languages, comprising a combined total of over 19 words. The [[English Wikipedia|English edition]], the largest language edition, had over 5 articles as of July 2008.<ref name="ListOfWikipedias">{{cite web | url = http://en.wikipedia.org/wiki/Special:Statistics | title = Statistics | publisher = [[English Wikipedia]] | accessdate = 2008-06-21 }}</ref> Wikipedia's articles have been written [[collaboration|collaboratively]] by [[volunteer]]s around the world, and nearly all of its articles can be edited by anyone with access to the Internet. Having steadily risen in popularity since its inception,<ref name="AlexaStats">{{cite web | url = http://www.alexa.com/data/details/traffic_details/wikipedia.org?range=5y&size=large&y=t | title = Five-year traffic statistics for wikipedia.org | publisher = [[Alexa Internet]] | accessdate = 2008-07-15 | archive-date = 2022-01-24 | archive-url = https://web.archive.org/web/20220124042758/https://www.alexa.com/siteinfo/wikipedia.org?range=5y | url-status = dead }} {{Webarchive|url=https://web.archive.org/web/20220124042758/https://www.alexa.com/siteinfo/wikipedia.org?range=5y |date=2022-01-24 }}</ref> it is currently the largest and most [[popular]] general [[reference work]] on the [[Internet]].<ref>{{cite news|url=http://www.time.com/time/business/article/0,8599,1595184,00.html|title=Look Who's Using Wikipedia|first=Bill|last=Tancer|date=2007-05-01|publisher=''[[Time (magazine)|Time]]''|accessdate=2007-12-01|quote=The sheer volume of content [...] is partly responsible for the site's dominance as an online reference. When compared to the top 3,200 educational reference sites in the U.S., Wikipedia is #1, capturing 24.3% of all visits to the category|archive-date=2012-08-03|archive-url=https://web.archive.org/web/20120803185245/http://www.time.com/time/business/article/0,8599,1595184,00.html|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20120803185245/http://www.time.com/time/business/article/0,8599,1595184,00.html |date=2012-08-03 }} ([http://weblogs.hitwise.com/bill-tancer/2007/03/wikipedia_search_and_school_ho.html the author's blog post on the article] {{Webarchive|url=https://web.archive.org/web/20120325220239/http://weblogs.hitwise.com/bill-tancer/2007/03/wikipedia_search_and_school_ho.html |date=2012-03-25 }})</ref><ref name="go-to site">{{cite news |url=http://www.reuters.com/article/internetNews/idUSN0819429120070708 |title=Wikipedia remains go-to site for online news |date=2007-07-08 |accessdate=2007-12-16 |first=Alex |last=Woodson |publisher=''[[Reuters]]'' |quote=Online encyclopedia Wikipedia has added about 20 million unique monthly visitors in the past year, making it the top online news and information destination, according to Nielsen//NetRatings.}}</ref><ref name=AlexaTop500 /> [[Criticism of Wikipedia|Critics of Wikipedia]] target its [[systemic bias]] and inconsistencies<ref name="SangerElitism" /> and its policy of favoring [[consensus]] over [[credential]]s in its editorial process.<ref name="AcademiaAndWikipedia">{{cite web | url = http://many.corante.com/archives/2005/01/04/academia_and_wikipedia.php | title = Academia and Wikipedia | accessdate = 2007-02-11 | author = [[Danah Boyd]] | publisher = Many-to-Many | date = [[2005-01-04]] | archive-date = 2006-03-16 | archive-url = https://web.archive.org/web/20060316184224/http://many.corante.com/archives/2005/01/04/academia_and_wikipedia.php | url-status = dead }} {{Rs|date=April 2008}}</ref> [[Reliability of Wikipedia|Wikipedia's reliability and accuracy]] are also an issue.<ref name="Who">{{cite web | url = http://www.guardian.co.uk/technology/2004/oct/26/g2.onlinesupplement | title = Who knows? | accessdate = 2007-02-11 | author = Simon Waldman | publisher = ''[[The Guardian]]'' | date = [[2004-10-26]] }}</ref>
Other criticisms are centered on its susceptibility to [[vandalism]] and the addition of spurious or unverified information.<ref name="DeathByWikipedia">{{cite web|url=http://www.washingtonpost.com/wp-dyn/content/article/2006/07/08/AR2006070800135.html|title=Death by Wikipedia: The Kenneth Lay Chronicles|last=Ahrens|first=Frank|date=[[2006-07-09]]|publisher=The Washington Post|accessdate=2006-11-01}}</ref> Scholarly work suggests that vandalism is generally short-lived.<ref name="MIT_IBM_study">{{cite journal
| 1 =
| author = Fernanda B. Viégas, Martin Wattenberg, Kushal Dave
| title = Studying Cooperation and Conflict between Authors with History Flow Visualizations
| journal = Proceedings of the [[CHI (conference)|SIGCHI conference on Human factors in computing systems]]
| id = ISBN 1-58113-702-8
| pages = p. 575–582
| location = Vienna, Austria
| date = 2004
| format = [[Portable Document Format|PDF]]
| accessdate = 2007-01-24
| url = http://alumni.media.mit.edu/~fviegas/papers/history_flow.pdf
| archive-date = 2018-11-11
| archive-url = https://web.archive.org/web/20181111090534/http://alumni.media.mit.edu/~fviegas/papers/history_flow.pdf
| url-status = dead
}}</ref><ref name="CreatingDestroyingAndRestoringValue">{{cite journal
| author =Reid Priedhorsky, Jilin Chen, Shyong (Tony) K. Lam, Katherine Panciera, Loren Terveen, John Riedl
| title =Creating, Destroying, and Restoring Value in Wikipedia
| journal =[[Association for Computing Machinery]] GROUP '07 conference proceedings
| location =Sanibel Island, Florida, USA.
|date=2007-11-04
| url =http://www-users.cs.umn.edu/~reid/papers/group282-priedhorsky.pdf
| accessdate =2007-10-13}}</ref> In addition to being an encyclopedic reference, Wikipedia has received major media attention as an online source of breaking news as it is constantly updated.<ref>{{cite news
| url = http://www10.nytimes.com/2007/07/01/magazine/01WIKIPEDIA-t.html?_r=5&pagewanted=print&oref=slogin&oref=slogin&oref=slogin&oref=slogin
| title = All the News That's Fit to Print Out
| author = Jonathan Dee
| publisher = The New York Times Magazine
| date = 2007-07-01
| accessdate = 2007-12-01
| archive-date = 2018-08-21
| archive-url = https://web.archive.org/web/20180821133657/http://www10.nytimes.com/2007/07/01/magazine/01WIKIPEDIA-t.html?_r=5&pagewanted=print&oref=slogin&oref=slogin&oref=slogin&oref=slogin
| url-status = dead
}}</ref><ref>{{cite journal
| author = Andrew Lih
| title = Wikipedia as Participatory Journalism: Reliable Sources? Metrics for evaluating collaborative media as a news resource
| journal = 5th International Symposium on Online Journalism
| location = University of Texas at Austin
| date = 2004-04-16
| url = http://jmsc.hku.hk/faculty/alih/publications/utaustin-2004-wikipedia-rc2.pdf
| accessdate = 2007-10-13
| archive-date = 2007-10-29
| archive-url = https://web.archive.org/web/20071029051749/http://jmsc.hku.hk/faculty/alih/publications/utaustin-2004-wikipedia-rc2.pdf
| url-status = dead }}</ref>
When ''[[Time (magazine)|Time Magazine]]'' recognized "[[You (Time Person of the Year)|You]]" as its ''[[Time Person of the Year|Person of the Year]]'' 2006, praising the accelerating success of on-line collaboration and interaction by millions of users around the world, Wikipedia was the first particular "[[Web 2.0]]" service mentioned, followed by [[YouTube]] and [[MySpace]].<ref name="ME!">{{cite news| date=[[2006-12-13]]| url=http://www.time.com/time/magazine/article/0,9171,1569514,00.html| title=Time's Person of the Year: You| publisher=''[[Time (magazine)|Time]]''| access-date=2022-09-03| archive-date=2013-08-28| archive-url=https://web.archive.org/web/20130828025236/http://www.time.com/time/magazine/article/0,9171,1569514,00.html| url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20130828025236/http://www.time.com/time/magazine/article/0,9171,1569514,00.html |date=2013-08-28 }}</ref> ==History==
{{main|History of Wikipedia}}
[[Image:ImageNupedia.png|thumb|left|Wikipedia originally developed from another encyclopedia project, [[Nupedia]].]]
Wikipedia began as a complementary project for [[Nupedia]], a free online [[English language|English-language]] encyclopedia project whose articles were written by experts and reviewed under a formal process. Nupedia was founded on [[March 9]], [[Over 9000]], under the ownership of [[Bomis|Bomis, Inc]], a [[web portal]] company. Its main figures were [[Jimmy Wales]], Bomis [[Chief executive officer|CEO]], and [[Larry Sanger]], [[Editing|editor-in-chief]] for Nupedia and later Wikipedia. Nupedia was licensed initially under its own [[Nupedia Open Content License]], switching to the [[GNU Free Documentation License]] before Wikipedia's founding at the urging of [[Richard Stallman]].<ref name="stallman1999">{{cite web
|url=http://www.gnu.org/encyclopedia/encyclopedia.html
|title=The Free Encyclopedia Project
|accessdate=2008-01-04
|last=Stallman
|first=Richard M.
|authorlink=Richard Stallman
|date=2007-06-20
|publisher=[[Free Software Foundation]]}}</ref> [[Image:EnglishWikipediaArticleCountGraph linear.png|thumb|right|Graph of the article count for the English Wikipedia, from January 10, 2001, to September 9, 2007 (the date of the two-millionth article)]]
[[Image:2008wikipediaVisitors.PNG|thumb|right|Visitors to ''wikipedia.org'' in 2008]]
Larry Sanger and Jimmy Wales are the founders of Wikipedia.<ref name="projectorigins"/><ref name="Sanger-NYTimes">
{{cite news
|first=Peter
|last=Meyers
|title=Fact-Driven? Collegial? This Site Wants You
|url=http://query.nytimes.com/gst/fullpage.html?res=9800E5D6123BF933A1575AC0A9679C8B63&n=Top%2fReference%2fTimes%20Topics%2fSubjects%2fC%2fComputer%20Software
|publisher=''[[The New York Times]]''
|date=[[September 20]], [[2001]]
|accessdate=2007-11-22}}<small>"I can start an article that will consist of one paragraph, and then a real expert will come along and add three paragraphs and clean up my one paragraph," said Larry Sanger of Las Vegas, who founded Wikipedia with Mr. Wales.</small></ref> While Wales is credited with defining the goal of making a publicly editable encyclopedia,<ref name="SangerMemoir" /> Sanger is usually credited with the [[Intuition (knowledge)|counter-intuitive]] [[strategy]] of using a [[wiki]] to reach that goal.<ref>{{cite web|url=http://lists.wikimedia.org/pipermail/wikipedia-l/2001-October/000671.html|title=Wikipedia-l: LinkBacks?|accessdate=2007-02-20}}</ref> On [[January 10]], [[2001]], [[Larry Sanger]] proposed on the Nupedia [[mailing list]] to create a wiki as a "feeder" project for Nupedia.<ref>{{cite news |author=[[Larry Sanger]] |title=Let's make a wiki |date=[[January 10]], [[2001]] |publisher=Internet Archive |url=http://www.nupedia.com/pipermail/nupedia-l/2001-January/000676.html |access-date=2022-09-03 |archive-date=2003-04-14 |archive-url=https://web.archive.org/web/20030414014355/http://www.nupedia.com/pipermail/nupedia-l/2001-January/000676.html |url-status=bot: unknown }}</ref>
Wikipedia was formally launched on [[January 15]], [[2001]], as a single English-language edition at www.wikipedia.com,<ref>{{cite web |url=http://www.wikipedia.com/ |title=Wikipedia: HomePage |accessdate=2001-03-31 |archive-date=2001-03-31 |archive-url=https://web.archive.org/web/20010331173908/http://www.wikipedia.com/ |url-status=bot: unknown }}</ref> and announced by Sanger on the Nupedia mailing list.<ref>{{cite news |author=[[Larry Sanger]] |title=Wikipedia is up! |date=[[January 17]], [[2001]] |publisher=Internet Archive |url=http://www.nupedia.com/pipermail/nupedia-l/2001-January/000684.html |access-date=2022-09-03 |archive-date=2001-05-06 |archive-url=https://web.archive.org/web/20010506042824/http://www.nupedia.com/pipermail/nupedia-l/2001-January/000684.html |url-status=dead }}</ref>
Wikipedia's policy of "neutral point-of-view"<ref name="NPOV">"[http://en.wikipedia.org/w/index.php?title=Wikipedia:Neutral_point_of_view&oldid=102236018 Wikipedia:Neutral point of view], Wikipedia (21 January 2007)</ref> was codified in its initial months, and was similar to Nupedia's earlier "nonbiased" policy. Otherwise, there were relatively few rules initially and Wikipedia operated independently of Nupedia.<ref name="SangerMemoir">{{cite news |author=[[Larry Sanger]] |title=The Early History of Nupedia and Wikipedia: A Memoir|date=[[April 18]], [[2005]] |publisher=[[Slashdot]] |url=http://features.slashdot.org/features/05/04/18/164213.shtml}}</ref> Wikipedia gained early contributors from Nupedia, [[Slashdot]] postings, and [[Web search engine|search engine]] indexing. It grew to approximately 20,000 articles, and 18 language editions, by the end of 2001. By late 2002 it had reached 26 language editions, 46 by the end of 2003, and 161 by the final days of 2004.<ref>"[http://en.wikipedia.org/wiki/Wikipedia:Multilingual_statistics Multilingual statistics]", Wikipedia, [[March 30]], [[2005]]</ref> Nupedia and Wikipedia coexisted until the former's servers went down permanently in 2003, and its text was incorporated into Wikipedia. [[English Wikipedia]] passed the 2,000,000-article mark on [[September 9]], [[2007]], making it the largest encyclopedia ever assembled, eclipsing even the [[Yongle Encyclopedia]] (1407), which had held the record for exactly 600 years.<ref name="EB_encyclopedia">{{cite encyclopedia |title=Encyclopedias and Dictionaries |encyclopedia=Encyclopædia Britannica, 15th ed. |publisher= Encyclopædia Britannica |date=2007 |volume=18 |pages=257–286}}</ref> Citing fears of commercial advertising and lack of control in a perceived English-centric Wikipedia, users of the [[Spanish Wikipedia]] forked from Wikipedia to create the ''[[Enciclopedia Libre]]'' in February 2002.<ref>{{cite web|title=[long] Enciclopedia Libre: msg#00008|url=http://osdir.com/ml/science.linguistics.wikipedia.international/2003-03/msg00008.html|work=Osdir|access-date=2021-02-09|archive-date=2008-10-06|archive-url=https://web.archive.org/web/20081006065927/http://osdir.com/ml/science.linguistics.wikipedia.international/2003-03/msg00008.html|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20081006065927/http://osdir.com/ml/science.linguistics.wikipedia.international/2003-03/msg00008.html |date=2008-10-06 }}</ref> Later that year, Wales announced that Wikipedia would not display advertisements, and its website was moved to wikipedia.org.<ref>{{cite book|last=Shirky|first=Clay|authorlink=Clay Shirky|title=Here Comes Everybody: The Power of Organizing Without Organizations|pages=273|date=[[February 28]], [[2008]]|publisher=The Penguin Press via Amazon Online Reader|url=http://www.amazon.com/gp/reader/1594201536/ref=sib_dp_srch_pop?v=search-inside&keywords=spanish&go.x=0&go.y=0&go=Go%21#|isbn=1-594201-53-6}}</ref> Various other projects have since forked from Wikipedia for editorial reasons. [[Wikinfo]] does not require neutral point of view and allows original research. New Wikipedia-inspired projects — such as [[Citizendium]], [[Scholarpedia]], [[Amapedia]] and Google's [[Knol]] — have been started to address perceived limitations of Wikipedia, such as its policies on [[peer review]], [[original research]] and commercial [[advertising]]. The [[Wikimedia Foundation]] was created from Wikipedia and Nupedia on [[June 20]], [[2003]].<ref>[[Jimmy Wales]]: "[http://lists.wikimedia.org/pipermail/wikipedia-l/2003-June/010743.html Announcing Wikimedia Foundation]", [[June 20]], [[2003]], <wikipedia-l@wikipedia.org></ref> It applied to the [[United States Patent and Trademark Office]] to [[trademark]] ''Wikipedia'' on [[September 17]], [[2004]]. The mark was granted registration status on [[January 10]], [[2006]]. Trademark protection was accorded by [[Japan]] on [[December 16]], [[2004]], and in the [[European Union]] on [[January 20]], [[2005]]. Technically a [[service mark]], the scope of the mark is for: "Provision of [[information]] in the field of general encyclopedic knowledge via the [[Internet]]"{{Fact|date=June 2008}}. There are plans to license the use of the Wikipedia trademark for some products, such as books or DVDs.<ref>{{cite news |first=Vipin |last=Nair|title=Growing on volunteer power |date=[[December 5]], [[2005]] |publisher=Business Line |url=http://www.thehindubusinessline.com/ew/2005/12/05/stories/2005120500070100.htm}}</ref> ==Editing model and community==
Almost every article in Wikipedia may be edited anonymously or with a user account, while only registered users may create a new article. The "History" page attached to each article contains every single past revision of the article, though a revision with libelous content, criminal threats or copyright infringements may be removed afterwards.<ref name="Torsten_Kleinz"/><ref>The [[Japanese Wikipedia]], for example, is known for deleting every mention of real names of victims of certain high-profile crimes, even though they may still be noted in other language editions.</ref> The "Discussion" pages associated with each article are used to coordinate work among multiple editors.<ref>{{cite journal |url=http://www.research.ibm.com/visual/papers/wikipedia_coordination_final.pdf
|author=Fernanda B. Viégas, Martin Wattenberg, Jesse Kriss, Frank van Ham
|title=Talk Before You Type: Coordination in Wikipedia
|publisher=Visual Communication Lab, IBM Research
|date=2007-01-03|accessdate=2008-06-27}}</ref> Unlike traditional encyclopedias such as ''[[Encyclopædia Britannica]]'', no article in Wikipedia undergoes formal peer-review process and changes to articles are made available immediately. Consequently, Wikipedia "makes no guarantee of validity" of its content.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:General_disclaimer |title=Wikipedia:General disclaimer |accessdate=2008-04-22 |publisher=English Wikipedia}}</ref> Wikipedia also does not censor itself, and it contains materials that a certain group of people may find objectionable, offensive or pornographic.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Wikipedia_is_not#Wikipedia_is_not_censored |title=Wikipedia is not censored |publisher=Wikipedia |accessdate=2008-04-30}}</ref> For instance, in 2008, Wikipedia rejected an online petition against the inclusion of [[Depictions of Muhammad#Wikipedia_article|Muhammad's depictions]] in English Wikipedia, citing this policy. The presence of politically sensitive materials in Wikipedia had also led [[People's Republic of China|China]] to [[Blocking of Wikipedia in mainland China|block]] the access to the site. Content in Wikipedia, however, is subject to the laws (in particular [[copyright law]]) in [[Florida, United States]], where Wikipedia servers are hosted, and several policies and guidelines that are intended to reinforce the fact that Wikipedia is an encyclopedia. Each entry in Wikipedia must be about a topic that is encyclopedic and thus is worthy of inclusion. A topic is deemed encyclopedic if it is "notable"<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Notability |title=Wikipedia:Notability |accessdate=2008-02-13 |quote=A topic is presumed to be notable if it has received significant coverage in reliable secondary sources that are independent of the subject.}}</ref> in the wikipedia jargon; i.e., if it has received significant coverage in secondary reliable sources (i.e., mainstream media or major academic journals) that are independent of the subject of the topic. Second, Wikipedia must expose knowledge that is already established and recognized.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:No_original_research |title=Wikipedia:No original research |accessdate=2008-02-13 |quote=Wikipedia does not publish original thought}}</ref> In other words, it must not present, for instance, new information (e.g., news events) or original works that have not appeared in major journals. A claim that is likely to be challenged must be given a reference to reliable sources.<ref>Coincidentally, the Wikipedia community regards Wikipedia as a unreliable source.</ref> Within the community of Wikipedia editors, this is often stated by saying "verifiability, not truth" to express the idea that the readers are left themselves to check the truthfulness of what appears in the articles.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Verifiability |title= Wikipedia:Verifiability |accessdate=2008-02-13 |quote=Material challenged or likely to be challenged, and all quotations, must be attributed to a reliable, published source.}}</ref> Finally, Wikipedia does not take a side.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Neutral_point_of_view |title= Wikipedia:Neutral_point_of_view |accessdate=2008-02-13 |quote=All Wikipedia articles and other encyclopedic content must be written from a neutral point of view, representing significant views fairly, proportionately and without bias.}}</ref> All opinions and viewpoints, if not original, must enjoy appropriate share of coverage within an article.<ref>{{cite web
|url=http://www.alternet.org/story/61365/?page=entire
|title=Will Unethical Editing Destroy Wikipedia's Credibility?
|author=Eric Haas
|publisher=AlterNet.org
|date=[[2007-10-26]]
|access-date=2022-09-03
|archive-date=2008-12-19
|archive-url=https://web.archive.org/web/20081219071808/http://www.alternet.org/story/61365/?page=entire
|url-status=dead
}} {{Webarchive|url=https://web.archive.org/web/20081219071808/http://www.alternet.org/story/61365/?page=entire |date=2008-12-19 }}</ref> Wikipedia editors, as a community, write and revise those policies and guidelines<ref>{{cite web |url=http://www.pcworld.idg.com.au/index.php/id;1866322157;fp;2;fpid;2 |title=Who's behind Wikipedia? |publisher=PC World |date=2008-02-06 |accessdate=2008-02-07 |archive-date=2008-02-09 |archive-url=https://web.archive.org/web/20080209110303/http://www.pcworld.idg.com.au/index.php/id%3B1866322157%3Bfp%3B2%3Bfpid%3B2 }}</ref> and enforce them by deleting and modifying materials failing to meet them. (See also [[Deletionism and inclusionism in Wikipedia|Deletionism and inclusionism]]<ref>{{cite news |title=The battle for Wikipedia's soul |url=http://www.economist.com/printedition/displaystory.cfm?story_id=10789354 |publisher=[[The Economist]] |date=2008-03-06 |accessdate=2008-03-07 }}</ref><ref>{{cite news |url=http://www.telegraph.co.uk/connected/main.jhtml?xml=/connected/2007/10/11/dlwiki11.xml |title=Wikipedia: an online encyclopedia torn apart |date=2007-11-10 |accessdate=2008-03-11 |publisher=[[The Daily Telegraph]]}}</ref>) The vandalism to articles is dealt with by Wikipedians or, more increasingly, by computer programs called [[Internet bot|bots]].<ref name="CreatingDestroyingAndRestoringValue" /> There have also been efforts within the community to improve the reliability of Wikipedia. The English-language Wikipedia has introduced an assessment scale against which the quality of articles is judged;<ref>{{cite web
|url=http://en.wikipedia.org/wiki/Wikipedia:Version_1.0_Editorial_Team/Assessment
|title=Wikipedia:Version 1.0 Editorial Team/Assessment
|accessdate=2007-10-28}}</ref> other editions have also adopted this. Roughly {{formatnum:{{#expr: 100 * floor({{FA number}}/100)}}}} articles in English have passed a rigorous set of criteria to reach the highest rank, "featured article" status; such articles are intended to provide thorough, well-written coverage of their topic, supported by many references to peer-reviewed publications.<ref>{{cite journal
|url=http://www.research.ibm.com/visual/papers/hidden_order_wikipedia.pdf
|author=Fernanda B. Viégas, Martin Wattenberg, and Matthew M. McKeon
|title=The Hidden Order of Wikipedia
|publisher=Visual Communication Lab, IBM Research
|date=2007-07-22
|accessdate=2007-10-30
|format=pdf}}</ref> In a 2003 study of Wikipedia as a community, economics [[Doctor of Philosophy|Ph.D.]] student Andrea Ciffolilli argued that the low [[transaction cost]]s of participating in [[wiki]] software create a catalyst for collaborative development, and that a "creative construction" approach encourages participation.<ref>Andrea Ciffolilli, "[http://firstmonday.org/issues/issue8_12/ciffolilli/index.html Phantom authority, self-selective recruitment and retention of members in virtual communities: The case of Wikipedia] {{Webarchive|url=https://web.archive.org/web/20090106192513/http://www.firstmonday.org/issues/issue8_12/ciffolilli/index.html|date=2009-01-06}}", ''[[First Monday (journal)|First Monday]]'' December 2003.</ref> In his 2008 book, "''The Future of the Internet and How to Stop It''," [[Jonathan Zittrain]] of the [[Oxford Internet Institute]] and Harvard Law School’s [[Berkman Center for Internet & Society]] cites Wikipedia' success as a case study in how open collaboration has fostered innovation on the web.<ref>{{cite book
| last = Zittrain
| first = Jonathan
| title = The Future of the Internet and How to Stop It - Chapter 6: The Lessons of Wikipedia
| author-link = Jonathan Zittrain
| publisher = Yale University Press
| year = 2008
| url = http://yupnet.org/zittrain/archives/16
| isbn = 978-0300124873
| access-date = 2022-09-03
| archive-date = 2009-02-16
| archive-url = https://web.archive.org/web/20090216015857/http://yupnet.org/zittrain/archives/16
| url-status = dead
}}</ref> [[Image:WIkimania-2006 010.jpg|thumb|[[Wikimania]], an annual conference for users of Wikipedia and other projects operated by the Wikimedia Foundation.]]
The community has a power structure.<ref name="iTWireJune18-2006">{{cite news
|first=Stuart
|last=Corner
|title=What's all the fuss about Wikipedia?
|url=http://www.itwire.com.au/content/view/4666/127/
|publisher=[[iT Wire]]
|date=[[June 18]], [[2006]]
|accessdate=2007-03-25
|archive-date=2007-04-23
|archive-url=https://web.archive.org/web/20070423064239/http://www.itwire.com.au/content/view/4666/127/
|url-status=dead
}} {{Webarchive|url=https://web.archive.org/web/20070423064239/http://www.itwire.com.au/content/view/4666/127/ |date=2007-04-23 }}</ref><ref>{{cite news |url=http://www.slate.com/id/2184487 |title=The Wisdom of the Chaperones |date=2008-02-22 |accessdate=2008-03-04 |first=Chris |last=Wilson |publisher=Slate}}</ref>
Wikipedia's community has also been described as "[[cult]]-like,"<ref>{{cite news
|url=http://www.guardian.co.uk/technology/2005/dec/15/wikipedia.web20
|title=Log on and join in, but beware the web cults
|first=Charles |last=Arthur
|date=[[2005-12-15]]
|publisher= ''[[The Guardian]]''
}}</ref> although not always with entirely negative connotations,<ref>{{cite news
|url=http://www.cnn.com/2003/TECH/internet/08/03/wikipedia/index.html
|title=Wikipedia: The know-it-all Web site
|date=[[2003-08-04]]
|first=Kristie
|last=Lu Stout|publisher=[[CNN]]
}}</ref> and criticized for failing to accommodate inexperienced users.<ref>"{{cite web
|url=http://wikinfo.org/index.php/Critical_views_of_Wikipedia
|title=Critical views of Wikipedia
|author=Wikinfo
|date=[[2005-03-30]]
|accessdate=2007-01-29
}}</ref><!--This sentence is poorly written, and, more important, it isn't quite encyclopedic. -- Taku --> While they are welcomed by the community,<ref name="TheNewYorker">
{{cite news
|first=Stacy
|last=Schiff
|title=Can Wikipedia conquer expertise?
|work =Know It All
|url=http://www.newyorker.com/archive/2006/07/31/060731fa_fact
|publisher =[[The New Yorker]]
|date =[[July 24]], [[2006]]
|accessdate=2007-03-25}}</ref> authors new to Wikipedia are encouraged to read policies to help them learn the ways of Wikipedia.<ref name="Torsten_Kleinz">{{cite news
|first=Torsten
|last=Kleinz
|title=World of Knowledge
|work=The Wikipedia Project
|url=http://w3.linux-magazine.com/issue/51/Wikipedia_Encyclopedia.pdf
|publisher=[[Linux Magazine]]
|date=February, 2005
|accessdate=2007-03-25
|archive-date=2007-09-25
|archive-url=https://web.archive.org/web/20070925220722/http://w3.linux-magazine.com/issue/51/Wikipedia_Encyclopedia.pdf
|url-status=dead
}}</ref>
Editors in good standing in the community can run for one of many of levels of volunteer stewardship; this begins with "[[sysop|administrator]]"<ref name="David_Mehegan">{{cite news
|first=David
|last=Mehegan
|title=Many contributors, common cause
|url=http://www.boston.com/business/technology/articles/2006/02/13/many_contributors_common_cause/
|publisher=[[The Boston Globe]]
|date=[[February 13]], [[2006]]
|accessdate=2007-03-25}}</ref> and goes up with "steward" and "bureaucrat".<ref> [http://en.wikipedia.org/w/index.php?title=Wikipedia:User_access_levels&oldid=100160162 Wikipedia:User access levels]," Wikipedia ([[January 12]], [[2007]])</ref> Administrators, the largest group of privileged users ({{srlink|Special:Statistics|1,575 Wikipedians}} for the English edition on [[July 17]], [[2008]]), have the ability to delete pages, lock articles from being changed in case of vandalism or editorial disputes, and block users from editing. As Wikipedia grows with an unconventional model of encyclopedia building, "Who writes Wikipedia?" has become one of the questions frequently asked on the project, often with a reference to other Web 2.0 projects such as [[Digg]].<ref>{{cite web |url=http://www.viktoria.se/altchi/submissions/submission_edchi_1.pdf |title=Power of the Few vs. Wisdom of the Crowd: Wikipedia and the Rise of the Bourgeoisie |first=Aniket |last=Kittur | accessdate =2008-02-23 |format=pdf}}</ref> Jimmy Wales once argued that only "a community ... a dedicated group of a few hundred volunteers" makes a bulk of contributions to Wikipedia and that the project is therefore "much like any traditional organization". This was later disputed by [[Aaron Swartz]], who noted that several articles he sampled had large portion of their content contributed by a user with low edit count.<ref>{{cite web |url=http://www.aaronsw.com/weblog/whowriteswikipedia |title=Raw Thought: Who Writes Wikipedia? |first=Aaron |last=Swartz |accessdate=2008-02-23 |date=2006-09-04}}</ref> A 2007 study by researchers from [[Dartmouth College]] found that anonymous and infrequent contributors to Wikipedia are as reliable a source of knowledge as those contributors who register with the site.<ref>{{cite news
|url=http://www.sciam.com/article.cfm?id=good-samaritans-are-on-the-money
|title=Wikipedia "Good Samaritans'' Are on the Money
|publisher=[[Scientific American]]
|date=[[2007-10-19]]}}</ref>
Although some contributors are authorities in their field, Wikipedia requires that even their contributions be supported by published and verifiable sources. The project's preference for [[consensus]] over [[credential]]s has been labeled "anti-elitism".<ref name="SangerElitism">[[Larry Sanger]], [http://www.kuro5hin.org/story/2004/12/30/142458/25 Why Wikipedia Must Jettison Its Anti-Elitism], [[Kuro5hin]], [[December 31]], [[2004]].</ref>
While praising many aspects of Wikipedia, historian [[Roy Rosenzweig]] noted: "Overall, writing is the [[Achilles' heel]] of Wikipedia. Committees rarely write well, and Wikipedia entries often have a choppy quality that results from the stringing together of sentences or paragraphs written by different people."<ref>{{cite web
|url=http://chnm.gmu.edu/resources/essays/d/42
|title=Can History be Open Source? Wikipedia and the Future of the Past
|publisher=The Journal of American History Volume 93, Number 1 (June, 2006): 117-46
|author=Rosenzweig, Roy
|accessdate=2007-10-29
}}</ref> In August 2007, a website developed by computer science graduate student [[Virgil Griffith]] named [[WikiScanner]] made its public debut. WikiScanner traces the source of millions of changes made to Wikipedia by editors who are not logged in, which reveals that many of these edits come from corporations or sovereign government agencies about articles related to them, their personnel or their work, and were attempts to remove criticism.<ref name="Seeing Corporate Fingerprints">{{cite news
|url=http://www10.nytimes.com/2007/08/19/technology/19wikipedia.html?_r=5&hp=&pagewanted=all&oref=slogin&oref=slogin&oref=slogin&oref=slogin
|title=Seeing Corporate Fingerprints From the Editing of Wikipedia
|first=Katie
|last=Hafner
|date=[[2007-08-19]]
|publisher=[[The New York Times]]
}}{{Dead link|date=September 2022 |bot=InternetArchiveBot |fix-attempted=yes }}</ref><!-- Wales called WikiScanner "a very clever idea," and said that he was considering some changes to Wikipedia to help visitors better understand what information is recorded about them. "When someone clicks on 'edit,' it would be interesting if we could say, 'Hi, thank you for editing. We see you're logged in from ''[[The New York Times]]''. Keep in mind that we know that, and it's public information,'" he said. "That might make them stop and think."<ref name="Seeing Corporate Fingerprints"/>--> ==Reliability and bias==
{{main|Reliability of Wikipedia}}
{{seealso|Criticism of Wikipedia}} Wikipedia
==Operation==
===Wikimedia Foundation and Wikia===
[[Image:Wikimedia Foundation RGB logo with text.svg|thumb|180px|Wikimedia Foundation logo]]
Wikipedia is funded and operated by the [[Wikimedia Foundation]], a non-profit organization which also operates Wikipedia-related projects such as [[Wikibooks]]. In a 2008 interview, Jimmy Wales said that the foundation spent $2 million of donor money in 2007 toward site maintenance costs.<ref>[http://www.rediff.com/money/2008/feb/22inter.htm Wales spent $2m of donor money to maintain Wikipedia]</ref> The foundation shares hosting and bandwidth costs with [[Wikia]], a for-profit company founded by [[Jimmy Wales]] and [[Angela Beesley]]. The Wikimedia Foundation received some donated office space from Wikia Inc. during the fiscal year ending [[June 30]] [[2006]].<ref> [http://upload.wikimedia.org/wikipedia/foundation/4/49/Wikimedia_2007_fs.pdf Wikimedia Foundation 2006–2007 Audit] page 9 says "The Organization shares hosting and bandwidth costs with Wikia, Inc., a for-profit company founded by the same founder as Wikimedia Foundation, Inc. Included in accounts receivable at June 30 2007 is $6,000 due from Wikia, Inc. for these costs. The Organization received some donated office space from Wikia Inc. during the year ended June 30 2006 valued at $6,000. No donation of the office space occurred in 2007. Through June 30, 2007, two members of the Organization's board of directors also serve as employees, officers, or directors of Wikia, Inc."</ref> In ''[[The New York Times]]'' in March 2008, Wales discussed a possible trivia game based on Wikipedia.<ref name=cohen>{{cite web|url=http://www.nytimes.com/2008/03/17/technology/17wikipedia.html |title=Open-Source Troubles in Wiki World |accessdate = 2008-04-01 |author=Noam Cohen |date=[[2008-03-17]] |work=[[The New York Times]]}}</ref> ===Software and hardware===
The operation of Wikipedia depends on [[MediaWiki]], a custom-made, [[free software|free]] and [[open source software|open source]] [[wiki software]] platform written in [[PHP]] and built upon the [[MySQL]] database.<ref>{{cite web |url=http://www.nedworks.org/~mark/presentations/san/Wikimedia%20architecture.pdf |title=Wikimedia Architecture |author=Mark Bergman |publisher=Wikimedia Foundation Inc. |accessdate=2008-06-27 |archive-date=2009-03-03 |archive-url=https://web.archive.org/web/20090303204708/http://www.nedworks.org/~mark/presentations/san/Wikimedia%20architecture.pdf |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20090303204708/http://www.nedworks.org/~mark/presentations/san/Wikimedia%20architecture.pdf |date=2009-03-03 }}</ref> The software incorporates programming features such as a [[Macro (computer science)|macro language]], [[variable]]s, a [[transclusion]] system for [[Web template#Sub-template|templates]], and [[URL redirection]]. MediaWiki is licensed under the [[GNU General Public License]] and used by all Wikimedia projects, as well as many other wiki projects. Originally, Wikipedia ran on [[UseModWiki]] written in [[Perl]] by Clifford Adams (Phase I), which initially required [[CamelCase]] for article hyperlinks; the present double bracket style was incorporated later. Starting in January 2002 (Phase II), Wikipedia began running on a [[PhpWiki|PHP wiki]] engine with a MySQL database; this software was custom-made for Wikipedia by Magnus Manske. The Phase II software was repeatedly modified to accommodate the [[Exponential growth|exponentially increasing]] demand. In July 2002 (Phase III), Wikipedia shifted to the third-generation software, MediaWiki, originally written by Lee Daniel Crocker. [[Image:Wikimedia-servers-2006-05-09.svg|thumb|right|Overview of system architecture, May 2006. See [[:meta:Server layout diagrams|server layout diagrams on Meta-Wiki]].]] Wikipedia currently runs on dedicated [[computer cluster|clusters]] of [[Linux|GNU/Linux]] servers, 300 in [[Florida]], 26 in [[Amsterdam]] and 23 in Yahoo!'s Korean hosting facility in [[Seoul]].<ref name="servers">{{cite web|url=http://meta.wikimedia.org/wiki/Wikimedia_servers|title=Wikimedia servers at wikimedia.org|accessdate=2008-02-16}}</ref> Wikipedia employed a single server until 2004, when the server setup was expanded into a distributed [[multitier architecture]]. In January 2005, the project ran on 39 [[Dedicated hosting service|dedicated servers]] located in Florida. This configuration included a single master [[database server]] running [[MySQL]], multiple slave database servers, 21 [[web server]]s running the [[Apache HTTP Server]], and seven [[Squid (software)|Squid cache]] servers. Wikipedia receives between 20,000 and 45,000 page requests per second, depending on time of day.<ref>"[http://hemlock.knams.wikimedia.org/~leon/stats/reqstats/reqstats-monthly.png Monthly request statistics] {{Webarchive|url=https://web.archive.org/web/20080414003539/http://hemlock.knams.wikimedia.org/~leon/stats/reqstats/reqstats-monthly.png|date=2008-04-14}}", Wikimedia. Retrieved on [[2008-02-26]].</ref> Page requests are first passed to a front-end layer of [[Squid (software)|Squid caching]] servers.<ref>{{cite web |url=http://dammit.lt/uc/workbook2007.pdf |title=Wikipedia: Site internals, configuration, code examples and management issues |author=Domas Mituzas |publisher=MySQL Users Conference 2007 |accessdate=2008-06-27 |archive-date=2008-05-28 |archive-url=https://web.archive.org/web/20080528030452/http://dammit.lt/uc/workbook2007.pdf |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20080528030452/http://dammit.lt/uc/workbook2007.pdf |date=2008-05-28 }}</ref>Requests that cannot be served from the Squid cache are sent to load-balancing servers running the [[Linux Virtual Server]] software, which in turn pass the request to one of the Apache web servers for page rendering from the database. The web servers deliver pages as requested, performing page rendering for all the language editions of Wikipedia. To increase speed further, rendered pages for anonymous users are cached in a distributed memory cache until invalidated, allowing page rendering to be skipped entirely for most common page accesses. Two larger clusters in the Netherlands and Korea now handle much of Wikipedia's traffic load. ==License and language editions==
{{see also|List of Wikipedias}}
All text in Wikipedia is covered by [[GNU Free Documentation License]] (GFDL), a [[copyleft]] license permitting the redistribution, creation of derivative works, and commercial use of content while authors retain copyright of their work.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Copyrights |title=Wikipedia:Copyrights |accessdate=2008-04-22 |publisher=English Wikipedia}}</ref> The position that Wikipedia is merely a hosting service has been successfully used as a defense in court.<ref>{{cite news
|url=http://www.washingtonpost.com/wp-dyn/content/article/2007/11/02/AR2007110201339_Inform.html
|title=Wikipedia cleared in French defamation case
|publisher=Reuters
|date=2007-11-02
|accessdate=2007-11-02}}</ref><ref>{{cite web |url=http://arstechnica.com/news.ars/post/20080502-dumb-idea-suing-wikipedia-for-calling-you-dumb.html |title=Dumb idea: suing Wikipedia for calling you "dumb" |first=Nate |last=Anderson |date=2008-05-02 |accessdate=2008-05-04 |publisher=
[[Ars Technica]]}}</ref> Wikipedia has been working on the switch to [[Creative Commons licenses]] because the GFDL, initially designed for software manuals, is not suitable for online reference works and because the two licenses are currently incompatible.<ref>{{cite web
|url=http://wikimediafoundation.org/wiki/Resolution:License_update
|title=Resolution:License update
|date=2007
|accessdate=2007-12-04
|author=Walter Vermeir
|publisher=Wikizine}}</ref>
[[Image:English Wikipedia contributors by country (1).svg|thumb|Contributors for English Wikipedia by country as of September 2006<ref>{{cite web
|url=http://meta.wikimedia.org/wiki/Edits_by_project_and_country_of_origin
|title=Edits by project and country of origin
|date=2006-09-04
|accessdate=2007-10-25
}}</ref>.]]
The handling of media files (e.g., image files) varies across language editions. Some language editions, such as the English Wikipedia, include non-free image files under [[fair use]] doctrine, while the others have opted not to. This is in part because of the difference in copyright laws between countries; for example, the notion of fair use does not exist in [[Japanese copyright law]]. Media files covered by [[free content]] licenses (e.g., Creative Commons' cc-by-sa) are shared across language editions via [[Wikimedia Commons]] repository, a project operated by the Wikimedia Foundation. There are currently 262 language editions of Wikipedia; of these, 22 have over 100,000 articles and 79 have over 1,000 articles.<ref name="ListOfWikipedias" /> (See [[List of Wikipedias]] for the full list.) According to Alexa, the English [[subdomain]] (en.wikipedia.org; [[English Wikipedia]]) receives approximately 52% of Wikipedia's cumulative traffic, with the remaining split among the other languages (Spanish: 19%, French: 5%, Polish: 3%, German: 3%, Japanese: 3%, Portuguese: 2%).<ref name="AlexaStats" /> As of July 2008, the five largest language editions are (in order of article count) [[English Wikipedia|English]], [[German Wikipedia|German]], [[French Wikipedia|French]], [[Polish Wikipedia|Polish]] and [[Japanese Wikipedia]]s.<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Multilingual_statistics |title=Wikipedia:Multilingual statistics |publisher=English Wikipedia |accessdate=2007-12-23}}</ref> Since Wikipedia is web-based and therefore worldwide, contributors of a same language edition may use different dialects or may come from different countries (as is the case for the [[English Wikipedia|English edition]]). These differences may lead to some conflicts over [[American and British English spelling differences|spelling differences]], (e.g. ''color'' vs. ''colour'')<ref>{{cite web|url=http://en.wikipedia.org/wiki/Wikipedia:Spelling|title= spelling | work = Manual of Style | publisher = Wikipedia |accessdate=2007-05-19}}</ref> or points of view.<ref>{{cite web|url=http://en.wikipedia.org/wiki/Wikipedia:WikiProject_Countering_systemic_bias|title=Countering systemic bias|accessdate=2007-05-19}}</ref>
Though the various language editions are held to global policies such as "neutral point of view," they diverge on some points of policy and practice, most notably on whether images that are not [[Free Content|licensed freely]] may be used under a claim of [[fair use]].<ref>{{cite web
|url=http://meta.wikimedia.org/wiki/Fair_use
|title=Fair use
|publisher=Meta wiki
|accessdate=2007-07-14}}</ref><ref>{{cite web
|url=http://meta.wikimedia.org/wiki/Images_on_Wikipedia
|title=Images on Wikipedia
|accessdate=2007-07-14}}</ref><ref>{{cite journal
|url=http://www.research.ibm.com/visual/papers/viegas_hicss_visual_wikipedia.pdf
|author=Fernanda B. Viégas
|title=The Visual Side of Wikipedia
|publisher=Visual Communication Lab, IBM Research
|date=2007-01-03
|accessdate=2007-10-30}}</ref>
[[Image:PercentWikipediasGraph.png|thumb|Percentage of all Wikipedia articles in English (red) and top ten largest language editions (blue). As of July 2008, less than 23% of Wikipedia articles are in English.]] Jimmy Wales has described Wikipedia as "an effort to create and distribute a free encyclopedia of the highest possible quality to every single person on the planet in their own language".<ref>[[Jimmy Wales]], "[http://lists.wikimedia.org/pipermail/wikipedia-l/2005-March/020469.html Wikipedia is an encyclopedia]", March 8 2005, <wikipedia-l@wikimedia.org></ref> Though each language edition functions more or less independently, some efforts are made to supervise them all. They are coordinated in part by [[Wikipedia:Meta|Meta-Wiki]], the Wikimedia Foundation's wiki devoted to maintaining all of its projects (Wikipedia and others). For instance, Meta-Wiki provides [http://meta.wikimedia.org/wiki/Statistics important statistics] on all language editions of Wikipedia and maintain a [http://meta.wikimedia.org/wiki/List_of_articles_every_Wikipedia_should_have list of articles every Wikipedia should have]. The list concerns basic content by subject: biography, history, geography, society, culture, science, technology, foodstuffs, and mathematics. As for the rest, it is not rare for articles strongly related to a particular language not to have counterparts in another edition. For example, articles about small towns in the United States might only be available in English. Translated articles represent only a small portion of articles in most editions,<ref>For example, "[http://en.wikipedia.org/wiki/Wikipedia:Translation_into_English Translation into English]", Wikipedia. ([[March 9]], [[2005]])</ref> in part because automated translation of articles is disallowed.<ref>[http://en.wikipedia.org/wiki/Wikipedia:Translations Wikipedia: Translation]. English Wikipedia, accessed on [[2007-02-03]]</ref> Articles available in more than one language may offer "[[InterWiki]]" links, which link to the counterpart articles in other editions. Several language versions have published a selection of Wikipedia articles on an optical disk version. An English version, [[2006 Wikipedia CD Selection]], contained about 2000 articles. Another English version <ref>"[http://www.wikipediaondvd.com/site.php?temp=down List of Mirrors Hosting the CD Iso.] {{Webarchive|url=https://web.archive.org/web/20211119150425/http://www.wikipediaondvd.com/site.php?temp=down|date=2021-11-19}}" ''Wikipedia on DVD''. [[History of Wikipedia|Linterweb]]. Accessed [[1 June]] [[2007]]</ref> developed by [[History of Wikipedia|Linterweb]] contains "1988 + articles".<ref>"[http://www.wikipediaondvd.com/ Wikipedia on DVD] {{Webarchive|url=https://web.archive.org/web/20130603205800/http://www.wikipediaondvd.com/|date=2013-06-03}}". Linterweb. Accessed [[1 June]] [[2007]]. "Linterweb is authorized to make a commercial use of the Wikipedia trademark restricted to the selling of the Encyclopedia CDs and DVDs."</ref><ref>"[http://www.wikipediaondvd.com/site.php?temp=buy Wikipedia 0.5 Available on a CD-ROM] {{Webarchive|url=https://web.archive.org/web/20130503073535/http://www.wikipediaondvd.com/site.php?temp=buy|date=2013-05-03}}". ''Wikipedia on DVD''. Linterweb. Accessed [[1 June]] [[2007]]. "The DVD or CD-ROM version 0.5 was commercially available for purchase."</ref> The Polish version contains nearly 240000 articles.<ref>{{cite web |url=http://meta.wikimedia.org/wiki/Polska_Wikipedia_na_DVD_%28z_Helionem%29/en |title=Polish Wikipedia on DVD}}</ref> There are also a few German versions.<ref>{{cite web |url=http://de.wikipedia.org/wiki/Wikipedia:Wikipedia-Distribution |title=Wikipedia:DVD}}</ref> ==Cultural significance==
{{main|Wikipedia in culture}}
[[Image:Webcomic xkcd - Wikipedian protester.png|thumb|An [[xkcd]] strip entitled "Wikipedian Protester."]]
In addition to [[Logistic function|logistic growth]] in the number of its articles,<ref>{{cite web |url=http://en.wikipedia.org/wiki/Wikipedia:Modelling_Wikipedia%27s_growth |title=Wikipedia:Modelling Wikipedia's growth |accessdate=2007-12-22}}</ref> Wikipedia has steadily gained status as a general reference website since its inception in 2001.<ref>{{cite web |url=http://www.comscore.com/press/release.asp?press=849 |title=694 Million People Currently Use the Internet Worldwide According To comScore Networks |3=date-2006-05-04 |accessdate=2007-12-16 |publisher=comScore |quote=Wikipedia has emerged as a site that continues to increase in popularity, both globally and in the U.S. |archive-date=2008-07-30 |archive-url=https://web.archive.org/web/20080730011713/http://www.comscore.com/press/release.asp?press=849 |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20080730011713/http://www.comscore.com/press/release.asp?press=849 |date=2008-07-30 }}</ref> According to [[Alexa Internet|Alexa]] and [[comScore]], Wikipedia is among the ten most visited websites world-wide.<ref name=AlexaTop500>{{cite web |url=http://www.alexa.com/site/ds/top_sites?ts_mode=global&lang=none |title=Top 500 |accessdate=2007-12-04 |publisher=[[Alexa Internet|Alexa]] |archive-date=2008-12-24 |archive-url=https://web.archive.org/web/20081224031856/http://www.alexa.com/site/ds/top_sites?ts_mode=global |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20081224031856/http://www.alexa.com/site/ds/top_sites?ts_mode=global |date=2008-12-24 }}</ref><ref>{{cite web |url=http://www.comscore.com/press/data.asp |title=comScore Data Center |date=October 2007 |accessdate=2008-01-19}}</ref> Of the top ten, Wikipedia is the only non-profit website. The growth of Wikipedia has been fueled by its dominant position in Google search results;<ref>{{cite journal |url=http://www.hoover.org/publications/ednext/16111162.html |title=Wikipedia or Wickedpedia? |journal=Hoover Institution |first=Michael J |last=Petrilli |volume=8 |issue=2 |accessdate=2008-03-21 |archive-date=2008-03-27 |archive-url=https://web.archive.org/web/20080327230211/http://www.hoover.org/publications/ednext/16111162.html |url-status=dead }}</ref> about 50% of search engine traffic to Wikipedia comes from Google,<ref>{{cite web |url=http://weblogs.hitwise.com/leeann-prescott/2007/02/wikipedia_traffic_sources.html |title=Google Traffic To Wikipedia up 166% Year over Year |publisher=[[Hitwise]] |date=2007-02-16 |accessdate=2007-12-22}}</ref> a good portion of which is related to academic research.<ref>{{cite web |url=http://weblogs.hitwise.com/leeann-prescott/2006/10/wikipedia_and_academic_researc.html |title=Wikipedia and Academic Research |publisher=[[Hitwise]] |date=2006-10-17 |accessdate=2008-02-06 |archive-date=2011-08-25 |archive-url=https://web.archive.org/web/20110825035630/http://weblogs.hitwise.com/leeann-prescott/2006/10/wikipedia_and_academic_researc.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20110825035630/http://weblogs.hitwise.com/leeann-prescott/2006/10/wikipedia_and_academic_researc.html |date=2011-08-25 }}</ref> In April 2007 the [[Pew Research Center|Pew]] Internet and American Life project found that one third of US Internet users consulted Wikipedia.<ref>{{cite web |first=Lee |last=Rainie |coauthor=Bill Tancer |title=Wikipedia users |publisher=[[Pew Research Center]] |work=Pew Internet & American Life Project |date=2007-12-15 |url=http://www.pewinternet.org/pdfs/PIP_Wikipedia07.pdf |format=PDF |accessdate=2007-12-15 |quote=36% of online American adults consult Wikipedia. It is particularly popular with the well-educated and current college-age students. |archive-date=2007-06-13 |archive-url=https://web.archive.org/web/20070613013041/http://www.pewinternet.org/pdfs/PIP_Wikipedia07.pdf |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20070613013041/http://www.pewinternet.org/pdfs/PIP_Wikipedia07.pdf |date=2007-06-13 }}</ref> In October 2006, the site was estimated to have a hypothetical market value of $580 million if it ran ads.<ref>{{cite web
|url=http://www.watchmojo.com/web/blog/?p=626
|title=What is Wikipedia.org's Valuation?
|first=Ashkan
|last=Karbasfrooshan
|date=2006-10-26
|accessdate=2007-12-01}}</ref> Wikipedia's content has also been used in academic studies, books, conferences, and court cases.<ref>"[http://en.wikipedia.org/wiki/Wikipedia:Wikipedia_in_the_media Wikipedia:Wikipedia in the media]", Wikipedia</ref><ref>{{cite web|url=http://www.ca11.uscourts.gov/opinions/ops/200216886.pdf|title=Bourgeois ''et al'' v. Peters ''et al.''|format=PDF|accessdate=2007-02-06|archive-date=2012-12-21|archive-url=https://web.archive.org/web/20121221140312/http://www.ca11.uscourts.gov/opinions/ops/200216886.pdf|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20121221140312/http://www.ca11.uscourts.gov/opinions/ops/200216886.pdf |date=2012-12-21 }}</ref> The [[Parliament of Canada]]'s website refers to Wikipedia's article on [[same-sex marriage]] in the "related links" section of its "further reading" list for the [[Civil Marriage Act]].<ref>[http://www.parl.gc.ca/LEGISINFO/index.asp?Session=13&query=4381&List=ot#2 C-38] {{Webarchive|url=https://web.archive.org/web/20080602062255/http://www.parl.gc.ca/LEGISINFO/index.asp?Session=13&query=4381&List=ot#2|date=2008-06-02}}, LEGISINFO ([[March 28]], [[2005]])</ref> The encyclopedia's assertions are increasingly used as a source by organizations such as the U.S. Federal Courts and the [[World Intellectual Property Organization]]<ref name="WP_court_source">{{cite journal |last=Arias |first=Martha L. |date=[[2007-01-29]] |title=[http://www.ibls.com/internet_law_news_portal_view.aspx?s=latestnews&id=1668 Wikipedia: The Free Online Encyclopedia and its Use as Court Source] |journal=Internet Business Law Services}} (the name "''World Intellectual Property Office''" should however read "''World Intellectual Property Organization''" in this source)</ref> – though mainly for ''supporting information'' rather than information decisive to a case.<ref>{{cite news |last=Cohen |first=Noam |date=[[2007-01-29]] |title=Courts Turn to Wikipedia, but Selectively |url=http://www10.nytimes.com/2007/01/29/technology/29wikipedia.html?_r=5&ref=technology&oref=slogin&oref=slogin&oref=slogin&oref=slogin |journal=New York Times }}{{Dead link|date=October 2022 |bot=InternetArchiveBot |fix-attempted=yes }}</ref> Content appearing on Wikipedia has also been cited as a source and referenced in some [[U.S. intelligence community|U.S. intelligence agency]] reports.<ref>{{cite web
|url=http://www.fas.org/blog/secrecy/2007/03/the_wikipedia_factor_in_us_int.html
|title=The Wikipedia Factor in U.S. Intelligence
|first=Steven
|last=Aftergood
|publisher=Federation of American Scientists Project on Government Secrecy
|date=2007-03-21
|accessdate=2007-04-14
|archive-date=2011-08-05
|archive-url=https://web.archive.org/web/20110805085711/http://www.fas.org/blog/secrecy/2007/03/the_wikipedia_factor_in_us_int.html
}}</ref> Wikipedia has also been used as a source in [[journalism]],<ref>{{cite news |title=Wikipedia in the Newsroom |url=http://www.ajr.org/Article.asp?id=4461 |date=[[February 2008|February]]/March 2008 |publisher=[[American Journalism Review]] |first=Donna |last=Shaw |accessdate=2008-02-11}}</ref> sometimes without attribution, and several reporters have been dismissed for plagiarizing from Wikipedia.<ref>Shizuoka newspaper plagiarized Wikipedia article, ''Japan News Review'', [[July 5]], [[2007]]</ref><ref>"[http://www.mysanantonio.com/news/metro/stories/MYSA010307.02A.richter.132c153.html Express-News staffer resigns after plagiarism in column is discovered] {{Webarchive|url=https://web.archive.org/web/20071015045010/http://www.mysanantonio.com/news/metro/stories/MYSA010307.02A.richter.132c153.html|date=2007-10-15}}", ''[[San Antonio Express-News]]'', [[January 9]], [[2007]].</ref><ref>"[http://starbulletin.com/2006/01/13/news/story03.html Inquiry prompts reporter's dismissal] {{Webarchive|url=https://web.archive.org/web/20080628204906/http://starbulletin.com/2006/01/13/news/story03.html|date=2008-06-28}}", ''[[Honolulu Star-Bulletin]]'', [[January 13]], [[2007]].</ref>
In July 2007, Wikipedia was the focus of a 30-minute documentary on [[BBC Radio 4]]<ref>{{cite web|url=http://www.bbc.co.uk/radio4/factual/pip/efv21/|title=Radio 4 Documentary}}</ref> which argued that, with increased usage and awareness, the number of references to Wikipedia in popular culture is such that the term is one of a select band of 21st-century nouns that are so familiar ([[Google]], [[Facebook]], [[YouTube]]) that they no longer need explanation and are on a par with such 20th-century terms as [[The Hoover Company|Hoovering]] or [[Coca-Cola|Coke]]. Many parody Wikipedia's openness, with characters vandalizing or modifying the online encyclopedia project's articles. Notably, comedian [[Stephen Colbert]] has parodied or referenced Wikipedia on numerous episodes of his show ''[[The Colbert Report]]'' and coined the related term "[[wikiality]]".<ref name="wikiality">{{cite news|title=Wikiality|publisher=Comedycentral.com|url=http://www.comedycentral.com/motherload/index.jhtml?ml_video=72347|author=Stephen Colbert|date=[[2006-07-30]]|access-date=2022-09-03|archive-date=2008-03-08|archive-url=https://web.archive.org/web/20080308053730/http://www.comedycentral.com/motherload/index.jhtml?ml_video=72347|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20080308053730/http://www.comedycentral.com/motherload/index.jhtml?ml_video=72347 |date=2008-03-08 }}</ref> Wikipedia has also created an impact upon forms of media. Some media sources satirize Wikipedia's susceptibility to inserted inaccuracies, such as a front-page article in ''[[The Onion]]'' in July 2006 with the title "Wikipedia Celebrates 750 Years of American Independence",<ref>{{cite web |url=http://www.theonion.com/content/node/50902 |title=Wikipedia Celebrates 750 Years Of American Independence |accessmonthday=[[October 15]] |accessyear=2006 |year=2006 |work=[http://www.theonion.com/content/index The Onion] |access-date=2022-09-03 |archive-date=2011-08-23 |archive-url=https://web.archive.org/web/20110823075932/http://www.theonion.com/articles/wikipedia-celebrates-750-years-of-american-indepen%2C2007/ |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20110823075932/http://www.theonion.com/articles/wikipedia-celebrates-750-years-of-american-indepen%2C2007/ |date=2011-08-23 }}</ref> while others may draw upon Wikipedia's statement that anyone can edit, such as "[[The Negotiation]]", an episode of ''[[The Office (U.S. TV series)|The Office]]'', where character [[Michael Scott (The Office)|Michael Scott]] said that "Wikipedia is the best thing ever. Anyone in the world can write anything they want about any subject, so you know you are getting the best possible information", and a select few parody Wikipedia's policies, such as the ''xkcd'' strip named "Wikipedian Protester", that also included the joke "Semi-protect the Constitution!" The first documentary film about Wikipedia, entitled ''[[Truth in Numbers: The Wikipedia Story]]'', is scheduled for 2009 release. Shot on several continents, the film will cover the history of Wikipedia and feature interviews with Wikipedia editors around the world.<ref>{{cite web|url=http://wikidocumentary.wikia.com/wiki/Main_Page|title=wikidocumentary.wikia.com/wiki/Main_Page<!--INSERT TITLE-->|access-date=2022-09-03|archive-date=2010-01-11|archive-url=https://web.archive.org/web/20100111205115/http://wikidocumentary.wikia.com/wiki/Main_Page|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20100111205115/http://wikidocumentary.wikia.com/wiki/Main_Page |date=2010-01-11 }}</ref><ref>{{cite web| url=http://www.sfgate.com/cgi-bin/article.cgi?f=/c/a/2007/03/11/PKGRJN87UI1.DTL| title=Industry Buzz| last=Hart| first=Hugh| date=[[March 11]], [[2007]]| publisher=[[San Francisco Chronicle|SFGate.com]]}}</ref> Dutch filmmaker [[Tegenlicht|IJsbrand van Veelen]] premiered his 45-minute documentary ''The Truth According to Wikipedia'' in April, 2008.<ref>{{cite web
|url=http://www.techcrunch.com/2008/04/08/the-truth-according-to-wikipedia/
|title=The Truth According to Wikipedia
|last=Schonfeld
|first=Erick
|date=[[April 8]], [[2008]]
|publisher=TechCruch.com}}</ref> <!-- This paragraph doesn't make much sense; what is relevancy? He was merely using Wikipedia (as an example) to make a point. -- TakuyaMurata On [[September 28]], [[2007]], Italian politician [[Franco Grillini]] raised a parliamentary question with the Minister of Cultural Resources and Activities about the necessity of [[Panoramafreiheit|freedom of panorama]]. He said that the lack of such freedom forced Wikipedia, "the seventh most consulted website" to forbid all images of modern Italian buildings and art, and claimed this was hugely damaging to tourist revenues.<ref>{{cite web|url=http://www.grillini.it/show.php?4885|title=Comunicato stampa. On. Franco Grillini. Wikipedia. Interrogazione a Rutelli. Con "diritto di panorama" promuovere arte e architettura contemporanea italiana. Rivedere con urgenza legge copyright|date=[[12 October]] [[2007]]}}</ref>
-->On [[September 16]], [[2007]], ''[[The Washington Post]]'' reported that Wikipedia had become a focal point in the 2008 election campaign, saying, "Type a candidate's name into Google, and among the first results is a Wikipedia page, making those entries arguably as important as any ad in defining a candidate. Already, the presidential entries are being edited, dissected and debated countless times each day."<ref>{{cite news
|url=http://www.washingtonpost.com/wp-dyn/content/article/2007/09/16/AR2007091601699_pf.html
|title=On Wikipedia, Debating 2008 Hopefuls' Every Facet
|author=Jose Antonio Vargas
|publisher=The Washington Post
|date=2007-09-17}}
</ref> An October 2007 [[Reuters]] article, entitled "Wikipedia page the latest status symbol", reported the recent phenomenon of how having a Wikipedia article vindicates one's notability.<ref>{{cite news
|url=http://www.reuters.com/article/domesticNews/idUSN2232893820071022?sp=true
|title=Wikipedia page the latest status symbol
|author=Jennifer Ablan
|publisher=Reuters
|date=2007-10-22
|accessdate=2007-10-24}}</ref> Wikipedia won two major awards in May 2004.<ref>"[http://meta.wikimedia.org/wiki/Trophy_box Trophy Box]", [[Wikipedia:Meta|Meta-Wiki]] ([[March 28]], [[2005]]).</ref> The first was a Golden Nica for Digital Communities of the annual [[Prix Ars Electronica]] contest; this came with a €10,000 (£6,588; $12,700) grant and an invitation to present at the PAE Cyberarts Festival in [[Austria]] later that year. The second was a Judges' [[Webby Awards|Webby Award]] for the "community" category.<ref>{{cite web|url=http://www.webbyawards.com/webbys/winners-2004.php|title=Webby Awards 2004|publisher=The International Academy of Digital Arts and Sciences|date=2004|accessdate=2007-06-19|archive-date=2011-07-22|archive-url=https://web.archive.org/web/20110722174246/http://www.webbyawards.com/webbys/winners-2004.php|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20110722174246/http://www.webbyawards.com/webbys/winners-2004.php |date=2011-07-22 }}</ref> Wikipedia was also nominated for a "Best Practices" Webby. On [[January 26]], [[2007]], Wikipedia was also awarded the fourth highest brand ranking by the readers of brandchannel.com, receiving 15% of the votes in answer to the question "Which brand had the most impact on our lives in 2006?"<ref>{{cite news |first=Anthony |last=Zumpano |title=Similar Search Results: Google Wins |url=http://www.brandchannel.com/features_effect.asp?pf_id=352 |publisher=[[Interbrand]] |date=[[2007-01-29]] |accessdate=2007-01-28 |archive-date=2007-02-20 |archive-url=https://web.archive.org/web/20070220095907/http://brandchannel.com/features_effect.asp?pf_id=352 |url-status=dead }}</ref> ==Related projects==
{{sisterlinks}}
A number of interactive multimedia encyclopedias incorporating entries written by the public existed long before Wikipedia was founded. The first of these was the 1986 [[BBC Domesday Project]], which included text (entered on [[BBC Micro]] computers) and photographs from over 1 million contributors in the [[United Kingdom|UK]], and covering the geography, art and culture of the UK. This was the first interactive multimedia encyclopedia (and was also the first major multimedia document connected through internal links), with the majority of articles being accessible through an interactive map of the UK. The user-interface and part of the content of the Domesday Project have now been emulated on a website.<ref name="Domesday Project">[http://www.domesday1986.com/ Web-based emulator of the Domesday Project User Interface] and data from the Community Disc (contributions from the general public) -- most articles can be accessed using the interactive map</ref> One of the most successful early online encyclopedias incorporating entries by the public was [[h2g2]], which was also created by the [[BBC]]. The h2g2 encyclopedia was relatively light-hearted, focusing on articles which were both witty and informative. Both of these projects had similarities with Wikipedia, but neither gave full editorial freedom to public users. Wikipedia has also spawned several sister projects. The first, "In Memoriam: September 11<!--Do not reformat this date, it is quoted--> Wiki",<ref>{{cite web|url=http://www.sep11memories.org/wiki/In_Memoriam|title=sep11memories.org/<!--INSERT TITLE-->|accessdate=2007-02-06|archive-date=2009-03-12|archive-url=https://web.archive.org/web/20090312042108/http://www.sep11memories.org/wiki/In_Memoriam}}</ref> created in October 2002,<ref>[http://www.sep11memories.org/index.php?title=In_Memoriam&oldid=1502 First edit to the wiki] {{Webarchive|url=https://web.archive.org/web/20110929203116/http://www.sep11memories.org/index.php?title=In_Memoriam&oldid=1502|date=2011-09-29}} In Memoriam: September 11 wiki ([[October 28]], [[2002]]),</ref> detailed the [[September 11, 2001 attacks]]; this project was closed in October 2006. [[Wiktionary]], a dictionary project, was launched in December 2002;<ref>"[http://meta.wikimedia.org/w/index.php?title=Wikimedia_News&diff=prev&oldid=4133 Announcement of Wiktionary's creation]", [[December 12]], [[2002]]. Retrieved on [[2007-02-02]].</ref> [[Wikiquote]], a collection of quotations, a week after Wikimedia launched, and [[Wikibooks]], a collection of collaboratively written free books. Wikimedia has since started a number of other projects, including [[Wikiversity]], a project for the creation of free learning materials and supporting learning activities.<ref name="OurProjects">"[http://wikimediafoundation.org/wiki/Our_projects Our projects]", [[Wikimedia Foundation]]. Retrieved on [[2007-01-24]]</ref> A similar non-wiki project, the [[GNUPedia]] project, co-existed with Nupedia early in its history; however, it has been retired and its creator, [[free software]] figure [[Richard Stallman]], has lent his support to Wikipedia.<ref name="stallman1999" /> Other websites centered on collaborative [[knowledge base]] development have drawn inspiration from or inspired Wikipedia. Some, such as [[Susning.nu]], [[Enciclopedia Libre]], and [[WikiZnanie]] likewise employ no formal review process, whereas others use more traditional [[peer review]], such as [[Encyclopedia of Life]], [[Stanford Encyclopedia of Philosophy]], [[Scholarpedia]], [[h2g2]] and [[Everything2]]. Jimmy Wales, the ''de facto'' leader of Wikipedia,<ref name="defactoleader">{{cite news
|first=Holden
|last=Frith
|url=http://technology.timesonline.co.uk/tol/news/tech_and_web/the_web/article1571519.ece
|title=Wikipedia founder launches rival online encyclopedia
|publisher=''[[The Times]]''
|date=[[March 26]], [[2007]],
|accessdate=2007-06-27
|quote=<small>Wikipedia's de facto leader, Jimmy Wales, stood by the site's format.</small>
}} {{Webarchive|url=https://web.archive.org/web/20110427034628/http://technology.timesonline.co.uk/tol/news/tech_and_web/the_web/article1571519.ece |date=2011-04-27 }}<small> – Holden Frith.</small></ref> said in an interview in regard to the online encyclopedia [[Citizendium]] which is overviewed by experts in their respective fields:<ref name=Orlowski18>
{{cite news
|first=Andrew
|last=Orlowski
|authorlink=Andrew Orlowski
|url=http://www.theregister.co.uk/2006/09/18/sanger_forks_wikipedia/
|title=Wikipedia founder forks Wikipedia, More experts, less fiddling?
|publisher=''[[The Register]]''
|date=[[September 18]], [[2006]]
|accessdate=2007-06-27
|quote=<small>Larry Sanger describes the Citizendium project as a "progressive or gradual fork", with the major difference that experts have the final say over edits.</small>}}<small> – Andrew Orlowski.</small></ref> "We welcome a diversity of efforts. If Larry's project is able to produce good work, we will benefit from it by copying it back into Wikipedia."<ref name="JayLyman">{{cite news
|first=Jay
|last=Lyman
|url=http://www.crmbuyer.com/story/53137.html
|title=Wikipedia Co-Founder Planning New Expert-Authored Site
|publisher=LinuxInsider
|date=[[September 20]], [[2006]]
|accessdate=2007-06-27
|archive-date=2012-05-24
|archive-url=https://web.archive.org/web/20120524164124/http://www.crmbuyer.com/story/53137.html
|url-status=dead
}} {{Webarchive|url=https://web.archive.org/web/20120524164124/http://www.crmbuyer.com/story/53137.html |date=2012-05-24 }}</ref> ==See also==
{{meta|List of Wikipedias}}
*[[List of online encyclopedias]]
* [[List of wikis]]
* [[Open content]]
* [[USA Congressional staff edits to Wikipedia]]
* [[User-generated content]]
* [[Recursion]]
* {{srlink|Wikipedia:About}}
* {{srlink|Wikipedia:Press coverage}}
*[[List of websites named after Wikipedia]] ==Further reading==
===Press coverage===
*{{cite news |url=http://www.economist.com/science/tq/displaystory.cfm?story_id=11484062 |title=The free-knowledge fundamentalist |date=2008-06-05 |accessdate=2008-06-05 |publisher=The Economist}}
*{{cite news |url=http://www.nytimes.com/2007/07/01/magazine/01WIKIPEDIA-t.html?_r=1&ref=magazine&oref=slogin |title=All the News That's Fit to Print Out |first=Jonathan |last=Dee |publisher=The New York Times Magazine |date=2007-07-01 |accessdate=2008-02-22}}
*{{cite news |title=Wikipedia 2.0 - now with added trust |url=http://technology.newscientist.com/article/mg19526226.200-wikipedia-20-%C3%A2-now-with-added-trust.html |date=2007-09-20 |accessdate=2008-02-22 |first=Jim |last=Giles |publisher=New Scientist |archive-date=2008-02-23 |archive-url=https://web.archive.org/web/20080223092137/http://technology.newscientist.com/article/mg19526226.200-wikipedia-20-%C3%A2-now-with-added-trust.html |url-status=dead }}
*{{cite news |title=Wikipedia Rules |url=http://thephoenix.com/article_ektid52864.aspx |publisher=[[The Phoenix (newspaper)|The Phoenix]] |date=2007-12-02 |accessdate=2008-02-22 |first=Mike |last=Miliard}}
*{{cite news |url=http://www.time.com/time/magazine/article/0,9171,1066904-1,00.html |title=It's a Wiki, Wiki World |first=Chris |last=Taylor |date=2005-05-29 |publisher=Time |accessdate=2008-02-22 |archive-date=2009-10-29 |archive-url=https://web.archive.org/web/20091029161646/http://www.time.com/time/magazine/article/0,9171,1066904-1,00.html |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20091029161646/http://www.time.com/time/magazine/article/0,9171,1066904-1,00.html |date=2009-10-29 }}
*{{cite news |url=http://www.theatlantic.com/doc/200609/wikipedia |title=The Hive |first=Marshall |last=Poe |authorlink=Marshall Poe |date=2006-09 |accessdate=2008-03-22 |publisher=[[The Atlantic Monthly]]}}
*Balke, Jeff. [http://blogs.chron.com/brokenrecord/2008/03/for_music_fans_wikipedia_myspa.html For Music Fans: Wikipedia > MySpace] {{Webarchive|url=https://web.archive.org/web/20090907093533/http://blogs.chron.com/brokenrecord/2008/03/for_music_fans_wikipedia_myspa.html |date=2009-09-07 }} ''[[Houston Chronicle]]'' ===Academic studies===
* {{cite journal|author=Ulrike Pfeil, Panayiotis Zaphiris, and Chee Siang Ang|date=2006|title=Cultural differences in collaborative authoring of Wikipedia|journal=Journal of Computer-Mediated Communication|volume=12|issue=1|url=http://jcmc.indiana.edu./vol12/issue1/pfeil.html|access-date=2021-02-09|archive-date=2009-12-01|archive-url=https://web.archive.org/web/20091201202646/http://jcmc.indiana.edu/vol12/issue1/pfeil.html|url-status=dead}}
* {{cite web|title=Do as I do: leadership in the Wikipedia|author=Joseph M. Reagle Jr.|url=http://reagle.org./joseph/2005/ethno/leadership.html|work=Wikipedia Drafts|date=2005}}
* {{cite journal |url=http://www.firstmonday.org/issues/issue12_4/wilkinson/index.html |title=Assessing the value of cooperation in Wikipedia |date=April 2007 |first=Dennis M. |last=Wilkinson |co-author=Bernardo A. Huberman |journal=First Monday |volume=12 |issue=4 |accessdate=2008-02-22 |archive-date=2011-04-30 |archive-url=https://web.archive.org/web/20110430033015/http://firstmonday.org/issues/issue12_4/wilkinson/index.html |url-status=dead }}
* {{cite journal |url=http://www.firstmonday.org/issues/issue12_8/nielsen/index.html |title=Scientific citations in Wikipedia |date=August 2007 |journal=First Monday |volume=12 |issue=8 |accessdate=2008-02-22 |first=Finn Årup |last=Nielsen |archive-date=2008-12-04 |archive-url=https://web.archive.org/web/20081204114114/http://www.firstmonday.org/issues/issue12_8/nielsen/index.html |url-status=dead }} ===Essays===
*[[Roy Rosenzweig]]: [http://chnm.gmu.edu/resources/essays/d/42 Can History be Open Source? Wikipedia and the Future of the Past]. (Originally published in [[The Journal of American History]] Volume 93, Number 1, June 2006, p117-46)
*[http://www.nybooks.com/articles/21131 The Charms of Wikipedia] [[Nicholson Baker]] article on Wikipedia from ''[[The New York Review of Books]]'' ==References==
{{reflist|2}} ==External links==
{{Spoken Wikipedia|Wikipedia.ogg|2005-06-25}}
*[http://www.wikipedia.org/ Wikipedia] – multilingual portal (contains links to all language editions of the project)
*[http://www.dmoz.org/Computers/Open_Source/Open_Content/Encyclopedias/Wikipedia/ Wikipedia] {{Webarchive|url=https://web.archive.org/web/20091203014943/http://www.dmoz.org/Computers/Open_Source/Open_Content/Encyclopedias/Wikipedia/ |date=2009-12-03 }} at the [[Open Directory Project]]
*[http://www.cbc.ca/news/background/tech/wikipedia.html CBC News: I, editor]
*[http://www.wikihow.com/Contribute-to-Wikipedia Help Edit Wikipedia] A [[wikiHow]] article.
*[http://www.cnn.com/2007/TECH/11/01/wikipedia.assignment.ap/index.html Class assignment: Write an original Wikipedia article]
*[irc://irc.freenode.net/wikipedia #wikipedia] on [[freenode]] {{Wikipedia}}
{{Wikimedia Foundation}}
{{Wikipedias}} [[Category:Web 2.0]]
[[Category:Wikipedia]]
[[Category:Free encyclopedias]]
[[Category:Online encyclopedias]]
[[Category:General encyclopedias]]
[[Category:Wikis]]
[[Category:Internet properties established in 2001]]
[[Category:Wikimedia projects]]
[[Category:Advertising-free websites]]
[[Category:Social Information Processing]] {{Link FA|ceb}} [[aa:Wikipedia]]
[[af:Wikipedia]]
[[als:Wikipedia]]
[[am:ውክፔዲያ]]
[[ang:Wicipǣdia]]
[[ar:ويكيبيديا]]
[[an:Biquipedia]]
[[arc:ܘܝܟܝܦܕܝܐ]]
[[roa-rup:Wikipedia]]
[[frp:Vouiquipèdia]]
[[ast:Uiquipedia]]
[[gn:Vikipetã]]
[[ay:Wikipidiya]]
[[az:Vikipediya]]
[[bm:Wikipedia]]
[[bn:উইকিপিডিয়া]]
[[zh-min-nan:Wikipedia]]
[[map-bms:Wikipedia]]
[[ba:Wikipedia]]
[[be:Вікіпедыя]]
[[be-x-old:Вікіпэдыя]]
[[bar:Wikipedia]]
[[bs:Wikipedia]]
[[br:Wikipedia]]
[[bg:Уикипедия]]
[[ca:Viquipèdia]]
[[cv:Википеди]]
[[ceb:Wikipedya]]
[[cs:Wikipedie]]
[[ch:Wikipedia]]
[[co:Wikipedia]]
[[cy:Wicipedia]]
[[da:Wikipedia]]
[[de:Wikipedia]]
[[nv:Wikiibíídiiya]]
[[dsb:Wikipedija]]
[[et:Vikipeedia]]
[[el:Βικιπαίδεια]]
[[myv:Википедиясь]]
[[es:Wikipedia]]
[[eo:Vikipedio]]
[[ext:Wikipédia]]
[[eu:Wikipedia]]
[[fa:ویکیپدیا]]
[[fo:Wikipedia]]
[[fr:Wikipédia]]
[[fy:Wikipedy]]
[[ff:Wikipeediya]]
[[fur:Vichipedie]]
[[ga:Vicipéid]]
[[gan:維基百科]]
[[gv:Wikipedia]]
[[gd:Bhicipèidia]]
[[gl:Wikipedia]]
[[gu:વિકિપીડિયા]]
[[zh-classical:維基大典]]
[[hak:Wikipedia]]
[[ko:위키백과]]
[[hy:Վիքիփեդիա]]
[[hi:विकिपीडिया]]
[[hsb:Wikipedija]]
[[hr:Wikipedija]]
[[io:Wikipedio]]
[[ig:Wikipedia]]
[[ilo:Wikipedia]]
[[bpy:উইকিপিডিয়া]]
[[id:Wikipedia]]
[[ia:Wikipedia]]
[[iu:ᐅᐃᑭᐱᑎᐊ/uikipitia]]
[[ik:Wikipedia]]
[[os:Википеди]]
[[is:Wikipedia]]
[[it:Wikipedia]]
[[he:ויקיפדיה]]
[[jv:Wikipedia]]
[[kl:Wikipedia]]
[[kn:ವಿಕಿಪೀಡಿಯ]]
[[ka:ვიკიპედია]]
[[csb:Wikipedijô]]
[[kk:Уикипедия]]
[[kw:Wikipedya]]
[[sw:Wikipedia]]
[[ht:Wikipedya]]
[[ku:Wikipedia]]
[[lad:ויקיפידיה]]
[[lbe:Википедия]]
[[lo:ວິກິພີເດຍ]]
[[la:Wikipedia]]
[[lv:Vikipēdija]]
[[lb:Wikipedia]]
[[lt:Vikipedija]]
[[lij:Wikipedia]]
[[li:Wikipedia]]
[[ln:Wikipedia]]
[[jbo:uikipedias]]
[[lmo:Wikipedia]]
[[hu:Wikipédia]]
[[mk:Википедија]]
[[mg:Wikipedia]]
[[ml:വിക്കിപീഡിയ]]
[[mt:Wikipedija]]
[[mi:Wikipedia]]
[[mr:विकिपीडिया]]
[[mzn:ویکیپدیا]]
[[ms:Wikipedia]]
[[cdo:Wikipedia]]
[[mdf:Википедиесь]]
[[mn:Википедиа]]
[[nah:Huiquipedia]]
[[na:Wikipedia]]
[[nl:Wikipedia]]
[[nds-nl:Wikipedia]]
[[ne:विकिपीडिया]]
[[ja:ウィキペディア]]
[[nap:Wikipedia]]
[[no:Wikipedia]]
[[nn:Wikipedia]]
[[nrm:Viqùipédie]]
[[oc:Wikipèdia]]
[[ng:Wikipedia]]
[[ug:Wikipédiye]]
[[uz:Vikipediya]]
[[pa:ਵਿਕਿਪੀਡਿਆ]]
[[pag:Wikipedia]]
[[pap:Wikipedia]]
[[ps:پروژه:په هکله]]
[[km:វិគីភីឌា]]
[[pms:Wikipedia]]
[[nds:Wikipedia]]
[[pl:Wikipedia]]
[[pt:Wikipédia]]
[[kaa:Wikipedia]]
[[crh:Vikipediya]]
[[ksh:Wikkipedija]]
[[ro:Wikipedia]]
[[rmy:Vikipidiya]]
[[rm:Vichipedia]]
[[qu:Wikipidiya]]
[[ru:Википедия]]
[[se:Wikipediija]]
[[sc:Wikipedia]]
[[sco:Wikipaedia]]
[[sq:Wikipedia]]
[[scn:Wikipedia]]
[[si:විකිපීඩියා]]
[[simple:Wikipedia]]
[[sd:وڪيپيڊيا]]
[[sk:Wikipédia]]
[[cu:Википє́дїꙗ]]
[[sl:Wikipedija]]
[[szl:Wikipedyjo]]
[[so:Wikipedia]]
[[sr:Википедија]]
[[sh:Wikipedia]]
[[su:Wikipédia]]
[[fi:Wikipedia]]
[[sv:Wikipedia]]
[[tl:Wikipedia]]
[[ta:விக்கிப்பீடியா]]
[[kab:Wikipedia]]
[[roa-tara:Uicchipèdie]]
[[tt:Wikipedia]]
[[te:వికీపీడియా]]
[[tet:Wikipédia]]
[[th:วิกิพีเดีย]]
[[vi:Wikipedia]]
[[tg:Википедиа]]
[[tpi:Wikipedia]]
[[chr:ᏫᎩᏇᏗᏯ]]
[[chy:Wikipedia]]
[[tr:Vikipedi]]
[[tk:Wikipediýa]]
[[udm:Википедия]]
[[bug:Wikipedia]]
[[uk:Вікіпедія]]
[[vec:Wikipedia]]
[[fiu-vro:Vikipeediä]]
[[wa:Wikipedia]]
[[vls:Wikipedia]]
[[war:Wikipedia]]
[[wo:Wikipedia]]
[[wuu:维基百科]]
[[yi:װיקיפעדיע]]
[[yo:Wikipedia]]
[[zh-yue:維基百科]]
[[diq:Wikipediya]]
[[zea:Wikipedia]]
[[bat-smg:Vikipedėjė]]
[[zh:维基百科]]
7e7rn4ubjhr70wbacmfsqpis0a59ob8
Home Islands (Queensland)
0
122219
737080
723190
2026-04-07T16:24:48Z
Reallylongstringksdjuhaifuhwefwjshfsefkhbfkbwhefkbwhebdwkshbdsjfh
72932
737080
wikitext
text/x-wiki
{{Use Australian English|date=June 2020}}
{{Use dmy dates|date=July 2019}}
'''Home Islands''' are a group of islands off the coast of [[Queensland]], [[Australia]]. They are one of the northernmost localities in Australia. Test edits for T296952 <ref>{{cite web|title=Map of Home Islands in Queensland|url=http://www.bonzle.com/c/a?a=p&p=19079&cmd=sp&place=Home%20Islands&file=Home_Islands.htm|publisher=Bonzle|accessdate=September 1, 2016|archive-date=19 October 2021|archive-url=https://web.archive.org/web/20211019072406/http://www.bonzle.com/c/a?a=p&p=19079&cmd=sp&place=Home%20Islands&file=Home_Islands.htm|url-status=dead}}</ref>
test edit
==References==
{{reflist}}
{{coord|11|58|S|143|16|E|display=title|region:AU_type:isle_source:GNS-enwiki}}
[[Category:Islands of Queensland]]
{{Queensland-geo-stub}}
4iq4cxilrbdq3038v7l40xdr3h8hurz
User:SongVĩ.Bot II
2
124239
737084
736976
2026-04-07T17:00:19Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1562 ngày...
737084
wikitext
text/x-wiki
Cập nhật lần cuối: 08-04-2026
Đã 1562 ngày...
h37xs3crnw42s9dpu3042m9kwshd124
Blockchain
0
127117
737079
733627
2026-04-07T15:58:33Z
AChou-WMF
58685
737079
wikitext
text/x-wiki
{{short description|Distributed data store for digital transactions}}
{{other uses|Block chain (disambiguation)}}
{{pp-semi|small=yes}}
{{Use dmy dates|date=May 2019}}
[[File:Bitcoin Block Data.svg|thumb|upright=1.3|[[Bitcoin]] blockchain structure{{Explain|date=January 2022}}]]
A '''blockchain''' is a growing list of [[Record (computer science)|records]], called ''blocks'', that are securely linked together using [[cryptography]].{{R|fortune20160515|nyt20160521}}<ref name="te20151031" /><ref name="cryptocurrencytech">{{Cite book |last1=Narayanan |first1=Arvind |title=Bitcoin and cryptocurrency technologies: a comprehensive introduction |last2=Bonneau |first2=Joseph |last3=Felten |first3=Edward |last4=Miller |first4=Andrew |last5=Goldfeder |first5=Steven |date=2016 |publisher=Princeton University Press |isbn=978-0-691-17169-2 |location=Princeton}}</ref> Each block contains a [[Cryptographic hash function|cryptographic hash]] of the previous block, a [[Trusted timestamping|timestamp]], and transaction data (generally represented as a [[Merkle tree]], where [[Node (computer science)|data nodes]] are represented by leafs). The timestamp proves that the transaction data existed when the block was published to get into its hash. As blocks each contain information about the block previous to it, they form a chain, with each additional block reinforcing the ones before it. Therefore, blockchains are resistant to modification of their data because once recorded, the data in any given block cannot be altered retroactively without altering all subsequent blocks. Test123.
Blockchain technology is amazing. Blockchains are typically managed by a [[peer-to-peer]] network for use as a publicly [[distributed ledger]], where nodes collectively adhere to a [[communication protocol|protocol]] to communicate and validate new blocks. Although blockchain records are not unalterable as [[Fork (blockchain)|forks]] are possible, blockchains may be considered [[secure by design]] and exemplify a distributed computing system with high [[Byzantine fault tolerance]].<ref>{{Cite news |last1=Iansiti |first1=Marco |last2=Lakhani |first2=Karim R. |date=January 2017 |title=The Truth About Blockchain |work=[[Harvard Business Review]] |publisher=[[Harvard University]] |url=https://hbr.org/2017/01/the-truth-about-blockchain |url-status=live |access-date=2017-01-17 |archive-url=https://web.archive.org/web/20170118052537/https://hbr.org/2017/01/the-truth-about-blockchain |archive-date=18 January 2017 |quote=The technology at the heart of bitcoin and other virtual currencies, blockchain is an open, distributed ledger that can record transactions between two parties efficiently and in a verifiable and permanent way. |df=dmy-all}}</ref>
The blockchain was popularized by a person (or group of people) using the name [[Satoshi Nakamoto]] in 2008 to serve as the public transaction [[ledger]] of the [[cryptocurrency]] [[bitcoin]], based on work by Stuart Haber, W. Scott Stornetta, and Dave Bayer.{{R|te20151031}}<ref name=":0">{{Cite web|title=The World's Oldest Blockchain Has Been Hiding in the New York Times Since 1995|url=https://www.vice.com/en/article/j5nzx4/what-was-the-first-blockchain|access-date=2021-10-09|website=www.vice.com|language=en}}</ref> The identity of Satoshi Nakamoto remains unknown to date. The implementation of the blockchain within bitcoin made it the first digital currency to solve the [[double-spending]] problem without the need of a trusted authority or central [[Server (computing)|server]]. The bitcoin design has inspired other applications{{R|te20151031|nyt20160521}} and blockchains that are readable by the public and are widely used by [[cryptocurrencies]]. The blockchain is considered a type of [[payment rail]].<ref>{{Cite web |date=10 February 2018 |title=Blockchain may finally disrupt payments from Micropayments to credit cards to SWIFT |url=https://dailyfintech.com/2018/02/10/bitcoin-will-finally-disrupt-the-credit-card-rails/ |url-status=live |archive-url=https://web.archive.org/web/20180927005613/https://dailyfintech.com/2018/02/10/bitcoin-will-finally-disrupt-the-credit-card-rails/ |archive-date=27 September 2018 |access-date=2018-11-18 |website=dailyfintech.com}}</ref>
Private blockchains have been proposed for business use. ''Computerworld'' called the marketing of such privatized blockchains without a proper security model "[[wikt:snake oil|snake oil]]";<ref name="cw20160905" /> however, others have argued that permissioned blockchains, if carefully designed, may be more decentralized and therefore more secure in practice than permissionless ones.<ref name="cryptocurrencytech" /><ref name="auto">{{Cite journal |last1=Bakos |first1=Yannis |last2=Halaburda |first2=Hanna |last3=Mueller-Bloch |first3=Christoph |date=February 2021 |title=When Permissioned Blockchains Deliver More Decentralization Than Permissionless |journal=Communications of the ACM |volume=64 |issue=2 |pages=20–22 |doi=10.1145/3442371 |s2cid=231704491}}</ref>
== History ==
[[File:2011-2021_blockchain_transactions.jpg|600px|thumb|right|[[Bitcoin]], [[Ethereum]] and [[Litecoin]] transactions per day (January 2011 – January 2021)]]
Cryptographer [[David Chaum]] first proposed a blockchain-like protocol in his 1982 dissertation "Computer Systems Established, Maintained, and Trusted by Mutually Suspicious Groups."<ref>{{Cite journal |last1=Sherman |first1=Alan T. |last2=Javani |first2=Farid |last3=Zhang |first3=Haibin |last4=Golaszewski |first4=Enis |date=January 2019 |title=On the Origins and Variations of Blockchain Technologies |journal=IEEE Security Privacy |volume=17 |issue=1 |pages=72–77 |arxiv=1810.06130 |doi=10.1109/MSEC.2019.2893730 |issn=1558-4046 |s2cid=53114747}}</ref> Further work on a cryptographically secured chain of blocks was described in 1991 by Stuart Haber and W. Scott Stornetta.<ref name="cryptocurrencytech" /><ref>{{Cite journal |last1=Haber |first1=Stuart |last2=Stornetta |first2=W. Scott |date=January 1991 |title=How to time-stamp a digital document |journal=Journal of Cryptology |volume=3 |issue=2 |pages=99–111 |citeseerx=10.1.1.46.8740 |doi=10.1007/bf00196791 |s2cid=14363020}}</ref> They wanted to implement a system wherein document timestamps could not be tampered with. In 1992, Haber, Stornetta, and [[Dave Bayer]] incorporated [[Merkle tree]]s into the design, which improved its efficiency by allowing several document certificates to be collected into one block.<ref name="cryptocurrencytech" /><ref>{{Cite book |last1=Bayer |first1=Dave |title=Improving the Efficiency and Reliability of Digital Time-Stamping |last2=Haber |first2=Stuart |last3=Stornetta |first3=W. Scott |date=March 1992 |work=Sequences |isbn=978-1-4613-9325-2 |volume=2 |pages=329–334 |citeseerx=10.1.1.71.4891 |doi=10.1007/978-1-4613-9323-8_24}}</ref> Under their company Surety, their document certificate hashes have been published in ''[[The New York Times]]'' every week since 1995.<ref name=":0" />
The first decentralized blockchain was conceptualized by a person (or group of people) known as [[Satoshi Nakamoto]] in 2008. Nakamoto improved the design in an important way using a [[Hashcash]]-like method to [[Timestamp-based concurrency control|timestamp]] blocks without requiring them to be signed by a trusted party and introducing a difficulty parameter to stabilize the rate at which blocks are added to the chain.{{r|cryptocurrencytech}} The design was implemented the following year by Nakamoto as a core component of the cryptocurrency [[bitcoin]], where it serves as the public [[ledger]] for all transactions on the network.{{R|te20151031}}
In August 2014, the bitcoin blockchain file size, containing records of all transactions that have occurred on the network, reached 20 GB ([[gigabyte]]s).<ref>{{Cite book |last1=Nian |first1=Lam Pak |title=Handbook of Digital Currency: Bitcoin, Innovation, Financial Instruments, and Big Data |last2=Chuen |first2=David LEE Kuo |publisher=Academic Press |year=2015 |isbn=978-0-12-802351-8 |editor-last=Chuen |editor-first=David LEE Kuo |page=319 |chapter=A Light Touch of Regulation for Virtual Currencies}}</ref> In January 2015, the size had grown to almost 30 GB, and from January 2016 to January 2017, the bitcoin blockchain grew from 50 GB to 100 GB in size. The ledger size had exceeded 200 GB by early 2020.<ref>{{Cite web |title=Blockchain Size |url=https://www.blockchain.com/en/charts/blocks-size?scale=1×pan=all&showDataPoints=true |url-status=live |archive-url=https://web.archive.org/web/20200519024720/https://www.blockchain.com/en/charts/blocks-size?scale=1×pan=all&showDataPoints=true |archive-date=19 May 2020 |access-date=25 February 2020}}</ref>
The words ''block'' and ''chain'' were used separately in Satoshi Nakamoto's original paper, but were eventually popularized as a single word, ''blockchain,'' by 2016.<ref>{{Cite book |last=Johnsen |first=Maria |date=12 May 2020 |url=https://books.google.com/books?id=uVbjDwAAQBAJ&pg=PA6 |title=Blockchain in Digital Marketing: A New Paradigm of Trust |publisher=Maria Johnsen |isbn=979-8-6448-7308-1|page=6|language=en|url-status=live}}</ref>
According to [[Accenture]], an application of the [[diffusion of innovations]] theory suggests that blockchains attained a 13.5% adoption rate within financial services in 2016, therefore reaching the [[early adopter]]s' phase.<ref>{{Cite news |date=27 June 2016 |title=The future of blockchain in 8 charts |work=Raconteur |url=http://raconteur.net/business/the-future-of-blockchain-in-8-charts |url-status=live |access-date=3 December 2016 |archive-url=https://web.archive.org/web/20161202234125/http://raconteur.net/business/the-future-of-blockchain-in-8-charts |archive-date=2 December 2016}}</ref> Industry trade groups joined to create the Global Blockchain Forum in 2016, an initiative of the [[Chamber of Digital Commerce]].
In May 2018, [[Gartner]] found that only 1% of [[Chief information officer|CIOs]] indicated any kind of blockchain adoption within their organisations, and only 8% of CIOs were in the short-term "planning or [looking at] active experimentation with blockchain".<ref>{{Cite news |date=4 May 2018 |title=Hype Killer - Only 1% of Companies Are Using Blockchain, Gartner Reports {{!}} Artificial Lawyer |language=en-GB |work=Artificial Lawyer |url=https://www.artificiallawyer.com/2018/05/04/hype-killer-only-1-of-companies-are-using-blockchain-gartner-reports/ |url-status=live |access-date=2018-05-22 |archive-url=https://web.archive.org/web/20180522111623/https://www.artificiallawyer.com/2018/05/04/hype-killer-only-1-of-companies-are-using-blockchain-gartner-reports/ |archive-date=22 May 2018}}</ref> For the year 2019 Gartner reported 5% of CIOs believed blockchain technology was a 'game-changer' for their business.<ref>Kasey Panetta. (31 October 2018). "Digital Business: CIO Agenda 2019: Exploit Transformational Technologies." [https://www.gartner.com/smarterwithgartner/cio-agenda-2019-exploit-transformational-technologies/ Gartner website] Retrieved 27 March 2021.</ref>
== Structure ==
[[File:Blockchain.svg|thumb|150px|Blockchain formation. The main chain (black) consists of the longest series of blocks from the genesis block (green) to the current block. Orphan blocks (purple) exist outside of the main chain.]]
A blockchain is a [[Decentralized computing|decentralized]], [[Distributed computing|distributed]], and oftentimes public, digital ledger consisting of records called ''blocks'' that are used to record transactions across many computers so that any involved block cannot be altered retroactively, without the alteration of all subsequent blocks.{{R|te20151031}}<ref>{{Cite news |last=Armstrong |first=Stephen |date=7 November 2016 |title=Move over Bitcoin, the blockchain is only just getting started |magazine=[[Wired (magazine)|Wired]] |url=https://www.wired.co.uk/article/unlock-the-blockchain |url-status=live |access-date=2016-11-09 |archive-url=https://web.archive.org/web/20161108130946/http://www.wired.co.uk/article/unlock-the-blockchain |archive-date=8 November 2016}}</ref> This allows the participants to verify and audit transactions independently and relatively inexpensively.<ref>{{Cite journal |last1=Catalini |first1=Christian |last2=Gans |first2=Joshua S. |date=23 November 2016 |title=Some Simple Economics of the Blockchain |journal=SSRN |url=http://www.nber.org/papers/w22952.pdf |url-status=live |doi=10.2139/ssrn.2874598 |hdl=1721.1/130500 |ssrn=2874598 |archive-url=https://web.archive.org/web/20200306014931/https://www.nber.org/papers/w22952.pdf |archive-date=6 March 2020 |access-date=16 September 2019 |s2cid=46904163}}</ref> A blockchain database is managed autonomously using a [[peer-to-peer]] network and a distributed timestamping server. They are [[Authentication|authenticated]] by [[mass collaboration]] powered by [[collective]] [[self-interest]]s.<ref>{{Cite magazine |last1=Tapscott |first1=Don |last2=Tapscott |first2=Alex |author-link2=Alex Tapscott |date=8 May 2016 |title=Here's Why Blockchains Will Change the World |url=http://fortune.com/2016/05/08/why-blockchains-will-change-the-world/ |magazine=Fortune |archive-url=https://web.archive.org/web/20161113134748/http://fortune.com/2016/05/08/why-blockchains-will-change-the-world/ |archive-date=13 November 2016 |access-date=16 November 2016 |author-link1=Don Tapscott |url-status=live}}</ref> Such a design facilitates [[robustness (computer science)|robust]] [[workflow]] where participants' uncertainty regarding data security is marginal. The use of a blockchain removes the characteristic of infinite [[Reproduction (economics)|reproducibility]] from a digital asset. It confirms that each unit of value was transferred only once, solving the long-standing problem of [[double spending|double-spending]]. A blockchain has been described as a ''value-exchange protocol''.<ref>{{Cite magazine |last=Bheemaiah |first=Kariappa |date=January 2015 |title=Block Chain 2.0: The Renaissance of Money |url=https://www.wired.com/insights/2015/01/block-chain-2-0/ |magazine=[[Wired (magazine)|Wired]] |archive-url=https://web.archive.org/web/20161114001509/https://www.wired.com/insights/2015/01/block-chain-2-0/ |archive-date=14 November 2016 |access-date=13 November 2016 |url-status=live}}</ref> A blockchain can maintain [[title (property)|title rights]] because, when properly set up to detail the exchange agreement, it provides a record that compels [[offer and acceptance]].{{cn|date=June 2022}}
Logically, a blockchain can be seen as consisting of several layers:<ref>{{cite journal |last1=Chen |first1=Huashan |last2=Pendleton |first2=Marcus |last3=Njilla |first3=Laurent |last4=Xu |first4=Shouhuai |title=A Survey on Ethereum Systems Security: Vulnerabilities, Attacks, and Defenses |journal=ACM Computing Surveys |date=12 June 2020 |volume=53 |issue=3 |pages=3–4 |doi=10.1145/3391195 |arxiv=1908.04507 |s2cid=199551841 |url=https://arxiv.org/abs/1908.04507 |issn=0360-0300}}</ref>
* infrastructure (hardware)
* [[network layer|networking]] (node discovery, information propagation<ref>{{Cite thesis |title=Structured Information Flow (SIF) Framework for Automating End-to-End Information Flow for Large Organizations |url=https://vtechworks.lib.vt.edu/handle/10919/31148 |publisher=Virginia Tech |date=2006-02-02 |language=en |first=Bhatia |last=Shishir}}</ref> and verification)
* [[consensus (computer science)|consensus]] ([[proof of work]], [[proof of stake]])
* data (blocks, transactions)
* [[application layer|application]] ([[smart contract]]s/[[decentralized application]]s, if applicable)
===Blocks===
Blocks hold batches of valid [[Transaction processing|transactions]] that are hashed and encoded into a [[Merkle tree]].<ref name="te20151031" /> Each block includes the [[cryptographic hash]] of the prior block in the blockchain, linking the two. The linked blocks form a chain.<ref name="te20151031" /> This [[Iteration|iterative]] process confirms the integrity of the previous block, all the way back to the initial block, which is known as the '''''genesis block'''''<!-- bolded per [[WP:MOSBOLD]], as a redirect target -->.<ref name="hadc" /> To assure the integrity of a block and the data contained in it, the block is usually [[Digital signature|digitally signed]].{{sfn|Knirsch|Unterweger|Engel|2019|page=2}}
Sometimes separate blocks can be produced concurrently, creating a temporary [[Fork (blockchain)|fork]]. In addition to a secure [[Hash-based cryptography|hash-based]] history, any blockchain has a specified algorithm for scoring different versions of the history so that one with a higher score can be selected over others. Blocks not selected for inclusion in the chain are called orphan blocks.<ref name="hadc" /> Peers supporting the database have different versions of the history from time to time. They keep only the highest-scoring version of the database known to them. Whenever a peer receives a higher-scoring version (usually the old version with a single new block added) they extend or overwrite their own database and retransmit the improvement to their peers. There is never an absolute guarantee that any particular entry will remain in the best version of history forever. Blockchains are typically built to add the score of new blocks onto old blocks and are given incentives to extend with new blocks rather than overwrite old blocks. Therefore, the probability of an entry becoming superseded decreases exponentially<ref name="bsm">{{Cite web |last=Antonopoulos |first=Andreas |date=20 February 2014 |title=Bitcoin security model: trust by computation |url=http://radar.oreilly.com/2014/02/bitcoin-security-model-trust-by-computation.html |url-status=live |archive-url=https://web.archive.org/web/20161031132328/http://radar.oreilly.com/2014/02/bitcoin-security-model-trust-by-computation.html |archive-date=31 October 2016 |access-date=19 November 2016 |website=Radar |publisher=O'Reilly}}</ref> as more blocks are built on top of it, eventually becoming very low.{{R|te20151031}}{{r|t12|p=ch. 08}}{{r|paper}} For example, bitcoin uses a [[proof-of-work system]], where the chain with the most cumulative proof-of-work is considered the valid one by the network. There are a number of methods that can be used to demonstrate a sufficient level of [[computation]]. Within a blockchain the computation is carried out redundantly rather than in the traditional segregated and [[Parallel computing|parallel]] manner.<ref>{{Cite web |title=Permissioned Blockchains |url=https://monax.io/explainers/permissioned_blockchains/ |url-status=live |archive-url=https://web.archive.org/web/20161120154948/https://monax.io/explainers/permissioned_blockchains/ |archive-date=20 November 2016 |access-date=20 November 2016 |website=Explainer |publisher=Monax}}</ref>
====Block time====
The ''block time'' is the average time it takes for the network to generate one extra block in the blockchain. Some blockchains create a new block as frequently as every five seconds.<ref>{{Cite book|last1=Strydom|first1=Moses|url=http://www.igi-global.com/book/big-data-potential-disruptive-innovation/222833|title=AI and Big Data's Potential for Disruptive Innovation|last2=Buckley|first2=Sheryl|date=July 2019|publisher=IGI Global|isbn=978-1-5225-9687-5|language=en}}</ref> By the time of block completion, the included data becomes verifiable. In cryptocurrency, this is practically when the transaction takes place, so a shorter block time means faster transactions. The block time for [[Ethereum]] is set to between 14 and 15 seconds, while for bitcoin it is on average 10 minutes.<ref>{{Cite journal |last1=Kumar |first1=Randhir |last2=Tripathi |first2=Rakesh |date=November 2019 |title=Implementation of Distributed File Storage and Access Framework using IPFS and Blockchain |journal=2019 Fifth International Conference on Image Information Processing (ICIIP) |publisher=IEEE |pages=246–251 |doi=10.1109/iciip47207.2019.8985677 |isbn=978-1-7281-0899-5 |s2cid=211119043}}</ref>
==== Hard forks ====
{{Excerpt|Fork (blockchain)|Hard fork}}
=== Decentralization ===
By storing data across its [[Peer-to-peer|peer-to-peer network]], the blockchain eliminates a number of risks that come with data being held centrally.{{R|te20151031}} The decentralized blockchain may use [[ad hoc]] [[message passing]] and [[distributed networking]]. One risk of a lack of decentralization is a so-called "51% attack" where a central entity can gain control of more than half of a network and can manipulate that specific blockchain record at will, allowing double-spending.<ref>{{Cite web|title=Bitcoin Spinoff Hacked in Rare '51% Attack'|url=https://fortune.com/2018/05/29/bitcoin-gold-hack/|access-date=2021-04-28|website=Fortune|language=en|archive-date=1 July 2022|archive-url=https://web.archive.org/web/20220701172330/https://fortune.com/2018/05/29/bitcoin-gold-hack/|url-status=dead}}</ref>
Peer-to-peer blockchain networks lack centralized points of vulnerability that [[Security hacker|computer crackers]] can exploit; likewise, they have no central point of [[failure]]. Blockchain security methods include the use of [[public-key cryptography]].{{r|primer|p=5}} A ''public key'' (a long, random-looking string of numbers) is an address on the blockchain. Value tokens sent across the network are recorded as belonging to that address. A ''private key'' is like a password that gives its owner access to their digital assets or the means to otherwise interact with the various capabilities that blockchains now support. Data stored on the blockchain is generally considered incorruptible.{{R|te20151031}}
Every [[Node (networking)|node]] in a decentralized system has a copy of the blockchain. [[Data quality]] is maintained by massive database [[Replication (computing)|replication]]<ref>{{Cite book |last=Raval |first=Siraj |url={{google books|fvywDAAAQBAJ|plainurl =yes}} |title=Decentralized Applications: Harnessing Bitcoin's Blockchain Technology |publisher=O'Reilly Media, Inc. |year=2016 |isbn=978-1-4919-2452-5 |pages=[ 1]–[{{google books |chapter=What Is a Decentralized Application? |oclc=968277125 |access-date=6 November 2016 |chapter-url= |via=Google Books |fvywDAAAQBAJ |plainurl=yes}} 2]}}</ref> and [[computational trust]]. No centralized "official" copy exists and no user is "trusted" more than any other.{{r|primer}} Transactions are broadcast to the network using the software. Messages are delivered on a [[Best-effort delivery|best-effort]] basis. Early blockchains rely on energy-intensive mining nodes to validate transactions,<ref name="hadc" /> add them to the block they are building, and then [[Broadcasting (networking)|broadcast]] the completed block to other nodes.{{R|t12|p=ch. 08}} Blockchains use various time-stamping schemes, such as [[Proof-of-work system|proof-of-work]], to serialize changes.{{r|kopstein}} Later consensus methods include [[proof-of-stake|proof of stake]].<ref name="hadc">{{Cite book |last1=Bhaskar |first1=Nirupama Devi |title=Handbook of Digital Currency |last2=Chuen |first2=David LEE Kuo |year=2015 |isbn=978-0-12-802117-0 |pages=45–65 |chapter=Bitcoin Mining Technology |doi=10.1016/B978-0-12-802117-0.00003-5}}</ref> The growth of a decentralized blockchain is accompanied by the risk of [[Centrality|centralization]] because the computer resources required to process larger amounts of data become more expensive.<ref>{{Cite web |last1=Gervais |first1=Arthur |last2=Karame |first2=Ghassan O. |last3=Capkun |first3=Vedran |last4=Capkun |first4=Srdjan |title=Is Bitcoin a Decentralized Currency? |url=https://www.infoq.com/articles/is-bitcoin-a-decentralized-currency/ |url-status=live |archive-url=https://web.archive.org/web/20161010042659/https://www.infoq.com/articles/is-bitcoin-a-decentralized-currency/ |archive-date=10 October 2016 |access-date=11 October 2016 |website=InfoQ |publisher=InfoQ & IEEE computer society}}</ref>
=== Openness ===
Open blockchains are more [[usability|user-friendly]] than some traditional ownership records, which, while open to the public, still require physical access to view. Because all early blockchains were permissionless, controversy has arisen over the blockchain definition. An issue in this ongoing debate is whether a private system with verifiers tasked and authorized (permissioned) by a central authority should be considered a blockchain.{{R|t16|t10|t11|t8|t9}} Proponents of permissioned or private chains argue that the term "blockchain" may be applied to any '''data structure'''<!-- bolded per [[WP:MOSBOLD as a redirect target --> that batches data into time-stamped blocks. These blockchains serve as a distributed version of [[multiversion concurrency control]] (MVCC) in databases.{{R|t4}} Just as MVCC prevents two transactions from concurrently modifying a single object in a database, blockchains prevent two transactions from spending the same single output in a blockchain.<ref name="tapscott201605">{{Cite book|last1=Tapscott|first1=Don|title=The Blockchain Revolution: How the Technology Behind Bitcoin is Changing Money, Business, and the World|last2=Tapscott|first2=Alex|date=May 2016|isbn=978-0-670-06997-2|author-link=Don Tapscott|author-link2=Alex Tapscott}}</ref>{{rp|30–31}} Opponents say that permissioned systems resemble traditional corporate databases, not supporting decentralized data verification, and that such systems are not hardened against operator tampering and revision.{{R|t16|t11}} Nikolai Hampton of ''[[Computerworld]]'' said that "many in-house blockchain solutions will be nothing more than cumbersome databases," and "without a clear security model, proprietary blockchains should be eyed with suspicion."<ref name="cw20160905" /><ref>{{Cite news |last=Barry |first=Levine |date=11 June 2018 |title=A new report bursts the blockchain bubble |publisher=MarTech |url=https://martechtoday.com/a-new-report-bursts-the-blockchain-bubble-216959 |url-status=live |access-date=13 July 2018 |archive-url=https://web.archive.org/web/20180713232406/https://martechtoday.com/a-new-report-bursts-the-blockchain-bubble-216959 |archive-date=13 July 2018}}</ref>
==== Permissionlessness ====
An advantage to an open, permissionless, or public, blockchain network is that guarding against bad actors is not required and no [[access control]] is needed.<ref name="bsm" /> This means that applications can be added to the network without the approval or trust of others, using the blockchain as a [[transport layer]].<ref name="bsm" />
Bitcoin and other cryptocurrencies currently secure their blockchain by requiring new entries to include proof of work. To prolong the blockchain, bitcoin uses [[Hashcash]] puzzles. While Hashcash was designed in 1997 by [[Adam Back]], the original idea was first proposed by [[Cynthia Dwork]] and [[Moni Naor]] and Eli Ponyatovski in their 1992 paper "Pricing via Processing or Combatting Junk Mail".
In 2016, [[venture capital]] investment for blockchain-related projects was weakening in the USA but increasing in China.<ref name="btt17" /> Bitcoin and many other cryptocurrencies use open (public) blockchains. , bitcoin has the highest [[market capitalization]].
==== Permissioned (private) blockchain ====
{{See also|Distributed ledger}}
Permissioned blockchains use an access control layer to govern who has access to the network.<ref name="btit">{{Cite news |last=Bob Marvin |date=30 August 2017 |title=Blockchain: The Invisible Technology That's Changing the World |work=PC MAG Australia |publisher=ZiffDavis, LLC |url=http://au.pcmag.com/amazon-web-services/46389/feature/blockchain-the-invisible-technology-thats-changing-the-world |url-status=live |access-date=25 September 2017 |archive-url=https://web.archive.org/web/20170925180800/http://au.pcmag.com/amazon-web-services/46389/feature/blockchain-the-invisible-technology-thats-changing-the-world |archive-date=25 September 2017}}</ref> In contrast to public blockchain networks, validators on private blockchain networks are vetted by the network owner. They do not rely on anonymous nodes to validate transactions nor do they benefit from the [[network effect]].<ref>{{Cite web|last=Prisco|first=Giulio|title=Sandia National Laboratories Joins the War on Bitcoin Anonymity|url=https://bitcoinmagazine.com/culture/sandia-national-laboratories-joins-the-war-on-bitcoin-anonymity-1472151009|website=Bitcoin Magazine: Bitcoin News, Articles, Charts, and Guides|language=en|access-date=20 May 2022}}</ref> Permissioned blockchains can also go by the name of 'consortium' blockchains.<ref>{{Cite web|title=Blockchains & Distributed Ledger Technologies|url=https://blockchainhub.net/blockchains-and-distributed-ledger-technologies-in-general/|website=BlockchainHub|language=en-US|access-date=20 May 2022|archive-date=19 January 2018|archive-url=https://web.archive.org/web/20180119120102/https://blockchainhub.net/blockchains-and-distributed-ledger-technologies-in-general/}}</ref> It has been argued that permissioned blockchains can guarantee a certain level of decentralization, if carefully designed, as opposed to permissionless blockchains, which are often centralized in practice.<ref name="auto" />
====Disadvantages of private blockchain====
Nikolai Hampton pointed out in ''[[Computerworld]]'' that "There is also no need for a '51 percent' attack on a private blockchain, as the private blockchain (most likely) already controls 100 percent of all block creation resources. If you could attack or damage the blockchain creation tools on a private corporate server, you could effectively control 100 percent of their network and alter transactions however you wished."<ref name="cw20160905" /> This has a set of particularly profound adverse implications during a [[financial crisis]] or [[debt crisis]] like the [[financial crisis of 2007–08]], where politically powerful actors may make decisions that favor some groups at the expense of others,<ref>{{Cite web |last1=O'Keeffe, M. |last2=Terzi, A. |date=7 July 2015 |title=The political economy of financial crisis policy |url=http://bruegel.org/2015/07/the-political-economy-of-financial-crisis-policy/ |url-status=live |archive-url=https://web.archive.org/web/20180519025919/http://bruegel.org/2015/07/the-political-economy-of-financial-crisis-policy/ |archive-date=19 May 2018 |access-date=8 May 2018 |website=Bruegel}}</ref> and "the bitcoin blockchain is protected by the massive group mining effort. It's unlikely that any private blockchain will try to protect records using [[gigawatts]] of computing power — it's time-consuming and expensive."<ref name="cw20160905" /> He also said, "Within a private blockchain there is also no 'race'; there's no incentive to use more power or discover blocks faster than competitors. This means that many in-house blockchain solutions will be nothing more than cumbersome databases."<ref name="cw20160905">{{Cite magazine |last=Hampton |first=Nikolai |date=5 September 2016 |title=Understanding the blockchain hype: Why much of it is nothing more than snake oil and spin |url=https://www2.computerworld.com.au/article/606253/understanding-blockchain-hype-why-much-it-nothing-more-than-snake-oil-spin/ |magazine=[[Computerworld]] |archive-url=https://web.archive.org/web/20160906171838/http://www.computerworld.com.au/article/606253/understanding-blockchain-hype-why-much-it-nothing-more-than-snake-oil-spin/ |archive-date=6 September 2016 |access-date=2016-09-05 |url-status=live}}</ref>
==== Blockchain analysis ====
The [[Blockchain analysis|analysis of public blockchains]] has become increasingly important with the popularity of [[bitcoin]], [[Ethereum]], [[litecoin]] and other [[cryptocurrencies]].<ref>{{Cite journal |last=Dr Garrick Hileman & Michel Rauchs |year=2017 |title=GLOBAL CRYPTOCURRENCY BENCHMARKING STUDY |url=https://cdn.crowdfundinsider.com/wp-content/uploads/2017/04/Global-Cryptocurrency-Benchmarking-Study.pdf |url-status=live |journal=Cambridge Centre for Alternative Finance |publisher=University of Cambridge Judge Business School |archive-url=https://web.archive.org/web/20190515100308/https://cdn.crowdfundinsider.com/wp-content/uploads/2017/04/Global-Cryptocurrency-Benchmarking-Study.pdf |archive-date=15 May 2019 |access-date=15 May 2019 |via=crowdfundinsider}}</ref> A blockchain, if it is public, provides anyone who wants access to observe and analyse the chain data, given one has the know-how. The process of understanding and accessing the flow of crypto has been an issue for many cryptocurrencies, crypto exchanges and banks.<ref>{{Cite journal |last=Raymaekers |first=Wim |date=March 2015 |title=Cryptocurrency Bitcoin: Disruption, challenges and opportunities |url=https://www.ingentaconnect.com/content/hsp/jpss/2015/00000009/00000001/art00005 |url-status=live |archive-url=https://web.archive.org/web/20190515100259/https://www.ingentaconnect.com/content/hsp/jpss/2015/00000009/00000001/art00005 |archive-date=15 May 2019 |access-date=2019-05-15 |journal=Journal of Payments Strategy & Systems |volume=9 |issue=1 |pages=30–46 |language=en}}</ref><ref>{{Cite news |date=2019-03-03 |title=Why Crypto Companies Still Can't Open Checking Accounts |language=en |url=https://www.bloomberg.com/news/articles/2019-03-03/why-crypto-companies-still-can-t-open-checking-accounts |url-status=live |access-date=2019-06-04 |archive-url=https://web.archive.org/web/20190604091359/https://www.bloomberg.com/news/articles/2019-03-03/why-crypto-companies-still-can-t-open-checking-accounts |archive-date=4 June 2019}}</ref> The reason for this is accusations of blockchain-enabled cryptocurrencies enabling illicit [[Darknet market|dark market]] trade of drugs, weapons, money laundering, etc.<ref>{{Cite journal |last=Christian Brenig, Rafael Accorsi & Günter Müller |date=Spring 2015 |title=Economic Analysis of Cryptocurrency Backed Money Laundering |url=https://aisel.aisnet.org/cgi/viewcontent.cgi?article=1019&context=ecis2015_cr |url-status=live |journal=Association for Information Systems AIS Electronic Library (AISeL) |archive-url=https://web.archive.org/web/20190828223207/https://aisel.aisnet.org/cgi/viewcontent.cgi?article=1019&context=ecis2015_cr |archive-date=28 August 2019 |access-date=15 May 2019}}</ref> A common belief has been that cryptocurrency is private and untraceable, thus leading many actors to use it for illegal purposes. This is changing and now specialised tech companies provide blockchain tracking services, making crypto exchanges, law-enforcement and banks more aware of what is happening with crypto funds and [[Fiat money|fiat]]-crypto exchanges. The development, some argue, has led criminals to prioritise the use of new cryptos such as [[Monero (cryptocurrency)|Monero]].<ref>{{Cite news |last=Greenberg |first=Andy |date=25 January 2017 |title=Monero, the Drug Dealer's Cryptocurrency of Choice, Is on Fire |magazine=Wired |url=https://www.wired.com/2017/01/monero-drug-dealers-cryptocurrency-choice-fire/ |url-status=live |access-date=2019-05-15 |archive-url=https://web.archive.org/web/20181210020727/https://www.wired.com/2017/01/monero-drug-dealers-cryptocurrency-choice-fire/ |archive-date=10 December 2018 |issn=1059-1028}}</ref><ref>{{Cite web |last=Orcutt |first=Mike |title=It's getting harder to hide money in Bitcoin |url=https://www.technologyreview.com/s/608763/criminals-thought-bitcoin-was-the-perfect-hiding-place-they-thought-wrong/ |access-date=2019-05-15 |website=MIT Technology Review |language=en-US}}</ref><ref>{{Cite news |date=15 May 2019 |title=Explainer: 'Privacy coin' Monero offers near total anonymity |language=en |work=Reuters |url=https://uk.reuters.com/article/us-crypto-currencies-altcoins-explainer-idUKKCN1SL0F0 |url-status=live |access-date=2019-05-15 |archive-url=https://web.archive.org/web/20190515075334/https://uk.reuters.com/article/us-crypto-currencies-altcoins-explainer-idUKKCN1SL0F0 |archive-date=15 May 2019 }} {{Webarchive|url=https://web.archive.org/web/20190515075334/https://uk.reuters.com/article/us-crypto-currencies-altcoins-explainer-idUKKCN1SL0F0 |date=15 May 2019 }}</ref> The question is about the public accessibility of blockchain data and the personal privacy of the very same data. It is a key debate in cryptocurrency and ultimately in the blockchain.<ref>{{Cite web |date=7 April 2018 |title=An Untraceable Currency? Bitcoin Privacy Concerns - FinTech Weekly |url=https://magazine.fintechweekly.com/articles/an-untraceable-currency-bitcoin-privacy-concerns |url-status=live |archive-url=https://web.archive.org/web/20190515100301/https://magazine.fintechweekly.com/articles/an-untraceable-currency-bitcoin-privacy-concerns |archive-date=15 May 2019 |access-date=2019-05-15 |website=FinTech Magazine Article |language=en}}</ref>
===Standardisation===
In April 2016, [[Standards Australia]] submitted a proposal to the [[International Organization for Standardization]] to consider developing standards to support blockchain technology. This proposal resulted in the creation of ISO Technical Committee 307, Blockchain and Distributed Ledger Technologies.<ref name="Blockchain">{{cite web |title=Blockchain |url=https://www.standards.org.au/engagement-events/flagship-projects/blockchain |website=standards.org.au |publisher=Standards Australia |access-date=21 June 2021}}</ref> The technical committee has working groups relating to blockchain terminology, reference architecture, security and privacy, identity, smart contracts, governance and interoperability for blockchain and DLT, as well as standards specific to industry sectors and generic government requirements.<ref name="ISO/TC 307 Blockchain and distributed ledger technologies">{{cite web |title=ISO/TC 307 Blockchain and distributed ledger technologies |url=https://www.iso.org/committee/6266604.html |website=iso.org |publisher=ISO |access-date=21 June 2021}}</ref> More than 50 countries are participating in the standardization process together with external liaisons such as the [[Society for Worldwide Interbank Financial Telecommunication]] (SWIFT), the [[European Commission]], the [[International Federation of Surveyors]], the [[International Telecommunication Union]] (ITU) and the [[United Nations Economic Commission for Europe]] (UNECE).<ref name="ISO/TC 307 Blockchain and distributed ledger technologies"/>
Many other national standards bodies and open standards bodies are also working on blockchain standards.<ref>{{cite web |last1=Deshmukh |first1=Sumedha |last2=Boulais |first2=Océane |last3=Koens |first3=Tommy |title=Global Standards Mapping Initiative: An overview of blockchain technical standards |url=http://www3.weforum.org/docs/WEF_GSMI_Technical_Standards_2020.pdf |website=weforum.org |publisher=World Economic Forum |access-date=23 June 2021}}</ref> These include the [[National Institute of Standards and Technology]]<ref name="BLOCKCHAIN Overview">{{cite web |title=Blockchain Overview |date=25 September 2019 |url=https://www.nist.gov/blockchain |publisher=NIST |access-date=21 June 2021}}</ref> (NIST), the [[European Committee for Electrotechnical Standardization]]<ref name="CEN and CENELEC publish a White Paper on standards in Blockchain & Distributed Ledger Technologies">{{cite web |title=CEN and CENELEC publish a White Paper on standards in Blockchain & Distributed Ledger Technologies |url=https://www.cencenelec.eu/news/brief_news/Pages/TN-2018-085.aspx |website=cencenelec.eu |publisher=CENELEC |access-date=21 June 2021}}</ref> (CENELEC), the [[Institute of Electrical and Electronics Engineers]]<ref name="Standards">{{cite web |title=Standards |url=https://blockchain.ieee.org/standards |website=ieee.org |publisher=IEEE Blockchain |access-date=21 June 2021}}</ref> (IEEE), the Organization for the Advancement of Structured Information Standards ([[OASIS (organization)|OASIS]]), and some individual participants in the [[Internet Engineering Task Force]]<ref name="An Interoperability Architecture for Blockchain/DLT Gateways draft-hardjono-blockchain-interop-arch-02">{{cite web |last1=Hardjono |first1=Thomas |title=An Interoperability Architecture for Blockchain/DLT Gateways |url=https://www.ietf.org/id/draft-hardjono-blockchain-interop-arch-02.txt |website=ietf.org |publisher=IETF |access-date=21 June 2021}}</ref> (IETF).
===Centralized Blockchain===
Although most of blockchain implementation are decentralised and distributed, [[Oracle Corporation|Oracle]] launched a centralised blockchain table feature in [[Oracle Database|Oracle 21c database]]. The Blockchain Table in [[Oracle Database|Oracle 21c database]] is a centralised blockchain which provide immutable feature. Compared to decentralized blockchains, centralized blockchains normally can provide a higher throughput and lower latency of transactions than consensus-based distributed blockchains.<ref name="OracleBlockchainDetails" /><ref name="OracleBlockchainTable" />
==Types==
Currently, there are at least four types of blockchain networks — public blockchains, private blockchains, [[consortium]] blockchains and hybrid blockchains.
===Public blockchains===
A public blockchain has absolutely no access restrictions. Anyone with an [[Internet]] connection can send [[Financial transaction|transactions]] to it as well as become a [[validator]] (i.e., participate in the execution of a [[Consensus (computer science)|consensus protocol]]).<ref>{{Cite web |title=How Companies Can Leverage Private Blockchains to Improve Efficiency and Streamline Business Processes |url=https://perfectial.com/blog/leveraging-private-blockchains-improve-efficiency-streamline-business-processes/ |website=Perfectial}}</ref> Usually, such networks offer [[Incentive|economic incentives]] for those who secure them and utilize some type of a [[Proof-of-stake|Proof of Stake]] or [[Proof-of-work system|Proof of Work]] algorithm.
Some of the largest, most known public blockchains are the bitcoin blockchain and the Ethereum blockchain.
===Private blockchains===
A private blockchain is permissioned.<ref name="btit" /> One cannot join it unless invited by the network administrators. Participant and validator access is [[Closed platform|restricted]]. To distinguish between open blockchains and other peer-to-peer decentralized database applications that are not open ad-hoc compute clusters, the terminology [[Distributed Ledger]] (DLT) is normally used for private blockchains.
===Hybrid blockchains===
A hybrid blockchain has a combination of centralized and decentralized features.<ref>[Distributed Ledger Technology: Hybrid Approach, Front-to-Back Designing and Changing Trade Processing Infrastructure, By Martin Walker, First published:, 24 OCT 2018 {{ISBN|978-1-78272-389-9}}]</ref> The exact workings of the chain can vary based on which portions of centralization and decentralization are used.
===Sidechains===
A sidechain is a designation for a blockchain ledger that runs in parallel to a primary blockchain.<ref>{{Cite book |last=Siraj Raval |url=https://books.google.com/books?id=fvywDAAAQBAJ&pg=PA22 |title=Decentralized Applications: Harnessing Bitcoin's Blockchain Technology |date=18 July 2016 |publisher="O'Reilly Media, Inc." |isbn=978-1-4919-2452-5 |pages=22–}}</ref><ref>{{Cite book |last=Niaz Chowdhury |url=https://books.google.com/books?id=_HatDwAAQBAJ&pg=PA22 |title=Inside Blockchain, Bitcoin, and Cryptocurrencies |date=16 August 2019 |publisher=CRC Press |isbn=978-1-00-050770-6 |pages=22–}}</ref> Entries from the primary blockchain (where said entries typically represent [[digital asset]]s) can be linked to and from the sidechain; this allows the sidechain to otherwise operate independently of the primary blockchain (e.g., by using an alternate means of record keeping, alternate [[Consensus (computer science)|consensus algorithm]], etc.).<ref></ref>
== Uses ==
[[File:Bitcoin.svg|thumb|220x220px|[[Bitcoin|Bitcoin's]] transactions are recorded on a publicly viewable blockchain.]]
Blockchain technology can be integrated into multiple areas. The primary use of blockchains is as a [[distributed ledger]] for [[cryptocurrency|cryptocurrencies]] such as [[bitcoin]]; there were also a few other operational products that had matured from [[proof of concept]] by late 2016.<ref name="btt17">{{Cite web |last=Ovenden |first=James |title=Blockchain Top Trends In 2017 |url=https://channels.theinnovationenterprise.com/articles/blockchain-top-trends-in-2017 |url-status=live |archive-url=https://web.archive.org/web/20161130143727/https://channels.theinnovationenterprise.com/articles/blockchain-top-trends-in-2017 |archive-date=30 November 2016 |access-date=4 December 2016 |publisher=The Innovation Enterprise}}</ref> As of 2016, some businesses have been testing the technology and conducting low-level implementation to gauge blockchain's effects on organizational efficiency in their [[back office]].<ref>{{Cite news |last=Katie Martin |date=27 September 2016 |title=CLS dips into blockchain to net new currencies |work=Financial Times |url=https://www.ft.com/content/c905b6fc-4dd2-3170-9d2a-c79cdbb24f16 |url-status=live |access-date=7 November 2016 |archive-url=https://web.archive.org/web/20161109152317/https://www.ft.com/content/c905b6fc-4dd2-3170-9d2a-c79cdbb24f16 |archive-date=9 November 2016}}</ref>
In 2019, it was estimated that around $2.9 billion were invested in blockchain technology, which represents an 89% increase from the year prior. Additionally, the International Data Corp has estimated that corporate investment into blockchain technology will reach $12.4 billion by 2022.<ref>{{Cite news|first=Michael|last=Castillo|date=16 April 2019|title=blockchain 50: Billion Dollar Babies|work=Financial Website|publisher=SourceMedia|url=https://www.forbes.com/sites/michaeldelcastillo/2019/04/16/blockchain-50-billion-dollar-babies/|access-date=1 February 2021}}</ref> Furthermore, According to [[PricewaterhouseCoopers]] (PwC), the second-largest professional services network in the world, blockchain technology has the potential to generate an annual business value of more than $3 trillion by 2030. PwC's estimate is further augmented by a 2018 study that they have conducted, in which PwC surveyed 600 business executives and determined that 84% have at least some exposure to utilizing blockchain technology, which indicts a significant demand and interest in blockchain technology.<ref>{{Cite news|first=Steve|last=Davies|date=2018|title=PwC's Global Blockchain Survey|work=Financial Website|publisher=SourceMedia|url=https://www.pwc.com/gx/en/industries/technology/blockchain/blockchain-in-business.html|access-date=1 February 2021}}</ref>
Individual use of blockchain technology has also greatly increased since 2016. According to statistics in 2020, there were more than 40 million blockchain wallets in 2020 in comparison to around 10 million blockchain wallets in 2016.<ref>{{Cite news|first=Shanhong|last=Liu|date=13 March 2020|title=Blockchain - Statistics & Facts|work=Statistics Website|publisher=SourceMedia|url=https://www.statista.com/topics/5122/blockchain/#dossierSummary__chapter6|access-date=17 February 2021}}</ref>
=== Cryptocurrencies ===
{{main|Cryptocurrency}}
Most cryptocurrencies use blockchain technology to record transactions. For example, the [[bitcoin network]] and [[Ethereum]] network are both based on blockchain. On 8 May 2018 [[Facebook]] confirmed that it would open a new blockchain group<ref>{{Cite web |last=Wagner |first=Kurt |date=8 May 2018 |title=Facebook is making its biggest executive shuffle in company history |url=https://www.recode.net/2018/5/8/17330226/facebook-reorg-mark-zuckerberg-whatsapp-messenger-ceo-blockchain |url-status=live |archive-url=https://web.archive.org/web/20180722053519/https://www.recode.net/2018/5/8/17330226/facebook-reorg-mark-zuckerberg-whatsapp-messenger-ceo-blockchain |archive-date=22 July 2018 |access-date=25 September 2018 |website=Recode}}</ref> which would be headed by [[David A. Marcus|David Marcus]], who previously was in charge of [[Facebook Messenger|Messenger]]. Facebook's planned cryptocurrency platform, [[Libra (cryptocurrency)|Libra]] (now known as Diem), was formally announced on June 18, 2019.<ref>{{Cite news |last1=Isaac |first1=Mike |last2=Popper |first2=Nathaniel |date=18 June 2019 |title=Facebook Plans Global Financial System Based on Cryptocurrency |work=[[The New York Times]] |url=https://www.nytimes.com/2019/06/18/technology/facebook-cryptocurrency-libra.html |url-status=live |access-date=18 June 2019 |archive-url=https://web.archive.org/web/20200519040623/https://www.nytimes.com/2019/06/18/technology/facebook-cryptocurrency-libra.html |archive-date=19 May 2020}}</ref><ref>{{Cite news |last=Constine |first=Josh |date=18 June 2019 |title=Facebook announces Libra cryptocurrency: All you need to know |work=[[TechCrunch]] |url=https://techcrunch.com/2019/06/18/facebook-libra/ |url-status=live |access-date=19 June 2019 |archive-url=https://web.archive.org/web/20190619003937/https://techcrunch.com/2019/06/18/facebook-libra/ |archive-date=19 June 2019}}</ref>
The criminal enterprise [[Silk Road (marketplace)|Silk Road]], which operated on [[Tor (anonymity network)|Tor]], utilized cryptocurrency for payments, some of which the [[US federal government]] has seized through research on the blockchain and [[Forfeiture (law)|forfeiture.]]<ref>KPIX-TV. (5 November 2020). "Silk Road: Feds Seize $1 Billion In Bitcoins Linked To Infamous Silk Road Dark Web Case; 'Where Did The Money Go'". [https://sanfrancisco.cbslocal.com/2020/11/05/silk-road-feds-seize-1-billion-in-bitcoins-linked-to-infamous-silk-road-dark-web-case-where-did-the-money-go/ KPIX website]{{Dead link|date=September 2022 |bot=InternetArchiveBot |fix-attempted=yes }} Retrieved 28 March 2021.</ref>
[[Legality of bitcoin by country or territory|Governments have mixed policies]] on the legality of their citizens or banks owning cryptocurrencies. China implements blockchain technology in several industries including a [[Central bank digital currency|national digital currency]] which launched in 2020.<ref>Aditi Kumar and Eric Rosenbach. (20 May 2020). "Could China's Digital Currency Unseat the Dollar?: American Economic and Geopolitical Power Is at Stake". [https://www.foreignaffairs.com/articles/china/2020-05-20/could-chinas-digital-currency-unseat-dollar Foreign Affairs website] Retrieved 31 March 2021.</ref> To strengthen their respective currencies, Western governments including the European Union and the United States have initiated similar projects.<ref>Staff. (16 February 2021). "The Economist Explains: What is the fuss over central-bank digital currencies?" [https://www.economist.com/the-economist-explains/2021/02/16/what-is-the-fuss-over-central-bank-digital-currencies The Economist website] Retrieved 1 April 2021.</ref>
=== Smart contracts ===
{{main|Smart contract}}
Blockchain-based [[smart contract]]s are proposed contracts that can be partially or fully executed or enforced without human interaction.<ref>{{Cite book |last=Franco |first=Pedro |url=https://books.google.com/books?id=YHfCBwAAQBAJ |title=Understanding Bitcoin: Cryptography, Engineering and Economics |publisher=John Wiley & Sons |year=2014 |isbn=978-1-119-01916-9 |page=9 |access-date=4 January 2017 |archive-url=https://web.archive.org/web/20170214204859/https://books.google.com/books?id=YHfCBwAAQBAJ |archive-date=14 February 2017 |url-status=live |via=Google Books}}</ref> One of the main objectives of a smart contract is [[automation|automated]] [[escrow]]. A key feature of smart contracts is that they do not need a trusted third party (such as a trustee) to act as an intermediary between contracting entities — the blockchain network executes the contract on its own. This may reduce friction between entities when transferring value and could subsequently open the door to a higher level of transaction automation.<ref>{{Cite book|last=Casey, Michael, 1967-|url=https://www.worldcat.org/oclc/1059331326|title=The impact of blockchain technology on finance : a catalyst for change|date=16 July 2018|isbn=978-1-912179-15-2|location=London, UK|oclc=1059331326}}</ref> An [[International Monetary Fund|IMF]] staff discussion from 2018 reported that smart contracts based on blockchain technology might reduce [[moral hazard]]s and optimize the use of contracts in general. But "no viable smart contract systems have yet emerged." Due to the lack of widespread use their legal status was unclear.<ref>{{Cite journal |last1=Governatori |first1=Guido |last2=Idelberger |first2=Florian |last3=Milosevic |first3=Zoran |last4=Riveret |first4=Regis |last5=Sartor |first5=Giovanni |last6=Xu |first6=Xiwei |year=2018 |title=On legal contracts, imperative and declarative smart contracts, and blockchain systems |journal=Artificial Intelligence and Law |language=en |volume=26 |issue=4 |pages=33 |doi=10.1007/s10506-018-9223-3 |s2cid=3663005}}</ref><ref>{{Cite book |url=https://www.imf.org/external/pubs/ft/sdn/2016/sdn1603.pdf |title=Virtual Currencies and Beyond: Initial Considerations |publisher=International Monetary Fund |year=2016 |isbn=978-1-5135-5297-2 |series=IMF Discussion Note |page=23 |access-date=2018-04-19 |archive-url=https://web.archive.org/web/20180414210721/https://www.imf.org/external/pubs/ft/sdn/2016/sdn1603.pdf |archive-date=14 April 2018 |url-status=live}}</ref>
===Financial services===
According to ''[[Reason (magazine)|Reason]]'', many banks have expressed interest in implementing [[distributed ledger]]s for use in [[banking]] and are cooperating with companies creating private blockchains,<ref>{{Cite news |last=Epstein |first=Jim |date=6 May 2016 |title=Is Blockchain Technology a Trojan Horse Behind Wall Street's Walled Garden? |work=[[Reason (magazine)|Reason]] |url=http://reason.com/reasontv/2016/05/06/bitcoin-consensus-blockchain-wall-street |url-status=live |access-date=2016-06-29 |archive-url=https://web.archive.org/web/20160708170429/http://reason.com/reasontv/2016/05/06/bitcoin-consensus-blockchain-wall-street |archive-date=8 July 2016 |quote=mainstream misgivings about working with a system that's open for anyone to use. Many banks are partnering with companies building so-called private blockchains that mimic some aspects of Bitcoin's architecture except they're designed to be closed off and accessible only to chosen parties. ... [but some believe] that open and permission-less blockchains will ultimately prevail even in the banking sector simply because they're more efficient.}}</ref><ref>{{Cite news |last=Redrup |first=Yolanda |date=29 June 2016 |title=ANZ backs private blockchain, but won't go public |work=Australia Financial Review |url=http://www.afr.com/technology/anz-backs-private-blockchain-but-wont-go-public-20160629-gpuf9z |url-status=live |access-date=2016-07-07 |archive-url=https://web.archive.org/web/20160703103852/http://www.afr.com/technology/anz-backs-private-blockchain-but-wont-go-public-20160629-gpuf9z |archive-date=3 July 2016 |quote=Blockchain networks can be either public or private. Public blockchains have many users and there are no controls over who can read, upload or delete the data and there are an unknown number of pseudonymous participants. In comparison, private blockchains also have multiple data sets, but there are controls in place over who can edit data and there are a known number of participants.}}</ref><ref>{{Cite web |last=Shah |first=Rakesh |date=1 March 2018 |title=How Can The Banking Sector Leverage Blockchain Technology? |url=http://www.postboxcommunications.com/blog/can-banking-sector-leverage-blockchain-technology/ |url-status=live |archive-url=https://web.archive.org/web/20180317232141/http://www.postboxcommunications.com/blog/can-banking-sector-leverage-blockchain-technology/ |archive-date=17 March 2018 |website=PostBox Communications |publisher=PostBox Communications Blog |quote=Banks preferably have a notable interest in utilizing Blockchain Technology because it is a great source to avoid fraudulent transactions. Blockchain is considered hassle free, because of the extra level of security it offers.}}</ref> and according to a September 2016 [[IBM]] study, this is occurring faster than expected.<ref>{{Cite news |last=Kelly, Jemima |date=28 September 2016 |title=Banks adopting blockchain 'dramatically faster' than expected: IBM |work=Reuters |url=https://www.reuters.com/article/us-tech-blockchain-ibm-idUSKCN11Y28D |url-status=live |access-date=2016-09-28 |archive-url=https://web.archive.org/web/20160928163048/http://www.reuters.com/article/us-tech-blockchain-ibm-idUSKCN11Y28D |archive-date=28 September 2016 |df=dmy-all}}</ref>
Banks are interested in this technology not least because it has the potential to speed up [[back office]] settlement systems.<ref>{{Cite news |last=Arnold |first=Martin |date=23 September 2013 |title=IBM in blockchain project with China UnionPay |work=Financial Times |url=https://www.ft.com/content/719f4e7e-80e1-11e6-bc52-0c7211ef3198 |url-status=live |access-date=7 November 2016 |archive-url=https://web.archive.org/web/20161109152822/https://www.ft.com/content/719f4e7e-80e1-11e6-bc52-0c7211ef3198 |archive-date=9 November 2016}}</ref> Moreover, as the blockchain industry has reached early maturity institutional appreciation has grown that it is, practically speaking, the infrastructure of a whole new financial industry, with all the implications which that entails.<ref>{{Cite web|url=https://corpgov.law.harvard.edu/2022/01/28/blockchain-in-the-banking-sector-a-review-of-the-landscape-and-opportunities/|title=Blockchain in the Banking Sector: A Review of the Landscape and Opportunities|first1=Arvind|last1=Ravichandran|first2=Christopher|last2=Fargo|first3=David|last3=Kappos|first4=David|last4=Portilla|first5=John|last5=Buretta|first6=Minh Van|last6=Ngo|first7=Sasha|last7=Rosenthal-Larrea|date=28 January 2022}}</ref>
[[Bank]]s such as [[UBS]] are opening new research labs dedicated to blockchain technology in order to explore how blockchain can be used in financial services to increase efficiency and reduce costs.<ref>{{Cite news |date=24 August 2016 |title=UBS leads team of banks working on blockchain settlement system |work=[[Reuters]] |url=https://www.reuters.com/article/us-banks-blockchain-ubs-idUSKCN10Z147 |url-status=live |access-date=13 May 2017 |archive-url=https://web.archive.org/web/20170519073429/http://www.reuters.com/article/us-banks-blockchain-ubs-idUSKCN10Z147 |archive-date=19 May 2017}}</ref><ref>{{Cite web |title=Cryptocurrency Blockchain |url=https://www.capgemini.com/beyond-the-buzz/cryptocurrency-blockchain |url-status=live |archive-url=https://web.archive.org/web/20161205202317/https://www.capgemini.com/beyond-the-buzz/cryptocurrency-blockchain |archive-date=5 December 2016 |access-date=13 May 2017 |website=capgemini.com}}</ref>
[[Berenberg Bank|Berenberg]], a German bank, believes that blockchain is an "overhyped technology" that has had a large number of "proofs of concept", but still has major challenges, and very few success stories.<ref>{{Cite news |last=Kelly |first=Jemima |date=31 October 2017 |title=Top banks and R3 build blockchain-based payments system |work=Reuters |url=https://www.reuters.com/article/us-banks-blockchain-r3/top-banks-and-r3-build-blockchain-based-payments-system-idUSKBN1D00ZB |url-status=live |access-date=9 July 2018 |archive-url=https://web.archive.org/web/20180710011735/https://www.reuters.com/article/us-banks-blockchain-r3/top-banks-and-r3-build-blockchain-based-payments-system-idUSKBN1D00ZB |archive-date=10 July 2018}}</ref>
The blockchain has also given rise to [[initial coin offering]]s (ICOs) as well as a new category of digital asset called security token offerings (STOs), also sometimes referred to as digital security offerings (DSOs).<ref>{{Cite web |title=Archived copy |url=https://www2.deloitte.com/content/dam/Deloitte/lu/Documents/technology/lu-token-assets-securities-tomorrow.pdf |url-status=live |archive-url=https://web.archive.org/web/20190623225917/https://www2.deloitte.com/content/dam/Deloitte/lu/Documents/technology/lu-token-assets-securities-tomorrow.pdf |archive-date=23 June 2019 |access-date=26 September 2019}}</ref> STO/DSOs may be conducted privately or on public, regulated stock exchange and are used to tokenize traditional assets such as company shares as well as more innovative ones like intellectual property, real estate,<ref>{{cite news |first=Jeff |last=Hammerberg |date=November 7, 2021 |url=https://www.washingtonblade.com/2021/11/07/potential-impact-of-blockchain-on-real-estate/ |title=Potential impact of blockchain on real estate |work=[[Washington Blade]]}}</ref> art, or individual products. A number of companies are active in this space providing services for compliant tokenization, private STOs, and public STOs.
===Games===
{{main|Blockchain game}}
Blockchain technology, such as cryptocurrencies and [[non-fungible token]]s (NFTs), has been used in video games for [[video game monetization|monetization]]. Many [[Games as a service|live-service games]] offer in-game customization options, such as character skins or other in-game items, which the players can earn and trade with other players using in-game currency. Some games also allow for trading of virtual items using real-world currency, but this may be illegal in some countries where video games are seen as akin to gambling, and has led to [[gray market]] issues such as [[skin gambling]], and thus publishers typically have shied away from allowing players to earn real-world funds from games.<ref name="Verge-Valve">{{cite news |last1=Clark |first1=Mitchell |title=Valve bans blockchain games and NFTs on Steam, Epic will try to make it work |url=https://www.theverge.com/2021/10/15/22728425/valve-steam-blockchain-nft-crypto-ban-games-age-of-rust |access-date=8 November 2021 |work=The Verge |date=15 October 2021 |language=en}}</ref> Blockchain games typically allow players to trade these in-game items for cryptocurrency, which can then be exchanged for money.<ref name="inverse blockchain games">{{cite web | url = https://www.inverse.com/gaming/blockchain-games-online-gaming | title = Blockchain Games Twist The Fundamentals Of Online Gaming | first = Mo | last = Mozuch | date = April 29, 2021 | accessdate = November 4, 2021 | work = [[Inverse (website)|Inverse]] }}</ref>
The first known game to use blockchain technologies was ''[[CryptoKitties]]'', launched in November 2017, where the player would purchase NFTs with Ethereum cryptocurrency, each NFT consisting of a [[virtual pet]] that the player could breed with others to create offspring with combined traits as new NFTs.<ref>{{Cite news |date=22 February 2018 |title=Internet firms try their luck at blockchain games |work=[[Asia Times]] |url=https://asiatimes.com/article/internet-firms-try-luck-blockchain-games/ |access-date=2018-02-28}}</ref><ref name="inverse blockchain games"/> The game made headlines in December 2017 when one virtual pet sold for more than [[United States dollar|US$]]100,000.<ref>{{Cite news |last=Evelyn Cheng |date=6 December 2017 |title=Meet CryptoKitties, the $100,000 digital beanie babies epitomizing the cryptocurrency mania |publisher=[[CNBC]] |url=https://www.cnbc.com/2017/12/06/meet-cryptokitties-the-new-digital-beanie-babies-selling-for-100k.html |url-status=live |access-date=2018-02-28 |archive-url=https://web.archive.org/web/20181120115903/https://www.cnbc.com/2017/12/06/meet-cryptokitties-the-new-digital-beanie-babies-selling-for-100k.html |archive-date=20 November 2018}}</ref> ''CryptoKitties'' also illustrated scalability problems for games on Ethereum when it created significant congestion on the Ethereum network in early 2018 with approximately 30% of all Ethereum transactions{{clarify|over what period of time? a day? a week? forever since 2018?|date=October 2021}} being for the game.<ref>{{Cite news |last=Laignee Barron |date=13 February 2018 |title=CryptoKitties is Going Mobile. Can Ethereum Handle the Traffic? |work=[[Fortune (magazine)|Fortune]] |url=http://fortune.com/2018/02/13/cryptokitties-ethereum-ios-launch-china-ether/ |url-status=live |access-date=2018-09-30 |archive-url=https://web.archive.org/web/20181028041832/http://fortune.com/2018/02/13/cryptokitties-ethereum-ios-launch-china-ether/ |archive-date=28 October 2018}}</ref><ref>{{Cite news |date=12 May 2017 |title=CryptoKitties craze slows down transactions on Ethereum |url=https://www.bbc.com/news/technology-42237162 |url-status=live |archive-url=https://web.archive.org/web/20180112143517/http://www.bbc.com/news/technology-42237162 |archive-date=12 January 2018}}</ref>
By the early 2020s, there had not been a breakout success in video games using blockchain, as these games tend to focus on using blockchain for speculation instead of more traditional forms of gameplay, which offers limited appeal to most players. Such games also represent a high risk to investors as their revenues can be difficult to predict.<ref name="inverse blockchain games"/> However, limited successes of some games, such as ''[[Axie Infinity]]'' during the [[COVID-19 pandemic]], and corporate plans towards [[metaverse]] content, refueled interest in the area of GameFi, a term describing the intersection of video games and financing typically backed by blockchain currency, in the second half of 2021.<ref>{{cite web | url = https://www.bloomberg.com/news/features/2021-10-30/what-is-the-metaverse-where-crypto-nft-capitalism-collide-in-games-like-axie | title = Into the Metaverse: Where Crypto, Gaming and Capitalism Collide | first1 = Charlie | last1 = Wells | first2 = Misrylena | last2 = Egkolfopoulou | date = October 30, 2021 | accessdate = November 11, 2021 | work = [[Bloomberg News]] }}</ref> Several major publishers, including [[Ubisoft]], [[Electronic Arts]], and [[Take Two Interactive]], have stated that blockchain and NFT-based games are under serious consideration for their companies in the future.<ref>{{cite web | url = https://arstechnica.com/gaming/2021/11/big-name-publishers-see-nfts-as-a-big-part-of-gamings-future/ | title = Big-name publishers see NFTs as a big part of gaming's future | first = Kyle | last = Orland | date = November 4, 2021 | accessdate = November 4, 2021 | work = [[Ars Technica]] }}</ref>
In October 2021, [[Valve Corporation]] banned blockchain games, including those using cryptocurrency and NFTs, from being hosted on its [[Steam (service)|Steam]] digital storefront service, which is widely used for personal computer gaming, claiming that this was an extension of their policy banning games that offered in-game items with real-world value. Valve's prior history with [[gambling]], specifically [[skin gambling]], was speculated to be a factor in the decision to ban blockchain games.<ref name=pcgamer/> Journalists and players responded positively to Valve's decision as blockchain and NFT games have a reputation for scams and fraud among most PC gamers,<ref name="Verge-Valve"/><ref name=pcgamer>{{cite news |last1=Knoop |first1=Joseph |title=Steam bans all games with NFTs or cryptocurrency |url=https://www.pcgamer.com/steam-bans-nfts-cryptocurrencies-blockchain/ |access-date=8 November 2021 |work=PC Gamer |date=15 October 2021 |language=en}}</ref> [[Epic Games]], which runs the [[Epic Games Store]] in competition to Steam, said that they would be open to accepted blockchain games, in the wake of Valve's refusal.<ref>{{cite web | url = https://www.theverge.com/2021/10/15/22729050/epic-game-store-open-to-blockchain-cryptocurrency-nft-games | title= Epic says it's 'open' to blockchain games after Steam bans them | first= Mitchell | last =Clark | date= October 15, 2021 | accessdate = November 11, 2021 | work = [[The Verge]] }}</ref>
===Supply chain===
There have been several different efforts to employ blockchains in [[supply chain management]].
* '''[[Mining|Precious commodities mining]]''' — Blockchain technology has been used for tracking the origins of gemstones and other precious commodities. In 2016, ''[[The Wall Street Journal]]'' reported that the blockchain technology company Everledger was partnering with [[IBM]]'s blockchain-based tracking service to trace the origin of diamonds to ensure that they were ethically mined.<ref>{{Cite news |last=Nash |first=Kim S. |date=14 July 2016 |title=IBM Pushes Blockchain into the Supply Chain |work=[[The Wall Street Journal]] |url=https://www.wsj.com/articles/ibm-pushes-blockchain-into-the-supply-chain-1468528824 |url-status=live |access-date=2016-07-24 |archive-url=https://web.archive.org/web/20160718032702/http://www.wsj.com/articles/ibm-pushes-blockchain-into-the-supply-chain-1468528824 |archive-date=18 July 2016}}</ref> As of 2019, the [[Diamond Trading Company]] (DTC) has been involved in building a diamond trading supply chain product called Tracr.<ref name="Wharton, Gstettner July 30, 2019">{{Cite web |last=Gstettner |first=Stefan |date=July 30, 2019 |title=How Blockchain Will Redefine Supply Chain Management |url=https://knowledge.wharton.upenn.edu/article/blockchain-supply-chain-management |access-date=28 August 2020 |website=Knowledge@Wharton |publisher=The Wharton School of the University of Pennsylvania}}</ref>
* '''[[Food supply]]''' — As of 2018, [[Walmart]] and [[IBM]] were running a trial to use a blockchain-backed system for [[supply chain]] monitoring for lettuce and spinach — all nodes of the blockchain were administered by Walmart and were located on the IBM [[cloud computing|cloud]].<ref>{{Cite news |last1=Corkery |first1=Michael |last2=Popper |first2=Nathaniel |date=24 September 2018 |title=From Farm to Blockchain: Walmart Tracks Its Lettuce |work=The New York Times |url=https://www.nytimes.com/2018/09/24/business/walmart-blockchain-lettuce.html |url-status=live |access-date=5 December 2018 |archive-url=https://web.archive.org/web/20181205103719/https://www.nytimes.com/2018/09/24/business/walmart-blockchain-lettuce.html |archive-date=5 December 2018}}</ref>
* Fashion industry — There is an opaque relationship between brands, distributors, and customers in the fashion industry, which will prevent the sustainable and stable development of the fashion industry. Blockchain makes up for this shortcoming and makes information transparent, solving the difficulty of sustainable development of the industry.<ref>{{Cite journal |date=2021-06-08 |title=Blockchain basics: Utilizing blockchain to improve sustainable supply chains in fashion |url=https://www.emerald.com/insight/content/doi/10.1108/SD-03-2021-0028/full/html |journal=Strategic Direction |language=en |volume=37 |issue=5 |pages=25–27 |doi=10.1108/SD-03-2021-0028 |s2cid=241322151 |issn=0258-0543}}</ref>
=== Domain names ===
There are several different efforts to offer [[domain name]] services via the blockchain. These domain names can be controlled by the use of a private key, which purports to allow for uncensorable websites. This would also bypass a registrar's ability to suppress domains used for fraud, abuse, or illegal content.<ref name=techrepublic>{{Cite web |last1=Sanders |first1=James |last2=August 28 |title=Blockchain-based Unstoppable Domains is a rehash of a failed idea |url=https://www.techrepublic.com/article/blockchain-based-unstoppable-domains-is-a-rehash-of-a-failed-idea/ |url-status=live |archive-url=https://web.archive.org/web/20191119015118/https://www.techrepublic.com/article/blockchain-based-unstoppable-domains-is-a-rehash-of-a-failed-idea/ |archive-date=19 November 2019 |access-date=2020-04-16 |website=TechRepublic |date=28 August 2019 |language=en}}</ref>
[[Namecoin]] is a cryptocurrency that supports the ".bit" [[top-level domain]] (TLD). Namecoin was forked from bitcoin in 2011. The .bit TLD is not sanctioned by [[ICANN]], instead requiring an [[alternative DNS root]].<ref name=techrepublic/> As of 2015, it was used by 28 websites, out of 120,000 registered names.<ref name=orcutt>{{cite news |last1=Orcutt |first1=Mike |title=The ambitious plan to reinvent how websites get their names |url=https://www.technologyreview.com/2019/06/04/239039/the-ambitious-plan-to-make-the-internets-phone-book-more-trustworthy/ |access-date=May 17, 2021 |work=MIT Technology Review |date=June 4, 2019 |language=en}}</ref> Namecoin was dropped by [[OpenNIC]] in 2019, due to malware and potential other legal issues.<ref>{{cite news |last1=Cimpanu |first1=Catalin |title=OpenNIC drops support for .bit domain names after rampant malware abuse |url=https://www.zdnet.com/article/opennic-drops-support-for-bit-domain-names-after-rampant-malware-abuse/ |access-date=May 17, 2021 |work=ZDNet |date=July 17, 2019 |language=en}}</ref> Other blockchain alternatives to ICANN include The Handshake Network,<ref name=orcutt/> EmerDNS, and Unstoppable Domains.<ref name=techrepublic/>
Specific TLDs include ".eth", ".luxe", and ".kred", which are associated with the Ethereum blockchain through the Ethereum Name Service (ENS). The .kred TLD also acts as an alternative to conventional [[cryptocurrency wallet]] addresses, as a convenience for transferring cryptocurrency.<ref>{{Cite web |date=2020-03-06 |title=.Kred launches as dual DNS and ENS domain |url=https://domainnamewire.com/2020/03/06/kred-blockchain-domain/ |url-status=live |archive-url=https://web.archive.org/web/20200308175635/https://domainnamewire.com/2020/03/06/kred-blockchain-domain/ |archive-date=8 March 2020 |access-date=2020-04-16 |website=Domain Name Wire {{!}} Domain Name News |language=en-US}}</ref>
=== Other uses ===
Blockchain technology can be used to create a permanent, public, transparent ledger system for compiling data on sales, tracking digital use and payments to content creators, such as wireless users<ref>K. Kotobi, and S. G. Bilen, [http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8269834 "Secure Blockchains for Dynamic Spectrum Access : A Decentralized Database in Moving Cognitive Radio Networks Enhances Security and User Access"], IEEE Vehicular Technology Magazine, 2018.</ref> or musicians.<ref>{{Cite web |date=22 September 2016 |title=Blockchain Could Be Music's Next Disruptor |url=http://fortune.com/2016/09/22/blockchain-music-disruption/ |url-status=live |archive-url=https://web.archive.org/web/20160923180800/http://fortune.com/2016/09/22/blockchain-music-disruption/ |archive-date=23 September 2016}}</ref> The Gartner 2019 CIO Survey reported 2% of higher education respondents had launched blockchain projects and another 18% were planning academic projects in the next 24 months.<ref>Susan Moore. (16 October 2019). "Digital Business: 4 Ways Blockchain Will Transform Higher Education". [https://www.gartner.com/smarterwithgartner/4-ways-blockchain-will-transform-higher-education/ Gartner website] Retrieved 27 March 2021.</ref> In 2017, [[IBM]] partnered with [[ASCAP]] and [[PRS for Music]] to adopt blockchain technology in music distribution.<ref>{{Cite web |date=9 April 2017 |title=ASCAP, PRS and SACEM Join Forces for Blockchain Copyright System |url=http://www.musicbusinessworldwide.com/ascap-prs-sacem-join-forces-blockchain-copyright-system/ |url-status=live |archive-url=https://web.archive.org/web/20170410050815/http://www.musicbusinessworldwide.com/ascap-prs-sacem-join-forces-blockchain-copyright-system/ |archive-date=10 April 2017 |publisher=Music Business Worldwide |df=dmy-all}}</ref> [[Imogen Heap]]'s Mycelia service has also been proposed as a blockchain-based alternative "that gives artists more control over how their songs and associated data circulate among fans and other musicians."<ref>{{Cite magazine |last1=Burchardi, K. |last2=Harle, N. |date=20 January 2018 |title=The blockchain will disrupt the music business and beyond |url=https://www.wired.co.uk/article/blockchain-disrupting-music-mycelia |url-status=live |magazine=Wired UK |archive-url=https://web.archive.org/web/20180508040911/https://www.wired.co.uk/article/blockchain-disrupting-music-mycelia |archive-date=8 May 2018 |access-date=8 May 2018}}</ref><ref>{{Cite web |last=Bartlett |first=Jamie |date=6 September 2015 |title=Imogen Heap: saviour of the music industry? |url=https://www.theguardian.com/music/2015/sep/06/imogen-heap-saviour-of-music-industry |url-status=live |archive-url=https://web.archive.org/web/20160422213153/http://www.theguardian.com/music/2015/sep/06/imogen-heap-saviour-of-music-industry |archive-date=22 April 2016 |access-date=18 June 2016 |website=The Guardian}}</ref>
New distribution methods are available for the [[insurance]] industry such as [[peer-to-peer insurance]], [[parametric insurance]] and [[microinsurance]] following the adoption of blockchain.<ref>{{Cite web |last1=Wang |first1=Kevin |last2=Safavi |first2=Ali |date=29 October 2016 |title=Blockchain is empowering the future of insurance |url=https://techcrunch.com/2016/10/29/blockchain-is-empowering-the-future-of-insurance/ |url-status=live |archive-url=https://web.archive.org/web/20161107160418/https://techcrunch.com/2016/10/29/blockchain-is-empowering-the-future-of-insurance/ |archive-date=7 November 2016 |access-date=7 November 2016 |website=Tech Crunch |publisher=AOL Inc}}</ref><ref>{{Cite journal |last1=Gatteschi |first1=Valentina |last2=Lamberti |first2=Fabrizio |last3=Demartini |first3=Claudio |last4=Pranteda |first4=Chiara |last5=Santamaría |first5=Víctor |date=20 February 2018 |title=Blockchain and Smart Contracts for Insurance: Is the Technology Mature Enough? |journal=Future Internet |language=en |volume=10 |issue=2 |pages=20 |doi=10.3390/fi10020020 |doi-access=free}}</ref> The [[sharing economy]] and [[Internet of Things|IoT]] are also set to benefit from blockchains because they involve many collaborating peers.<ref>{{Cite web |title=Blockchain reaction: Tech companies plan for critical mass |url=http://www.ey.com/Publication/vwLUAssets/ey-blockchain-reaction-tech-companies-plan-for-critical-mass/$FILE/ey-blockchain-reaction.pdf |url-status=live |archive-url=https://web.archive.org/web/20161114001423/http://www.ey.com/Publication/vwLUAssets/ey-blockchain-reaction-tech-companies-plan-for-critical-mass/$FILE/ey-blockchain-reaction.pdf |archive-date=14 November 2016 |access-date=13 November 2016 |publisher=Ernst & Young |page=5}}</ref> The use of blockchain in libraries is being studied with a grant from the U.S. Institute of Museum and Library Services.<ref>Carrie Smith. [https://americanlibrariesmagazine.org/2019/03/01/library-blockchain-reaction/ Blockchain Reaction: How library professionals are approaching blockchain technology and its potential impact.] {{Webarchive|url=https://web.archive.org/web/20190912044927/https://americanlibrariesmagazine.org/2019/03/01/library-blockchain-reaction/ |date=12 September 2019}} ''American Libraries'' March 2019.</ref>
Other blockchain designs include [[Hyperledger]], a collaborative effort from the [[Linux Foundation]] to support blockchain-based distributed ledgers, with projects under this initiative including Hyperledger Burrow (by Monax) and Hyperledger Fabric (spearheaded by IBM).<ref>{{Cite web |date=9 January 2018 |title=IBM Blockchain based on Hyperledger Fabric from the Linux Foundation |url=https://www.ibm.com/blockchain/hyperledger.html |url-status=live |archive-url=https://web.archive.org/web/20171207035925/https://www.ibm.com/blockchain/hyperledger.html |archive-date=7 December 2017 |access-date=2018-01-18 |website=IBM.com |language=en-US}}</ref><ref>{{Cite news |last=Hyperledger |date=22 January 2019 |title=Announcing Hyperledger Grid, a new project to help build and deliver supply chain solutions! |url=https://www.hyperledger.org/blog/2019/01/22/announcing-hyperledger-grid-a-new-project-to-help-build-and-deliver-supply-chain-solutions |url-status=live |access-date=8 March 2019 |archive-url=https://web.archive.org/web/20190204125611/https://www.hyperledger.org/blog/2019/01/22/announcing-hyperledger-grid-a-new-project-to-help-build-and-deliver-supply-chain-solutions |archive-date=4 February 2019}}</ref><ref>{{Cite news |last=Mearian |first=Lucas |date=23 January 2019 |title=Grid, a new project from the Linux Foundation, will offer developers tools to create supply chain-specific applications running atop distributed ledger technology |work=Computerworld |url=https://www.computerworld.com/article/3336036/blockchain/linuxs-hyperledger-to-give-developers-supply-chain-building-blocks.html |url-status=live |access-date=8 March 2019 |archive-url=https://web.archive.org/web/20190203225703/https://www.computerworld.com/article/3336036/blockchain/linuxs-hyperledger-to-give-developers-supply-chain-building-blocks.html |archive-date=3 February 2019 }} {{Webarchive|url=https://web.archive.org/web/20190203225703/https://www.computerworld.com/article/3336036/blockchain/linuxs-hyperledger-to-give-developers-supply-chain-building-blocks.html |date=3 February 2019 }}</ref> Another is Quorum, a permissionable private blockchain by [[JPMorgan Chase]] with private storage, used for contract applications.<ref>{{Cite web |title=Why J.P. Morgan Chase Is Building a Blockchain on Ethereum |url=http://fortune.com/2016/10/04/jp-morgan-chase-blockchain-ethereum-quorum/ |url-status=live |archive-url=https://web.archive.org/web/20170202033844/http://fortune.com/2016/10/04/jp-morgan-chase-blockchain-ethereum-quorum/ |archive-date=2 February 2017 |access-date=2017-01-24 |website=Fortune}}</ref>
[[Oracle Corporation|Oracle]] introduced a blockchain table feature in its [[Oracle database|Oracle 21c database]].<ref name="OracleBlockchainDetails">{{Cite web|title=Details: Oracle Blockchain Table
|url=https://docs.oracle.com/en/database/oracle/oracle-database/21/nfcon/details-oracle-blockchain-table-282449857.html|access-date=2022-01-01|archive-url=https://web.archive.org/web/20210120232657/https://docs.oracle.com/en/database/oracle/oracle-database/21/nfcon/details-oracle-blockchain-table-282449857.html|archive-date=2021-01-20|url-status=live}}</ref><ref name="OracleBlockchainTable">{{Cite web|title=Oracle Blockchain Table|url=https://docs.oracle.com/en/database/oracle/oracle-database/21/nfcon/oracle-blockchain-table-268779556.html|access-date=2022-01-01|archive-url=https://web.archive.org/web/20210516130824/https://docs.oracle.com/en/database/oracle/oracle-database/21/nfcon/oracle-blockchain-table-268779556.html|archive-date=2021-05-16|url-status=live}}</ref>
Blockchain is also being used in [[peer-to-peer energy trading]].<ref>{{Cite journal |title=Blockchain technology in the energy sector: A systematic review of challenges and opportunities |year=2019 |doi=10.1016/j.rser.2018.10.014 |url=https://www.sciencedirect.com/science/article/pii/S1364032118307184 |url-status=live |archive-url=https://web.archive.org/web/20200622122351/https://www.sciencedirect.com/science/article/pii/S1364032118307184 |archive-date=22 June 2020 |access-date=7 June 2020|last1=Andoni |first1=Merlinda |last2=Robu |first2=Valentin |last3=Flynn |first3=David |last4=Abram |first4=Simone |last5=Geach |first5=Dale |last6=Jenkins |first6=David |last7=McCallum |first7=Peter |last8=Peacock |first8=Andrew |journal=Renewable and Sustainable Energy Reviews |volume=100 |pages=143–174 |s2cid=116422191 }}</ref><ref>{{Cite web |title=This Blockchain-Based Energy Platform Is Building A Peer-To-Peer Grid |date=16 October 2017 |url=https://www.fastcompany.com/40479952/this-blockchain-based-energy-platform-is-building-a-peer-to-peer-grid |url-status=live |archive-url=https://web.archive.org/web/20200607085107/https://www.fastcompany.com/40479952/this-blockchain-based-energy-platform-is-building-a-peer-to-peer-grid |archive-date=7 June 2020 |access-date=7 June 2020}}</ref><ref>{{Cite web |title=Blockchain-based microgrid gives power to consumers in New York |url=https://www.newscientist.com/article/2079334-blockchain-based-microgrid-gives-power-to-consumers-in-new-york/ |url-status=live |archive-url=https://web.archive.org/web/20160322204730/https://www.newscientist.com/article/2079334-blockchain-based-microgrid-gives-power-to-consumers-in-new-york/ |archive-date=22 March 2016 |access-date=7 June 2020}}</ref>
Blockchain could be used in detecting counterfeits by associating unique identifiers to products, documents and shipments, and storing records associated with transactions that cannot be forged or altered.<ref>{{Cite journal|last1=Ma|first1=Jinhua|last2=Lin|first2=Shih-Ya|last3=Chen|first3=Xin|last4=Sun|first4=Hung-Min|last5=Chen|first5=Yeh-Cheng|last6=Wang|first6=Huaxiong|date=2020|title=A Blockchain-Based Application System for Product Anti-Counterfeiting|journal=IEEE Access|volume=8|pages=77642–77652|doi=10.1109/ACCESS.2020.2972026|s2cid=214205788|issn=2169-3536|doi-access=free}}</ref><ref>{{Cite journal|last1=Alzahrani|first1=Naif|last2=Bulusu|first2=Nirupama|date=2018-06-15|title=Block-Supply Chain: A New Anti-Counterfeiting Supply Chain Using NFC and Blockchain|journal=Proceedings of the 1st Workshop on Cryptocurrencies and Blockchains for Distributed Systems|series=CryBlock'18|location=Munich, Germany|publisher=Association for Computing Machinery|pages=30–35|doi=10.1145/3211933.3211939|isbn=978-1-4503-5838-5|s2cid=169188795}}</ref> It is however argued that blockchain technology needs to be supplemented with technologies that provide a strong binding between physical objects and blockchain systems.<ref>{{Cite journal|last1=Balagurusamy|first1=V. S. K.|last2=Cabral|first2=C.|last3=Coomaraswamy|first3=S.|last4=Delamarche|first4=E.|last5=Dillenberger|first5=D. N.|last6=Dittmann|first6=G.|last7=Friedman|first7=D.|last8=Gökçe|first8=O.|last9=Hinds|first9=N.|last10=Jelitto|first10=J.|last11=Kind|first11=A.|date=2019-03-01|title=Crypto anchors|url=https://ieeexplore.ieee.org/document/8645638|journal=IBM Journal of Research and Development|volume=63|issue=2/3|pages=4:1–4:12|doi=10.1147/JRD.2019.2900651|s2cid=201109790|issn=0018-8646}}</ref> The [[European Union Intellectual Property Office|EUIPO]] established an Anti-Counterfeiting Blockathon Forum, with the objective of "defining, piloting and implementing" an anti-counterfeiting infrastructure at the European level.<ref>{{Cite web|last=Brett|first=Charles|date=2018-04-18|title=EUIPO Blockathon Challenge 2018 -|url=https://www.enterprisetimes.co.uk/2018/04/18/euipo-blockathon-challenge-2018/|access-date=2020-09-01|website=Enterprise Times|language=en-GB}}</ref><ref>{{Cite web|title=EUIPO Anti-Counterfeiting Blockathon Forum|url=https://euipo.europa.eu/ohimportal/en/web/observatory/blockathon}}</ref> The Dutch Standardisation organisation NEN uses blockchain together with QR Codes to authenticate certificates.<ref>{{Cite web |title=PT Industrieel Management |url=https://privacy.vakmedianet.nl/ptindustrieelmanagement/?ref=https%3A%2F%2Fwww.ptindustrieelmanagement.nl%2Fhse%2Fnieuws%2F2020%2F06%2Fnen-innoveert-certificatencheck-en-blockchain-1013171%3F_ga%3D2.11339894.336094574.1598975931-710406553.1598975931 |access-date=2020-09-01 |website=PT Industrieel Management |archive-date=24 May 2022 |archive-url=https://web.archive.org/web/20220524211503/https://privacy.vakmedianet.nl/ptindustrieelmanagement/?ref=https%3A%2F%2Fwww.ptindustrieelmanagement.nl%2Fhse%2Fnieuws%2F2020%2F06%2Fnen-innoveert-certificatencheck-en-blockchain-1013171%3F_ga%3D2.11339894.336094574.1598975931-710406553.1598975931 }}</ref>
2022 Jan 30 Beijing and Shanghai are among the cities designated by China to trial blockchain applications.<ref>{{cite news | url=https://www.reuters.com/world/china/china-selects-pilot-zones-application-areas-blockchain-project-2022-01-30/ | title=China selects pilot zones, application areas for blockchain project | newspaper=Reuters | date=31 January 2022 }}</ref>
==Blockchain interoperability==
With the increasing number of blockchain systems appearing, even only those that support cryptocurrencies, blockchain interoperability is becoming a topic of major importance. The objective is to support transferring assets from one blockchain system to another blockchain system. Wegner{{R|wegner}} stated that "interoperability is the ability of two or more software components to cooperate despite differences in language, interface, and execution platform". The objective of blockchain interoperability is therefore to support such cooperation among blockchain systems, despite those kinds of differences.
There are already several blockchain interoperability solutions available.{{R|survey}} They can be classified into three categories: cryptocurrency interoperability approaches, blockchain engines, and blockchain connectors.
Several individual IETF participants produced the draft of a blockchain interoperability architecture.{{R|ietf-draft}}
== Energy consumption concerns ==
Some cryptocurrencies use blockchain mining — the peer-to-peer computer computations by which transactions are validated and verified. This requires a large amount of energy. In June 2018 the [[Bank for International Settlements]] criticized the use of public [[proof-of-work]] blockchains for their high energy consumption.<ref>{{Cite web |last=Hyun Song Shin |date=June 2018 |title=Chapter V. Cryptocurrencies: looking beyond the hype |url=https://www.bis.org/publ/arpdf/ar2018e5.pdf |url-status=live |archive-url=https://web.archive.org/web/20180618080358/https://www.bis.org/publ/arpdf/ar2018e5.pdf |archive-date=18 June 2018 |access-date=19 June 2018 |website=BIS 2018 Annual Economic Report |publisher=Bank for International Settlements |quote=Put in the simplest terms, the quest for decentralised trust has quickly become an environmental disaster.}}</ref><ref>{{Cite news |last=Janda |first=Michael |date=18 June 2018 |title=Cryptocurrencies like bitcoin cannot replace money, says Bank for International Settlements |publisher=ABC (Australia) |url=http://www.abc.net.au/news/2018-06-18/cryptocurrencies-cannot-replace-money-bis/9879448 |url-status=live |access-date=18 June 2018 |archive-url=https://web.archive.org/web/20180618072225/http://www.abc.net.au/news/2018-06-18/cryptocurrencies-cannot-replace-money-bis/9879448 |archive-date=18 June 2018}}</ref><ref>{{Cite news |last=Hiltzik |first=Michael |date=18 June 2018 |title=Is this scathing report the death knell for bitcoin? |work=Los Angeles Times |url=https://latimes.com/business/hiltzik/la-fi-hiltzik-bitcoin-bank-20180618-story.html |url-status=live |access-date=19 June 2018 |archive-url=https://web.archive.org/web/20180618233532/http://www.latimes.com/business/hiltzik/la-fi-hiltzik-bitcoin-bank-20180618-story.html |archive-date=18 June 2018}}</ref>
Early concern over the high energy consumption was a factor in later blockchains such as [[Cardano (blockchain platform)|Cardano]] (2017), [[Solana (blockchain platform)|Solana]] (2020) and [[Polkadot (cryptocurrency)|Polkadot]] (2020) adopting the less energy-intensive [[proof of stake|proof-of-stake]] model. Researchers have estimated that Bitcoin consumes 100,000 times as much energy as proof-of-stake networks.<ref>{{Cite news |last=Ossinger |first=Joanna |date=2 February 2022 |title=Polkadot Has Least Carbon Footprint, Crypto Researcher Says |work=Bloomberg |url=https://www.bloomberg.com/news/articles/2022-02-02/polkadot-has-smallest-carbon-footprint-crypto-researcher-says |access-date=1 June 2022}}</ref><ref>{{Cite web |last=Jones |first=Jonathan Spencer |date=13 September 2021 |title=Blockchain proof-of-stake – not all are equal |url=https://www.smart-energy.com/industry-sectors/new-technology/proof-of-stake-blockchains-not-all-are-equal/ |website=www.smart-energy.com |access-date=1 July 2022 |archive-date=25 September 2021 |archive-url=https://web.archive.org/web/20210925061707/https://www.smart-energy.com/industry-sectors/new-technology/proof-of-stake-blockchains-not-all-are-equal/ |url-status=dead }}</ref>
In 2021, a study by [[University of Cambridge|Cambridge University]] determined that Bitcoin (at 121 terawatt-hours per year) used more electricity than Argentina (at 121TWh) and the Netherlands (109TWh).<ref>Criddle, Christina (February 20, 2021) [https://www.bbc.com/news/technology-56012952 "Bitcoin consumes 'more electricity than Argentina'."] BBC News. (Retrieved April 26, 2021.)</ref> According to Digiconomist, one bitcoin transaction required 708 kilowatt-hours of electrical energy, the amount an average U.S. household consumed in 24 days.<ref>Ponciano, Jonathan (March 9, 2021) [https://www.forbes.com/sites/jonathanponciano/2021/03/09/bill-gates-bitcoin-crypto-climate-change/ "Bill Gates Sounds Alarm On Bitcoin's Energy Consumption–Here's Why Crypto Is Bad For Climate Change."] Forbes.com. (Retrieved April 26, 2021.)</ref>
In February 2021, U.S. Treasury secretary [[Janet Yellen]] called Bitcoin "an extremely inefficient way to conduct transactions", saying "the amount of energy consumed in processing those transactions is staggering".<ref>Rowlatt, Justin (February 27, 2021) [https://www.bbc.com/news/science-environment-56215787 "How Bitcoin's vast energy use could burst its bubble."] BBC News. (Retrieved April 26, 2021.)</ref> In March 2021, [[Bill Gates]] stated that "Bitcoin uses more electricity per transaction than any other method known to mankind", adding "It's not a great climate thing."<ref>Sorkin, Andrew et al. (March 9, 2021) [https://www.nytimes.com/2021/03/09/business/dealbook/bill-gates-bitcoin.html "Why Bill Gates Is Worried About Bitcoin."] ''New York Times''. (Retrieved April 25, 2021.)</ref>
Nicholas Weaver, of the [[International Computer Science Institute]] at the [[University of California, Berkeley]], examined blockchain's online security, and the energy efficiency of proof-of-work public blockchains, and in both cases found it grossly inadequate.<ref>{{Cite web |last=Illing |first=Sean |date=April 11, 2018 |title=Why Bitcoin is bullshit, explained by an expert |url=https://www.vox.com/conversations/2018/4/11/17206018/bitcoin-blockchain-cryptocurrency-weaver |url-status=live |archive-url=https://web.archive.org/web/20180717041640/https://www.vox.com/conversations/2018/4/11/17206018/bitcoin-blockchain-cryptocurrency-weaver |archive-date=17 July 2018 |access-date=17 July 2018 |website=[[Vox (website)|Vox]]}}</ref><ref>{{Cite web |last=Weaver |first=Nicholas |title=Blockchains and Cryptocurrencies: Burn It With Fire |url=https://www.youtube.com/watch?v=xCHab0dNnj4 |url-status=live |archive-url=https://web.archive.org/web/20190219015620/https://www.youtube.com/watch?v=xCHab0dNnj4 |archive-date=19 February 2019 |access-date=17 July 2018 |website=YouTube video |publisher=Berkeley School of Information}}</ref> The 31TWh-45TWh of electricity used for bitcoin in 2018 produced 17-23 million tonnes of CO2.<ref>{{Cite journal |last1=Köhler |first1=Susanne |last2=Pizzol |first2=Massimo |date=20 November 2019 |title=Life Cycle Assessment of Bitcoin Mining |journal=Environmental Science & Technology |volume=53 |issue=23 |pages=13598–13606 |bibcode=2019EnST...5313598K |doi=10.1021/acs.est.9b05687 |pmid=31746188|doi-access=free}}</ref><ref>{{Cite journal |last1=Stoll |first1=Christian |last2=Klaaßen |first2=Lena |last3=Gallersdörfer |first3=Ulrich |year=2019 |title=The Carbon Footprint of Bitcoin |journal=Joule |volume=3 |issue=7 |pages=1647–1661 |doi=10.1016/j.joule.2019.05.012 |doi-access=free}}</ref> By 2022, the University of Cambridge and Digiconomist estimated that the two largest proof-of-work blockchains, Bitcoin and Ethereum, together used twice as much electricity in one year as the whole of Sweden, leading to the release of up to 120 million tonnes of CO2 each year.<ref>{{Cite news|url=https://www.business-standard.com/article/international/us-lawmakers-begin-probe-into-bitcoin-miners-high-energy-use-122012900282_1.html#:~:text=Eight+US+lawmakers+have+come,being+felt+across+the+globe|title=US lawmakers begin probe into Bitcoin miners' high energy use|newspaper=Business Standard India |date=29 January 2022|via=Business Standard}}</ref>
Some cryptocurrency developers are considering moving from the [[proof of work|proof-of-work]] model to the [[proof of stake|proof-of-stake]] model.<ref>Cuen, Leigh (March 21, 2021) [https://techcrunch.com/2021/03/21/the-debate-about-cryptocurrency-and-energy-consumption/ "The debate about cryptocurrency and data consumption."] TechCrunch. (Retrieved April 26, 2021.)</ref>
==Academic research==
[[File:BlockchainPanelDiscussionAtIEEETechIgnite2017.jpg|thumb|Blockchain panel discussion at the first [[IEEE Computer Society]] TechIgnite conference]]
In October 2014, the MIT Bitcoin Club, with funding from MIT alumni, provided undergraduate students at the [[Massachusetts Institute of Technology]] access to $100 of bitcoin. The adoption rates, as studied by [[Christian Catalini|Catalini]] and [[Catherine Tucker|Tucker]] (2016), revealed that when people who typically adopt technologies early are given delayed access, they tend to reject the technology.<ref>{{Cite journal |last1=Catalini |first1=Christian |last2=Tucker |first2=Catherine E. |date=11 August 2016 |title=Seeding the S-Curve? The Role of Early Adopters in Diffusion |journal=SSRN |doi=10.2139/ssrn.2822729 |ssrn=2822729 |s2cid=157317501 |url=http://www.netinst.org/Catalini_16-02.pdf}}</ref> Many universities have founded departments focusing on crypto and blockchain, including [[Massachusetts Institute of Technology|MIT]], in 2017. In the same year, [[university of Edinburgh|Edinburgh]] became "one of the first big European universities to launch a blockchain course", according to the ''Financial Times''.<ref>[https://www.ft.com/content/f736b04e-3708-11e7-99bd-13beb0903fa3 Arnold, M. (2017) "Universities add blockchain to course list", Financial Times: Masters in Finance, Retrieved 26 January 2022.]</ref>
===Adoption decision===
Motivations for adopting blockchain technology (an aspect of [[Diffusion of innovations|innovation adoptation]]) have been investigated by researchers. For example, Janssen, et al. provided a framework for analysis,<ref>{{Cite journal |last1=Janssen |first1=Marijn |last2=Weerakkody |first2=Vishanth |last3=Ismagilova |first3=Elvira |last4=Sivarajah |first4=Uthayasankar |last5=Irani |first5=Zahir |year=2020 |title=A framework for analysing blockchain technology adoption: Integrating institutional, market and technical factors |journal=International Journal of Information Management |publisher=Elsevier |volume=50 |pages=302–309 |doi=10.1016/j.ijinfomgt.2019.08.012 |doi-access=free}}</ref> and Koens & Poll pointed out that adoption could be heavily driven by non-technical factors.<ref>{{Citation |last1=Koens |first1=Tommy |title=Euro-Par 2018: Parallel Processing Workshops |volume=11339 |pages=535–546 |year=2019 |series=Lecture Notes in Computer Science |chapter=The Drivers Behind Blockchain Adoption: The Rationality of Irrational Choices |doi=10.1007/978-3-030-10549-5_42 |isbn=978-3-030-10548-8 |last2=Poll |first2=Erik|hdl=2066/200787 |s2cid=57662305 }}</ref> Based on behavioral models, Li<ref>{{Cite journal|url=https://doi.org/10.1145/3396743.3396750|archiveurl=https://web.archive.org/web/20200605023137/https://dl.acm.org/doi/abs/10.1145/3396743.3396750|url-status=dead|title=Blockchain Technology Adoption: Examining the Fundamental Drivers|first=Jerry|last=Li|date=7 April 2020|archivedate=5 June 2020|publisher=Association for Computing Machinery|pages=253–260|via=ACM Digital Library|doi=10.1145/3396743.3396750|s2cid=218982506 }}</ref> has discussed the differences between adoption at the individual level and organizational levels.
=== Collaboration ===
Scholars in business and management have started studying the role of blockchains to support collaboration.<ref>{{Cite journal |last1=Hsieh |first1=Ying-Ying |last2=Vergne |first2=Jean-Philippe |last3=Anderson |first3=Philip |last4=Lakhani |first4=Karim |last5=Reitzig |first5=Markus |date=2019-02-12 |title=Correction to: Bitcoin and the rise of decentralized autonomous organizations |journal=Journal of Organization Design |volume=8 |issue=1 |pages=3 |doi=10.1186/s41469-019-0041-1 |issn=2245-408X |doi-access=free}}</ref><ref>{{Cite journal |last1=Felin |first1=Teppo |last2=Lakhani |first2=Karim |year=2018 |title=What Problems Will You Solve With Blockchain? |journal=MIT Sloan Management Review}}</ref> It has been argued that blockchains can foster both cooperation (i.e., prevention of opportunistic behavior) and coordination (i.e., communication and information sharing). Thanks to reliability, transparency, traceability of records, and information immutability, blockchains facilitate collaboration in a way that differs both from the traditional use of contracts and from relational norms. Contrary to contracts, blockchains do not directly rely on the legal system to enforce agreements.<ref>{{Cite journal |last1=Beck |first1=Roman |last2=Mueller-Bloch |first2=Christoph |last3=King |first3=John Leslie |year=2018 |title=Governance in the Blockchain Economy: A Framework and Research Agenda |journal=Journal of the Association for Information Systems|pages=1020–1034 |doi=10.17705/1jais.00518 |url=https://aisel.aisnet.org/cgi/viewcontent.cgi?article=1835&context=jais}}</ref> In addition, contrary to the use of relational norms, blockchains do not require a trust or direct connections between collaborators.
===Blockchain and internal audit===
{{external media | width = 210px | float = right | video1= [https://www.youtube.com/watch?v=0UvVOMZqpEA Blockchain Basics & Cryptography], [[Gary Gensler]], [[Massachusetts Institute of Technology]], 0:30<ref>{{cite web |last1=Popper |first1=Nathaniel |title=What is the Blockchain? Explaining the Tech Behind Cryptocurrencies (Published 2018) |url=https://www.nytimes.com/2018/06/27/business/dealbook/blockchains-guide-information.html |website=The New York Times |date=27 June 2018}}</ref>}}
The need for internal audits to provide effective oversight of organizational efficiency will require a change in the way that information is accessed in new formats.<ref>Hugh Rooney, Brian Aiken, & Megan Rooney. (2017). Q&A. Is Internal Audit Ready for Blockchain? ''Technology Innovation Management Review'', (10), 41.</ref> Blockchain adoption requires a framework to identify the risk of exposure associated with transactions using blockchain. The [[Institute of Internal Auditors]] has identified the need for internal auditors to address this transformational technology. New methods are required to develop audit plans that identify threats and risks. The Internal Audit Foundation study, ''Blockchain and Internal Audit,'' assesses these factors.<ref>Richard C. Kloch, Jr Simon J. Little, ''Blockchain and Internal Audit'' Internal Audit Foundation, 2019 {{ISBN|978-1-63454-065-0}}</ref> The [[American Institute of Certified Public Accountants]] has outlined new roles for auditors as a result of blockchain.<ref>Alexander, A. (2019). The audit, transformed: New advancements in technology are reshaping this core service. ''Accounting Today'', 33(1)</ref>
=== Journals ===
{{main|Ledger (journal)|l1=''Ledger'' (journal)}}
In September 2015, the first peer-reviewed academic journal dedicated to cryptocurrency and blockchain technology research, ''Ledger'', was announced. The inaugural issue was published in December 2016.<ref>{{Cite journal |last=Extance |first=Andy |date=30 September 2015 |title=The future of cryptocurrencies: Bitcoin and beyond |journal=Nature |volume=526 |issue=7571 |pages=21–23 |bibcode=2015Natur.526...21E |doi=10.1038/526021a |issn=0028-0836 |oclc=421716612 |pmid=26432223 |doi-access=free}}</ref> The journal covers aspects of [[mathematics]], [[computer science]], [[engineering]], [[law]], [[economics]] and [[philosophy]] that relate to cryptocurrencies such as bitcoin.<ref>{{Cite book |title=Ledger (eJournal / eMagazine, 2015) |publisher=OCLC |oclc=910895894}}</ref><ref>{{Cite web |last=Hertig |first=Alyssa |date=15 September 2015 |title=Introducing Ledger, the First Bitcoin-Only Academic Journal |url=http://motherboard.vice.com/read/introducing-ledger-the-first-bitcoin-only-academic-journal |url-status=live |archive-url=https://web.archive.org/web/20170110172807/http://motherboard.vice.com/read/introducing-ledger-the-first-bitcoin-only-academic-journal |archive-date=10 January 2017 |access-date=10 January 2017 |website=Motherboard}}</ref>
The journal encourages authors to [[Digital signature|digitally sign]] a [[Hash function|file hash]] of submitted papers, which are then [[Trusted timestamping|timestamped]] into the bitcoin blockchain. Authors are also asked to include a personal bitcoin address on the first page of their papers for non-repudiation purposes.<ref>{{Cite journal |last1=Rizun |first1=Peter R. |last2=Wilmer |first2=Christopher E. |last3=Burley |first3=Richard Ford |last4=Miller |first4=Andrew |year=2015 |title=How to Write and Format an Article for Ledger |url=http://ledger.pitt.edu/ojs/public/journals/1/AuthorGuide.pdf |url-status=live |journal=Ledger |volume=1 |issue=1 |pages=1–12 |doi=10.5195/LEDGER.2015.1 |doi-broken-date=28 February 2022 |issn=2379-5980 |oclc=910895894 |archive-url=https://web.archive.org/web/20150922190603/http://ledger.pitt.edu/ojs/public/journals/1/AuthorGuide.pdf |archive-date=22 September 2015 |access-date=11 January 2017}} </ref>
==See also==
{{Portal|Economics}}
* [[Changelog]] – a record of all notable changes made to a project
* [[Checklist]] – an informational aid used to reduce failure
* [[Economics of digitization]]
* [[Privacy and blockchain]]
* [[Version control]] – a record of all changes (mostly of software project) in a form of a graph
==References==
{{reflist|refs=
<ref name="fortune20160515">{{Cite news |last=Morris |first=David Z. |date=15 May 2016 |title=Leaderless, Blockchain-Based Venture Capital Fund Raises $100 Million, And Counting |work=[[Fortune (magazine)|Fortune]] |url=http://fortune.com/2016/05/15/leaderless-blockchain-vc-fund/ |url-status=live |access-date=2016-05-23 |archive-url=https://web.archive.org/web/20160521015602/http://fortune.com/2016/05/15/leaderless-blockchain-vc-fund/ |archive-date=21 May 2016}}</ref>
<ref name="nyt20160521">{{Cite news |last=Popper |first=Nathan |date=21 May 2016 |title=A Venture Fund With Plenty of Virtual Capital, but No Capitalist |work=[[The New York Times]] |url=https://www.nytimes.com/2016/05/22/business/dealbook/crypto-ether-bitcoin-currency.html |url-status=live |access-date=2016-05-23 |archive-url=https://web.archive.org/web/20160522034932/http://www.nytimes.com/2016/05/22/business/dealbook/crypto-ether-bitcoin-currency.html |archive-date=22 May 2016}}</ref>
<ref name="kopstein">{{Cite magazine |last=Kopfstein, Janus |date=12 December 2013 |title=The Mission to Decentralize the Internet |url=https://www.newyorker.com/tech/elements/the-mission-to-decentralize-the-internet |url-status=live |magazine=[[The New Yorker]] |archive-url=https://web.archive.org/web/20141231031044/http://www.newyorker.com/tech/elements/the-mission-to-decentralize-the-internet |archive-date=31 December 2014 |access-date=30 December 2014 |quote=The network's 'nodes' — users running the bitcoin software on their computers — collectively check the integrity of other nodes to ensure that no one spends the same coins twice. All transactions are published on a shared public ledger, called the 'block chain.' |df=dmy-all}}</ref>
<ref name="te20151031">{{Cite news |date=31 October 2015 |title=Blockchains: The great chain of being sure about things |url=https://www.economist.com/news/briefing/21677228-technology-behind-bitcoin-lets-people-who-do-not-know-or-trust-each-other-build-dependable |url-status=live |newspaper=[[The Economist]] |archive-url=https://web.archive.org/web/20160703000844/http://www.economist.com/news/briefing/21677228-technology-behind-bitcoin-lets-people-who-do-not-know-or-trust-each-other-build-dependable |archive-date=3 July 2016 |access-date=18 June 2016 |quote=The technology behind bitcoin lets people who do not know or trust each other build a dependable ledger. This has implications far beyond the crypto currency. |df=dmy-all}}</ref>
<ref name="primer">{{Cite report |url=http://mercatus.org/sites/default/files/Brito_BitcoinPrimer.pdf |title=Bitcoin: A Primer for Policymakers |last1=Brito |first1=Jerry |last2=Castillo |first2=Andrea |publisher=Mercatus Center, George Mason University |location=Fairfax, VA |access-date=22 October 2013 |archive-url=https://web.archive.org/web/20130921060724/http://mercatus.org/sites/default/files/Brito_BitcoinPrimer.pdf |year=2013 |url-status=live |archive-date=21 September 2013 |df=dmy-all}}</ref>
<ref name="t4">{{Cite web |last=Greenspan, Gideon |date=19 July 2015 |title=Ending the bitcoin vs blockchain debate |url=http://www.multichain.com/blog/2015/07/bitcoin-vs-blockchain-debate/ |url-status=live |archive-url=https://web.archive.org/web/20160608070620/http://www.multichain.com/blog/2015/07/bitcoin-vs-blockchain-debate/ |archive-date=8 June 2016 |access-date=2016-06-18 |website=multichain.com |df=dmy-all}}</ref>
<ref name="t8">{{Cite web |date=3 November 2015 |title=The 'Blockchain Technology' Bandwagon Has A Lesson Left To Learn |url=http://news.dinbits.com/2015/11/the-blockchain-technology-bandwagon-has.html |url-status=live |archive-url=https://web.archive.org/web/20160629234654/http://news.dinbits.com/2015/11/the-blockchain-technology-bandwagon-has.html |archive-date=29 June 2016 |access-date=2016-06-18 |website=dinbits.com |df=dmy-all}}</ref>
<ref name="t9">{{Cite web |last=DeRose, Chris |date=26 June 2015 |title=Why the Bitcoin Blockchain Beats Out Competitors |url=http://www.americanbanker.com/bankthink/why-the-bitcoin-blockchain-beats-out-competitors-1075100-1.html |url-status=live |archive-url=https://web.archive.org/web/20160330060709/http://www.americanbanker.com/bankthink/why-the-bitcoin-blockchain-beats-out-competitors-1075100-1.html |archive-date=30 March 2016 |access-date=18 June 2016 |website=American Banker |df=dmy-all}}</ref>
<ref name="t10">{{Cite web |last=Reutzel, Bailey |date=13 July 2015 |title=A Very Public Conflict Over Private Blockchains |url=http://www.paymentssource.com/news/technology/a-very-public-confluct-over-private-blockchains-3021831-1.html |url-status=live |archive-url=https://web.archive.org/web/20160421112045/http://www.paymentssource.com/news/technology/a-very-public-confluct-over-private-blockchains-3021831-1.html |archive-date=21 April 2016 |access-date=18 June 2016 |website=PaymentsSource |publisher=SourceMedia, Inc. |location=New York, NY |df=dmy-all}}</ref>
<ref name="t11">{{Cite journal |last=Casey, Michael J. |date=15 April 2015 |title=Moneybeat/BitBeat: Blockchains Without Coins Stir Tensions in Bitcoin Community |url=https://blogs.wsj.com/moneybeat/2015/04/14/bitbeat-blockchains-without-coins-stir-tensions-in-bitcoin-community/ |url-status=live |journal=[[The Wall Street Journal]] |archive-url=https://web.archive.org/web/20160610224428/http://blogs.wsj.com/moneybeat/2015/04/14/bitbeat-blockchains-without-coins-stir-tensions-in-bitcoin-community/ |archive-date=10 June 2016 |access-date=18 June 2016 |df=dmy-all}}</ref>
<ref name="t12">{{Cite book |last=Antonopoulos, Andreas M. |url=http://chimera.labs.oreilly.com/books/1234000001802/ch01.html |title=Mastering Bitcoin. Unlocking Digital Cryptocurrencies |date=2014 |publisher=O'Reilly Media |isbn=978-1449374037 |location=Sebastopol, CA |access-date=3 November 2015 |archive-url=https://web.archive.org/web/20161201131627/http://chimera.labs.oreilly.com/books/1234000001802/ch01.html |archive-date=1 December 2016 |url-status=dead |df=dmy-all}}</ref>
<ref name="t16">{{Cite web |last=Voorhees, Erik |date=30 October 2015 |title=It's All About the Blockchain |url=http://moneyandstate.com/its-all-about-the-blockchain/ |url-status=dead |archive-url=https://web.archive.org/web/20151101235750/http://moneyandstate.com/its-all-about-the-blockchain/ |archive-date=1 November 2015 |access-date=2015-11-02 |website=Money and State |df=dmy-all}}</ref>
<ref name="paper">{{Cite web |last=Nakamoto |first=Satoshi |date=October 2008 |title=Bitcoin: A Peer-to-Peer Electronic Cash System |url=https://bitcoin.org/bitcoin.pdf |url-status=live |archive-url=https://web.archive.org/web/20140320135003/https://bitcoin.org/bitcoin.pdf |archive-date=20 March 2014 |access-date=28 April 2014 |publisher=bitcoin.org}}</ref>
<ref name="wegner">{{cite journal| last1 = Wegner| first1 = Peter| date = March 1996| title = Interoperability| url = https://dl.acm.org/doi/10.1145/234313.234424| journal = ACM Computing Surveys| volume = 28| pages = 285–287| doi = 10.1145/234313.234424| access-date = October 24, 2020}}</ref>
<ref name="survey">{{cite arXiv| last1 = Belchior| first1 = Rafael| last2 = Vasconcelos| first2 = André| last3 = Guerreiro| first3 = Sérgio| last4 = Correia| first4 = Miguel| date = May 2020| title = A Survey on Blockchain Interoperability: Past, Present, and Future Trends| class = cs.DC| eprint = 2005.14282}}</ref>
<ref name="ietf-draft"></ref>
}}
==Further reading==
{{Refbegin|30em}}
* {{Cite report |url=http://scet.berkeley.edu/wp-content/uploads/BlockchainPaper.pdf |title=BlockChain Technology: Beyond Bitcoin |last1=Crosby |first1=Michael |last2=Nachiappan |date=16 October 2015 |publisher=University of California, Berkeley |last3=Pattanayak |first3=Pradhan |last4=Verma |first4=Sanjeev |last5=Kalyanaraman |first5=Vignesh |access-date=2017-03-19 |series=Sutardja Center for Entrepreneurship & Technology Technical Report }}
* {{Cite book |last=Jaikaran |first=Chris |url=https://crsreports.congress.gov/product/pdf/R/R45116/3 |title=Blockchain: Background and Policy Issues |date=28 February 2018 |publisher=Congressional Research Service |location=Washington, DC |access-date=2 December 2018 |archive-date=2 December 2018 |archive-url=https://web.archive.org/web/20181202202739/https://crsreports.congress.gov/product/pdf/R/R45116/3 |url-status=dead }}
* {{Cite report |title=The Blockchain Revolution: An Analysis of Regulation and Technology Related to Distributed Ledger Technologies |last1=Kakavand |first1=Hossein |last2=De Sevres |first2=Nicolette Kost |date=12 October 2016 |publisher=Luther Systems & DLA Piper |last3=Chilton |first3=Bart |ssrn=2849251}}
* {{Cite web |last=Mazonka |first=Oleg |date=29 December 2016 |title=Blockchain: Simple Explanation |url=http://jrxv.net/x/16/chain.pdf |website=Journal of Reference |access-date=1 July 2022 |archive-date=7 August 2017 |archive-url=https://web.archive.org/web/20170807214027/http://jrxv.net/x/16/chain.pdf |url-status=dead }}
* {{Cite book |last1=Tapscott |first1=Don |title=Blockchain Revolution: How the Technology Behind Bitcoin Is Changing Money, Business and the World |last2=Tapscott |first2=Alex |date=2016 |publisher=Portfolio Penguin |isbn=978-0-241-23785-4 |location=London |oclc=971395169}}
*
* {{Cite book |last=Raval |first=Siraj |url=http://shop.oreilly.com/product/0636920039334.do?sortby=publicationDate |title=Decentralized Applications: Harnessing Bitcoin's Blockchain Technology |date=2016 |publisher=Oreilly |isbn=9781491924549 }}
* {{Cite book |last=Bashir |first=Imran |title=Mastering Blockchain |date=2017 |publisher=Packt Publishing, Ltd. |isbn=978-1-78712-544-5 |oclc=967373845}}
* {{cite journal|first1=Fabian|last1=Knirsch|first2=Andread|last2=Unterweger|first3=Dominik|last3=Engel|title=Implementing a blockchain from scratch: why, how, and what we learned|journal=EURASIP Journal on Information Security|year=2019|volume=2019|doi=10.1186/s13635-019-0085-3|s2cid=84837476|url=https://jis-eurasipjournals.springeropen.com/articles/10.1186/s13635-019-0085-3#citeas}}
* D. Puthal, N. Malik, [[Saraju Mohanty|S. P. Mohanty]], E. Kougianos, and G. Das, "[http://www.smohanty.org/Publications_Journals/2018/Mohanty_IEEE-CEM_2018-Jul_Blockchain.pdf Everything you Wanted to Know about the Blockchain] {{Webarchive|url=https://web.archive.org/web/20220811064304/http://www.smohanty.org/Publications_Journals/2018/Mohanty_IEEE-CEM_2018-Jul_Blockchain.pdf |date=11 August 2022 }}", ''IEEE Consumer Electronics Magazine'', Volume 7, Issue 4, July 2018, pp. 06–14.
* David L. Portilla, David J. Kappos, Minh Van Ngo, Sasha Rosenthal-Larrea, John D. Buretta and Christopher K. Fargo, Cravath, Swaine & Moore LLP, "[https://corpgov.law.harvard.edu/2022/01/28/blockchain-in-the-banking-sector-a-review-of-the-landscape-and-opportunities Blockchain in the Banking Sector: A Review of the Landscape and Opportunities]", ''Harvard Law School of Corporate Governance'', posted on Friday, January 28, 2022
{{Refend}}
== External links ==
* {{Commons category-inline}}
{{Authority control}}
[[Category:Bitcoin]]
[[Category:Blockchains| ]]
[[Category:Cryptocurrencies]]
[[Category:Database models]]
[[Category:Emerging technologies]]
[[Category:Financial metadata]]
[[Category:Computer-related introductions in 2009]]
[[Category:Information systems]]
[[Category:Writing systems]]
[[Category:Mathematical tools]]
[[Category:Counting instruments]]
[[Category:Encodings]]
[[Category:Decentralization]]
[[Category:21st-century inventions]]
m9ozu8i1g4n5cwtgy8f17lht46s43j4
User:WBrown (WMF)/sandbox
2
152472
737094
727725
2026-04-07T18:05:49Z
WBrown (WMF)
58818
Blanked the page
737094
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
Comp
0
153430
737124
726622
2026-04-07T20:10:33Z
Ponor
47975
test
737124
wikitext
text/x-wiki
Any line breaks before "%"?
10 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 000 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 000 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 000 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 000 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 4 % 300 % 200 % 10 000 % 20 % 30 %, 4000 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 % 200 % 10 % 20 % 30 %, 400 % 300 000 % 200 %
10 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 000 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 000 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 000 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 4 °C 300 °C 200 °C 10 000 °C 20 °C 30 °C, 4000 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 °C 200 °C 10 °C 20 °C 30 °C, 400 °C 300 000 °C 200 °C
10% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10 000% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10 000% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10 000% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10 000% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 4% 300% 200% 10 000% 20% 30%, 4000% 300% 200% 10% 20% 30%, 400% 300% 200% 10% 20% 30%, 400% 300 000% 200%
[[file:No flag 2.svg]]
[[file:No flag 2.svg|link=]]
[[file:No flag 2.svg|link=[[computer]] ]]
{{Infobox comp|name={{PAGENAME}}|codename=X.23|silly=Me}}
{{-}}
----
{{Infobox comp|name={{PAGENAME}}|codename=X.23|silly=Me|picture=}}
{{-}}
----
{{Infobox comp|name={{PAGENAME}}|codename=X.23|silly=Me|picture=Example image not to be used in article namespace.jpg}}
{{-}}
----
{{#invoke:If not given or empty|check_parameter|picture|||when=NG NE|notgiven=[[File:Red pog.svg|100px|center]]|empty=|notempty=[[File:{{{picture}}}|200px]]}}
{{-}}
----
* 1 {{#invoke:If not given or empty|check_parameter}}
* 2 {{#invoke:If not given or empty|check_parameter|picture}}
* 3 {{#invoke:If not given or empty|check_parameter|picture }}
-
59cyo4bvxxqyppds28e2xw5rupiv1qv
User talk:JWBTH/CD test page
3
154341
737093
737033
2026-04-07T17:59:31Z
JWBTH
52211
/* Section to add test comments */ reply: test (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737093
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)
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)
=== 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)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
k8aly0sd5tomjwglzfm2p330eif4lv8
737105
737093
2026-04-07T18:53:30Z
JWBTH
52211
/* Comment with complex markup */ reply to Example: test (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737105
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)
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)
=== 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 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
4tk3k27d6tmql5ot8sidwtcih4b8j7j
737141
737105
2026-04-08T07:38:11Z
JWBTH
52211
/* Comment with complex markup */ reply to Example: iaos dhioas dhoia sdhiosa d (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737141
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)
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)
=== 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 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
*:iaos dhioas dhoia sdhiosa d [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:38, 8 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
8lnmk84wad4izmxog3g5srjruzn6o1v
737159
737141
2026-04-08T11:15:18Z
JWBTH
52211
/* Comment with complex markup */ reply to Example: reply (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737159
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)
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)
=== 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 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
*:iaos dhioas dhoia sdhiosa d [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:38, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
tmja4u42f42ydh4m7ndbjvi8938ch9c
737160
737159
2026-04-08T11:15:42Z
JWBTH
52211
/* Comment with complex markup */ reply to Example: reply (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737160
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)
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)
=== 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 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
*:iaos dhioas dhoia sdhiosa d [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:38, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
nvvf3bekqpvyh74odl96197c9ostn8k
737161
737160
2026-04-08T11:17:59Z
JWBTH
52211
/* test2 */ addition: test (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737161
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)
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 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
*:iaos dhioas dhoia sdhiosa d [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:38, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
audbysthtr4s5crjpec0aeky9pm1mvo
737163
737161
2026-04-08T11:57:29Z
JWBTH
52211
/* Comment with complex markup */ reply to Example: tests (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737163
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)
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 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:53, 7 April 2026 (UTC)
*:iaos dhioas dhoia sdhiosa d [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:38, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
*:reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
*:tests [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:57, 8 April 2026 (UTC)
=== Transcluded comments ===
{{User talk:JWBTH/CD test page/comment}}
=== Last subsection ===
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 14:56, 14 September 2025 (UTC)
::::::::::::::: <nowiki>__NOGALLERY__</nowiki> [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 09:24, 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.08406505844874512 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:32, 16 March 2026 (UTC)
: Test. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:16, 16 March 2026 (UTC)
: Test. test3 [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:17, 16 March 2026 (UTC)
: Test comment with random number 0.8357927622675184 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.47460188540542925 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 18:53, 16 March 2026 (UTC)
: Test comment with random number 0.687062002939545 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:03, 23 March 2026 (UTC)
: Test comment with random number 0.21500952410025898 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 12:22, 23 March 2026 (UTC)
: Test comment with random number 0.6571328205265842 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:21, 23 March 2026 (UTC)
: Test comment with random number 0.8725721668943434 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:21, 24 March 2026 (UTC)
: Test comment with random number 0.9535110784110594 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 11:22, 24 March 2026 (UTC)
: Test comment with random number 0.4330065153484025 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 03:55, 27 March 2026 (UTC)
: Test comment with random number 0.7353033907097808 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.44304195516553146 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:23, 27 March 2026 (UTC)
: Test comment with random number 0.02243804450899023 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.520846091950367 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:24, 27 March 2026 (UTC)
: Test comment with random number 0.9946058761624214 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:25, 27 March 2026 (UTC)
: Test comment with random number 0.1691580237328757 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:28, 27 March 2026 (UTC)
: Test comment with random number 0.06490355868980668 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:30, 27 March 2026 (UTC)
: Test comment with random number 0.9392023221346153 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 10:32, 27 March 2026 (UTC)
: Test comment with random number 0.5537697536904882 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 16:38, 1 April 2026 (UTC)
: Test comment with random number 0.470848341599752 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:14, 1 April 2026 (UTC)
: Test comment with random number 0.41673313374066356 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:15, 1 April 2026 (UTC)
: Test comment with random number 0.671732439038764 [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 17:35, 1 April 2026 (UTC)
:testtt [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:10, 7 April 2026 (UTC)
: test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 17:59, 7 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>
== Section for moving ==
test [[User:Test account 8|Test account 8]] ([[User talk:Test account 8|talk]]) 19:52, 15 March 2026 (UTC)
ea8vd1ze34d58mhobd4m53ddz3ykh40
Test
0
155073
737138
736093
2026-04-08T07:09:05Z
~2026-21716-09
73454
737138
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 = 1959
|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 =
|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]].
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
== 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
42mlmdrxk4v9c8nhura68fdmpvb4281
737139
737138
2026-04-08T07:09:27Z
~2026-21716-09
73454
737139
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 = 1959
|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 =
|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]].
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
== 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
of21vnz8udyhg6ksui4439ahbw1yntb
User:KHarlan (WMF)/Sandbox
2
162841
737074
663563
2026-04-07T13:30:17Z
KHarlan (WMF)
40269
737074
wikitext
text/x-wiki
The quick brown fox jumps over the lazy dog
5l538vehydhyhvhdqqto838i5g6045u
737075
737074
2026-04-07T13:30:49Z
KHarlan (WMF)
40269
737075
wikitext
text/x-wiki
The quick brown fox jumps over the [[:en:Wikipedia:Vandalism|lazy dog]]
obk2mzm2jukspmc90r49hrog4akmm9l
737076
737075
2026-04-07T13:30:56Z
KHarlan (WMF)
40269
737076
wikitext
text/x-wiki
The quick, brown fox jumps over the [[:en:Wikipedia:Vandalism|lazy dog]].
90pygcd8geqimnmzs2sj9ys2ipqa9sv
737077
737076
2026-04-07T14:27:49Z
KHarlan (WMF)
40269
737077
wikitext
text/x-wiki
The brown fox jumps over the [[:en:Wikipedia:Vandalism|lazy dog]].
40ssso4e2zvyy9o8dtc33rez474rrdr
File:Wikitech-2021-blue-large-icon(2).svg
6
166430
737087
676578
2026-04-07T17:30:03Z
Ladsgroup
2217
Ladsgroup uploaded a new version of [[File:Wikitech-2021-blue-large-icon(2).svg]]
659173
wikitext
text/x-wiki
== Licensing ==
{{self|GFDL|cc-by-sa-all|migration=redundant}}
5ln3dh380i59r738tvezfwc817fpsth
737088
737087
2026-04-07T17:30:47Z
Ladsgroup
2217
Ladsgroup uploaded a new version of [[File:Wikitech-2021-blue-large-icon(2).svg]]
659173
wikitext
text/x-wiki
== Licensing ==
{{self|GFDL|cc-by-sa-all|migration=redundant}}
5ln3dh380i59r738tvezfwc817fpsth
Earthling
0
166682
737081
688917
2026-04-07T16:51:42Z
Ponor
47975
Ponor moved page [[Earthling]] to [[Earthlings]] without leaving a redirect: rename ([[:en:User:Ponor/wAwB|wAwB]])
688917
wikitext
text/x-wiki
words, words, words
{{sitelink|es|qid=Q42}}
== If then show ==
_{{if then show|x|| b | c }}_
_ b x c _
9o75dyqm2k5i0fm2iztswqnyuqwnpkq
737082
737081
2026-04-07T16:52:27Z
Ponor
47975
Ponor moved page [[Earthlings]] to [[Earthling]]: rename ([[:en:User:Ponor/wAwB|wAwB]])
688917
wikitext
text/x-wiki
words, words, words
{{sitelink|es|qid=Q42}}
== If then show ==
_{{if then show|x|| b | c }}_
_ b x c _
9o75dyqm2k5i0fm2iztswqnyuqwnpkq
737097
737082
2026-04-07T18:34:44Z
Ponor
47975
Ponor moved page [[Earthling]] to [[Earthlings]] over redirect: test ([[:en:User:Ponor/wAwB|wAwB]])
688917
wikitext
text/x-wiki
words, words, words
{{sitelink|es|qid=Q42}}
== If then show ==
_{{if then show|x|| b | c }}_
_ b x c _
9o75dyqm2k5i0fm2iztswqnyuqwnpkq
737099
737097
2026-04-07T18:35:00Z
Ponor
47975
Ponor moved page [[Earthlings]] to [[Earthling]] over redirect: test ([[:en:User:Ponor/wAwB|wAwB]])
688917
wikitext
text/x-wiki
words, words, words
{{sitelink|es|qid=Q42}}
== If then show ==
_{{if then show|x|| b | c }}_
_ b x c _
9o75dyqm2k5i0fm2iztswqnyuqwnpkq
Usuario:~2025-28536-58/Taller/TestPage20251012011131
0
168415
737130
725011
2026-04-07T21:25:17Z
Arcchie
73447
Grammatical correction
737130
wikitext
text/x-wiki
Hi, I'm a new page!
{{Ficha de ecorregión
| nombre = Sabana del Gran Escarpe de Angola
| ecozona = AT
| bioma = 10
| extensión = 74 400
| estado = VU
| mapa = AT1002_map.png
| países = {{ANG}}
}}
La '''sabana del Gran Escarpe de Angola''' es una [[ecorregión]] de la [[ecozona afrotropical]], definida por [[World Wide Fund for Nature|WWF]], que ocupa el [[Gran Escarpa (África del Sur)|Gran Escarpe]] que separa la [[meseta centroafricana]] de la costa del [[océano Atlántico]].
== Descripción ==
Es una ecorregión de [[pradera de montaña]] que ocupa 74 400 kilómetros cuadrados a lo largo de la estrecha franja de tierra que va desde la costa atlántica hasta la cumbre del Gran Escarpe, a más de 1000 {{esd|[[m s. n. m.]]}}, entre los 6 y los 12° S.
Limita al norte con el [[manglar de África central]], al noreste con el [[mosaico de selva y sabana del Congo occidental]], al noroeste con el [[océano Atlántico]], al sur, sureste y suroeste con la [[sabana arbolada de miombo de Angola]] y al sureste también con el [[Mosaico montano de selva y pradera de Angola]].
El clima es tropical, con lluvias estivales.
== Flora ==
La vegetación es muy variada, aunque hay pocos estudios sobre la diversidad de especies. De norte a sur se distinguen tres zonas principales: un mosaico de selvas de galería y praderas de [[hierba]] alta, entre 2 y 4 metros, con manglares y pantanos en la desembocadura de los ríos principales, al norte del [[río Cuanza]]; selva semicaducifolia en las cumbres más altas del Escarpe; y sabanas arboladas áridas y semiáridas al sur del Cuanza.
== Fauna ==
Sólo se ha estudiado en detalle la [[Aves|avifauna]].
Entre los [[Mammalia|mamíferos]] destacan, en las selvas, el elefante de selva (''[[Loxodonta cyclotis]]''), el [[Cephalophus|duíquero]] bayo (''[[Cephalophus dorsalis]]''), el cefalofo silvicultor (''[[Cephalophus sylvicultor]]''), el duíquero de frente negra (''[[Cephalophus nigrifrons]]''), el cefalofo azul (''[[Cephalophus monticola]]''), el [[Tragulidae|hiemosco]] (''[[Hyemoschus aquaticus]]''), la [[Anomaluridae|ardilla voladora]] de Beecroft (''[[Anomulurus beecrofti]]''), la [[Sciuridae|ardilla]] gigante de Stanger (''[[Protoxerus stangeri]]''), el [[Lorisidae|poto]] dorado (''[[Arctocebus aureus]]''), el poto de Bosman (''[[Perodicticus potto]]'') y el [[Manis|pangolín]] arborícola (''[[Manis tricuspis]]''); en las sabanas cabe citar el elefante de sabana (''[[Loxodonta africana]]''), el antílope ruano (''[[Hippotragus equinus]]''), el [[redunca]] común (''[[Redunca arundinum]]''), el [[Tragelaphus|bushbuck]] (''[[Tragelaphus scriptus]]'') y el [[Taurotragus|eland]] de El Cabo (''[[Taurotragus oryx]]'').
Por toda la ecorregión se encuentra el búfalo rojo (''[[Syncerus caffer]] nanus'').
== [[Endemismo]]s ==
Hay dos especies endémicas de [[reptil]]es: el [[Hemidactylus|geco]] (''[[Hemidactylus bayonii]]'') y la [[Amphisbaenia|culebrilla ciega]] (''[[Monopeltis luandae]]''); y cuatro de [[rana]]s: ''[[Hydrophylax parkerianus]]'', ''[[Hyperolius punctulatus]]'', ''[[Leptopelis jordani]]'' y ''[[Leptopelis marginatus]]''.
== [[Estado de conservación]] ==
Vulnerable. El impacto humano se limita a las zonas densamente pobladas, como la capital de Angola, [[Luanda]].
== Protección ==
* [[Parque Nacional de Kissama]]
* [[Reserva natural integral del Ilheu dos Passaros]]
== Enlaces externos ==
*[http://www.worldwildlife.org/wildworld/profiles/terrestrial/at/at1002_full.html Angolan Scarp savanna and woodlands (World Wildlife Fund)]
* [http://www.nationalgeographic.com/wildworld/profiles/terrestrial/at/1002.html Angolan scarp savanna and woodlands (National Geographic)]
{{Control de autoridades}}
[[Categoría:Ecorregiones de Angola]]
f84ywj255hol8un1t8my6v6xvfltvhb
Aethereouss
0
171607
737123
725173
2026-04-07T20:10:16Z
Ponor
47975
test
737123
wikitext
text/x-wiki
Checking my options
-
rywdwk20dtk42c97o3hw0ngemqgwwsz
User:Ponor/wAwB-worker.js
2
171632
737085
732250
2026-04-07T17:12:32Z
Ponor
47975
page/file-movers can move, admins can also delete and protect
737085
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function() {});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
run: function(payload) {
return new Promise(function(resolve, reject) {
var libCode = payload.libraryCode || "";
var scriptContent = libCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);
var timer = setTimeout(function() {
worker.terminate();
URL.revokeObjectURL(blobURL);
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
worker.onmessage = function(e) {
clearTimeout(timer);
worker.terminate();
URL.revokeObjectURL(blobURL);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
worker.postMessage(payload);
});
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
$tr.on('click', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
rightLineNum++;
}
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
f1belsww9xux9d1z0dkuwcoyoe5et3h
737086
737085
2026-04-07T17:23:37Z
Ponor
47975
accident. del
737086
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function() {});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
run: function(payload) {
return new Promise(function(resolve, reject) {
var libCode = payload.libraryCode || "";
var scriptContent = libCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);
var timer = setTimeout(function() {
worker.terminate();
URL.revokeObjectURL(blobURL);
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
worker.onmessage = function(e) {
clearTimeout(timer);
worker.terminate();
URL.revokeObjectURL(blobURL);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
worker.postMessage(payload);
});
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
$tr.on('click', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
rightLineNum++;
}
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
ry8yyaie2m323j7njwap5j7xwb1hgpi
737095
737086
2026-04-07T18:23:09Z
Ponor
47975
([[m:Special:MyLanguage/User:Jon Harald Søby/diffedit|diffedit]])
737095
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function() {});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
run: function(payload) {
return new Promise(function(resolve, reject) {
var libCode = payload.libraryCode || "";
var scriptContent = libCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);
var timer = setTimeout(function() {
worker.terminate();
URL.revokeObjectURL(blobURL);
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
worker.onmessage = function(e) {
clearTimeout(timer);
worker.terminate();
URL.revokeObjectURL(blobURL);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
worker.postMessage(payload);
});
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
$tr.on('click', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
rightLineNum++;
}
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
if (IS_ADMIN) updateInterfaceMode();
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
tcrtc1jnngga4k5yxd5vv17gszsegjj
737096
737095
2026-04-07T18:28:05Z
Ponor
47975
737096
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function() {});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
run: function(payload) {
return new Promise(function(resolve, reject) {
var libCode = payload.libraryCode || "";
var scriptContent = libCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);
var timer = setTimeout(function() {
worker.terminate();
URL.revokeObjectURL(blobURL);
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
worker.onmessage = function(e) {
clearTimeout(timer);
worker.terminate();
URL.revokeObjectURL(blobURL);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
worker.postMessage(payload);
});
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
$tr.on('click', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
rightLineNum++;
}
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
mf45howu08gpmjfby6bylsmb3sct84q
737103
737096
2026-04-07T18:50:55Z
Ponor
47975
Attach a single delegated click listener to the diff table instead of every row
737103
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function() {});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
run: function(payload) {
return new Promise(function(resolve, reject) {
var libCode = payload.libraryCode || "";
var scriptContent = libCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);
var timer = setTimeout(function() {
worker.terminate();
URL.revokeObjectURL(blobURL);
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
worker.onmessage = function(e) {
clearTimeout(timer);
worker.terminate();
URL.revokeObjectURL(blobURL);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
worker.postMessage(payload);
});
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
rightLineNum++;
}
});
// Attach a single delegated click listener to the table instead of every row
$visualOut.find('table.diff').on('click', 'tr[data-line]', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
aq0pd5no0jmyd39pcc3vveo114owo33
737106
737103
2026-04-07T18:56:01Z
Ponor
47975
catch codemirror load errors
737106
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function(err) {
console.error("CM Load Error:", err);
});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
run: function(payload) {
return new Promise(function(resolve, reject) {
var libCode = payload.libraryCode || "";
var scriptContent = libCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);
var timer = setTimeout(function() {
worker.terminate();
URL.revokeObjectURL(blobURL);
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
worker.onmessage = function(e) {
clearTimeout(timer);
worker.terminate();
URL.revokeObjectURL(blobURL);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
worker.postMessage(payload);
});
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
rightLineNum++;
}
});
// Attach a single delegated click listener to the table instead of every row
$visualOut.find('table.diff').on('click', 'tr[data-line]', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
ct74ws9myntmovfvcgzip5bwnh0la8f
737109
737106
2026-04-07T19:24:22Z
Ponor
47975
Refactor WorkerEngine to use a persistent Web Worker
737109
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " ([[:en:User:Ponor/wAwB|wAwB]])";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror.v6'))($textArea[0], (require('ext.CodeMirror.v6.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function(err) {
console.error("CM Load Error:", err);
});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
activeWorker: null,
workerURL: null,
currentLibCode: null,
timeoutTimer: null,
initWorker: function(libCode) {
this.destroy(); // Clean up existing if any
this.currentLibCode = libCode || "";
var scriptContent = this.currentLibCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
this.workerURL = URL.createObjectURL(blob);
this.activeWorker = new Worker(this.workerURL);
},
run: function(payload) {
var self = this;
return new Promise(function(resolve, reject) {
// Re-init if no worker exists, or if the user changed the library code
if (!self.activeWorker || self.currentLibCode !== (payload.libraryCode || "")) {
self.initWorker(payload.libraryCode);
}
if (self.timeoutTimer) clearTimeout(self.timeoutTimer);
self.timeoutTimer = setTimeout(function() {
self.destroy(); // Assassinate the stuck worker
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
self.activeWorker.onmessage = function(e) {
clearTimeout(self.timeoutTimer);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
self.activeWorker.postMessage(payload);
});
},
destroy: function() {
if (this.activeWorker) {
this.activeWorker.terminate();
this.activeWorker = null;
}
if (this.workerURL) {
URL.revokeObjectURL(this.workerURL);
this.workerURL = null;
}
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror.v6', 'ext.CodeMirror.v6.modes']).then(function(require) {
var CM = require('ext.CodeMirror.v6');
var modes = require('ext.CodeMirror.v6.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
rightLineNum++;
}
});
// Attach a single delegated click listener to the table instead of every row
$visualOut.find('table.diff').on('click', 'tr[data-line]', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
WorkerEngine.destroy();
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
WorkerEngine.destroy();
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = DO_TAG ? inputSummary.getValue() : inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
l9v4ktsuc3c907pno6tayeesajolqkl
User:AsteraBot/pages to fix
2
171953
737135
736665
2026-04-08T01:05:16Z
AsteraBot
69554
testing again
737135
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
User:Enbi/common.js
2
172174
737117
736370
2026-04-07T19:45:01Z
Enbi
72574
Installing [[User:Enbi/testScript2.js]] ([[User:Enterprisey/script-installer|script-installer]])
737117
javascript
text/javascript
importScript('User:Enbi/UnnamedScript.js');
importScript('User:Enbi/testScript.js');
importScript('User:Enbi/simul.js');
importScript('User:Enbi/newUserFilter.js');
importScript('User:Enbi/new.js');
importScript('User:Enbi/wikiCMD.js');
importScript('User:Enbi/LAC.js');
importScript('User:enbi/LTA-undo.js');
importScript('User:Enbi/LACsuppresssed.js');
importScript('w:en:MediaWiki:Gadget-script-installer.js')
importScript('User:Enbi/highlightUsernamePatterns.js'); // Backlink: [[User:Enbi/highlightUsernamePatterns.js]]
importScript('User:Enbi/testScript2.js'); // Backlink: [[User:Enbi/testScript2.js]]
71z4f92bwskbbleswmgs2t1chyd5fw6
User:ToluAyod/Main Page
2
174233
737112
736430
2026-04-07T19:42:08Z
ToluAyod
69650
Updated Main Page via StarterKit tool
737112
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 {{SITENAME}} 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 {{SITENAME}}</span>
</div>
{{User:ToluAyod/Starter kit/Content categories}}
{{User:ToluAyod/Starter kit/In the news}}
{{User:ToluAyod/Starter kit/On this day}}
<div style="margin-top: 10px;">{{User:ToluAyod/Starter kit/Wikipedia's sister projects}}</div>
<div style="margin-top: 10px;">{{User:ToluAyod/Starter kit/Wikipedia languages}}</div>
__NOTOC__
4slljevilluf8qemfzom97q96rejxuj
737127
737112
2026-04-07T20:49:58Z
ToluAyod
69650
Updated Main Page via StarterKit tool
737127
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 {{SITENAME}} 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 {{SITENAME}}</span>
</div>
{{User:ToluAyod/Starter kit/Content categories}}
{{User:ToluAyod/Starter kit/Ninu iroyin}}
{{User:ToluAyod/Starter kit/On this day}}
<div style="margin-top: 10px;">{{User:ToluAyod/Starter kit/Wikipedia's sister projects}}</div>
<div style="margin-top: 10px;">{{User:ToluAyod/Starter kit/Ede Wikipedia}}</div>
__NOTOC__
4u7uxy1wp1qvh45uh38f43q8nrl2buo
Wikipedia:Lua/Module/AccountUtl/Test
4
174625
737072
736678
2026-04-07T12:50:51Z
PerfektesChaos
18104
speedy deletion
737072
wikitext
text/x-wiki
{{delete}}
35r2j9t4ectnt1cmb7mlgcqvwz6h5k6
User:MrJaroslavik/CustomSidebar.js
2
174667
737078
736598
2026-04-07T15:58:09Z
MrJaroslavik
44012
+
737078
javascript
text/javascript
// LINKS IN SIDEBAR
$(function () {
const separator = ' / ';
const dbName = mw.config.get('wgDBname');
const pageName = mw.config.get('wgPageName');
const relevantUser = mw.config.get('wgRelevantUserName');
const pEnc = encodeURIComponent(pageName);
// Helper function for portal creation
const createPortal = (id, label) => {
return $('<div>', { 'class': 'portal', 'role': 'navigation', 'id': id })
.append($('<h3>', { 'text': label }))
.append($('<div>', { 'class': 'body' }).append($('<ul>')));
};
// Helper function for adding a link
const addLink = ($list, href, text, title) => {
return $('<a>', { 'href': href, 'title': title, 'text': text }).appendTo($list);
};
// --- 1. LIST USERS ---
const $pList = createPortal('p-listusers', 'ListUsers');
const $liList = $('<li>', { 'id': 't-listusers' }).appendTo($pList.find('ul'));
addLink($liList, '/wiki/Special:ListUsers/sysop', 'A', 'Administrators');
$liList.append(separator);
addLink($liList, '/wiki/Special:ListUsers/bureaucrat', 'B', 'Bureaucrats');
$liList.append(separator);
addLink($liList, '/wiki/Special:ListUsers/checkuser', 'CU', 'CheckUsers');
$liList.append(separator);
addLink($liList, '/wiki/Special:ListUsers/suppress', 'OS', 'Oversighters');
// --- 2. RECENT CHANGES ---
const $pPages = createPortal('p-recenttchanges', 'Recent Changes');
const $ulPages = $pPages.find('ul');
$('<li>', { 'id': 't-meta-rc' }).append(addLink($ulPages, 'https://meta.wikimedia.org/wiki/Special:RecentChanges', 'Meta-Wiki', 'Open Meta-Wiki Recent Changes')).appendTo($ulPages);
$('<li>', { 'id': 't-cswiki-rc' }).append(addLink($ulPages, 'https://cs.wikipedia.org/wiki/Special:RecentChanges', 'Czech Wikipedia', 'Open Czech Wikipedia Recent Changes')).appendTo($ulPages);
$('<li>', { 'id': 't-skwiki-rc' }).append(addLink($ulPages, 'https://sk.wikipedia.org/wiki/Special:RecentChanges', 'Slovak Wikipedia', 'Open Slovak Wikipedia Recent Changes')).appendTo($ulPages);
$('<li>', { 'id': 't-ombuds-rc' }).append(addLink($ulPages, 'https://ombuds.wikimedia.org/wiki/Special:RecentChanges', 'Ombuds Wiki', 'Open Ombuds Wiki Recent Changes')).appendTo($ulPages);
$('<li>', { 'id': 't-cu-rc' }).append(addLink($ulPages, 'https://checkuser.wikimedia.org/wiki/Special:RecentChanges', 'CheckUser Wiki', 'Open CheckUser Wiki Recent Changes')).appendTo($ulPages);
// --- 3. LOGS ---
const $pLogs = createPortal('p-logs', 'Logs');
const $liLogs = $('<li>', { 'id': 't-logs-links' }).appendTo($pLogs.find('ul'));
addLink($liLogs, '/wiki/Special:AbuseLog', 'A', 'AbuseLog');
$liLogs.append(separator);
addLink($liLogs, '/wiki/Special:Logs/block', 'B', 'Blocks');
$liLogs.append(separator);
addLink($liLogs, '/wiki/Special:CheckUserLog', 'CU', 'CheckUserLog');
$liLogs.append(separator);
addLink($liLogs, '/wiki/Special:Logs/delete', 'D', 'Deletions');
$liLogs.append(separator);
addLink($liLogs, '/wiki/Special:Logs/protect', 'P', 'Protections');
$liLogs.append(separator);
addLink($liLogs, '/wiki/Special:Logs/rights', 'R', 'Rights Log');
$liLogs.append(separator);
addLink($liLogs, '/wiki/Special:Logs/suppress', 'S', 'SuppressionLog');
// --- 4. USER TOOLS ---
let $pUserTools = $();
if (relevantUser !== null) {
$pUserTools = createPortal('p-user-custom', 'User tools');
const $ulUser = $pUserTools.find('ul');
const uEnc = encodeURIComponent(relevantUser);
const server = mw.config.get('wgServerName');
// MOVE IP REVEAL TOOL (Přenese nástroj z Toolboxu do User tools, pokud existuje)
const $ipReveal = $('#t-checkuser-ip-auto-reveal');
if ($ipReveal.length) {
$ipReveal.detach().appendTo($ulUser);
}
// Standardní odkazy
$('<li>').append(addLink($ulUser, '/wiki/Special:Contributions/' + uEnc, 'User contributions', 'Show user contributions')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + uEnc, 'Global Contributions', 'Show global contributions')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, '/wiki/Special:Log/' + uEnc, 'Logs', 'Show user logs')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, '/wiki/Special:EmailUser/' + uEnc, 'Send mail to this user', 'Email user')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, '/wiki/Special:UserRights/' + uEnc, 'Show user groups', 'User rights')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + uEnc, 'CentralAuth', 'Global account info')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, 'https://xtools.wmcloud.org/ec/' + server + '/' + uEnc, 'XTools', 'XTools Edit Counter')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, 'https://xtools.wmcloud.org/ec-rightschanges/' + server + '/' + uEnc, 'Rights Changes', 'XTools Rights Changes')).appendTo($ulUser);
$('<li>').append(addLink($ulUser, 'https://meta.toolforge.org/userpages/' + uEnc, 'Global userpages', 'Global userpages')).appendTo($ulUser);
}
// --- 5. PAGE TOOLS ---
const $pPageTools = createPortal('p-page-custom', 'Page tools');
const $ulPage = $pPageTools.find('ul');
$('<li>').append(addLink($ulPage, mw.util.getUrl(null, { action: 'info' }), 'Page info', 'Page information')).appendTo($ulPage);
$('<li>').append(addLink($ulPage, '/wiki/Special:WhatLinksHere/' + pEnc, 'What links here', 'Show pages that link here')).appendTo($ulPage);
$('<li>').append(addLink($ulPage, mw.util.getUrl('Special:PrefixIndex/' + pageName + '/'), 'Subpages', 'Show subpages')).appendTo($ulPage);
$('<li>').append(addLink($ulPage, 'https://swviewer.toolforge.org/', 'SWViewer', 'Global patrol')).appendTo($ulPage);
$('<li>').append(addLink($ulPage, 'https://meta.wikimedia.org/wiki/Special:UrlShortener?url=https:' + mw.config.get('wgServer') + '/wiki/' + pageName, 'URL Shortener', 'Short URL')).appendTo($ulPage);
$('<li>').append(addLink($ulPage, mw.util.getUrl(null, { action: 'history', feed: 'rss' }), 'RSS history', 'RSS feed for page history')).appendTo($ulPage);
if (dbName === 'cswiki') {
const cvUrl = 'https://copyvios.toolforge.org/?lang=cs&project=wikipedia&title=' + pEnc;
$('<li>').append(addLink($ulPage, cvUrl, 'CVDetector', 'Copyvio Check')).appendTo($ulPage);
}
// Inject all portals
$("#p-navigation").after($pList, $pPages, $pLogs, $pUserTools, $pPageTools);
});
t9vw8ezcykss9q7jxaz75lwg8zo6mhl
User:MrJaroslavik/GlobalCheckUserStats.js
2
174673
737071
737070
2026-04-07T12:50:04Z
MrJaroslavik
44012
dropdown
737071
javascript
text/javascript
// GlobalCheckUserStats.js
// Features:
// - Global Audit: Scans CheckUser logs across 900+ Wikimedia projects at once.
// - Smart Categorization: Identifies roles (Local CU, Steward, Staff, etc.).
// - Steward Logic: Detects temporary access (<48h) using metadata and expiry logs.
// - Deep Scan: Automated pagination to bypass the 500-entry API limit (essential for enwiki).
// - Robust Connection: Automated retries for 429 rate-limits and custom domain mapping.
// - Full Reporting: Generates sortable Wikitables and detailed rights change logs.
// - Custom UI: Integrated control panel on Special:BlankPage/GlobalCheckUserStats.
// - With help of Gemini 3
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Blankpage' || mw.config.get('wgTitle').indexOf('GlobalCheckUserStats') === -1) return;
const rawWikis = ['abstractwiki','abwiki','acewiki','adywiki','afwiki','afwikibooks','afwikiquote','afwiktionary','alswiki','altwiki','amiwiki','amwiki','amwiktionary','angwiki','angwiktionary','annwiki','anpwiki','anwiki','anwiktionary','arcwiki','arwiki','arwikibooks','arwikimedia','arwikinews','arwikiquote','arwikisource','arwikiversity','arwiktionary','arywiki','arzwiki','astwiki','astwiktionary','aswiki','aswikiquote','aswikisource','atjwiki','avkwiki','avwiki','awawiki','aywiki','aywiktionary','azbwiki','azwiki','azwikibooks','azwikiquote','azwikisource','azwiktionary','banwiki','banwikisource','barwiki','bat_smgwiki','bawiki','bawikibooks','bbcwiki','bclwiki','bclwikiquote','bclwikisource','bclwiktionary','bdrwiki','bdwikimedia','be_x_oldwiki','betawikiversity','bewiki','bewikibooks','bewikimedia','bewikiquote','bewikisource','bewiktionary','bewwiki','bewwiktionary','bgwiki','bgwikibooks','bgwikiquote','bgwikisource','bgwiktionary','bhwiki','biwiki','bjnwiki','bjnwikiquote','bjnwiktionary','blkwiki','blkwiktionary','bmwiki','bnwiki','bnwikibooks','bnwikiquote','bnwikisource','bnwikivoyage','bnwiktionary','bowiki','bpywiki','brwiki','brwikimedia','brwikiquote','brwikisource','brwiktionary','bswiki','bswikibooks','bswikinews','bswikiquote','bswikisource','bswiktionary','btmwiki','btmwiktionary','bugwiki','bxrwiki','cawiki','cawikibooks','cawikimedia','cawikinews','cawikiquote','cawikisource','cawiktionary','cbk_zamwiki','cdowiki','cebwiki','cewiki','chrwiki','chrwiktionary','chwiki','chywiki','ckbwiki','ckbwiktionary','commonswiki','cowiki','cowikimedia','cowiktionary','crhwiki','csbwiki','csbwiktionary','cswiki','cswikibooks','cswikinews','cswikiquote','cswikisource','cswikiversity','cswikivoyage','cswiktionary','cuwiki','cvwiki','cvwikibooks','cywiki','cywikibooks','cywikiquote','cywikisource','cywiktionary','dagwiki','dawiki','dawikibooks','dawikiquote','dawikisource','dawiktionary','dewiki','dewikibooks','dewikinews','dewikiquote','dewikisource','dewikiversity','dewikivoyage','dewiktionary','dgawiki','dinwiki','diqwiki','diqwiktionary','dkwikimedia','dsbwiki','dtpwiki','dtywiki','dvwiki','dvwiktionary','dzwiki','eewiki','elwiki','elwikibooks','elwikinews','elwikiquote','elwikisource','elwikiversity','elwikivoyage','elwiktionary','emlwiki','enwiki','enwikibooks','enwikinews','enwikiquote','enwikisource','enwikiversity','enwikivoyage','enwiktionary','eowiki','eowikibooks','eowikinews','eowikiquote','eowikisource','eowikivoyage','eowiktionary','eswiki','eswikibooks','eswikinews','eswikiquote','eswikisource','eswikiversity','eswikivoyage','eswiktionary','etwiki','etwikibooks','eewikimedia','etwikiquote','etwikisource','etwiktionary','euwiki','euwikibooks','euwikiquote','euwikisource','euwiktionary','extwiki','fatwiki','fawiki','fawikibooks','fawikinews','fawikiquote','fawikisource','fawikivoyage','fawiktionary','ffwiki','fiu_vrowiki','fiwiki','fiwikibooks','fiwikimedia','fiwikinews','fiwikiquote','fiwikisource','fiwikiversity','fiwikivoyage','fiwiktionary','fjwiki','fjwiktionary','fonwiki','foundationwiki','fowiki','fowikisource','fowiktionary','frpwiki','frrwiki','frwiki','frwikibooks','frwikinews','frwikiquote','frwikisource','frwikiversity','frwikivoyage','frwiktionary','furwiki','fywiki','fywikibooks','fywiktionary','gagwiki','ganwiki','gawiki','gawiktionary','gcrwiki','gdwiki','gdwiktionary','glkwiki','glwiki','glwikibooks','glwikiquote','glwikisource','glwiktionary','gnwiki','gnwiktionary','gomwiki','gomwiktionary','gorwiki','gorwikiquote','gorwiktionary','gotwiki','gpewiki','gucwiki','gurwiki','guwiki','guwikiquote','guwikisource','guwiktionary','guwwiki','guwwikinews','guwwikiquote','guwwiktionary','gvwiki','gvwiktionary','hakwiki','hawiki','hawiktionary','hawwiki','hewiki','hewikibooks','hewikinews','hewikiquote','hewikisource','hewikivoyage','hewiktionary','hifwiki','hifwiktionary','hiwiki','hiwikibooks','hiwikiquote','hiwikisource','hiwikiversity','hiwikivoyage','hiwiktionary','hrwiki','hrwikibooks','hrwikiquote','hrwikisource','hrwiktionary','hsbwiki','hsbwiktionary','htwiki','huwiki','huwikibooks','huwikisource','huwiktionary','hywiki','hywikibooks','hywikiquote','hywikisource','hywiktionary','hywwiki','iawiki','iawikibooks','iawiktionary','ibawiki','idwiki','idwikibooks','idwikiquote','idwikisource','idwikivoyage','idwiktionary','iewiki','iewiktionary','iglwiki','igwiki','igwikiquote','igwiktionary','ikwiki','ilowiki','incubatorwiki','inhwiki','iowiki','iowiktionary','iswiki','iswikibooks','iswikiquote','iswikisource','iswiktionary','itwiki','itwikibooks','itwikinews','itwikiquote','itwikisource','itwikiversity','itwikivoyage','itwiktionary','iuwiki','iuwiktionary','jamwiki','jawiki','jawikibooks','jawikinews','jawikisource','jawikiversity','jawikivoyage','jawiktionary','jbowiki','jbowiktionary','jvwiki','jvwikisource','jvwiktionary','kaawiki','kaawiktionary','kabwiki','kaiwiki','kajwiki','kawiki','kawikibooks','kawikiquote','kawikisource','kawiktionary','kbdwiki','kbdwiktionary','kbpwiki','kcgwiki','kcgwiktionary','kgewiki','kgwiki','kiwiki','kkwiki','kkwikibooks','kkwiktionary','klwiktionary','kmwiki','kmwikibooks','kmwiktionary','kncwiki','knwiki','knwikiquote','knwikisource','knwiktionary','koiwiki','kowiki','kowikibooks','kowikinews','kowikiquote','kowikisource','kowikiversity','kowiktionary','krcwiki','kshwiki','kswiki','kswiktionary','kuswiki','kuwiki','kuwikibooks','kuwikiquote','kuwiktionary','kvwiki','kwwiki','kwwiktionary','kywiki','kywikiquote','kywiktionary','labswiki','ladwiki','lawiki','lawikibooks','lawikiquote','lawikisource','lawiktionary','lbewiki','lbwiki','lbwiktionary','lezwiki','lfnwiki','lgwiki','lijwiki','lijwikisource','liwiki','liwikibooks','liwikinews','liwikiquote','liwikisource','liwiktionary','lldwiki','lmowiki','lmowiktionary','lnwiki','lnwiktionary','lowiki','lowiktionary','ltgwiki','ltwiki','ltwikibooks','ltwikiquote','ltwikisource','ltwiktionary','lvwiki','lvwiktionary','madwiki','madwikisource','madwiktionary','maiwiki','map_bmswiki','mdfwiki','mediawikiwiki','metawiki','mgwiki','mgwikibooks','mgwiktionary','mhrwiki','minwiki','minwikibooks','minwikisource','minwiktionary','miwiki','miwiktionary','mkwiki','mkwikibooks','mkwikimedia','mkwikisource','mkwiktionary','mlwiki','mlwikibooks','mlwikiquote','mlwikisource','mlwiktionary','mniwiki','mniwiktionary','mnwiki','mnwiktionary','mnwwiki','mnwwiktionary','moswiki','mrjwiki','mrwiki','mrwikibooks','mrwikiquote','mrwikisource','mrwiktionary','mswiki','mswikibooks','mswikiquote','mswikisource','mswiktionary','mtwiki','mtwiktionary','mwlwiki','mxwikimedia','myvwiki','mywiki','mywikisource','mywiktionary','mznwiki','nahwiki','nahwiktionary','napwiki','napwikisource','nawiktionary','nds_nlwiki','ndswiki','ndswiktionary','newiki','newikibooks','newiktionary','newwiki','niawiki','niawiktionary','nlwiki','nlwikibooks','nlwikimedia','nlwikinews','nlwikiquote','nlwikisource','nlwikivoyage','nlwiktionary','nnwiki','nnwikiquote','nnwiktionary','novwiki','nowiki','nowikibooks','nowikimedia','nowikinews','nowikiquote','nowikisource','nowiktionary','nqowiki','nrmwiki','nrwiki','nsowiki','nupwiki','nvwiki','nycwikimedia','nywiki','ocwiki','ocwikibooks','ocwiktionary','olowiki','omwiki','omwiktionary','orwiki','orwikisource','orwiktionary','oswiki','outreachwiki','pagwiki','pamwiki','papwiki','pawiki','pawikibooks','pawikisource','pawiktionary','pcdwiki','pcmwiki','pcmwikiquote','pdcwiki','pflwiki','piwiki','plwiki','plwikibooks','plwikimedia','plwikinews','plwikiquote','plwikisource','plwikivoyage','plwiktionary','pmswiki','pmswikisource','pnbwiki','pnbwiktionary','pntwiki','pplwiki','pswiki','pswikivoyage','pswiktionary','ptwiki','ptwikibooks','ptwikimedia','ptwikinews','ptwikiquote','ptwikisource','ptwikiversity','ptwikivoyage','ptwiktionary','pwnwiki','quwiktionary','rkiwiki','rmwiki','rmywiki','rnwiki','roa_rupwiki','roa_rupwiktionary','roa_tarawiki','rowiki','rowikibooks','rowikinews','rowikiquote','rowiktionary','rskwiki','ruewiki','ruwiki','ruwikibooks','ruwikinews','ruwikiquote','ruwikisource','ruwikiversity','ruwikivoyage','ruwiktionary','rwwiki','rwwiktionary','sahwiki','sahwikiquote','sahwikisource','satwiki','satwiktionary','sawiki','sawikibooks','sawikiquote','sawikisource','sawiktionary','scnwiki','scnwiktionary','scowiki','scwiki','sdwiki','sdwiktionary','sewiki','sewikimedia','sgwiki','sgwiktionary','shiwiki','shnwiki','shnwikibooks','shnwikinews','shnwikivoyage','shnwiktionary','shwiki','shwiktionary','shywiktionary','simplewiki','simplewiktionary','siwiki','siwikibooks','siwiktionary','skwiki','skrwiki','skrwiktionary','slwiki','slwikibooks','slwikiquote','slwikisource','slwikiversity','slwiktionary','smnwiki','smwiki','smwiktionary','snwiki','sourceswiki','sowiki','sowiktionary','specieswiki','sqwiki','sqwikibooks','sqwikinews','sqwikiquote','sqwiktionary','srnwiki','srwiki','srwikibooks','srwikinews','srwikiquote','srwikisource','srwiktionary','sswiki','sswiktionary','stqwiki','stwiki','stwiktionary','suwiki','suwikiquote','suwikisource','suwiktionary','svwiki','svwikibooks','svwikinews','svwikiquote','svwikisource','svwikiversity','svwikivoyage','svwiktionary','swwiki','swwiktionary','sylwiki','szlwiki','szywiki','tawiki','tawikibooks','tawikinews','tawikiquote','tawikisource','tawiktionary','taywiki','tcywiki','tcywikisource','tcywiktionary','tddwiki','tewiki','tewikibooks','tewikiquote','tewikisource','tewiktionary','tgwiki','tgwikibooks','tgwiktionary','thwiki','thwikibooks','thwikimedia','thwikiquote','thwikisource','thwiktionary','tigwiki','tiwiki','tiwiktionary','tkwiki','tkwiktionary','tlwiki','tlwikibooks','tlwikiquote','tlwikisource','tlwiktionary','tlywiki','tnwiki','tnwiktionary','tokwiki','towiki','tpiwiki','tpiwiktionary','trvwiki','trwiki','trwikibooks','trwikimedia','trwikinews','trwikiquote','trwikisource','trwikivoyage','trwiktionary','tswiki','tswiktionary','ttwiki','ttwikibooks','ttwikiquote','ttwiktionary','tumwiki','twwiki','twwiktionary','tyvwiki','tywiki','uawikimedia','udmwiki','ugwiki','ugwiktionary','ukwiki','ukwikibooks','ukwikinews','ukwikiquote','ukwikisource','ukwikivoyage','ukwiktionary','urwiki','urwikibooks','urwikiquote','urwikisource','urwiktionary','uzwiki','uzwikiquote','uzwiktionary','vecwiki','vecwikisource','vecwiktionary','vepwiki','vewiki','viwiki','viwikibooks','viwikiquote','viwikisource','viwikivoyage','viwiktionary','vlswiki','vowiki','vowiktionary','warwiki','wawiki','wawikisource','wawiktionary','wikidatawiki','wikimaniawiki','wowiki','wowiktionary','wuuwiki','xalwiki','xhwiki','xmfwiki','yiwiki','yiwikisource','yiwiktionary','yowiki','zawiki','zeawiki','zghwiki','zghwiktionary','zh_classicalwiki','zh_min_nanwiki','zh_min_nanwikisource','zh_min_nanwiktionary','zh_yuewiki','zhwiki','zhwikibooks','zhwikinews','zhwikiquote','zhwikisource','zhwikiversity','zhwikivoyage','zhwiktionary','zuwiki','zuwiktionary','test2wiki','testwiki'];
const DELAY_MS = 800; // Safe balanced speed
const sleep = ms => new Promise(r => setTimeout(r, ms));
let userCache = {};
let historyCache = {}; // Added this to prevent the ReferenceError
function getDomain(db) {
const mapping = {
'commonswiki': 'commons.wikimedia.org', 'metawiki': 'meta.wikimedia.org', 'wikidatawiki': 'www.wikidata.org',
'mediawikiwiki': 'www.mediawiki.org', 'sourceswiki': 'wikisource.org', 'foundationwiki': 'foundation.wikimedia.org',
'incubatorwiki': 'incubator.wikimedia.org', 'outreachwiki': 'outreach.wikimedia.org',
'be_x_oldwiki': 'be-tarask.wikipedia.org', 'labswiki': 'wikitech.wikimedia.org',
'wikimaniawiki': 'wikimania.wikimedia.org', 'specieswiki': 'species.wikimedia.org',
'abstractwiki': 'abstract.wikipedia.org', 'betawikiversity': 'beta.wikiversity.org',
'mo_wiki': 'ro.wikipedia.org',
'testwiki': 'test.wikipedia.org', 'test2wiki': 'test2.wikipedia.org',
'wikifunctionswiki': 'www.wikifunctions.org',
'testcommonswiki': 'test-commons.wikimedia.org',
'testwikidatawiki': 'test.wikidata.org',
};
if (mapping[db]) return mapping[db];
const name = db.replace(/_/g, '-');
if (name.endsWith('wikisource')) return name.slice(0, -10) + '.wikisource.org';
if (name.endsWith('wikiversity')) return name.slice(0, -11) + '.wikiversity.org';
if (name.endsWith('wiktionary')) return name.slice(0, -10) + '.wiktionary.org';
if (name.endsWith('wikivoyage')) return name.slice(0, -10) + '.wikivoyage.org';
if (name.endsWith('wikibooks')) return name.slice(0, -9) + '.wikibooks.org';
if (name.endsWith('wikiquote')) return name.slice(0, -9) + '.wikiquote.org';
if (name.endsWith('wikinews')) return name.slice(0, -8) + '.wikinews.org';
if (name.endsWith('wikimedia')) return name.slice(0, -9) + '.wikimedia.org';
if (name.endsWith('wiki')) return name.slice(0, -4) + '.wikipedia.org';
return name + '.wikipedia.org';
}
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function() {
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php', { anonymous: true });
const now = new Date();
let results = {}, emptyWikis = [], failedWikis = [], isRunning = false;
function setupUI() {
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
// CHANGE: Dynamically generate dropdown options for years and months
let yearOpts = '';
for (let y = currentYear; y >= 2005; y--) {
yearOpts += `<option value="${y}">${y}</option>`;
}
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 === currentMonth ? 'selected' : ''}>${m}</option>`;
}
$('#firstHeading').text('GlobalCheckUserStats.js');
$('#mw-content-text').empty().append(`
<div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;">
<h3>Audit Range</h3>
From:
<select id="y-f" style="width:70px;">${yearOpts}</select>
<select id="m-f" style="width:50px;">${monthOptsFrom}</select>
To:
<select id="y-t" style="width:70px;">${yearOpts}</select>
<select id="m-t" style="width:50px;">${monthOptsTo}</select>
<button id="start" class="mw-ui-button mw-ui-progressive" style="margin-left:15px;">Run GlobalCheckUserStats.js</button>
<button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button>
<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="${rawWikis.length}" style="width:100%"></progress></div>
<textarea id="out" style="width:100%; height:450px; margin-top:10px; display:none; font-family:monospace; font-size:12px;"></textarea>
</div>
`);
$('#start').click(() => runAudit($('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val()));
$('#stop').click(() => isRunning = false);
}
// Helper function to check global rights history on Meta-Wiki
async function checkGlobalHistory(user, start, end) {
try {
const res = await metaApi.get({
action: 'query',
list: 'logevents',
letype: 'gblrights',
letitle: 'User:' + user,
formatversion: 2
});
const logs = res.query.logevents || [];
const auditStart = new Date(start);
const auditEnd = new Date(end);
let wasSteward = false, wasStaff = false, wasOmbuds = false;
for (const e of logs) {
const ts = new Date(e.timestamp);
const p = e.params || {};
const added = p.newGroups || p[1] || [];
const removed = p.oldGroups || p[0] || [];
// If a removal happened AFTER the audit started, the user held the rights during the period
if (ts > auditStart) {
if (removed.includes('steward')) wasSteward = true;
if (removed.includes('staff')) wasStaff = true;
if (removed.includes('ombuds')) wasOmbuds = true;
}
// If an addition happened BEFORE the audit ended, the user held the rights during the period
if (ts < auditEnd) {
if (added.includes('steward')) wasSteward = true;
if (added.includes('staff')) wasStaff = true;
if (added.includes('ombuds')) wasOmbuds = true;
}
}
return { wasSteward, wasStaff, wasOmbuds };
} catch (e) {
// Fallback for API errors
return { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
async function fetchUserData(user, db, start, end) {
// Priority 0: Use cache for current global groups
if (!userCache[user]) {
try {
const gres = await metaApi.get({ action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 });
userCache[user] = gres.query.globaluserinfo.groups || [];
} catch(e) { userCache[user] = []; }
}
const gGroups = userCache[user];
const isGloballyStaff = gGroups.includes('staff');
const isGloballySteward = gGroups.includes('steward');
const isGloballyOmbuds = gGroups.includes('ombuds');
// Check current local CheckUser rights
let isCurrentLocal = false;
try {
const localApi = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
const ures = await localApi.get({ action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 });
const lGroups = (ures.query.users[0] && ures.query.users[0].groups) || [];
isCurrentLocal = lGroups.includes('checkuser');
} catch(e) { /* API failure fallback */ }
// Rights log analysis for the specific Wiki
const target = 'User:' + user + '@' + db;
let logText = ''; // Default is empty, filled only if there are logs
let addTime = null, removeTime = null, isSelfAssign = false;
// CHANGE: Variables to track the longest duration and the number of actions
let maxDurationMins = -1;
let longestTimeStr = "";
let assignCount = 0;
try {
const res = await metaApi.get({
action: 'query', list: 'logevents', letype: 'rights', letitle: target,
lestart: end, leend: start, ledir: 'older',
lelimit: 'max', // ADDED: Loads up to 500 logs instead of 10
formatversion: 2
});
const events = res.query.logevents || [];
let filtered = [];
// Variables to pair removal and addition (iterating newest to oldest)
let tempRemoveTime = null;
let tempRemoveTimeStr = "Not removed";
events.forEach(e => {
const p = e.params || {};
const cuAdded = (p.add || []).includes('checkuser') || ((p.newgroups || []).includes('checkuser') && !(p.oldgroups || []).includes('checkuser'));
const cuRemoved = (p.remove || []).includes('checkuser') || (!(p.newgroups || []).includes('checkuser') && (p.oldgroups || []).includes('checkuser'));
const exactTimeLog = e.timestamp.replace('T', ' ').replace('Z', '');
if (cuRemoved) {
tempRemoveTime = new Date(e.timestamp);
tempRemoveTimeStr = exactTimeLog;
// Save the most recent removal for the overall table role categorization
if (!removeTime) {
removeTime = tempRemoveTime;
if (e.user === user || !e.user) isSelfAssign = true;
}
}
if (cuAdded) {
const currentAddTime = new Date(e.timestamp);
let metadataRemoveTime = tempRemoveTime;
let metadataRemoveTimeStr = tempRemoveTimeStr;
// Save the most recent addition for the overall table role categorization
if (!addTime) {
addTime = currentAddTime;
isSelfAssign = (e.user === user);
}
// Fallback: Check expiry in metadata if explicit removal log is missing
const metadata = p.newmetadata || [];
const cuMeta = metadata.find(m => m.group === 'checkuser');
if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity' && tempRemoveTimeStr === "Not removed") {
metadataRemoveTime = new Date(cuMeta.expiry);
metadataRemoveTimeStr = cuMeta.expiry.replace('T', ' ').replace('Z', '');
if (!removeTime) removeTime = metadataRemoveTime;
}
// Calculate duration for this specific 1-line pair
let pairDuration = "Unknown";
if (metadataRemoveTime) {
const diffMs = Math.abs(metadataRemoveTime - currentAddTime);
const diffMins = Math.round(diffMs / 60000); // CHANGE: Rounds to the nearest whole minute
const days = Math.floor(diffMins / 1440);
const hours = Math.floor((diffMins % 1440) / 60);
const mins = diffMins % 60;
if (days > 0) pairDuration = `${days}d ${hours}h ${mins}m`;
else if (hours > 0) pairDuration = `${hours}h ${mins}m`;
else if (mins > 0) pairDuration = `${mins}m`;
else pairDuration = "<1m";
// CHANGE: Track the total number of assignments and store the absolute longest duration
assignCount++;
if (diffMins > maxDurationMins) {
maxDurationMins = diffMins;
longestTimeStr = pairDuration;
}
}
// Push the combined 1-line format to the log using commas
filtered.push(`* Added: ${exactTimeLog}, Removed: ${metadataRemoveTimeStr}, Duration: ${pairDuration}`);
// Reset pair variables for the next older event in the loop
tempRemoveTime = null;
tempRemoveTimeStr = "Not removed";
}
});
// Edge case: If a removal was found but the addition happened before the audit period
if (tempRemoveTime && tempRemoveTimeStr !== "Not removed") {
filtered.push(`* Added: Before audit period, Removed: ${tempRemoveTimeStr}, Duration: Unknown`);
}
// Joins logs with newlines for the report
if (filtered.length) {
logText = '\n' + filtered.join('\n');
}
} catch (err) { /* Rights log query failed */ }
// --- GLOBAL HISTORY CHECK (WITH CACHE) ---
if (!historyCache[user]) {
if (!isGloballySteward || !isGloballyStaff || !isGloballyOmbuds) {
// Check Meta-Wiki history logs only if user is missing at least one global role today
historyCache[user] = await checkGlobalHistory(user, start, end);
} else {
historyCache[user] = { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
const history = historyCache[user];
// --- HIERARCHY LOGIC (CheckUser Audit Focus) ---
let roleLabel = "";
// 1. STAFF (Highest audit priority for WMF accountability)
if (isGloballyStaff || history.wasStaff) {
roleLabel = isGloballyStaff ? "Current Staff" : "Former Staff (in period)";
}
// 2. LOCAL CHECKUSER (Overrides Steward/Ombudsman roles if local rights exist)
else if (isCurrentLocal) {
roleLabel = "Current Local CheckUser";
}
// 3. STEWARD - SPECIFIC INTERVENTION (Using exact self-assignment duration or max duration if multiple)
else if (longestTimeStr && isSelfAssign) {
if (assignCount > 1) {
roleLabel = `Steward action (Self-assign: longest ${longestTimeStr}, see log)`;
} else {
roleLabel = `Steward action (Self-assign: ${longestTimeStr})`;
}
}
// 4. STEWARD - GENERAL (Global rights usage on external wikis)
else if (isGloballySteward || history.wasSteward) {
roleLabel = isGloballySteward ? "Current Steward" : "Former Steward (in period)";
}
// 5. OMBUDSMAN (Global oversight role)
else if (isGloballyOmbuds || history.wasOmbuds) {
roleLabel = isGloballyOmbuds ? "Current Ombudsman" : "Former Ombudsman (in period)";
}
// 6. FORMER LOCAL CheckUser (Fallback for those who lost rights but performed actions)
else {
roleLabel = "Former Local CheckUser";
}
return { role: roleLabel, log: logText };
}
async function runAudit(yf, mf, yt, mt) {
isRunning = true;
results = {};
emptyWikis = [];
failedWikis = [];
userCache = {};
historyCache = {};
$('#start').prop('disabled', true);
$('#stop').prop('disabled', false);
$('#out').hide();
// Format ISO timestamps for the API
const START = `${yf}-${mf.toString().padStart(2, '0')}-01T00:00:00Z`;
const lastDay = new Date(Date.UTC(yt, mt, 0)).getUTCDate();
const END = `${yt}-${mt.toString().padStart(2, '0')}-${lastDay.toString().padStart(2, '0')}T23:59:59Z`;
// Generate list of months for dynamic table columns
const monthCols = [];
let currY = parseInt(yf), currM = parseInt(mf);
const endY = parseInt(yt), endM = parseInt(mt);
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
while (currY < endY || (currY === endY && currM <= endM)) {
monthCols.push({
key: `${currY}-${String(currM).padStart(2, '0')}`,
label: `${monthNames[currM - 1]} ${currY}` // e.g., "Jan 2024"
});
currM++;
if (currM > 12) { currM = 1; currY++; }
}
for (let i = 0; i < rawWikis.length && isRunning; i++) {
const db = rawWikis[i];
$('#status-msg').text(`Scanning ${db} (${i + 1}/${rawWikis.length})...`);
let successLocal = false;
let continueToken = null; // Token for pagination
while (!successLocal && isRunning) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
let params = {
action: 'query',
list: 'checkuserlog',
culfrom: START,
culto: END,
culdir: 'newer',
cullimit: 'max',
formatversion: 2
};
// Add continue token if we are on page 2 or further
if (continueToken) {
Object.assign(params, continueToken);
}
let res = await api.get(params);
const ent = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) || [];
if (ent.length) {
if (!results[db]) results[db] = {};
// Sum up actions per user (total and per month)
ent.forEach(e => {
const u = e.checkuser;
if (!results[db][u]) results[db][u] = { total: 0, months: {} };
const date = new Date(e.timestamp);
const mKey = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
results[db][u].total++;
results[db][u].months[mKey] = (results[db][u].months[mKey] || 0) + 1;
});
}
// Pagination check
if (res.continue && isRunning) {
continueToken = res.continue;
$('#status-msg').text(`Scanning ${db} (fetching next page)...`);
} else {
// Wiki scan finished (either no entries at all or all pages fetched)
if (!results[db]) emptyWikis.push(db);
successLocal = true;
}
} catch (err) {
const status = (err && err.xhr) ? err.xhr.status : (err && err.status);
if (status === 429) {
// Rate limit hit, wait 30s and retry the SAME wiki/page
$('#status-msg').text(`Rate limit on ${db}. Waiting 30s...`);
await sleep(30000);
} else {
// Critical error (CORS, DNS, etc.), skip this wiki
console.error(`Skipping ${db} due to error:`, err);
failedWikis.push(db);
successLocal = true;
}
}
}
$('#bar').val(i + 1);
await sleep(DELAY_MS);
}
// --- REPORT GENERATION ---
$('#status-msg').text(isRunning ? `Generating report...` : `Generating partial report...`);
// Get current date and time in UTC
const now = new Date();
const timestamp = now.getUTCFullYear() + '-' +
String(now.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(now.getUTCDate()).padStart(2, '0') + ' ' +
String(now.getUTCHours()).padStart(2, '0') + ':' +
String(now.getUTCMinutes()).padStart(2, '0') + ' (UTC)';
let wt = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`;
wt += `''Report generated on: ${timestamp}''\n\n`;
if (!isRunning) wt += `'''Note: This is a partial report (stopped manually).'''\n\n`;
else wt += `\n`;
// Build dynamic table header with months
let headerMonths = monthCols.map(m => `!! ${m.label}`).join(' ');
wt += `{| class="wikitable sortable"\n! Wiki !! User (Category) !! '''Total Actions''' ${headerMonths}\n`;
let rightsLog = `\n== Rights Log (In Period) ==\n`;
const sortedDBs = Object.keys(results).sort();
for (const db of sortedDBs) {
const sortedUsers = Object.keys(results[db]).sort();
for (const user of sortedUsers) {
// Check global status and rights changes for each active user
const m = await fetchUserData(user, db, START, END);
const userData = results[db][user];
let rowStr = `|-\n| ${db} || ${user} (${m.role}) || '''${userData.total}'''`;
// Add monthly counts to the row (outputs 0 if no actions in that month)
monthCols.forEach(col => {
rowStr += ` || ${userData.months[col.key] || 0}`;
});
wt += `${rowStr}\n`;
// Only add to the Rights Log if there are actual logged events for this user
if (m.log) {
rightsLog += `'''${user}@${db}''':${m.log}\n\n`;
}
}
}
wt += `|}\n\n== Projects with 0 actions ==\n${emptyWikis.sort().join(', ')}\n`;
if (failedWikis.length) {
wt += `\n== Errors (Failed to scan) ==\n${failedWikis.sort().join(', ')}\n`;
}
wt += rightsLog;
$('#out').val(wt).show();
$('#status-msg').text(isRunning ? `Audit complete!` : `Audit stopped. Partial report ready.`);
$('#start').prop('disabled', false);
$('#stop').prop('disabled', true);
}
setupUI();
});
})();
89cu3l86g5bpeywnfkpbpd7ar75uzbg
737073
737071
2026-04-07T12:58:09Z
MrJaroslavik
44012
e
737073
javascript
text/javascript
// GlobalCheckUserStats.js
// Features:
// - Global Audit: Scans CheckUser logs across 900+ Wikimedia projects at once.
// - Smart Categorization: Identifies roles (Local CU, Steward, Staff, etc.).
// - Steward Logic: Detects temporary access, exact durations, and longest active periods.
// - Deep Scan: Automated pagination to bypass the 500-entry API limit (essential for enwiki).
// - Robust Connection: Automated retries for 429 rate-limits and custom domain mapping.
// - Full Reporting: Outputs sortable Wikitables and detailed rights change logs.
// - Custom UI: Integrated control panel on [[Special:BlankPage/GlobalCheckUserStats]].
// - With help of Gemini 3
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Blankpage' || mw.config.get('wgTitle').indexOf('GlobalCheckUserStats') === -1) return;
const rawWikis = ['abstractwiki','abwiki','acewiki','adywiki','afwiki','afwikibooks','afwikiquote','afwiktionary','alswiki','altwiki','amiwiki','amwiki','amwiktionary','angwiki','angwiktionary','annwiki','anpwiki','anwiki','anwiktionary','arcwiki','arwiki','arwikibooks','arwikimedia','arwikinews','arwikiquote','arwikisource','arwikiversity','arwiktionary','arywiki','arzwiki','astwiki','astwiktionary','aswiki','aswikiquote','aswikisource','atjwiki','avkwiki','avwiki','awawiki','aywiki','aywiktionary','azbwiki','azwiki','azwikibooks','azwikiquote','azwikisource','azwiktionary','banwiki','banwikisource','barwiki','bat_smgwiki','bawiki','bawikibooks','bbcwiki','bclwiki','bclwikiquote','bclwikisource','bclwiktionary','bdrwiki','bdwikimedia','be_x_oldwiki','betawikiversity','bewiki','bewikibooks','bewikimedia','bewikiquote','bewikisource','bewiktionary','bewwiki','bewwiktionary','bgwiki','bgwikibooks','bgwikiquote','bgwikisource','bgwiktionary','bhwiki','biwiki','bjnwiki','bjnwikiquote','bjnwiktionary','blkwiki','blkwiktionary','bmwiki','bnwiki','bnwikibooks','bnwikiquote','bnwikisource','bnwikivoyage','bnwiktionary','bowiki','bpywiki','brwiki','brwikimedia','brwikiquote','brwikisource','brwiktionary','bswiki','bswikibooks','bswikinews','bswikiquote','bswikisource','bswiktionary','btmwiki','btmwiktionary','bugwiki','bxrwiki','cawiki','cawikibooks','cawikimedia','cawikinews','cawikiquote','cawikisource','cawiktionary','cbk_zamwiki','cdowiki','cebwiki','cewiki','chrwiki','chrwiktionary','chwiki','chywiki','ckbwiki','ckbwiktionary','commonswiki','cowiki','cowikimedia','cowiktionary','crhwiki','csbwiki','csbwiktionary','cswiki','cswikibooks','cswikinews','cswikiquote','cswikisource','cswikiversity','cswikivoyage','cswiktionary','cuwiki','cvwiki','cvwikibooks','cywiki','cywikibooks','cywikiquote','cywikisource','cywiktionary','dagwiki','dawiki','dawikibooks','dawikiquote','dawikisource','dawiktionary','dewiki','dewikibooks','dewikinews','dewikiquote','dewikisource','dewikiversity','dewikivoyage','dewiktionary','dgawiki','dinwiki','diqwiki','diqwiktionary','dkwikimedia','dsbwiki','dtpwiki','dtywiki','dvwiki','dvwiktionary','dzwiki','eewiki','elwiki','elwikibooks','elwikinews','elwikiquote','elwikisource','elwikiversity','elwikivoyage','elwiktionary','emlwiki','enwiki','enwikibooks','enwikinews','enwikiquote','enwikisource','enwikiversity','enwikivoyage','enwiktionary','eowiki','eowikibooks','eowikinews','eowikiquote','eowikisource','eowikivoyage','eowiktionary','eswiki','eswikibooks','eswikinews','eswikiquote','eswikisource','eswikiversity','eswikivoyage','eswiktionary','etwiki','etwikibooks','eewikimedia','etwikiquote','etwikisource','etwiktionary','euwiki','euwikibooks','euwikiquote','euwikisource','euwiktionary','extwiki','fatwiki','fawiki','fawikibooks','fawikinews','fawikiquote','fawikisource','fawikivoyage','fawiktionary','ffwiki','fiu_vrowiki','fiwiki','fiwikibooks','fiwikimedia','fiwikinews','fiwikiquote','fiwikisource','fiwikiversity','fiwikivoyage','fiwiktionary','fjwiki','fjwiktionary','fonwiki','foundationwiki','fowiki','fowikisource','fowiktionary','frpwiki','frrwiki','frwiki','frwikibooks','frwikinews','frwikiquote','frwikisource','frwikiversity','frwikivoyage','frwiktionary','furwiki','fywiki','fywikibooks','fywiktionary','gagwiki','ganwiki','gawiki','gawiktionary','gcrwiki','gdwiki','gdwiktionary','glkwiki','glwiki','glwikibooks','glwikiquote','glwikisource','glwiktionary','gnwiki','gnwiktionary','gomwiki','gomwiktionary','gorwiki','gorwikiquote','gorwiktionary','gotwiki','gpewiki','gucwiki','gurwiki','guwiki','guwikiquote','guwikisource','guwiktionary','guwwiki','guwwikinews','guwwikiquote','guwwiktionary','gvwiki','gvwiktionary','hakwiki','hawiki','hawiktionary','hawwiki','hewiki','hewikibooks','hewikinews','hewikiquote','hewikisource','hewikivoyage','hewiktionary','hifwiki','hifwiktionary','hiwiki','hiwikibooks','hiwikiquote','hiwikisource','hiwikiversity','hiwikivoyage','hiwiktionary','hrwiki','hrwikibooks','hrwikiquote','hrwikisource','hrwiktionary','hsbwiki','hsbwiktionary','htwiki','huwiki','huwikibooks','huwikisource','huwiktionary','hywiki','hywikibooks','hywikiquote','hywikisource','hywiktionary','hywwiki','iawiki','iawikibooks','iawiktionary','ibawiki','idwiki','idwikibooks','idwikiquote','idwikisource','idwikivoyage','idwiktionary','iewiki','iewiktionary','iglwiki','igwiki','igwikiquote','igwiktionary','ikwiki','ilowiki','incubatorwiki','inhwiki','iowiki','iowiktionary','iswiki','iswikibooks','iswikiquote','iswikisource','iswiktionary','itwiki','itwikibooks','itwikinews','itwikiquote','itwikisource','itwikiversity','itwikivoyage','itwiktionary','iuwiki','iuwiktionary','jamwiki','jawiki','jawikibooks','jawikinews','jawikisource','jawikiversity','jawikivoyage','jawiktionary','jbowiki','jbowiktionary','jvwiki','jvwikisource','jvwiktionary','kaawiki','kaawiktionary','kabwiki','kaiwiki','kajwiki','kawiki','kawikibooks','kawikiquote','kawikisource','kawiktionary','kbdwiki','kbdwiktionary','kbpwiki','kcgwiki','kcgwiktionary','kgewiki','kgwiki','kiwiki','kkwiki','kkwikibooks','kkwiktionary','klwiktionary','kmwiki','kmwikibooks','kmwiktionary','kncwiki','knwiki','knwikiquote','knwikisource','knwiktionary','koiwiki','kowiki','kowikibooks','kowikinews','kowikiquote','kowikisource','kowikiversity','kowiktionary','krcwiki','kshwiki','kswiki','kswiktionary','kuswiki','kuwiki','kuwikibooks','kuwikiquote','kuwiktionary','kvwiki','kwwiki','kwwiktionary','kywiki','kywikiquote','kywiktionary','labswiki','ladwiki','lawiki','lawikibooks','lawikiquote','lawikisource','lawiktionary','lbewiki','lbwiki','lbwiktionary','lezwiki','lfnwiki','lgwiki','lijwiki','lijwikisource','liwiki','liwikibooks','liwikinews','liwikiquote','liwikisource','liwiktionary','lldwiki','lmowiki','lmowiktionary','lnwiki','lnwiktionary','lowiki','lowiktionary','ltgwiki','ltwiki','ltwikibooks','ltwikiquote','ltwikisource','ltwiktionary','lvwiki','lvwiktionary','madwiki','madwikisource','madwiktionary','maiwiki','map_bmswiki','mdfwiki','mediawikiwiki','metawiki','mgwiki','mgwikibooks','mgwiktionary','mhrwiki','minwiki','minwikibooks','minwikisource','minwiktionary','miwiki','miwiktionary','mkwiki','mkwikibooks','mkwikimedia','mkwikisource','mkwiktionary','mlwiki','mlwikibooks','mlwikiquote','mlwikisource','mlwiktionary','mniwiki','mniwiktionary','mnwiki','mnwiktionary','mnwwiki','mnwwiktionary','moswiki','mrjwiki','mrwiki','mrwikibooks','mrwikiquote','mrwikisource','mrwiktionary','mswiki','mswikibooks','mswikiquote','mswikisource','mswiktionary','mtwiki','mtwiktionary','mwlwiki','mxwikimedia','myvwiki','mywiki','mywikisource','mywiktionary','mznwiki','nahwiki','nahwiktionary','napwiki','napwikisource','nawiktionary','nds_nlwiki','ndswiki','ndswiktionary','newiki','newikibooks','newiktionary','newwiki','niawiki','niawiktionary','nlwiki','nlwikibooks','nlwikimedia','nlwikinews','nlwikiquote','nlwikisource','nlwikivoyage','nlwiktionary','nnwiki','nnwikiquote','nnwiktionary','novwiki','nowiki','nowikibooks','nowikimedia','nowikinews','nowikiquote','nowikisource','nowiktionary','nqowiki','nrmwiki','nrwiki','nsowiki','nupwiki','nvwiki','nycwikimedia','nywiki','ocwiki','ocwikibooks','ocwiktionary','olowiki','omwiki','omwiktionary','orwiki','orwikisource','orwiktionary','oswiki','outreachwiki','pagwiki','pamwiki','papwiki','pawiki','pawikibooks','pawikisource','pawiktionary','pcdwiki','pcmwiki','pcmwikiquote','pdcwiki','pflwiki','piwiki','plwiki','plwikibooks','plwikimedia','plwikinews','plwikiquote','plwikisource','plwikivoyage','plwiktionary','pmswiki','pmswikisource','pnbwiki','pnbwiktionary','pntwiki','pplwiki','pswiki','pswikivoyage','pswiktionary','ptwiki','ptwikibooks','ptwikimedia','ptwikinews','ptwikiquote','ptwikisource','ptwikiversity','ptwikivoyage','ptwiktionary','pwnwiki','quwiktionary','rkiwiki','rmwiki','rmywiki','rnwiki','roa_rupwiki','roa_rupwiktionary','roa_tarawiki','rowiki','rowikibooks','rowikinews','rowikiquote','rowiktionary','rskwiki','ruewiki','ruwiki','ruwikibooks','ruwikinews','ruwikiquote','ruwikisource','ruwikiversity','ruwikivoyage','ruwiktionary','rwwiki','rwwiktionary','sahwiki','sahwikiquote','sahwikisource','satwiki','satwiktionary','sawiki','sawikibooks','sawikiquote','sawikisource','sawiktionary','scnwiki','scnwiktionary','scowiki','scwiki','sdwiki','sdwiktionary','sewiki','sewikimedia','sgwiki','sgwiktionary','shiwiki','shnwiki','shnwikibooks','shnwikinews','shnwikivoyage','shnwiktionary','shwiki','shwiktionary','shywiktionary','simplewiki','simplewiktionary','siwiki','siwikibooks','siwiktionary','skwiki','skrwiki','skrwiktionary','slwiki','slwikibooks','slwikiquote','slwikisource','slwikiversity','slwiktionary','smnwiki','smwiki','smwiktionary','snwiki','sourceswiki','sowiki','sowiktionary','specieswiki','sqwiki','sqwikibooks','sqwikinews','sqwikiquote','sqwiktionary','srnwiki','srwiki','srwikibooks','srwikinews','srwikiquote','srwikisource','srwiktionary','sswiki','sswiktionary','stqwiki','stwiki','stwiktionary','suwiki','suwikiquote','suwikisource','suwiktionary','svwiki','svwikibooks','svwikinews','svwikiquote','svwikisource','svwikiversity','svwikivoyage','svwiktionary','swwiki','swwiktionary','sylwiki','szlwiki','szywiki','tawiki','tawikibooks','tawikinews','tawikiquote','tawikisource','tawiktionary','taywiki','tcywiki','tcywikisource','tcywiktionary','tddwiki','tewiki','tewikibooks','tewikiquote','tewikisource','tewiktionary','tgwiki','tgwikibooks','tgwiktionary','thwiki','thwikibooks','thwikimedia','thwikiquote','thwikisource','thwiktionary','tigwiki','tiwiki','tiwiktionary','tkwiki','tkwiktionary','tlwiki','tlwikibooks','tlwikiquote','tlwikisource','tlwiktionary','tlywiki','tnwiki','tnwiktionary','tokwiki','towiki','tpiwiki','tpiwiktionary','trvwiki','trwiki','trwikibooks','trwikimedia','trwikinews','trwikiquote','trwikisource','trwikivoyage','trwiktionary','tswiki','tswiktionary','ttwiki','ttwikibooks','ttwikiquote','ttwiktionary','tumwiki','twwiki','twwiktionary','tyvwiki','tywiki','uawikimedia','udmwiki','ugwiki','ugwiktionary','ukwiki','ukwikibooks','ukwikinews','ukwikiquote','ukwikisource','ukwikivoyage','ukwiktionary','urwiki','urwikibooks','urwikiquote','urwikisource','urwiktionary','uzwiki','uzwikiquote','uzwiktionary','vecwiki','vecwikisource','vecwiktionary','vepwiki','vewiki','viwiki','viwikibooks','viwikiquote','viwikisource','viwikivoyage','viwiktionary','vlswiki','vowiki','vowiktionary','warwiki','wawiki','wawikisource','wawiktionary','wikidatawiki','wikimaniawiki','wowiki','wowiktionary','wuuwiki','xalwiki','xhwiki','xmfwiki','yiwiki','yiwikisource','yiwiktionary','yowiki','zawiki','zeawiki','zghwiki','zghwiktionary','zh_classicalwiki','zh_min_nanwiki','zh_min_nanwikisource','zh_min_nanwiktionary','zh_yuewiki','zhwiki','zhwikibooks','zhwikinews','zhwikiquote','zhwikisource','zhwikiversity','zhwikivoyage','zhwiktionary','zuwiki','zuwiktionary','test2wiki','testwiki'];
const DELAY_MS = 800; // Safe balanced speed
const sleep = ms => new Promise(r => setTimeout(r, ms));
let userCache = {};
let historyCache = {}; // Added this to prevent the ReferenceError
function getDomain(db) {
const mapping = {
'commonswiki': 'commons.wikimedia.org', 'metawiki': 'meta.wikimedia.org', 'wikidatawiki': 'www.wikidata.org',
'mediawikiwiki': 'www.mediawiki.org', 'sourceswiki': 'wikisource.org', 'foundationwiki': 'foundation.wikimedia.org',
'incubatorwiki': 'incubator.wikimedia.org', 'outreachwiki': 'outreach.wikimedia.org',
'be_x_oldwiki': 'be-tarask.wikipedia.org', 'labswiki': 'wikitech.wikimedia.org',
'wikimaniawiki': 'wikimania.wikimedia.org', 'specieswiki': 'species.wikimedia.org',
'abstractwiki': 'abstract.wikipedia.org', 'betawikiversity': 'beta.wikiversity.org',
'mo_wiki': 'ro.wikipedia.org',
'testwiki': 'test.wikipedia.org', 'test2wiki': 'test2.wikipedia.org',
'wikifunctionswiki': 'www.wikifunctions.org',
'testcommonswiki': 'test-commons.wikimedia.org',
'testwikidatawiki': 'test.wikidata.org',
};
if (mapping[db]) return mapping[db];
const name = db.replace(/_/g, '-');
if (name.endsWith('wikisource')) return name.slice(0, -10) + '.wikisource.org';
if (name.endsWith('wikiversity')) return name.slice(0, -11) + '.wikiversity.org';
if (name.endsWith('wiktionary')) return name.slice(0, -10) + '.wiktionary.org';
if (name.endsWith('wikivoyage')) return name.slice(0, -10) + '.wikivoyage.org';
if (name.endsWith('wikibooks')) return name.slice(0, -9) + '.wikibooks.org';
if (name.endsWith('wikiquote')) return name.slice(0, -9) + '.wikiquote.org';
if (name.endsWith('wikinews')) return name.slice(0, -8) + '.wikinews.org';
if (name.endsWith('wikimedia')) return name.slice(0, -9) + '.wikimedia.org';
if (name.endsWith('wiki')) return name.slice(0, -4) + '.wikipedia.org';
return name + '.wikipedia.org';
}
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function() {
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php', { anonymous: true });
const now = new Date();
let results = {}, emptyWikis = [], failedWikis = [], isRunning = false;
function setupUI() {
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
// CHANGE: Dynamically generate dropdown options for years and months
let yearOpts = '';
for (let y = currentYear; y >= 2005; y--) {
yearOpts += `<option value="${y}">${y}</option>`;
}
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 === currentMonth ? 'selected' : ''}>${m}</option>`;
}
$('#firstHeading').text('GlobalCheckUserStats.js');
$('#mw-content-text').empty().append(`
<div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;">
<h3>Audit Range</h3>
From:
<select id="y-f" style="width:70px;">${yearOpts}</select>
<select id="m-f" style="width:50px;">${monthOptsFrom}</select>
To:
<select id="y-t" style="width:70px;">${yearOpts}</select>
<select id="m-t" style="width:50px;">${monthOptsTo}</select>
<button id="start" class="mw-ui-button mw-ui-progressive" style="margin-left:15px;">Run GlobalCheckUserStats.js</button>
<button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button>
<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="${rawWikis.length}" style="width:100%"></progress></div>
<textarea id="out" style="width:100%; height:450px; margin-top:10px; display:none; font-family:monospace; font-size:12px;"></textarea>
</div>
`);
$('#start').click(() => runAudit($('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val()));
$('#stop').click(() => isRunning = false);
}
// Helper function to check global rights history on Meta-Wiki
async function checkGlobalHistory(user, start, end) {
try {
const res = await metaApi.get({
action: 'query',
list: 'logevents',
letype: 'gblrights',
letitle: 'User:' + user,
formatversion: 2
});
const logs = res.query.logevents || [];
const auditStart = new Date(start);
const auditEnd = new Date(end);
let wasSteward = false, wasStaff = false, wasOmbuds = false;
for (const e of logs) {
const ts = new Date(e.timestamp);
const p = e.params || {};
const added = p.newGroups || p[1] || [];
const removed = p.oldGroups || p[0] || [];
// If a removal happened AFTER the audit started, the user held the rights during the period
if (ts > auditStart) {
if (removed.includes('steward')) wasSteward = true;
if (removed.includes('staff')) wasStaff = true;
if (removed.includes('ombuds')) wasOmbuds = true;
}
// If an addition happened BEFORE the audit ended, the user held the rights during the period
if (ts < auditEnd) {
if (added.includes('steward')) wasSteward = true;
if (added.includes('staff')) wasStaff = true;
if (added.includes('ombuds')) wasOmbuds = true;
}
}
return { wasSteward, wasStaff, wasOmbuds };
} catch (e) {
// Fallback for API errors
return { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
async function fetchUserData(user, db, start, end) {
// Priority 0: Use cache for current global groups
if (!userCache[user]) {
try {
const gres = await metaApi.get({ action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 });
userCache[user] = gres.query.globaluserinfo.groups || [];
} catch(e) { userCache[user] = []; }
}
const gGroups = userCache[user];
const isGloballyStaff = gGroups.includes('staff');
const isGloballySteward = gGroups.includes('steward');
const isGloballyOmbuds = gGroups.includes('ombuds');
// Check current local CheckUser rights
let isCurrentLocal = false;
try {
const localApi = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
const ures = await localApi.get({ action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 });
const lGroups = (ures.query.users[0] && ures.query.users[0].groups) || [];
isCurrentLocal = lGroups.includes('checkuser');
} catch(e) { /* API failure fallback */ }
// Rights log analysis for the specific Wiki
const target = 'User:' + user + '@' + db;
let logText = ''; // Default is empty, filled only if there are logs
let addTime = null, removeTime = null, isSelfAssign = false;
// CHANGE: Variables to track the longest duration and the number of actions
let maxDurationMins = -1;
let longestTimeStr = "";
let assignCount = 0;
try {
const res = await metaApi.get({
action: 'query', list: 'logevents', letype: 'rights', letitle: target,
lestart: end, leend: start, ledir: 'older',
lelimit: 'max', // ADDED: Loads up to 500 logs instead of 10
formatversion: 2
});
const events = res.query.logevents || [];
let filtered = [];
// Variables to pair removal and addition (iterating newest to oldest)
let tempRemoveTime = null;
let tempRemoveTimeStr = "Not removed";
events.forEach(e => {
const p = e.params || {};
const cuAdded = (p.add || []).includes('checkuser') || ((p.newgroups || []).includes('checkuser') && !(p.oldgroups || []).includes('checkuser'));
const cuRemoved = (p.remove || []).includes('checkuser') || (!(p.newgroups || []).includes('checkuser') && (p.oldgroups || []).includes('checkuser'));
const exactTimeLog = e.timestamp.replace('T', ' ').replace('Z', '');
if (cuRemoved) {
tempRemoveTime = new Date(e.timestamp);
tempRemoveTimeStr = exactTimeLog;
// Save the most recent removal for the overall table role categorization
if (!removeTime) {
removeTime = tempRemoveTime;
if (e.user === user || !e.user) isSelfAssign = true;
}
}
if (cuAdded) {
const currentAddTime = new Date(e.timestamp);
let metadataRemoveTime = tempRemoveTime;
let metadataRemoveTimeStr = tempRemoveTimeStr;
// Save the most recent addition for the overall table role categorization
if (!addTime) {
addTime = currentAddTime;
isSelfAssign = (e.user === user);
}
// Fallback: Check expiry in metadata if explicit removal log is missing
const metadata = p.newmetadata || [];
const cuMeta = metadata.find(m => m.group === 'checkuser');
if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity' && tempRemoveTimeStr === "Not removed") {
metadataRemoveTime = new Date(cuMeta.expiry);
metadataRemoveTimeStr = cuMeta.expiry.replace('T', ' ').replace('Z', '');
if (!removeTime) removeTime = metadataRemoveTime;
}
// Calculate duration for this specific 1-line pair
let pairDuration = "Unknown";
if (metadataRemoveTime) {
const diffMs = Math.abs(metadataRemoveTime - currentAddTime);
const diffMins = Math.round(diffMs / 60000); // CHANGE: Rounds to the nearest whole minute
const days = Math.floor(diffMins / 1440);
const hours = Math.floor((diffMins % 1440) / 60);
const mins = diffMins % 60;
if (days > 0) pairDuration = `${days}d ${hours}h ${mins}m`;
else if (hours > 0) pairDuration = `${hours}h ${mins}m`;
else if (mins > 0) pairDuration = `${mins}m`;
else pairDuration = "<1m";
// CHANGE: Track the total number of assignments and store the absolute longest duration
assignCount++;
if (diffMins > maxDurationMins) {
maxDurationMins = diffMins;
longestTimeStr = pairDuration;
}
}
// Push the combined 1-line format to the log using commas
filtered.push(`* Added: ${exactTimeLog}, Removed: ${metadataRemoveTimeStr}, Duration: ${pairDuration}`);
// Reset pair variables for the next older event in the loop
tempRemoveTime = null;
tempRemoveTimeStr = "Not removed";
}
});
// Edge case: If a removal was found but the addition happened before the audit period
if (tempRemoveTime && tempRemoveTimeStr !== "Not removed") {
filtered.push(`* Added: Before audit period, Removed: ${tempRemoveTimeStr}, Duration: Unknown`);
}
// Joins logs with newlines for the report
if (filtered.length) {
logText = '\n' + filtered.join('\n');
}
} catch (err) { /* Rights log query failed */ }
// --- GLOBAL HISTORY CHECK (WITH CACHE) ---
if (!historyCache[user]) {
if (!isGloballySteward || !isGloballyStaff || !isGloballyOmbuds) {
// Check Meta-Wiki history logs only if user is missing at least one global role today
historyCache[user] = await checkGlobalHistory(user, start, end);
} else {
historyCache[user] = { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
const history = historyCache[user];
// --- HIERARCHY LOGIC (CheckUser Audit Focus) ---
let roleLabel = "";
// 1. STAFF (Highest audit priority for WMF accountability)
if (isGloballyStaff || history.wasStaff) {
roleLabel = isGloballyStaff ? "Current Staff" : "Former Staff (in period)";
}
// 2. LOCAL CHECKUSER (Overrides Steward/Ombudsman roles if local rights exist)
else if (isCurrentLocal) {
roleLabel = "Current Local CheckUser";
}
// 3. STEWARD - SPECIFIC INTERVENTION (Using exact self-assignment duration or max duration if multiple)
else if (longestTimeStr && isSelfAssign) {
if (assignCount > 1) {
roleLabel = `Steward action (Self-assign: longest ${longestTimeStr}, see log)`;
} else {
roleLabel = `Steward action (Self-assign: ${longestTimeStr})`;
}
}
// 4. STEWARD - GENERAL (Global rights usage on external wikis)
else if (isGloballySteward || history.wasSteward) {
roleLabel = isGloballySteward ? "Current Steward" : "Former Steward (in period)";
}
// 5. OMBUDSMAN (Global oversight role)
else if (isGloballyOmbuds || history.wasOmbuds) {
roleLabel = isGloballyOmbuds ? "Current Ombudsman" : "Former Ombudsman (in period)";
}
// 6. FORMER LOCAL CheckUser (Fallback for those who lost rights but performed actions)
else {
roleLabel = "Former Local CheckUser";
}
return { role: roleLabel, log: logText };
}
async function runAudit(yf, mf, yt, mt) {
isRunning = true;
results = {};
emptyWikis = [];
failedWikis = [];
userCache = {};
historyCache = {};
$('#start').prop('disabled', true);
$('#stop').prop('disabled', false);
$('#out').hide();
// Format ISO timestamps for the API
const START = `${yf}-${mf.toString().padStart(2, '0')}-01T00:00:00Z`;
const lastDay = new Date(Date.UTC(yt, mt, 0)).getUTCDate();
const END = `${yt}-${mt.toString().padStart(2, '0')}-${lastDay.toString().padStart(2, '0')}T23:59:59Z`;
// Generate list of months for dynamic table columns
const monthCols = [];
let currY = parseInt(yf), currM = parseInt(mf);
const endY = parseInt(yt), endM = parseInt(mt);
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
while (currY < endY || (currY === endY && currM <= endM)) {
monthCols.push({
key: `${currY}-${String(currM).padStart(2, '0')}`,
label: `${monthNames[currM - 1]} ${currY}` // e.g., "Jan 2024"
});
currM++;
if (currM > 12) { currM = 1; currY++; }
}
for (let i = 0; i < rawWikis.length && isRunning; i++) {
const db = rawWikis[i];
$('#status-msg').text(`Scanning ${db} (${i + 1}/${rawWikis.length})...`);
let successLocal = false;
let continueToken = null; // Token for pagination
while (!successLocal && isRunning) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
let params = {
action: 'query',
list: 'checkuserlog',
culfrom: START,
culto: END,
culdir: 'newer',
cullimit: 'max',
formatversion: 2
};
// Add continue token if we are on page 2 or further
if (continueToken) {
Object.assign(params, continueToken);
}
let res = await api.get(params);
const ent = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) || [];
if (ent.length) {
if (!results[db]) results[db] = {};
// Sum up actions per user (total and per month)
ent.forEach(e => {
const u = e.checkuser;
if (!results[db][u]) results[db][u] = { total: 0, months: {} };
const date = new Date(e.timestamp);
const mKey = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
results[db][u].total++;
results[db][u].months[mKey] = (results[db][u].months[mKey] || 0) + 1;
});
}
// Pagination check
if (res.continue && isRunning) {
continueToken = res.continue;
$('#status-msg').text(`Scanning ${db} (fetching next page)...`);
} else {
// Wiki scan finished (either no entries at all or all pages fetched)
if (!results[db]) emptyWikis.push(db);
successLocal = true;
}
} catch (err) {
const status = (err && err.xhr) ? err.xhr.status : (err && err.status);
if (status === 429) {
// Rate limit hit, wait 30s and retry the SAME wiki/page
$('#status-msg').text(`Rate limit on ${db}. Waiting 30s...`);
await sleep(30000);
} else {
// Critical error (CORS, DNS, etc.), skip this wiki
console.error(`Skipping ${db} due to error:`, err);
failedWikis.push(db);
successLocal = true;
}
}
}
$('#bar').val(i + 1);
await sleep(DELAY_MS);
}
// --- REPORT GENERATION ---
$('#status-msg').text(isRunning ? `Generating report...` : `Generating partial report...`);
// Get current date and time in UTC
const now = new Date();
const timestamp = now.getUTCFullYear() + '-' +
String(now.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(now.getUTCDate()).padStart(2, '0') + ' ' +
String(now.getUTCHours()).padStart(2, '0') + ':' +
String(now.getUTCMinutes()).padStart(2, '0') + ' (UTC)';
let wt = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`;
wt += `''Report generated on: ${timestamp}''\n\n`;
if (!isRunning) wt += `'''Note: This is a partial report (stopped manually).'''\n\n`;
else wt += `\n`;
// Build dynamic table header with months
let headerMonths = monthCols.map(m => `!! ${m.label}`).join(' ');
wt += `{| class="wikitable sortable"\n! Wiki !! User (Category) !! '''Total Actions''' ${headerMonths}\n`;
let rightsLog = `\n== Rights Log (In Period) ==\n`;
const sortedDBs = Object.keys(results).sort();
for (const db of sortedDBs) {
const sortedUsers = Object.keys(results[db]).sort();
for (const user of sortedUsers) {
// Check global status and rights changes for each active user
const m = await fetchUserData(user, db, START, END);
const userData = results[db][user];
let rowStr = `|-\n| ${db} || ${user} (${m.role}) || '''${userData.total}'''`;
// Add monthly counts to the row (outputs 0 if no actions in that month)
monthCols.forEach(col => {
rowStr += ` || ${userData.months[col.key] || 0}`;
});
wt += `${rowStr}\n`;
// Only add to the Rights Log if there are actual logged events for this user
if (m.log) {
rightsLog += `'''${user}@${db}''':${m.log}\n\n`;
}
}
}
wt += `|}\n\n== Projects with 0 actions ==\n${emptyWikis.sort().join(', ')}\n`;
if (failedWikis.length) {
wt += `\n== Errors (Failed to scan) ==\n${failedWikis.sort().join(', ')}\n`;
}
wt += rightsLog;
$('#out').val(wt).show();
$('#status-msg').text(isRunning ? `Audit complete!` : `Audit stopped. Partial report ready.`);
$('#start').prop('disabled', false);
$('#stop').prop('disabled', true);
}
setupUI();
});
})();
h6wbhok242ih0760ea4bxqoys99hivp
737126
737073
2026-04-07T20:38:12Z
MrJaroslavik
44012
e
737126
javascript
text/javascript
// GlobalCheckUserStats.js
// Features:
// - Global Audit: Scans CheckUser logs across 900+ Wikimedia projects at once.
// - Smart Categorization: Identifies roles (Local CU, Steward, Staff, etc.).
// - Steward Logic: Detects temporary access, exact durations, and longest active periods.
// - Deep Scan: Automated pagination to bypass the 500-entry API limit (essential for enwiki).
// - Robust Connection: Automated retries for 429 rate-limits and custom domain mapping.
// - Full Reporting: Outputs sortable Wikitables and detailed rights change logs.
// - Custom UI: Integrated control panel on [[Special:BlankPage/GlobalCheckUserStats]].
// - With help of Gemini 3
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Blankpage' || mw.config.get('wgTitle').indexOf('GlobalCheckUserStats') === -1) return;
const rawWikis = ['abstractwiki','abwiki','acewiki','adywiki','afwiki','afwikibooks','afwikiquote','afwiktionary','alswiki','altwiki','amiwiki','amwiki','amwiktionary','angwiki','angwiktionary','annwiki','anpwiki','anwiki','anwiktionary','arcwiki','arwiki','arwikibooks','arwikimedia','arwikinews','arwikiquote','arwikisource','arwikiversity','arwiktionary','arywiki','arzwiki','astwiki','astwiktionary','aswiki','aswikiquote','aswikisource','atjwiki','avkwiki','avwiki','awawiki','aywiki','aywiktionary','azbwiki','azwiki','azwikibooks','azwikiquote','azwikisource','azwiktionary','banwiki','banwikisource','barwiki','bat_smgwiki','bawiki','bawikibooks','bbcwiki','bclwiki','bclwikiquote','bclwikisource','bclwiktionary','bdrwiki','bdwikimedia','be_x_oldwiki','betawikiversity','bewiki','bewikibooks','bewikimedia','bewikiquote','bewikisource','bewiktionary','bewwiki','bewwiktionary','bgwiki','bgwikibooks','bgwikiquote','bgwikisource','bgwiktionary','bhwiki','biwiki','bjnwiki','bjnwikiquote','bjnwiktionary','blkwiki','blkwiktionary','bmwiki','bnwiki','bnwikibooks','bnwikiquote','bnwikisource','bnwikivoyage','bnwiktionary','bowiki','bpywiki','brwiki','brwikimedia','brwikiquote','brwikisource','brwiktionary','bswiki','bswikibooks','bswikinews','bswikiquote','bswikisource','bswiktionary','btmwiki','btmwiktionary','bugwiki','bxrwiki','cawiki','cawikibooks','cawikimedia','cawikinews','cawikiquote','cawikisource','cawiktionary','cbk_zamwiki','cdowiki','cebwiki','cewiki','chrwiki','chrwiktionary','chwiki','chywiki','ckbwiki','ckbwiktionary','commonswiki','cowiki','cowikimedia','cowiktionary','crhwiki','csbwiki','csbwiktionary','cswiki','cswikibooks','cswikinews','cswikiquote','cswikisource','cswikiversity','cswikivoyage','cswiktionary','cuwiki','cvwiki','cvwikibooks','cywiki','cywikibooks','cywikiquote','cywikisource','cywiktionary','dagwiki','dawiki','dawikibooks','dawikiquote','dawikisource','dawiktionary','dewiki','dewikibooks','dewikinews','dewikiquote','dewikisource','dewikiversity','dewikivoyage','dewiktionary','dgawiki','dinwiki','diqwiki','diqwiktionary','dkwikimedia','dsbwiki','dtpwiki','dtywiki','dvwiki','dvwiktionary','dzwiki','eewiki','elwiki','elwikibooks','elwikinews','elwikiquote','elwikisource','elwikiversity','elwikivoyage','elwiktionary','emlwiki','enwiki','enwikibooks','enwikinews','enwikiquote','enwikisource','enwikiversity','enwikivoyage','enwiktionary','eowiki','eowikibooks','eowikinews','eowikiquote','eowikisource','eowikivoyage','eowiktionary','eswiki','eswikibooks','eswikinews','eswikiquote','eswikisource','eswikiversity','eswikivoyage','eswiktionary','etwiki','etwikibooks','eewikimedia','etwikiquote','etwikisource','etwiktionary','euwiki','euwikibooks','euwikiquote','euwikisource','euwiktionary','extwiki','fatwiki','fawiki','fawikibooks','fawikinews','fawikiquote','fawikisource','fawikivoyage','fawiktionary','ffwiki','fiu_vrowiki','fiwiki','fiwikibooks','fiwikimedia','fiwikinews','fiwikiquote','fiwikisource','fiwikiversity','fiwikivoyage','fiwiktionary','fjwiki','fjwiktionary','fonwiki','foundationwiki','fowiki','fowikisource','fowiktionary','frpwiki','frrwiki','frwiki','frwikibooks','frwikinews','frwikiquote','frwikisource','frwikiversity','frwikivoyage','frwiktionary','furwiki','fywiki','fywikibooks','fywiktionary','gagwiki','ganwiki','gawiki','gawiktionary','gcrwiki','gdwiki','gdwiktionary','glkwiki','glwiki','glwikibooks','glwikiquote','glwikisource','glwiktionary','gnwiki','gnwiktionary','gomwiki','gomwiktionary','gorwiki','gorwikiquote','gorwiktionary','gotwiki','gpewiki','gucwiki','gurwiki','guwiki','guwikiquote','guwikisource','guwiktionary','guwwiki','guwwikinews','guwwikiquote','guwwiktionary','gvwiki','gvwiktionary','hakwiki','hawiki','hawiktionary','hawwiki','hewiki','hewikibooks','hewikinews','hewikiquote','hewikisource','hewikivoyage','hewiktionary','hifwiki','hifwiktionary','hiwiki','hiwikibooks','hiwikiquote','hiwikisource','hiwikiversity','hiwikivoyage','hiwiktionary','hrwiki','hrwikibooks','hrwikiquote','hrwikisource','hrwiktionary','hsbwiki','hsbwiktionary','htwiki','huwiki','huwikibooks','huwikisource','huwiktionary','hywiki','hywikibooks','hywikiquote','hywikisource','hywiktionary','hywwiki','iawiki','iawikibooks','iawiktionary','ibawiki','idwiki','idwikibooks','idwikiquote','idwikisource','idwikivoyage','idwiktionary','iewiki','iewiktionary','iglwiki','igwiki','igwikiquote','igwiktionary','ikwiki','ilowiki','incubatorwiki','inhwiki','iowiki','iowiktionary','iswiki','iswikibooks','iswikiquote','iswikisource','iswiktionary','itwiki','itwikibooks','itwikinews','itwikiquote','itwikisource','itwikiversity','itwikivoyage','itwiktionary','iuwiki','iuwiktionary','jamwiki','jawiki','jawikibooks','jawikinews','jawikisource','jawikiversity','jawikivoyage','jawiktionary','jbowiki','jbowiktionary','jvwiki','jvwikisource','jvwiktionary','kaawiki','kaawiktionary','kabwiki','kaiwiki','kajwiki','kawiki','kawikibooks','kawikiquote','kawikisource','kawiktionary','kbdwiki','kbdwiktionary','kbpwiki','kcgwiki','kcgwiktionary','kgewiki','kgwiki','kiwiki','kkwiki','kkwikibooks','kkwiktionary','klwiktionary','kmwiki','kmwikibooks','kmwiktionary','kncwiki','knwiki','knwikiquote','knwikisource','knwiktionary','koiwiki','kowiki','kowikibooks','kowikinews','kowikiquote','kowikisource','kowikiversity','kowiktionary','krcwiki','kshwiki','kswiki','kswiktionary','kuswiki','kuwiki','kuwikibooks','kuwikiquote','kuwiktionary','kvwiki','kwwiki','kwwiktionary','kywiki','kywikiquote','kywiktionary','labswiki','ladwiki','lawiki','lawikibooks','lawikiquote','lawikisource','lawiktionary','lbewiki','lbwiki','lbwiktionary','lezwiki','lfnwiki','lgwiki','lijwiki','lijwikisource','liwiki','liwikibooks','liwikinews','liwikiquote','liwikisource','liwiktionary','lldwiki','lmowiki','lmowiktionary','lnwiki','lnwiktionary','lowiki','lowiktionary','ltgwiki','ltwiki','ltwikibooks','ltwikiquote','ltwikisource','ltwiktionary','lvwiki','lvwiktionary','madwiki','madwikisource','madwiktionary','maiwiki','map_bmswiki','mdfwiki','mediawikiwiki','metawiki','mgwiki','mgwikibooks','mgwiktionary','mhrwiki','minwiki','minwikibooks','minwikisource','minwiktionary','miwiki','miwiktionary','mkwiki','mkwikibooks','mkwikimedia','mkwikisource','mkwiktionary','mlwiki','mlwikibooks','mlwikiquote','mlwikisource','mlwiktionary','mniwiki','mniwiktionary','mnwiki','mnwiktionary','mnwwiki','mnwwiktionary','moswiki','mrjwiki','mrwiki','mrwikibooks','mrwikiquote','mrwikisource','mrwiktionary','mswiki','mswikibooks','mswikiquote','mswikisource','mswiktionary','mtwiki','mtwiktionary','mwlwiki','mxwikimedia','myvwiki','mywiki','mywikisource','mywiktionary','mznwiki','nahwiki','nahwiktionary','napwiki','napwikisource','nawiktionary','nds_nlwiki','ndswiki','ndswiktionary','newiki','newikibooks','newiktionary','newwiki','niawiki','niawiktionary','nlwiki','nlwikibooks','nlwikimedia','nlwikinews','nlwikiquote','nlwikisource','nlwikivoyage','nlwiktionary','nnwiki','nnwikiquote','nnwiktionary','novwiki','nowiki','nowikibooks','nowikimedia','nowikinews','nowikiquote','nowikisource','nowiktionary','nqowiki','nrmwiki','nrwiki','nsowiki','nupwiki','nvwiki','nycwikimedia','nywiki','ocwiki','ocwikibooks','ocwiktionary','olowiki','omwiki','omwiktionary','orwiki','orwikisource','orwiktionary','oswiki','outreachwiki','pagwiki','pamwiki','papwiki','pawiki','pawikibooks','pawikisource','pawiktionary','pcdwiki','pcmwiki','pcmwikiquote','pdcwiki','pflwiki','piwiki','plwiki','plwikibooks','plwikimedia','plwikinews','plwikiquote','plwikisource','plwikivoyage','plwiktionary','pmswiki','pmswikisource','pnbwiki','pnbwiktionary','pntwiki','pplwiki','pswiki','pswikivoyage','pswiktionary','ptwiki','ptwikibooks','ptwikimedia','ptwikinews','ptwikiquote','ptwikisource','ptwikiversity','ptwikivoyage','ptwiktionary','pwnwiki','quwiktionary','rkiwiki','rmwiki','rmywiki','rnwiki','roa_rupwiki','roa_rupwiktionary','roa_tarawiki','rowiki','rowikibooks','rowikinews','rowikiquote','rowiktionary','rskwiki','ruewiki','ruwiki','ruwikibooks','ruwikinews','ruwikiquote','ruwikisource','ruwikiversity','ruwikivoyage','ruwiktionary','rwwiki','rwwiktionary','sahwiki','sahwikiquote','sahwikisource','satwiki','satwiktionary','sawiki','sawikibooks','sawikiquote','sawikisource','sawiktionary','scnwiki','scnwiktionary','scowiki','scwiki','sdwiki','sdwiktionary','sewiki','sewikimedia','sgwiki','sgwiktionary','shiwiki','shnwiki','shnwikibooks','shnwikinews','shnwikivoyage','shnwiktionary','shwiki','shwiktionary','shywiktionary','simplewiki','simplewiktionary','siwiki','siwikibooks','siwiktionary','skwiki','skrwiki','skrwiktionary','slwiki','slwikibooks','slwikiquote','slwikisource','slwikiversity','slwiktionary','smnwiki','smwiki','smwiktionary','snwiki','sourceswiki','sowiki','sowiktionary','specieswiki','sqwiki','sqwikibooks','sqwikinews','sqwikiquote','sqwiktionary','srnwiki','srwiki','srwikibooks','srwikinews','srwikiquote','srwikisource','srwiktionary','sswiki','sswiktionary','stqwiki','stwiki','stwiktionary','suwiki','suwikiquote','suwikisource','suwiktionary','svwiki','svwikibooks','svwikinews','svwikiquote','svwikisource','svwikiversity','svwikivoyage','svwiktionary','swwiki','swwiktionary','sylwiki','szlwiki','szywiki','tawiki','tawikibooks','tawikinews','tawikiquote','tawikisource','tawiktionary','taywiki','tcywiki','tcywikisource','tcywiktionary','tddwiki','tewiki','tewikibooks','tewikiquote','tewikisource','tewiktionary','tgwiki','tgwikibooks','tgwiktionary','thwiki','thwikibooks','thwikimedia','thwikiquote','thwikisource','thwiktionary','tigwiki','tiwiki','tiwiktionary','tkwiki','tkwiktionary','tlwiki','tlwikibooks','tlwikiquote','tlwikisource','tlwiktionary','tlywiki','tnwiki','tnwiktionary','tokwiki','towiki','tpiwiki','tpiwiktionary','trvwiki','trwiki','trwikibooks','trwikimedia','trwikinews','trwikiquote','trwikisource','trwikivoyage','trwiktionary','tswiki','tswiktionary','ttwiki','ttwikibooks','ttwikiquote','ttwiktionary','tumwiki','twwiki','twwiktionary','tyvwiki','tywiki','uawikimedia','udmwiki','ugwiki','ugwiktionary','ukwiki','ukwikibooks','ukwikinews','ukwikiquote','ukwikisource','ukwikivoyage','ukwiktionary','urwiki','urwikibooks','urwikiquote','urwikisource','urwiktionary','uzwiki','uzwikiquote','uzwiktionary','vecwiki','vecwikisource','vecwiktionary','vepwiki','vewiki','viwiki','viwikibooks','viwikiquote','viwikisource','viwikivoyage','viwiktionary','vlswiki','vowiki','vowiktionary','warwiki','wawiki','wawikisource','wawiktionary','wikidatawiki','wikimaniawiki','wowiki','wowiktionary','wuuwiki','xalwiki','xhwiki','xmfwiki','yiwiki','yiwikisource','yiwiktionary','yowiki','zawiki','zeawiki','zghwiki','zghwiktionary','zh_classicalwiki','zh_min_nanwiki','zh_min_nanwikisource','zh_min_nanwiktionary','zh_yuewiki','zhwiki','zhwikibooks','zhwikinews','zhwikiquote','zhwikisource','zhwikiversity','zhwikivoyage','zhwiktionary','zuwiki','zuwiktionary','test2wiki','testwiki'];
const DELAY_MS = 800; // Safe balanced speed
const sleep = ms => new Promise(r => setTimeout(r, ms));
let userCache = {};
let historyCache = {}; // Added this to prevent the ReferenceError
function getDomain(db) {
const mapping = {
'commonswiki': 'commons.wikimedia.org', 'metawiki': 'meta.wikimedia.org', 'wikidatawiki': 'www.wikidata.org',
'mediawikiwiki': 'www.mediawiki.org', 'sourceswiki': 'wikisource.org', 'foundationwiki': 'foundation.wikimedia.org',
'incubatorwiki': 'incubator.wikimedia.org', 'outreachwiki': 'outreach.wikimedia.org',
'be_x_oldwiki': 'be-tarask.wikipedia.org', 'labswiki': 'wikitech.wikimedia.org',
'wikimaniawiki': 'wikimania.wikimedia.org', 'specieswiki': 'species.wikimedia.org',
'abstractwiki': 'abstract.wikipedia.org', 'betawikiversity': 'beta.wikiversity.org',
'mo_wiki': 'ro.wikipedia.org',
'testwiki': 'test.wikipedia.org', 'test2wiki': 'test2.wikipedia.org',
'wikifunctionswiki': 'www.wikifunctions.org',
'testcommonswiki': 'test-commons.wikimedia.org',
'testwikidatawiki': 'test.wikidata.org',
};
if (mapping[db]) return mapping[db];
const name = db.replace(/_/g, '-');
if (name.endsWith('wikisource')) return name.slice(0, -10) + '.wikisource.org';
if (name.endsWith('wikiversity')) return name.slice(0, -11) + '.wikiversity.org';
if (name.endsWith('wiktionary')) return name.slice(0, -10) + '.wiktionary.org';
if (name.endsWith('wikivoyage')) return name.slice(0, -10) + '.wikivoyage.org';
if (name.endsWith('wikibooks')) return name.slice(0, -9) + '.wikibooks.org';
if (name.endsWith('wikiquote')) return name.slice(0, -9) + '.wikiquote.org';
if (name.endsWith('wikinews')) return name.slice(0, -8) + '.wikinews.org';
if (name.endsWith('wikimedia')) return name.slice(0, -9) + '.wikimedia.org';
if (name.endsWith('wiki')) return name.slice(0, -4) + '.wikipedia.org';
return name + '.wikipedia.org';
}
mw.loader.using(['mediawiki.api', 'mediawiki.ForeignApi']).then(function() {
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php', { anonymous: true });
const now = new Date();
let results = {}, emptyWikis = [], failedWikis = [], isRunning = false;
function setupUI() {
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
// CHANGE: Dynamically generate dropdown options for years and months
let yearOpts = '';
for (let y = currentYear; y >= 2005; y--) {
yearOpts += `<option value="${y}">${y}</option>`;
}
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 === currentMonth ? 'selected' : ''}>${m}</option>`;
}
$('#firstHeading').text('GlobalCheckUserStats.js');
$('#mw-content-text').empty().append(`
<div style="border:1px solid #a2a9b1; padding:15px; background:#f8f9fa;">
<h3>Audit Range</h3>
From:
<select id="y-f" style="width:70px;">${yearOpts}</select>
<select id="m-f" style="width:50px;">${monthOptsFrom}</select>
To:
<select id="y-t" style="width:70px;">${yearOpts}</select>
<select id="m-t" style="width:50px;">${monthOptsTo}</select>
<button id="start" class="mw-ui-button mw-ui-progressive" style="margin-left:15px;">Run GlobalCheckUserStats.js</button>
<button id="stop" class="mw-ui-button mw-ui-destructive" disabled>Stop</button>
<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="${rawWikis.length}" style="width:100%"></progress></div>
<textarea id="out" style="width:100%; height:450px; margin-top:10px; display:none; font-family:monospace; font-size:12px;"></textarea>
</div>
`);
$('#start').click(() => runAudit($('#y-f').val(), $('#m-f').val(), $('#y-t').val(), $('#m-t').val()));
$('#stop').click(() => isRunning = false);
}
// NEW: Fetch monthly averaged editors and total edits from Wikimedia Analytics API
async function fetchWikiMetrics(db, start, end) {
const project = getDomain(db);
// Convert ISO START/END (YYYY-MM-DD...) to AQS format (YYYYMMDD)
const aqsStart = start.split('T')[0].replace(/-/g, '');
const aqsEnd = end.split('T')[0].replace(/-/g, '');
// API Endpoints for Editors and Edits
const editorsUrl = `https://wikimedia.org/api/rest_v1/metrics/editors/aggregate/${project}/all-editor-types/all-page-types/all-activity-levels/monthly/${aqsStart}/${aqsEnd}`;
const editsUrl = `https://wikimedia.org/api/rest_v1/metrics/edits/aggregate/${project}/all-editor-types/all-page-types/monthly/${aqsStart}/${aqsEnd}`;
try {
const [edRes, etRes] = await Promise.all([
fetch(editorsUrl).then(r => r.json()),
fetch(editsUrl).then(r => r.json())
]);
const editorsResults = edRes.items[0].results;
// Calculate monthly average for editors
const avgEditors = editorsResults.reduce((sum, m) => sum + m.editors, 0) / editorsResults.length;
// Calculate sum of all edits in period
const totalEdits = etRes.items[0].results.reduce((sum, m) => sum + m.edits, 0);
return { avgEditors, totalEdits };
} catch (e) {
// Fallback if AQS has no data for the project/period
return { avgEditors: null, totalEdits: null };
}
}
// Helper function to check global rights history on Meta-Wiki
async function checkGlobalHistory(user, start, end) {
try {
const res = await metaApi.get({
action: 'query',
list: 'logevents',
letype: 'gblrights',
letitle: 'User:' + user,
formatversion: 2
});
const logs = res.query.logevents || [];
const auditStart = new Date(start);
const auditEnd = new Date(end);
let wasSteward = false, wasStaff = false, wasOmbuds = false;
for (const e of logs) {
const ts = new Date(e.timestamp);
const p = e.params || {};
const added = p.newGroups || p[1] || [];
const removed = p.oldGroups || p[0] || [];
// If a removal happened AFTER the audit started, the user held the rights during the period
if (ts > auditStart) {
if (removed.includes('steward')) wasSteward = true;
if (removed.includes('staff')) wasStaff = true;
if (removed.includes('ombuds')) wasOmbuds = true;
}
// If an addition happened BEFORE the audit ended, the user held the rights during the period
if (ts < auditEnd) {
if (added.includes('steward')) wasSteward = true;
if (added.includes('staff')) wasStaff = true;
if (added.includes('ombuds')) wasOmbuds = true;
}
}
return { wasSteward, wasStaff, wasOmbuds };
} catch (e) {
// Fallback for API errors
return { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
async function fetchUserData(user, db, start, end) {
// Priority 0: Use cache for current global groups
if (!userCache[user]) {
try {
const gres = await metaApi.get({ action: 'query', meta: 'globaluserinfo', guiprop: 'groups', guiuser: user, formatversion: 2 });
userCache[user] = gres.query.globaluserinfo.groups || [];
} catch(e) { userCache[user] = []; }
}
const gGroups = userCache[user];
const isGloballyStaff = gGroups.includes('staff');
const isGloballySteward = gGroups.includes('steward');
const isGloballyOmbuds = gGroups.includes('ombuds');
// Check current local CheckUser rights
let isCurrentLocal = false;
try {
const localApi = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
const ures = await localApi.get({ action: 'query', list: 'users', ususers: user, usprop: 'groups', formatversion: 2 });
const lGroups = (ures.query.users[0] && ures.query.users[0].groups) || [];
isCurrentLocal = lGroups.includes('checkuser');
} catch(e) { /* API failure fallback */ }
// Rights log analysis for the specific Wiki
const target = 'User:' + user + '@' + db;
let logText = ''; // Default is empty, filled only if there are logs
let addTime = null, removeTime = null, isSelfAssign = false;
// CHANGE: Variables to track the longest duration and the number of actions
let maxDurationMins = -1;
let longestTimeStr = "";
let assignCount = 0;
try {
const res = await metaApi.get({
action: 'query', list: 'logevents', letype: 'rights', letitle: target,
lestart: end, leend: start, ledir: 'older',
lelimit: 'max', // ADDED: Loads up to 500 logs instead of 10
formatversion: 2
});
const events = res.query.logevents || [];
let filtered = [];
// Variables to pair removal and addition (iterating newest to oldest)
let tempRemoveTime = null;
let tempRemoveTimeStr = "Not removed";
events.forEach(e => {
const p = e.params || {};
const cuAdded = (p.add || []).includes('checkuser') || ((p.newgroups || []).includes('checkuser') && !(p.oldgroups || []).includes('checkuser'));
const cuRemoved = (p.remove || []).includes('checkuser') || (!(p.newgroups || []).includes('checkuser') && (p.oldgroups || []).includes('checkuser'));
const exactTimeLog = e.timestamp.replace('T', ' ').replace('Z', '');
if (cuRemoved) {
tempRemoveTime = new Date(e.timestamp);
tempRemoveTimeStr = exactTimeLog;
// Save the most recent removal for the overall table role categorization
if (!removeTime) {
removeTime = tempRemoveTime;
if (e.user === user || !e.user) isSelfAssign = true;
}
}
if (cuAdded) {
const currentAddTime = new Date(e.timestamp);
let metadataRemoveTime = tempRemoveTime;
let metadataRemoveTimeStr = tempRemoveTimeStr;
// Save the most recent addition for the overall table role categorization
if (!addTime) {
addTime = currentAddTime;
isSelfAssign = (e.user === user);
}
// Fallback: Check expiry in metadata if explicit removal log is missing
const metadata = p.newmetadata || [];
const cuMeta = metadata.find(m => m.group === 'checkuser');
if (cuMeta && cuMeta.expiry && cuMeta.expiry !== 'infinity' && tempRemoveTimeStr === "Not removed") {
metadataRemoveTime = new Date(cuMeta.expiry);
metadataRemoveTimeStr = cuMeta.expiry.replace('T', ' ').replace('Z', '');
if (!removeTime) removeTime = metadataRemoveTime;
}
// Calculate duration for this specific 1-line pair
let pairDuration = "Unknown";
if (metadataRemoveTime) {
const diffMs = Math.abs(metadataRemoveTime - currentAddTime);
const diffMins = Math.round(diffMs / 60000); // CHANGE: Rounds to the nearest whole minute
const days = Math.floor(diffMins / 1440);
const hours = Math.floor((diffMins % 1440) / 60);
const mins = diffMins % 60;
if (days > 0) pairDuration = `${days}d ${hours}h ${mins}m`;
else if (hours > 0) pairDuration = `${hours}h ${mins}m`;
else if (mins > 0) pairDuration = `${mins}m`;
else pairDuration = "<1m";
// CHANGE: Track the total number of assignments and store the absolute longest duration
assignCount++;
if (diffMins > maxDurationMins) {
maxDurationMins = diffMins;
longestTimeStr = pairDuration;
}
}
// Push the combined 1-line format to the log using commas
filtered.push(`* Added: ${exactTimeLog}, Removed: ${metadataRemoveTimeStr}, Duration: ${pairDuration}`);
// Reset pair variables for the next older event in the loop
tempRemoveTime = null;
tempRemoveTimeStr = "Not removed";
}
});
// Edge case: If a removal was found but the addition happened before the audit period
if (tempRemoveTime && tempRemoveTimeStr !== "Not removed") {
filtered.push(`* Added: Before audit period, Removed: ${tempRemoveTimeStr}, Duration: Unknown`);
}
// Joins logs with newlines for the report
if (filtered.length) {
logText = '\n' + filtered.join('\n');
}
} catch (err) { /* Rights log query failed */ }
// --- GLOBAL HISTORY CHECK (WITH CACHE) ---
if (!historyCache[user]) {
if (!isGloballySteward || !isGloballyStaff || !isGloballyOmbuds) {
// Check Meta-Wiki history logs only if user is missing at least one global role today
historyCache[user] = await checkGlobalHistory(user, start, end);
} else {
historyCache[user] = { wasSteward: false, wasStaff: false, wasOmbuds: false };
}
}
const history = historyCache[user];
// --- HIERARCHY LOGIC (CheckUser Audit Focus) ---
let roleLabel = "";
// 1. STAFF (Highest audit priority for WMF accountability)
if (isGloballyStaff || history.wasStaff) {
roleLabel = isGloballyStaff ? "Current Staff" : "Former Staff (in period)";
}
// 2. LOCAL CHECKUSER (Overrides Steward/Ombudsman roles if local rights exist)
else if (isCurrentLocal) {
roleLabel = "Current Local CheckUser";
}
// 3. STEWARD - SPECIFIC INTERVENTION (Using exact self-assignment duration or max duration if multiple)
else if (longestTimeStr && isSelfAssign) {
if (assignCount > 1) {
roleLabel = `Steward action (Self-assign: longest ${longestTimeStr}, see log)`;
} else {
roleLabel = `Steward action (Self-assign: ${longestTimeStr})`;
}
}
// 4. STEWARD - GENERAL (Global rights usage on external wikis)
else if (isGloballySteward || history.wasSteward) {
roleLabel = isGloballySteward ? "Current Steward" : "Former Steward (in period)";
}
// 5. OMBUDSMAN (Global oversight role)
else if (isGloballyOmbuds || history.wasOmbuds) {
roleLabel = isGloballyOmbuds ? "Current Ombudsman" : "Former Ombudsman (in period)";
}
// 6. FORMER LOCAL CheckUser (Fallback for those who lost rights but performed actions)
else {
roleLabel = "Former Local CheckUser";
}
return { role: roleLabel, log: logText };
}
async function runAudit(yf, mf, yt, mt) {
isRunning = true;
results = {};
emptyWikis = [];
failedWikis = [];
userCache = {};
historyCache = {};
$('#start').prop('disabled', true);
$('#stop').prop('disabled', false);
$('#out').hide();
// Format ISO timestamps for the API
const START = `${yf}-${mf.toString().padStart(2, '0')}-01T00:00:00Z`;
const lastDay = new Date(Date.UTC(yt, mt, 0)).getUTCDate();
const END = `${yt}-${mt.toString().padStart(2, '0')}-${lastDay.toString().padStart(2, '0')}T23:59:59Z`;
// Generate list of months for dynamic table columns
const monthCols = [];
let currY = parseInt(yf), currM = parseInt(mf);
const endY = parseInt(yt), endM = parseInt(mt);
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
while (currY < endY || (currY === endY && currM <= endM)) {
monthCols.push({
key: `${currY}-${String(currM).padStart(2, '0')}`,
label: `${monthNames[currM - 1]} ${currY}` // e.g., "Jan 2024"
});
currM++;
if (currM > 12) { currM = 1; currY++; }
}
for (let i = 0; i < rawWikis.length && isRunning; i++) {
const db = rawWikis[i];
$('#status-msg').text(`Scanning ${db} (${i + 1}/${rawWikis.length})...`);
let successLocal = false;
let continueToken = null; // Token for pagination
while (!successLocal && isRunning) {
try {
const api = new mw.ForeignApi(`https://${getDomain(db)}/w/api.php`);
let params = {
action: 'query',
list: 'checkuserlog',
culfrom: START,
culto: END,
culdir: 'newer',
cullimit: 'max',
formatversion: 2
};
// Add continue token if we are on page 2 or further
if (continueToken) {
Object.assign(params, continueToken);
}
let res = await api.get(params);
const ent = (res.query && res.query.checkuserlog && res.query.checkuserlog.entries) || [];
if (ent.length) {
if (!results[db]) results[db] = {};
// Sum up actions per user (total and per month)
ent.forEach(e => {
const u = e.checkuser;
if (!results[db][u]) results[db][u] = { total: 0, months: {} };
const date = new Date(e.timestamp);
const mKey = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
results[db][u].total++;
results[db][u].months[mKey] = (results[db][u].months[mKey] || 0) + 1;
});
}
// Pagination check
if (res.continue && isRunning) {
continueToken = res.continue;
$('#status-msg').text(`Scanning ${db} (fetching next page)...`);
} else {
// Wiki scan finished (either no entries at all or all pages fetched)
if (!results[db]) emptyWikis.push(db);
successLocal = true;
}
} catch (err) {
const status = (err && err.xhr) ? err.xhr.status : (err && err.status);
if (status === 429) {
// Rate limit hit, wait 30s and retry the SAME wiki/page
$('#status-msg').text(`Rate limit on ${db}. Waiting 30s...`);
await sleep(30000);
} else {
// Critical error (CORS, DNS, etc.), skip this wiki
console.error(`Skipping ${db} due to error:`, err);
failedWikis.push(db);
successLocal = true;
}
}
}
$('#bar').val(i + 1);
await sleep(DELAY_MS);
}
// --- REPORT GENERATION ---
$('#status-msg').text(isRunning ? `Generating report...` : `Generating partial report...`);
const now = new Date();
const timestamp = now.getUTCFullYear() + '-' +
String(now.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(now.getUTCDate()).padStart(2, '0') + ' ' +
String(now.getUTCHours()).padStart(2, '0') + ':' +
String(now.getUTCMinutes()).padStart(2, '0') + ' (UTC)';
let wt = `== Global CheckUser Stats (${mf}/${yf} - ${mt}/${yt}) ==\n`;
wt += `''Report generated on: ${timestamp}''\n\n`;
if (!isRunning) wt += `'''Note: This is a partial report (stopped manually).'''\n\n`;
else wt += `\n`;
// FIX: Define rightsLog here so it's accessible globally within the function scope
let rightsLog = `\n== Rights Log (In Period) ==\n`;
// 1. UPDATE HEADER: Added normalized columns with references
let headerMonths = monthCols.map(m => `!! ${m.label}`).join(' ');
wt += `{| class="wikitable sortable"\n! Wiki !! User (Category) !! '''Total Actions''' !! Actions per 1000 active users <ref>The count of editors with one or more edits, including on redirect pages (Source: Wikimedia Analytics API - AQS).</ref> !! Actions per 1000 edits <ref>Normalized against the total volume of all edits in the selected period (Source: Wikimedia Analytics API - AQS).</ref> ${headerMonths}\n`;
// 2. DATA PROCESSING LOOP
const sortedDBs = Object.keys(results).sort();
for (const db of sortedDBs) {
// NEW: Fetch project-wide metrics once per wiki
const metrics = await fetchWikiMetrics(db, START, END);
const sortedUsers = Object.keys(results[db]).sort();
for (const user of sortedUsers) {
const m = await fetchUserData(user, db, START, END);
const userData = results[db][user];
// NEW: Calculate normalized statistics
let per1kUsers = "N/A";
let per1kEdits = "N/A";
if (metrics.avgEditors && metrics.avgEditors > 0) {
per1kUsers = ((userData.total / metrics.avgEditors) * 1000).toFixed(1);
}
if (metrics.totalEdits && metrics.totalEdits > 0) {
per1kEdits = ((userData.total / metrics.totalEdits) * 1000).toFixed(2);
}
let rowStr = `|-\n| ${db} || ${user} (${m.role}) || '''${userData.total}''' || ${per1kUsers} || ${per1kEdits}`;
monthCols.forEach(col => {
rowStr += ` || ${userData.months[col.key] || 0}`;
});
wt += `${rowStr}\n`;
if (m.log) {
rightsLog += `'''${user}@${db}''':${m.log}\n\n`;
}
}
}
wt += `|}\n\n== Projects with 0 actions ==\n${emptyWikis.sort().join(', ')}\n`;
if (failedWikis.length) {
wt += `\n== Errors (Failed to scan) ==\n${failedWikis.sort().join(', ')}\n`;
}
// FIX: Append the populated rightsLog to the main wikitext
wt += rightsLog;
// ADD: References section for the new <ref> tags
wt += `\n== References ==\n<references />\n`;
$('#out').val(wt).show();
$('#status-msg').text(isRunning ? `Audit complete!` : `Audit stopped. Partial report ready.`);
$('#start').prop('disabled', false);
$('#stop').prop('disabled', true);
}
setupUI();
});
})();
g2hyufbqtffk7tdxlqrmj7ltlg9soh4
User talk:JWBTH/CD test page/comment
3
174723
737101
737034
2026-04-07T18:35:25Z
JWBTH
52211
/* Transcluded comments */ reply: {| | table |} (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737101
wikitext
text/x-wiki
transcluded comment. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:21, 7 April 2026 (UTC)
:test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:22, 7 April 2026 (UTC)
::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:23, 7 April 2026 (UTC)
:::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 05:49, 7 April 2026 (UTC)
::::test
::::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 06:18, 7 April 2026 (UTC)
::::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:12, 7 April 2026 (UTC)
: {|
| table
|} [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:35, 7 April 2026 (UTC)
{{text|comment in a template. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:30, 7 April 2026 (UTC)}}
:test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:31, 7 April 2026 (UTC)
{{User_talk:JWBTH/CD_test_page/closed|comment in a closed template. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:38, 7 April 2026 (UTC)}}
some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text.
{{User_talk:JWBTH/CD_test_page/end closed}}
kqzka2363zwahpjupkr3i22y1wlcxk6
737158
737101
2026-04-08T11:15:07Z
JWBTH
52211
/* Transcluded comments */ addition: reply (-) ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737158
wikitext
text/x-wiki
transcluded comment. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:21, 7 April 2026 (UTC)
:test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:22, 7 April 2026 (UTC)
::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:23, 7 April 2026 (UTC)
:::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 05:49, 7 April 2026 (UTC)
::::test
::::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 06:18, 7 April 2026 (UTC)
::::test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 07:12, 7 April 2026 (UTC)
: {|
| table
|} [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 18:35, 7 April 2026 (UTC)
{{text|comment in a template. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:30, 7 April 2026 (UTC)}}
:test [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:31, 7 April 2026 (UTC)
: reply [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 11:15, 8 April 2026 (UTC)
{{User_talk:JWBTH/CD_test_page/closed|comment in a closed template. [[User:JWBTH|JWBTH]] ([[User talk:JWBTH|talk]]) 04:38, 7 April 2026 (UTC)}}
some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text. some text.
{{User_talk:JWBTH/CD_test_page/end closed}}
td074zueipadp5r28sebwkvsprewhpw
Wikipedia:Requests/Permissions/CaptainEek
4
174729
737089
2026-04-07T17:34:02Z
CaptainEek
47282
+RfA
737089
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:CaptainEek|CaptainEek]] ===
* {{User3|CaptainEek}}, [[Special:CentralAuth/CaptainEek|global contribs]] 17:34, 7 April 2026 (UTC)
* '''Motive for request:''' <!-- Important! Please include a good motive for your request here. --> Howdy hello folks, I'm CaptainEek. I'm an administrator and an arbitrator on English Wikipedia. I am seeking adminship here to help the PSI team test the Private Incident Reporting System. I've been helping out as an end-to-end tester, and having access to edit templates and the config would help me to help Eric Mill and his team. [[User:CaptainEek|CaptainEek]] ([[User talk:CaptainEek|talk]]) 17:34, 7 April 2026 (UTC)
* '''Requested rights:''' Administrator
* '''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__
ttw8bt8n8edhzg20l94xdgjza4k0r9k
737091
737089
2026-04-07T17:39:19Z
Vermont
37989
/* CaptainEek */ d
737091
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}}.-->
{{Request-done|1=[[User:Vermont|Vermont]] ([[User talk:Vermont|talk]]) 17:39, 7 April 2026 (UTC)|2=Valid request.}}
=== [[User:CaptainEek|CaptainEek]] ===
* {{User3|CaptainEek}}, [[Special:CentralAuth/CaptainEek|global contribs]] 17:34, 7 April 2026 (UTC)
* '''Motive for request:''' <!-- Important! Please include a good motive for your request here. --> Howdy hello folks, I'm CaptainEek. I'm an administrator and an arbitrator on English Wikipedia. I am seeking adminship here to help the PSI team test the Private Incident Reporting System. I've been helping out as an end-to-end tester, and having access to edit templates and the config would help me to help Eric Mill and his team. [[User:CaptainEek|CaptainEek]] ([[User talk:CaptainEek|talk]]) 17:34, 7 April 2026 (UTC)
* '''Requested rights:''' Administrator
* '''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}}
dd3ccqd1q8iqoej5dhwd9cblb00k3kt
Earthlings
0
174731
737100
2026-04-07T18:35:00Z
Ponor
47975
Ponor moved page [[Earthlings]] to [[Earthling]] over redirect: test ([[:en:User:Ponor/wAwB|wAwB]])
737100
wikitext
text/x-wiki
#REDIRECT [[Earthling]]
{{Redirect category shell|
{{R from move}}
}}
qi4p0dtw2rs2mic9wb9gqtileuk89k7
User:ToluAyod/Starter kit/Ninu iroyin
2
174732
737110
2026-04-07T19:30:07Z
ToluAyod
69650
Initialised by StarterKit tool — ready for translation
737110
wikitext
text/x-wiki
<div style="border:1px solid #cedff2;border-radius:4px;padding:8px;background:#f5faff;margin-bottom:2px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #a3b0bf;border-radius:4px;background:#cedff2;">'''In the news'''</div>
<!-- Add recent events or news relevant to your community or topic area.
No need to update daily — refresh when something noteworthy happens.
To add an image: [[File:Filename.jpg|64px|right|alt=description]] -->
* Add a recent event or news item and link to a relevant article here.
* Add a recent event or news item and link to a relevant article here.
* Add a recent event or news item and link to a relevant article here.
</div>
[[Category:Starter Kit templates]][[Category:Main page templates]]
ov78wfo8w8x7fjlnuahoq193ccbon5i
User:ToluAyod/Starter kit/Ede Wikipedia
2
174733
737111
2026-04-07T19:41:41Z
ToluAyod
69650
Initialised by StarterKit tool — ready for translation
737111
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:#ffffff;overflow:hidden;">
<div style="background:#F8FAFC;border-bottom:1px solid #CBD5E1;padding:8px 16px;font-weight:bold;">Wikipedia languages</div>
<div style="padding:16px;">
Many [https://meta.wikimedia.org/wiki/List_of_Wikipedias other Wikipedias are available]; some of the largest are listed below.
<ul style="list-style:none;margin:8px 0 0 0;padding:0;">
<li style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">1,000,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://ar.wikipedia.org/wiki/ العربية] ·
[https://de.wikipedia.org/wiki/ Deutsch] ·
[https://es.wikipedia.org/wiki/ Español] ·
[https://fa.wikipedia.org/wiki/ فارسی]‎ ·
[https://fr.wikipedia.org/wiki/ Français] ·
[https://it.wikipedia.org/wiki/ Italiano] ·
[https://nl.wikipedia.org/wiki/ Nederlands] ·
[https://ja.wikipedia.org/wiki/ 日本語] ·
[https://pl.wikipedia.org/wiki/ Polski] ·
[https://pt.wikipedia.org/wiki/ Português] ·
[https://ru.wikipedia.org/wiki/ Русский] ·
[https://sv.wikipedia.org/wiki/ Svenska] ·
[https://uk.wikipedia.org/wiki/ Українська] ·
[https://vi.wikipedia.org/wiki/ Tiếng Việt] ·
[https://zh.wikipedia.org/wiki/ 中文]
</div>
</li>
<li style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">250,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://id.wikipedia.org/wiki/ Bahasa Indonesia] ·
[https://ms.wikipedia.org/wiki/ Bahasa Melayu] ·
[https://nan.wikipedia.org/wiki/ 閩南語] ·
[https://bg.wikipedia.org/wiki/ Български] ·
[https://ca.wikipedia.org/wiki/ Català] ·
[https://cs.wikipedia.org/wiki/ Čeština] ·
[https://da.wikipedia.org/wiki/ Dansk] ·
[https://et.wikipedia.org/wiki/ Eesti] ·
[https://el.wikipedia.org/wiki/ Ελληνικά] ·
[https://eo.wikipedia.org/wiki/ Esperanto] ·
[https://eu.wikipedia.org/wiki/ Euskara] ·
[https://he.wikipedia.org/wiki/ עברית] ·
[https://hy.wikipedia.org/wiki/ Հայերեն] ·
[https://ko.wikipedia.org/wiki/ 한국어] ·
[https://hu.wikipedia.org/wiki/ Magyar] ·
[https://no.wikipedia.org/wiki/ Norsk] ·
[https://ro.wikipedia.org/wiki/ Română] ·
[https://simple.wikipedia.org/wiki/ Simple English] ·
[https://sk.wikipedia.org/wiki/ Slovenčina] ·
[https://sr.wikipedia.org/wiki/ Српски] ·
[https://sh.wikipedia.org/wiki/ Srpskohrvatski] ·
[https://fi.wikipedia.org/wiki/ Suomi] ·
[https://tr.wikipedia.org/wiki/ Türkçe] ·
[https://uz.wikipedia.org/wiki/ Oʻzbek]
</div>
</li>
<li style="margin-bottom:4px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">50,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://ast.wikipedia.org/wiki/ Asturianu] ·
[https://az.wikipedia.org/wiki/ Azərbaycanca] ·
[https://bn.wikipedia.org/wiki/ বাংলা] ·
[https://bs.wikipedia.org/wiki/ Bosanski] ·
[https://ckb.wikipedia.org/wiki/ کوردی] ·
[https://fy.wikipedia.org/wiki/ Frysk] ·
[https://ga.wikipedia.org/wiki/ Gaeilge] ·
[https://gl.wikipedia.org/wiki/ Galego] ·
[https://hr.wikipedia.org/wiki/ Hrvatski] ·
[https://ka.wikipedia.org/wiki/ ქართული] ·
[https://ku.wikipedia.org/wiki/ Kurdî] ·
[https://lv.wikipedia.org/wiki/ Latviešu] ·
[https://lt.wikipedia.org/wiki/ Lietuvių] ·
[https://ml.wikipedia.org/wiki/ മലയാളം] ·
[https://mk.wikipedia.org/wiki/ Македонски] ·
[https://my.wikipedia.org/wiki/ မြန်မာဘာသာ] ·
[https://nn.wikipedia.org/wiki/ Norsk nynorsk] ·
[https://pa.wikipedia.org/wiki/ ਪੰਜਾਬੀ] ·
[https://sq.wikipedia.org/wiki/ Shqip] ·
[https://sl.wikipedia.org/wiki/ Slovenščina] ·
[https://th.wikipedia.org/wiki/ ไทย] ·
[https://te.wikipedia.org/wiki/ తెలుగు] ·
[https://ur.wikipedia.org/wiki/ اردو]
</div>
</li>
</ul>
</div>
</div>
<noinclude>[[Category:Starter Kit templates]][[Category:Main page templates]]</noinclude>
exir7rqcraea7skf3rre6cimfwg2mmq
User:ToluAyod/Starter kit/Lojoor oni
2
174734
737113
2026-04-07T19:43:35Z
ToluAyod
69650
Initialised by StarterKit tool — ready for translation
737113
wikitext
text/x-wiki
<div style="border:1px solid #cedff2;border-radius:4px;padding:8px;background:#f5faff;margin-bottom:2px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #a3b0bf;border-radius:4px;background:#cedff2;">'''On this day'''</div>
<!-- Add 2–3 historical events relevant to your community or topic area.
No need to update daily — refresh occasionally as your wiki grows.
To add an image: [[File:Filename.jpg|80px|right|alt=description]] -->
Add a date here
* Add a historical event and link to a relevant article here.
* Add a historical event and link to a relevant article here.
</div>
[[Category:Starter Kit templates]][[Category:Main page templates]]
oawkmcoyobjvtr2l018429c6lkvtbv7
User:ToluAyod/Starter kit/Ninu Iroyin
2
174735
737114
2026-04-07T19:44:01Z
ToluAyod
69650
Initialised by StarterKit tool — ready for translation
737114
wikitext
text/x-wiki
<div style="border:1px solid #cedff2;border-radius:4px;padding:8px;background:#f5faff;margin-bottom:2px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #a3b0bf;border-radius:4px;background:#cedff2;">'''In the news'''</div>
<!-- Add recent events or news relevant to your community or topic area.
No need to update daily — refresh when something noteworthy happens.
To add an image: [[File:Filename.jpg|64px|right|alt=description]] -->
* Add a recent event or news item and link to a relevant article here.
* Add a recent event or news item and link to a relevant article here.
* Add a recent event or news item and link to a relevant article here.
</div>
[[Category:Starter Kit templates]][[Category:Main page templates]]
ov78wfo8w8x7fjlnuahoq193ccbon5i
User:ToluAyod/Starter kit/c
2
174736
737115
2026-04-07T19:44:18Z
ToluAyod
69650
Initialised by StarterKit tool — ready for translation
737115
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:#ffffff;overflow:hidden;margin-bottom:16px;">
<div style="background:#F8FAFC;border-bottom:1px solid #CBD5E1;padding:8px 16px;font-weight:bold;">Wikipedia's sister projects</div>
<div style="padding:16px;">
Wikipedia is written by volunteer editors and hosted by the [https://wikimediafoundation.org/ Wikimedia Foundation], a non-profit organization that also hosts a range of other volunteer [https://wikimediafoundation.org/our-work/wikimedia-projects/ projects]:
<ul style="list-style:none;margin:8px 0 0 0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;">
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Commons-logo.svg|31px|link=https://commons.wikimedia.org/|alt=Commons logo]]<span>[https://commons.wikimedia.org/ Commons]<br/><small style="color:#555;">Free media repository</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:MediaWiki-2020-icon.svg|35px|link=https://www.mediawiki.org/|alt=MediaWiki logo]]<span>[https://www.mediawiki.org/ MediaWiki]<br/><small style="color:#555;">Wiki software development</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikimedia Community Logo.svg|35px|link=https://meta.wikimedia.org/|alt=Meta-Wiki logo]]<span>[https://meta.wikimedia.org/ Meta-Wiki]<br/><small style="color:#555;">Wikimedia project coordination</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikibooks-logo.svg|35px|link=https://www.wikibooks.org/|alt=Wikibooks logo]]<span>[https://www.wikibooks.org/ Wikibooks]<br/><small style="color:#555;">Free textbooks and manuals</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikidata-logo.svg|47px|link=https://www.wikidata.org/|alt=Wikidata logo]]<span>[https://www.wikidata.org/ Wikidata]<br/><small style="color:#555;">Free knowledge base</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikinews-logo.svg|51px|link=https://www.wikinews.org/|alt=Wikinews logo]]<span>[https://www.wikinews.org/ Wikinews]<br/><small style="color:#555;">Free-content news</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikiquote-logo.svg|35px|link=https://www.wikiquote.org/|alt=Wikiquote logo]]<span>[https://www.wikiquote.org/ Wikiquote]<br/><small style="color:#555;">Collection of quotations</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikisource-logo.svg|35px|link=https://www.wikisource.org/|alt=Wikisource logo]]<span>[https://www.wikisource.org/ Wikisource]<br/><small style="color:#555;">Free-content library</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikispecies-logo.svg|35px|link=https://species.wikimedia.org/|alt=Wikispecies logo]]<span>[https://species.wikimedia.org/ Wikispecies]<br/><small style="color:#555;">Directory of species</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikiversity logo 2017.svg|41px|link=https://www.wikiversity.org/|alt=Wikiversity logo]]<span>[https://www.wikiversity.org/ Wikiversity]<br/><small style="color:#555;">Free learning tools</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikivoyage-Logo-v3-icon.svg|35px|link=https://www.wikivoyage.org/|alt=Wikivoyage logo]]<span>[https://www.wikivoyage.org/ Wikivoyage]<br/><small style="color:#555;">Free travel guide</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wiktionary-logo-v2.svg|35px|link=https://www.wiktionary.org/|alt=Wiktionary logo]]<span>[https://www.wiktionary.org/ Wiktionary]<br/><small style="color:#555;">Dictionary and thesaurus</small></span></li>
</ul>
</div>
</div>
<noinclude>[[Category:Starter Kit templates]][[Category:Main page templates]]</noinclude>
6gmjkri3ke7d1hjrnav9a1xwnkp9dzr
User:ToluAyod/Starter kit/Ede wikipedia
2
174737
737116
2026-04-07T19:44:31Z
ToluAyod
69650
Initialised by StarterKit tool — ready for translation
737116
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:#ffffff;overflow:hidden;">
<div style="background:#F8FAFC;border-bottom:1px solid #CBD5E1;padding:8px 16px;font-weight:bold;">Wikipedia languages</div>
<div style="padding:16px;">
Many [https://meta.wikimedia.org/wiki/List_of_Wikipedias other Wikipedias are available]; some of the largest are listed below.
<ul style="list-style:none;margin:8px 0 0 0;padding:0;">
<li style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">1,000,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://ar.wikipedia.org/wiki/ العربية] ·
[https://de.wikipedia.org/wiki/ Deutsch] ·
[https://es.wikipedia.org/wiki/ Español] ·
[https://fa.wikipedia.org/wiki/ فارسی]‎ ·
[https://fr.wikipedia.org/wiki/ Français] ·
[https://it.wikipedia.org/wiki/ Italiano] ·
[https://nl.wikipedia.org/wiki/ Nederlands] ·
[https://ja.wikipedia.org/wiki/ 日本語] ·
[https://pl.wikipedia.org/wiki/ Polski] ·
[https://pt.wikipedia.org/wiki/ Português] ·
[https://ru.wikipedia.org/wiki/ Русский] ·
[https://sv.wikipedia.org/wiki/ Svenska] ·
[https://uk.wikipedia.org/wiki/ Українська] ·
[https://vi.wikipedia.org/wiki/ Tiếng Việt] ·
[https://zh.wikipedia.org/wiki/ 中文]
</div>
</li>
<li style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">250,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://id.wikipedia.org/wiki/ Bahasa Indonesia] ·
[https://ms.wikipedia.org/wiki/ Bahasa Melayu] ·
[https://nan.wikipedia.org/wiki/ 閩南語] ·
[https://bg.wikipedia.org/wiki/ Български] ·
[https://ca.wikipedia.org/wiki/ Català] ·
[https://cs.wikipedia.org/wiki/ Čeština] ·
[https://da.wikipedia.org/wiki/ Dansk] ·
[https://et.wikipedia.org/wiki/ Eesti] ·
[https://el.wikipedia.org/wiki/ Ελληνικά] ·
[https://eo.wikipedia.org/wiki/ Esperanto] ·
[https://eu.wikipedia.org/wiki/ Euskara] ·
[https://he.wikipedia.org/wiki/ עברית] ·
[https://hy.wikipedia.org/wiki/ Հայերեն] ·
[https://ko.wikipedia.org/wiki/ 한국어] ·
[https://hu.wikipedia.org/wiki/ Magyar] ·
[https://no.wikipedia.org/wiki/ Norsk] ·
[https://ro.wikipedia.org/wiki/ Română] ·
[https://simple.wikipedia.org/wiki/ Simple English] ·
[https://sk.wikipedia.org/wiki/ Slovenčina] ·
[https://sr.wikipedia.org/wiki/ Српски] ·
[https://sh.wikipedia.org/wiki/ Srpskohrvatski] ·
[https://fi.wikipedia.org/wiki/ Suomi] ·
[https://tr.wikipedia.org/wiki/ Türkçe] ·
[https://uz.wikipedia.org/wiki/ Oʻzbek]
</div>
</li>
<li style="margin-bottom:4px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">50,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://ast.wikipedia.org/wiki/ Asturianu] ·
[https://az.wikipedia.org/wiki/ Azərbaycanca] ·
[https://bn.wikipedia.org/wiki/ বাংলা] ·
[https://bs.wikipedia.org/wiki/ Bosanski] ·
[https://ckb.wikipedia.org/wiki/ کوردی] ·
[https://fy.wikipedia.org/wiki/ Frysk] ·
[https://ga.wikipedia.org/wiki/ Gaeilge] ·
[https://gl.wikipedia.org/wiki/ Galego] ·
[https://hr.wikipedia.org/wiki/ Hrvatski] ·
[https://ka.wikipedia.org/wiki/ ქართული] ·
[https://ku.wikipedia.org/wiki/ Kurdî] ·
[https://lv.wikipedia.org/wiki/ Latviešu] ·
[https://lt.wikipedia.org/wiki/ Lietuvių] ·
[https://ml.wikipedia.org/wiki/ മലയാളം] ·
[https://mk.wikipedia.org/wiki/ Македонски] ·
[https://my.wikipedia.org/wiki/ မြန်မာဘာသာ] ·
[https://nn.wikipedia.org/wiki/ Norsk nynorsk] ·
[https://pa.wikipedia.org/wiki/ ਪੰਜਾਬੀ] ·
[https://sq.wikipedia.org/wiki/ Shqip] ·
[https://sl.wikipedia.org/wiki/ Slovenščina] ·
[https://th.wikipedia.org/wiki/ ไทย] ·
[https://te.wikipedia.org/wiki/ తెలుగు] ·
[https://ur.wikipedia.org/wiki/ اردو]
</div>
</li>
</ul>
</div>
</div>
<noinclude>[[Category:Starter Kit templates]][[Category:Main page templates]]</noinclude>
exir7rqcraea7skf3rre6cimfwg2mmq
User:Enbi/testScript2.js
2
174738
737118
2026-04-07T19:46:57Z
Enbi
72574
Created page with "document.querySelector('a[href="/wiki/User:Enbi"], a[href*="/w/index.php?title=User:Enbi"]').forEach(i => { i.textContent = '...enbi'; })"
737118
javascript
text/javascript
document.querySelector('a[href="/wiki/User:Enbi"], a[href*="/w/index.php?title=User:Enbi"]').forEach(i => {
i.textContent = '...enbi';
})
c3xsvr70n2j4j1o9n52bdmwytixsw4h
737119
737118
2026-04-07T19:47:42Z
Enbi
72574
737119
javascript
text/javascript
document.querySelectorAll('a[href="/wiki/User:Enbi"], a[href*="/w/index.php?title=User:Enbi"]').forEach(i => {
i.textContent = '...enbi';
})
703mnemlfidtfjqk8wmyj0lp7bjrn0u
737121
737119
2026-04-07T19:48:48Z
Enbi
72574
737121
javascript
text/javascript
document.querySelectorAll('a[href="/wiki/User:Enbi"]').forEach(i => {
i.textContent = '...enbi';
})
sa6nzrg0yb5cbwfipvr82r53s6gq6jn
737122
737121
2026-04-07T19:49:53Z
Enbi
72574
737122
javascript
text/javascript
document.querySelectorAll('a[href="/wiki/User:Enbi"]').forEach(i => {
i.textContent = '.enbi';
})
kx12d7ybf7mtyh2o4lear46ajjridt5
User:PieAlt/GA sandbox
2
174739
737131
2026-04-07T23:37:50Z
PieAlt
73198
Import from the revision [[simple:Special:PermaLink/10808578|10808578]] of [[simple:Black-footed cat]] edited by [[simple:Special:Contributions/Peterdownunder|Peterdownunder]] at 2026-04-06T11:18:32Z via [[w:zh:User:臺灣象象/transwiki-importer.js|transwiki-importer]]
737131
wikitext
text/x-wiki
{{complex|date=February 2026}}
{{pgood}}
{{Speciesbox
| image = Zoo Wuppertal Schwarzfusskatze.jpg
| status = VU
| status_ref =<ref name ="IUCN">{{cite IUCN|url=https://www.iucnredlist.org/species/8542/50652196|author1=Sliwa, A.|author2= Wilson, B.|author3=Küsters, M.|author4=Tordiffe, A.|year=2016|title=Felis nigripes|publisher=The IUCN Red List of Threatened Species|volume= 2016|page=e.T8542A50652196|doi=10.2305/IUCN.UK.2016-1.RLTS.T8542A50652196.en}}</ref>
| status_system = IUCN3.1
| taxon = Felis nigripes
| authority =
| synonyms =
| range_map = Black-footedCat distribution.jpg
}}
The '''black-footed cat''' ('''''Felis nigripes'''''), also called the '''small-spotted cat''', is the smallest wild [[simple:Felis|cat]] species in Africa.<ref name=Guggisberg1975>{{Cite book |last=Guggisberg |first=C. A. W. |title=Wild Cats of the World |url=https://archive.org/details/wildcatsofworld00gugg |year=1975 |publisher=Taplinger Publishing |location=New York |isbn=978-0-8008-8324-9 |chapter=Black-footed Cat ''Felis nigripes'' (Burchell, 1842) |pages=[https://archive.org/details/wildcatsofworld00gugg/page/40 40]–42}}</ref><ref name="skinner">{{cite book|last=Mills|first=M. G. L.|editor1-last=Skinner|editor1-first=J. D.|editor2-last=Chimimba|editor2-first=C. T.|title=The Mammals of the Southern African Subregion|edition=3rd|date=2005|publisher=Cambridge University Press|location=Cape Town|isbn=978-0-521-84418-5|chapter=Felis nigripes Burchell, 1824 (Black-footed cat)|pages=405–408|chapter-url=https://books.google.com/books?id=iqwEYkTDZf4C&pg=PA405}}</ref> Its body is about {{cvt|35|-|52|cm}} long.<ref name=ms>{{cite journal |last1=Renard |first1=A. |last2=Lavoie |first2=M. |last3=Pitt |first3=J. A. |last4=Larivière |first4=S. |title=''Felis nigripes'' (Carnivora: Felidae) |journal=Mammalian Species |date=2015 |volume=47 |issue=925 |pages=78–83 |doi=10.1093/mspecies/sev008 |doi-access=free}}</ref> Only the bottoms of its feet are black or dark brown.<ref name="Burchell1824">{{cite book|last=Burchell|first=W. J.|author-link=William John Burchell|title=Travels in the Interior of Southern Africa|volume=II|date=1824|publisher=Longman, Hurst, Rees, Orme, Brown, and Green|location=London|chapter=Felis nigripes|page=592|chapter-url=https://books.google.com/books?id=AGpdAAAAcAAJ&pg=PA592|access-date=29 April 2020|archive-date=19 July 2023|archive-url=https://web.archive.org/web/20230719111228/https://books.google.com/books?id=AGpdAAAAcAAJ&pg=PA592|url-status=live}}</ref> Its yellow-brown fur has small dark spots and stripes that help it stay hidden at night.<ref name="Sliwa2013">{{cite book|last=Sliwa|first=A.|date=2013|chapter=Felis nigripes Black-footed cat|pages=203–206|title=Mammals of Africa|volume=V. Carnivores, Pangolins, Equids and Rhinoceroses|publisher=Bloomsbury|location=London; New Delhi; New York; Sydney|isbn=978-1-4081-8994-8|editor1-last=Kingdon|editor1-first=J.|editor2-last=Happold|editor2-first=D.|editor3-last=Hoffmann|editor3-first=M.|editor4-last=Butynski|editor4-first=T.|editor5-last=Happold|editor5-first=M.|editor6-last=Kalina|editor6-first=J.|chapter-url=https://books.google.com/books?id=B_07noCPc4kC&pg=RA4-PA205}}</ref> It has black lines running from its eyes to its cheeks, and its tail has dark bands with a black tip.<ref name=skinner/>
Scientists discovered the first black-footed cat in the northern Karoo of South Africa and described it in 1824.<ref name=Burchell1824/> It lives only in the dry steppes and grasslands of [[simple:Southern Africa]].<ref name="iucn">{{cite journal|last1=Sliwa|first1=A.|last2=Wilson|first2=B.|last3=Lawrenz|first3=A.|name-list-style=amp|date=2016|title=Felis nigripes|journal=The IUCN Red List of Threatened Species|volume=2016|issue=e.T8540A50657483|doi=10.2305/IUCN.UK.2016-1.RLTS.T8540A50657483.en|doi-broken-date=6 February 2026 |url=https://www.iucnredlist.org/species/8540/50657483|access-date=30 April 2020}}</ref> It has been seen in southern Botswana,<ref name=Smithers1971>{{cite book |last=Smithers |first=R. N. H. |date=1971 |title=The Mammals of Botswana |location=Pretoria |publisher=University of Pretoria |chapter=''Felis nigripes'' Blackfooted Cat |pages=128–130}}</ref> and is rarely found in Namibia, southern Angola, and southern Zimbabwe.<ref name=iucn/> Because it lives in only a few places, it has been listed as [[simple:vulnerable species|Vulnerable]] on the [[simple:IUCN Red List]] since 2002.<ref name=iucn/> Its population is believed to be decreasing due to the [[simple:poaching]] of its prey for [[simple:bushmeat]], people killing it, [[simple:road kill]]s, and attacks by [[simple:herding dog]]s.<ref name=iucn/>
Since 1993, scientists have studied the black-footed cat using radio [[simple:telemetry]].<ref name=Olbricht97>{{cite journal |last1=Olbricht |first1=G. |last2=Sliwa |first2=A. |date=1997 |title=''In situ'' and ''ex situ'' observations and management of black-footed cats ''Felis nigripes'' |journal=International Zoo Yearbook |volume=35 |issue=35 |pages=81–89 |doi=10.1111/j.1748-1090.1997.tb01194.x}}</ref> This research showed how it lives in the wild: it rests in burrows during the day and hunts at night.<ref name=Sliwa2004>{{cite journal |last=Sliwa |first=A. |date=2004 |title=Home range size and social organization of black-footed cats (''Felis nigripes'') |journal=Mammalian Biology |volume=69 |issue=2 |pages=96–107 |doi=10.1078/1616-5047-00124}}</ref> It usually travels {{cvt|5|to|16|km|0}} each night to search for small rodents and birds.<ref name=Sliwa2004/> It eats around 40 types of [[simple:vertebrate]]s and may kill up to 14 small animals in one night.<ref name=Sliwa94>{{cite journal |last=Sliwa |first=A. |date=1994 |title=Diet and feeding behaviour of the Black-footed Cat (''Felis nigripes'' Burchell, 1824) in the Kimberley Region, South Africa |journal=Der Zoologische Garten N.F. |volume=64 |issue=2 |pages=83–96}}</ref> It can jump up to {{cvt|1.4|m|ft|0}} high to catch birds in the air,<ref name=Sliwa94/> and it sometimes attacks prey that is heavier than itself.<ref name=Sliwa94/> Females usually give birth to two kittens between October and March.<ref name=Sliwa2004/> The kittens stop drinking milk at about two months old and live on their own by four months of age.<ref name=Olbricht97/>
==Taxonomy==
The [[simple:binomial nomenclature|scientific name]] ''Felis nigripes'' was given in 1824 by British explorer William John Burchell. He named it after seeing skins of small spotted cats near Litákun (now Dithakong) in South Africa.<ref name=Burchell1824/>
In 1931, South African mammalogist Guy C. Shortridge suggested a [[simple:subspecies]] called ''Felis nigripes thomasi''. He based this idea on darker skins found in Griqualand West.<ref>{{cite journal |last1=Shortridge |first1=G. C. |author-link=Guy C. Shortridge |date=1931 |title=''Felis nigripes thomasi'' subsp. nov. |journal=Records of the Albany Museum |volume=4 |issue=1 |pages=119–120}}</ref>
Later, British zoologist Reginald Innes Pocock confirmed that the black-footed cat belongs to the genus ''[[simple:Felis]]''.<ref name="Pocock1951">{{cite book|last=Pocock|first=R. I.|date=1951|title=Catalogue of the genus Felis|publisher=British Museum (Natural History)|location=London|chapter=Felis nigripes Burchell|pages=145–150|chapter-url=https://archive.org/stream/catalogueofgenus00brit#page/145/mode/1up}}</ref>
The idea that the black-footed cat has subspecies was later questioned because there are no clear natural barriers, like a mountain, river, or desert, between its populations.<ref name=Olbricht97/> In 2017, the [[simple:IUCN]] Cat Specialist Group reviewed cat classification and said the black-footed cat is most likely a [[simple:monotype|monotypic]] species, meaning it has no subspecies.<ref name="CatSG2017">{{cite journal|last1=Kitchener|first1=A. C.|last2=Breitenmoser-Würsten|first2=C.|last3=Eizirik|first3=E.|last4=Gentry|first4=A.|last5=Werdelin|first5=L.|last6=Wilting|first6=A.|last7=Yamaguchi|first7=N.|last8=Abramov|first8=A. V.|last9=Christiansen|first9=P.|last10=Driscoll|first10=C.|last11=Duckworth|first11=J. W.|last12=Johnson|first12=W.|last13=Luo|first13=S.-J.|last14=Meijaard|first14=E.|last15=O'Donoghue|first15=P.|last16=Sanderson|first16=J.|last17=Seymour|first17=K.|last18=Bruford|first18=M.|last19=Groves|first19=C.|last20=Hoffmann|first20=M.|last21=Nowell|first21=K.|last22=Timmons|first22=Z.|last23=Tobe|first23=S.|name-list-style=amp|date=2017|title=A revised taxonomy of the Felidae: The final report of the Cat Classification Task Force of the IUCN Cat Specialist Group|journal=Cat News|issue=Special Issue 11|page=13|url=https://repository.si.edu/bitstream/handle/10088/32616/A_revised_Felidae_Taxonomy_CatNews.pdf?sequence=1&isAllowed=y|access-date=16 March 2019|archive-date=30 July 2018|archive-url=https://web.archive.org/web/20180730142355/https://repository.si.edu/bitstream/handle/10088/32616/A_revised_Felidae_Taxonomy_CatNews.pdf?sequence=1&isAllowed=y|url-status=live}}</ref>
=== Evolution ===
Studies of [[simple:DNA]] show that cats ([[simple:Felidae]]) first spread across Asia during the [[simple:Miocene]], about {{Ma|14.45|8.38|million years ago|round=2}}.<ref name="Johnson2006" /><ref name="Werdelin2010" /> Studies of [[simple:mitochondrial DNA]] suggest this may have happened about {{mya|16.76|6.46}}.<ref name="Li_al2016" />
The black-footed cat belongs to the ''Felis'' group. Based on DNA found in the cell’s [[simple:Cell nucleus|nucleus]], it split from the common ancestor of all ''Felis'' species about {{mya|4.44|2.16}}.<ref name="Johnson2006" /><ref name="Werdelin2010" /> Studies of mitochondrial DNA suggest it split earlier, about {{Ma|6.52|1.03|million years ago|round=2}}.<ref name="Li_al2016" /> Both studies agree that the [[simple:jungle cat]] (''F. chaus'') split first, and the black-footed cat split after that.<ref name="Johnson2006" /><ref name="Li_al2016" />
No fossils of the black-footed cat have been found.<ref name="Werdelin2010" /> It probably moved into Africa during the [[simple:Pleistocene]].<ref name="Johnson2006" /> This may have been possible because sea levels were lower between Asia and Africa.<ref name="Li_al2016" />
The following [[simple:cladogram]] shows its relationships based on nuclear DNA:
<ref name="Johnson2006" /><ref name="Werdelin2010" />
{{cladogram |align=left |style=font-size:90%;line-height:100%;width:500px;
|cladogram={{clade |label1=Felidae
|1={{clade |label1=[[simple:Felinae]]
|1={{clade
|1={{clade
|1={{clade |label1=''Felis''
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1=[[simple:Domestic cat]] (''F. catus'')
|2=[[simple:European wildcat]] (''F. silvestris'')
}}
|2={{clade
|1=[[simple:African wildcat]] (''F. lybica'')
|2=[[simple:Chinese mountain cat]] (''F. bieti'')
}}}}
|2=[[simple:Sand cat]] (''F. margarita'')
}}
|2='''Black-footed cat'''
}}
|2=[[simple:Jungle cat]] (''F. chaus'')
}}}}
|2=other Felinae lineages
}}}}}}
|2=[[simple:Pantherinae]]
}}}}}}
{{clear}}
==Distribution and habitat==
The black-footed cat is found only in Southern Africa, and it has a smaller range than other small cats in the area.<ref>{{cite book |last1=Nowell |first1=K. |last2=Jackson |first2=P. |date=1996 |title=Wild Cats Status Survey and Conservation Action Plan |publisher=IUCN Cat Specialist Group |pages=8–10 |chapter=Black-footed cat, ''Felis nigripes'' Burchell, 1824}}</ref> It lives in South Africa and southern Botswana, where it was first recorded in the 1960s.<ref name=Smithers1971/> It has also been recorded in Namibia, southern Angola, and southern Zimbabwe, but it is unlikely to live in Lesotho or Eswatini.<ref name=iucn/>
Its habitat includes open, dry [[simple:savanna]]s and semi-dry [[simple:shrubland]] in the Karoo and the southwestern Kalahari. These places have short grass, low bushes, and some taller grasses.<ref name=Smithers1971/> Rainfall in these areas is about {{cvt|100|to|500|mm}} each year.<ref name=skinner/><ref name=Sliwa2013/> In the [[simple:Drakensberg]], it has been found at heights of up to {{cvt|2000|m|ft|-2}} above sea level.<ref name=skinner/>
==Behaviour==
[[File:Black-footed cat, Zoo Wuppertal, July 2009.jpg|thumb|An adult black-footed cat resting]]
[[File:Suspicious Black-Footed Cat.jpg|thumb|A black-footed cat under cover]]
The black-footed cat is [[simple:nocturnal]] and usually lives alone, except when females are raising kittens.<ref name=Smithers1971/><ref name=Sliwa2004/> During the day, it rests in hollow [[simple:termite]] mounds or empty burrows made by animals such as the springhare, [[simple:aardvark]], or Cape porcupine. It often digs to make these burrows larger.<ref name=Olbricht97/> At night, it comes out to hunt. When disturbed, it hides quickly, often in termite mounds. If it cannot escape, it fights aggressively. This behaviour gave it the Afrikaans name {{lang|af|miershooptier}} (“anthill tiger”). A [[simple:San people|San]] legend even says it can kill a [[simple:giraffe]], showing how brave it is believed to be.<ref>{{cite magazine |last=Sliwa |first=A. |date=November 2006 |title=Atomic Kitten: the secrets of Africa's black-footed cat |magazine=BBC Wildlife |volume=24 |issue=12 |pages=36–40}}</ref>
It is not a good climber because of its short, strong body and short tail,<ref name=Armstrong1977>{{cite book |last=Armstrong |first=J. |date=1977 |chapter=The development and hand-rearing of black-footed cats |pages=71–80 |title=The World's Cats: The Proceedings of an International Symposium |volume=3 |editor-last=Eaton |editor-first=R. L. |publisher=Winston Wildlife Safari |location=Oregon}}</ref> although one was once seen resting in a camelthorn tree.<ref>{{cite magazine |last=Sliwa |first=A. |date=2013 |title=Black-footed Lightning |magazine=Africa Geographic |issue=June |pages=27–31}}</ref>
Females usually live in areas of {{cvt|6.23|-|15.53|km2}}, while males range over larger areas of {{cvt|19.44|-|23.61|km2}}. A male’s range often overlaps with those of several females. They mark their territories using scent, such as spraying urine, rubbing, claw marks, and leaving faeces in easy-to-see places.<ref name=Sliwa2004/> Their calls are louder than those of most other small cats, but they also purr and make gurgling sounds when close together. When threatened, they hiss and growl.<ref name=Sliwa2004/> Adults usually travel about {{cvt|8.42|±|2.09|km}} each night while hunting.<ref>{{cite book |last1=Sliwa |first1=A. |last2=Herbst |first2=M. |last3=Mills |first3=M. |date=2010 |chapter=Black-footed cats (''Felis nigripes'') and African wild cats (''Felis silvestris''): A comparison of two small felids from South African arid lands |pages=537–558 |title=The Biology and Conservation of Wild Felids |publisher=Oxford University Press |isbn=978-0-19-959283-8}}</ref>
Because it is shy and moves fast without using roads, the black-footed cat is difficult to study. In South Africa, its population density was estimated at {{cvt|0.17|/sqkm}} near [[simple:Kimberley, Northern Cape|Kimberley]] in 1998–1999, but this dropped to {{cvt|0.08|/sqkm|1}} between 2005 and 2014. In Nuwejaarsfontein, densities from 2009 to 2014 were about {{cvt|0.06|/sqkm}}. In poorer habitats, numbers may be as low as {{cvt|0.03|/sqkm|2}}.<ref name=iucn/>
===Reproduction and life cycle===
In captivity, males become able to reproduce at about nine months old, and females at about seven months.<ref name="Olbricht97"/> A female’s [[simple:oestrus]] lasts around 36 hours, and [[simple:gestation]] lasts 63–68 days.<ref name=Leyhausen>{{cite journal |last1=Leyhausen |first1=P. |last2=Tonkin |first2=B. |name-list-style=amp |date=1966 |title=Breeding the black-footed cat (''Felis nigripes'') in captivity |journal=International Zoo Yearbook |volume=6 |issue=6 |pages=178–182 |doi=10.1111/j.1748-1090.1966.tb01744.x}}</ref> She can give birth to one or two [[simple:Litter|litters]] each year between October and March. Litters usually have one or two kittens, but can have up to four.<ref name=Olbricht97/>
In the wild, females are ready to mate for only five to ten hours, so males must find them quickly. Males may fight each other, and [[simple:Sexual intercourse|copulation]] happens every 20–50 minutes.<ref name=Sliwa2004/>
Kittens weigh {{cvt|60|to|93|g}} at birth. They are born blind but can crawl within a few hours. Their eyes open after 3–10 days, and their teeth appear at 2–3 weeks. They start eating solid food at about one month old and are [[simple:Weaning|weaned]] at two months. Adult teeth appear at 148–158 days.<ref name=Olbricht97/>
In captivity, mothers move their kittens to new hiding places every 6–10 days, which is more often than in other small cats.<ref name=Leyhausen/> In the wild, kittens are born in springhare burrows or termite mounds. From four days old, the mother may leave them alone at night for up to 10 hours. By six weeks, the kittens move quickly and leave the den often. Young cats may be killed by predators such as the [[simple:black-backed jackal]], [[simple:caracal]], and nocturnal [[simple:raptor]]s.<ref>{{cite journal |author=Sliwa, A. |year=1996 |title=Pleasures and Worries of a Black-Footed Cat Field Study in South Africa |journal=Cat Times |volume=23 |pages=1–3}}</ref> They become independent at 3–4 months old but usually stay close to their mother’s area. In captivity, they can live for up to 15 years.<ref name=Sliwa2013/>
===Diseases===
Black-footed cats often suffer from AA amyloidosis, a disease that causes long-term [[simple:inflammation]] and usually leads to [[simple:kidney failure]] and death.<ref name=Olbricht97/><ref>{{cite journal |last1=Terio |first1=K.A. |last2=O'Brien |first2=T. |last3=Lamberski |first3=N. |last4=Famula |first4=T. R. |last5=Munson |first5=L. |date=2008 |title=Amyloidosis in black-footed cats (''Felis nigripes'') |journal=Veterinary Pathology Online |volume=45 |issue=3 |pages=393–400 |doi=10.1354/vp.45-3-393 |pmid=18487501 |s2cid=43387363 |doi-access=free}}</ref> Wild cats are also at risk from diseases spread by domestic dogs and cats.<ref>{{cite book |last1=Lamberski |first1=N. |last2=Sliwa |first2=A. |last3=Wilson |first3=B. |last4=Herrick |first4=J. |last5=Lawrenz |first5=A. |date=2009 |chapter=Conservation of black-footed cats (''Felis nigripes'') and prevalence of infectious diseases in sympatric carnivores in the Northern Cape Province, South Africa |pages=243–245 |title=Proceedings of the International Conference on Diseases of Zoo and Wild Animals 2009 |publisher=Leibniz-Institut für Zoo- und Wildtierforschung |location=Berlin}}</ref>
==Threats==
Threats include accidental killing during predator control, such as poison bait and steel traps, [[simple:habitat destruction]] caused by [[simple:overgrazing]], fewer springhares, attacks by other carnivores, diseases, and poor farming methods. Some cats are killed by herding [[simple:dog]]s. Many protected areas are too small to support healthy populations.<ref name=iucn />
==Conservation==
The black-footed cat is listed on CITES Appendix I and is protected in most of its range, including [[simple:Botswana]] and [[simple:South Africa]], where hunting is not allowed.<ref name=iucn />
===Field research===
The Black-footed Cat Working Group runs a long-term study at Benfontein Nature Reserve and Nuwejaarsfontein Farm near [[simple:Kimberley, Northern Cape]].<ref>{{cite report |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lawrenz |first3=A. |date=2010 |title=Report on surveying and catching black-footed cats (''Felis nigripes'') on Nuwejaarsfontein Farm / Benfontein Nature Reserve, 4–20 July 2010 |publisher=Black-footed Cat Working Group}}</ref> In 2012, the study expanded to Biesiesfontein Farm near Victoria West.<ref>{{cite report |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lamberski |first3=N. |last4=Lawrenz |first4=A. |date=2013 |title=Report on surveying, catching and monitoring black-footed cats (''Felis nigripes'') on Benfontein, Nuwejaarsfontein, and Biesiesfontein in 2012 |publisher=Black-footed Cat Working Group}}</ref> Between 1992 and 2018, 65 cats were fitted with radio collars to study their social behaviour, home ranges, hunting, and diet.<ref>{{cite book |author=Sliwa, A. |year=2018 |pages=7–8 |chapter=25 years of Black-footed Cat ''Felis nigripes'' field research and conservation |title=Proceedings of the First International Small Wild Cat Conservation Summit |publisher=Wild Cat Network}}</ref> Camera traps are also used to study their behaviour and interactions with [[simple:aardwolf]]s.<ref>{{cite journal |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lawrenz |first3=A. |last4=Lamberski |first4=N. |last5=Herrick |first5=J. |last6=Küsters |first6=M. |title=Camera trap use in the study of black-footed cats (''Felis nigripes'') |journal=African Journal of Ecology |date=2018 |volume=56 |issue=4 |pages=895–897 |doi=10.1111/aje.12564 |bibcode=2018AfJEc..56..895S }}</ref>
=== In captivity ===
The Wuppertal Zoo in Germany obtained black-footed cats in 1957 and successfully bred them in 1963. In 1993, the European Endangered Species Programme was set up to manage breeding and reduce inbreeding. The ''International Studbook for the Black-footed Cat'' was maintained at Wuppertal Zoo.<ref>{{cite book |last1=Olbricht |first1=G. |last2=Schürer |first2=U. |date=1994 |title=International Studbook for the Black-footed Cat 1994 |publisher=Zoologischer Garten der Stadt Wuppertal}}</ref> {{as of|2011|July}}, records showed that 726 black-footed cats had been kept in captivity since 1964. At that time, 74 cats were living in 23 institutions in Germany, the United Arab Emirates, the US, the UK, and South Africa.<ref>{{cite book |last=Stadler |first=A. |date=2011 |title=International studbook for the black-footed cat (''Felis nigripes'') |volume=15 |publisher=Zoologischer Garten der Stadt Wuppertal}}</ref>
Several zoos have reported successful breeding, including Cleveland Metroparks Zoo,<ref>{{cite web|title=Press Release: Animal News: Second Litter of Black-footed Cats|publisher=Cleveland Metroparks Zoo|date=2012|url=http://www.clemetzoo.com/whats_new/new_animals.asp|archive-date=20 September 2013|archive-url=https://web.archive.org/web/20130920181420/http://www.clemetzoo.com/whats_new/new_animals.asp}}</ref> Fresno Chaffee Zoo,<ref>{{cite web|last=Condoian|first=L.|date=2011|title=General Meeting of the Board of Directors|publisher=Fresno Chaffee Zoo Corporation|website=FresnoChaffeeZoo.org|url=http://www.fresnochaffeezoo.org/pdf/2011-0609minutes.pdf|archive-date=14 November 2011|archive-url=https://web.archive.org/web/20111114152306/http://www.fresnochaffeezoo.org/pdf/2011-0609minutes.pdf}}</ref> Brookfield Zoo,<ref>{{cite web|author=Katzen, S.|date=2012|title=Black-footed cats born: A first at Brookfield Zoo|website=Chicago Zoological Society|url=http://www.czs.org/CZS/About-CZS/Press-Room/Press-Releases/Black-Footed-Cats-Born-A-First-at-Brookfield-Zoo|archive-date=31 March 2012|archive-url=https://web.archive.org/web/20120331001315/http://www.czs.org/CZS/About-CZS/Press-Room/Press-Releases/Black-Footed-Cats-Born-A-First-at-Brookfield-Zoo}}</ref> and Philadelphia Zoo.<ref>{{cite news|url=http://www.nj.com/indulge/index.ssf/2014/06/philadelphia_zoo_visitors_paws_to_gush_over_black-footed_cat_kittens.html|title=Philadelphia Zoo visitors 'paws' to gush over black-footed cat kittens|last=Rearick|first=K.|work=South Jersey Times|date=2014|access-date=10 June 2014|archive-date=14 July 2014|archive-url=https://web.archive.org/web/20140714112337/http://www.nj.com/indulge/index.ssf/2014/06/philadelphia_zoo_visitors_paws_to_gush_over_black-footed_cat_kittens.html|url-status=live}}</ref>
The Audubon Nature Institute’s Center for Research of Endangered Species in New Orleans has carried out advanced genetic research on cats.<ref>{{cite web|last=Jeffries|first=A.|date=2013|title=Where cats glow green: Weird feline science in New Orleans|website=The Verge|url=https://www.theverge.com/2013/11/6/4841714/where-cats-glow-green-weird-feline-science-acres-in-new-orleans|access-date=30 August 2017|archive-date=21 January 2018|archive-url=https://web.archive.org/web/20180121071408/https://www.theverge.com/2013/11/6/4841714/where-cats-glow-green-weird-feline-science-acres-in-new-orleans|url-status=live}}</ref> In February 2011, a female there gave birth to two male kittens. These were the first black-footed cats born using [[simple:In-vitro fertilisation|{{lang|la|nocat=y|in vitro}} fertilization]] with frozen sperm and embryos. The sperm was collected in 2003, used to fertilize an egg in 2005, and the embryos were frozen. In 2010, the embryos were thawed and placed into a surrogate female, who carried the pregnancy and gave birth to the kittens.<ref>{{cite web|last=Burnette|first=S.|date=2011|title=Rare cats born through amazing science at Audubon Center for Research of Endangered Species|publisher=Audubon Nature Institute|website=AudubonInstitute.org|url=http://www.auduboninstitute.org/media/audubonheadlines/rare-cats-born-through-amazing-science|archive-date=18 February 2014|archive-url=https://web.archive.org/web/20140218041529/http://www.auduboninstitute.org/media/releases/rare-cats-born-through-amazing-science}}</ref>
On 6 February 2012, the same center reported another success. A female kitten named Crystal was born to a domestic cat [[simple:Surrogacy|surrogate]] after interspecies [[simple:embryo transfer]].<ref>{{cite news|last=Waller|first=M.|date=2012|title=Audubon center in Algiers logs another breakthrough in genetic engineering of endangered cats|work=NOLA.com|publisher=New Orleans Net|url=http://www.nola.com/education/index.ssf/2012/03/audubon_center_in_algiers_logs.html|archive-date=20 January 2018|archive-url=https://web.archive.org/web/20180120235949/http://www.nola.com/education/index.ssf/2012/03/audubon_center_in_algiers_logs.html}}</ref>
== References ==
{{reflist}}
==Other websites==
{{Wikispecies|Felis nigripes}}
{{Commons category|Felis nigripes
}}
*{{cite web |url=http://www.black-footed-cat.wild-cat.org/ |title=Black-footed Cat Working Group |work=Wild Cat Network}}
*{{cite web |url=http://www.catsg.org/index.php?id=105 |work=IUCN/SSC Cat Specialist Group |title=''Felis nigripes'' |access-date=2026-01-02 |archive-date=2017-10-31 |archive-url=https://web.archive.org/web/20171031040815/http://www.catsg.org/index.php?id=105 |url-status=dead }}
*{{cite web |url=http://www.animalinfo.org/species/carnivor/felinigr.htm |work=Animal Info |title=''Felis nigripes''}}
*{{cite web |url=https://www.youtube.com/watch?v=nl8o9PsJPAQ&ab_channel=NatureonPBS |work=Nature on PBS |title=''Documentary: Meet the Deadliest Cat on the Planet''|date=25 October 2018 }}
*{{cite web |url=https://www.youtube.com/watch?v=pxFGkfYeUeA |work=Wildlife Wonder |title=''Documentary: Black-footed cats''|date=18 May 2015 }}
{{Carnivora|Fe.}}
{{Taxonbar|from=Q204814}}
[[simple:Category:Felines]]
[[simple:Category:Mammals of Africa]]
miyxu27n99djav7lguc66snp33cyeqg
737132
737131
2026-04-07T23:41:14Z
PieWriter
72123
737132
wikitext
text/x-wiki
{{complex|date=February 2026}}
{{pgood}}
{{Speciesbox
| image = Zoo Wuppertal Schwarzfusskatze.jpg
| status = VU
| status_ref =<ref name ="IUCN">{{cite IUCN|url=https://www.iucnredlist.org/species/8542/50652196|author1=Sliwa, A.|author2= Wilson, B.|author3=Küsters, M.|author4=Tordiffe, A.|year=2016|title=Felis nigripes|publisher=The IUCN Red List of Threatened Species|volume= 2016|page=e.T8542A50652196|doi=10.2305/IUCN.UK.2016-1.RLTS.T8542A50652196.en}}</ref>
| status_system = IUCN3.1
| taxon = Felis nigripes
| authority =
| synonyms =
| range_map = Black-footedCat distribution.jpg
}}
The '''black-footed cat''' ('''''Felis nigripes'''''), also called the '''small-spotted cat''', is the smallest wild [[simple:Felis|cat]] species in Africa.<ref name=Guggisberg1975>{{Cite book |last=Guggisberg |first=C. A. W. |title=Wild Cats of the World |url=https://archive.org/details/wildcatsofworld00gugg |year=1975 |publisher=Taplinger Publishing |location=New York |isbn=978-0-8008-8324-9 |chapter=Black-footed Cat ''Felis nigripes'' (Burchell, 1842) |pages=[https://archive.org/details/wildcatsofworld00gugg/page/40 40]–42}}</ref><ref name="skinner">{{cite book|last=Mills|first=M. G. L.|editor1-last=Skinner|editor1-first=J. D.|editor2-last=Chimimba|editor2-first=C. T.|title=The Mammals of the Southern African Subregion|edition=3rd|date=2005|publisher=Cambridge University Press|location=Cape Town|isbn=978-0-521-84418-5|chapter=Felis nigripes Burchell, 1824 (Black-footed cat)|pages=405–408|chapter-url=https://books.google.com/books?id=iqwEYkTDZf4C&pg=PA405}}</ref> Its body is about {{cvt|35|-|52|cm}} long.<ref name=ms>{{cite journal |last1=Renard |first1=A. |last2=Lavoie |first2=M. |last3=Pitt |first3=J. A. |last4=Larivière |first4=S. |title=''Felis nigripes'' (Carnivora: Felidae) |journal=Mammalian Species |date=2015 |volume=47 |issue=925 |pages=78–83 |doi=10.1093/mspecies/sev008 |doi-access=free}}</ref> Only the bottoms of its feet are black or dark brown.<ref name="Burchell1824">{{cite book|last=Burchell|first=W. J.|author-link=William John Burchell|title=Travels in the Interior of Southern Africa|volume=II|date=1824|publisher=Longman, Hurst, Rees, Orme, Brown, and Green|location=London|chapter=Felis nigripes|page=592|chapter-url=https://books.google.com/books?id=AGpdAAAAcAAJ&pg=PA592|access-date=29 April 2020|archive-date=19 July 2023|archive-url=https://web.archive.org/web/20230719111228/https://books.google.com/books?id=AGpdAAAAcAAJ&pg=PA592|url-status=live}}</ref> Its yellow-brown fur has small dark spots and stripes that help it stay hidden at night.<ref name="Sliwa2013">{{cite book|last=Sliwa|first=A.|date=2013|chapter=Felis nigripes Black-footed cat|pages=203–206|title=Mammals of Africa|volume=V. Carnivores, Pangolins, Equids and Rhinoceroses|publisher=Bloomsbury|location=London; New Delhi; New York; Sydney|isbn=978-1-4081-8994-8|editor1-last=Kingdon|editor1-first=J.|editor2-last=Happold|editor2-first=D.|editor3-last=Hoffmann|editor3-first=M.|editor4-last=Butynski|editor4-first=T.|editor5-last=Happold|editor5-first=M.|editor6-last=Kalina|editor6-first=J.|chapter-url=https://books.google.com/books?id=B_07noCPc4kC&pg=RA4-PA205}}</ref> It has black lines running from its eyes to its cheeks, and its tail has dark bands with a black tip.<ref name=skinner/>
Scientists discovered the first black-footed cat in the northern Karoo of South Africa and described it in 1824.<ref name=Burchell1824/> It lives only in the dry steppes and grasslands of [[simple:Southern Africa]].<ref name="iucn">{{cite journal|last1=Sliwa|first1=A.|last2=Wilson|first2=B.|last3=Lawrenz|first3=A.|name-list-style=amp|date=2016|title=Felis nigripes|journal=The IUCN Red List of Threatened Species|volume=2016|issue=e.T8540A50657483|doi=10.2305/IUCN.UK.2016-1.RLTS.T8540A50657483.en|doi-broken-date=6 February 2026 |url=https://www.iucnredlist.org/species/8540/50657483|access-date=30 April 2020}}</ref> It has been seen in southern Botswana,<ref name=Smithers1971>{{cite book |last=Smithers |first=R. N. H. |date=1971 |title=The Mammals of Botswana |location=Pretoria |publisher=University of Pretoria |chapter=''Felis nigripes'' Blackfooted Cat |pages=128–130}}</ref> and is rarely found in Namibia, southern Angola, and southern Zimbabwe.<ref name=iucn/> Because it lives in only a few places, it has been listed as [[simple:vulnerable species|Vulnerable]] on the [[simple:IUCN Red List]] since 2002.<ref name=iucn/> Its population is believed to be decreasing due to the [[simple:poaching]] of its prey for [[simple:bushmeat]], people killing it, [[simple:road kill]]s, and attacks by [[simple:herding dog]]s.<ref name=iucn/>
Since 1993, scientists have studied the black-footed cat using radio [[simple:telemetry]].<ref name=Olbricht97>{{cite journal |last1=Olbricht |first1=G. |last2=Sliwa |first2=A. |date=1997 |title=''In situ'' and ''ex situ'' observations and management of black-footed cats ''Felis nigripes'' |journal=International Zoo Yearbook |volume=35 |issue=35 |pages=81–89 |doi=10.1111/j.1748-1090.1997.tb01194.x}}</ref> This research showed how it lives in the wild: it rests in burrows during the day and hunts at night.<ref name=Sliwa2004>{{cite journal |last=Sliwa |first=A. |date=2004 |title=Home range size and social organization of black-footed cats (''Felis nigripes'') |journal=Mammalian Biology |volume=69 |issue=2 |pages=96–107 |doi=10.1078/1616-5047-00124}}</ref> It usually travels {{cvt|5|to|16|km|0}} each night to search for small rodents and birds.<ref name=Sliwa2004/> It eats around 40 types of [[simple:vertebrate]]s and may kill up to 14 small animals in one night.<ref name=Sliwa94>{{cite journal |last=Sliwa |first=A. |date=1994 |title=Diet and feeding behaviour of the Black-footed Cat (''Felis nigripes'' Burchell, 1824) in the Kimberley Region, South Africa |journal=Der Zoologische Garten N.F. |volume=64 |issue=2 |pages=83–96}}</ref> It can jump up to {{cvt|1.4|m|ft|0}} high to catch birds in the air,<ref name=Sliwa94/> and it sometimes attacks prey that is heavier than itself.<ref name=Sliwa94/> Females usually give birth to two kittens between October and March.<ref name=Sliwa2004/> The kittens stop drinking milk at about two months old and live on their own by four months of age.<ref name=Olbricht97/>
==Taxonomy==
The [[simple:binomial nomenclature|scientific name]] ''Felis nigripes'' was given in 1824 by British explorer William John Burchell. He named it after seeing skins of small spotted cats near Litákun (now Dithakong) in South Africa.<ref name=Burchell1824/>
In 1931, South African mammalogist Guy C. Shortridge suggested a [[simple:subspecies]] called ''Felis nigripes thomasi''. He based this idea on darker skins found in Griqualand West.<ref>{{cite journal |last1=Shortridge |first1=G. C. |author-link=Guy C. Shortridge |date=1931 |title=''Felis nigripes thomasi'' subsp. nov. |journal=Records of the Albany Museum |volume=4 |issue=1 |pages=119–120}}</ref>
Later, British zoologist Reginald Innes Pocock confirmed that the black-footed cat belongs to the genus ''[[simple:Felis]]''.<ref name="Pocock1951">{{cite book|last=Pocock|first=R. I.|date=1951|title=Catalogue of the genus Felis|publisher=British Museum (Natural History)|location=London|chapter=Felis nigripes Burchell|pages=145–150|chapter-url=https://archive.org/stream/catalogueofgenus00brit#page/145/mode/1up}}</ref>
The idea that the black-footed cat has subspecies was later questioned because there are no clear natural barriers, like a mountain, river, or desert, between its populations.<ref name=Olbricht97/> In 2017, the [[simple:IUCN]] Cat Specialist Group reviewed cat classification and said the black-footed cat is most likely a [[simple:monotype|monotypic]] species, meaning it has no subspecies.<ref name="CatSG2017">{{cite journal|last1=Kitchener|first1=A. C.|last2=Breitenmoser-Würsten|first2=C.|last3=Eizirik|first3=E.|last4=Gentry|first4=A.|last5=Werdelin|first5=L.|last6=Wilting|first6=A.|last7=Yamaguchi|first7=N.|last8=Abramov|first8=A. V.|last9=Christiansen|first9=P.|last10=Driscoll|first10=C.|last11=Duckworth|first11=J. W.|last12=Johnson|first12=W.|last13=Luo|first13=S.-J.|last14=Meijaard|first14=E.|last15=O'Donoghue|first15=P.|last16=Sanderson|first16=J.|last17=Seymour|first17=K.|last18=Bruford|first18=M.|last19=Groves|first19=C.|last20=Hoffmann|first20=M.|last21=Nowell|first21=K.|last22=Timmons|first22=Z.|last23=Tobe|first23=S.|name-list-style=amp|date=2017|title=A revised taxonomy of the Felidae: The final report of the Cat Classification Task Force of the IUCN Cat Specialist Group|journal=Cat News|issue=Special Issue 11|page=13|url=https://repository.si.edu/bitstream/handle/10088/32616/A_revised_Felidae_Taxonomy_CatNews.pdf?sequence=1&isAllowed=y|access-date=16 March 2019|archive-date=30 July 2018|archive-url=https://web.archive.org/web/20180730142355/https://repository.si.edu/bitstream/handle/10088/32616/A_revised_Felidae_Taxonomy_CatNews.pdf?sequence=1&isAllowed=y|url-status=live}}</ref>
=== Evolution ===
Studies of [[simple:DNA]] show that cats ([[simple:Felidae]]) first spread across Asia during the [[simple:Miocene]], about {{Ma|14.45|8.38|million years ago|round=2}}. <ref name="Johnson2006">{{cite journal |last1=Johnson |first1=W. E. |last2=Eizirik |first2=E. |last3=Pecon-Slattery |first3=J. |last4=Murphy |first4=W. J. |last5=Antunes |first5=A. |last6=Teeling |first6=E. |last7=O'Brien |first7=S. J. |name-list-style=amp |title=The late miocene radiation of modern Felidae: A genetic assessment |journal=[[Science (journal)|Science]] |volume=311 |issue=5757 |pages=73–77 |date=2006 |pmid=16400146 |doi=10.1126/science.1122277 |bibcode=2006Sci...311...73J |s2cid=41672825 |url=https://zenodo.org/record/1230866 |access-date=29 August 2020 |archive-date=4 October 2020 |archive-url=https://web.archive.org/web/20201004075725/https://zenodo.org/record/1230866 |url-status=live }}</ref><ref name="Werdelin2010">{{cite book |last1=Werdelin |first1=L. |last2=Yamaguchi |first2=N. |last3=Johnson |first3=W. E. |last4=O'Brien |first4=S. J. |name-list-style=amp |chapter=Phylogeny and evolution of cats (Felidae) |date=2010 |pages=59–82 |publisher=Oxford University Press |location=Oxford, UK |isbn=978-0-19-923445-5 |chapter-url=https://www.researchgate.net/publication/266755142 |editor1-last=Macdonald |editor1-first=D. W. |editor2-last=Loveridge |editor2-first=A. J. |title=Biology and Conservation of Wild Felids |access-date=3 May 2020 |archive-date=25 September 2018 |archive-url=https://web.archive.org/web/20180925141956/https://www.researchgate.net/publication/266755142 |url-status=live }}</ref> Studies of [[simple:mitochondrial DNA]] suggest this may have happened about {{mya|16.76|6.46}}.<ref name="Li_al2016" />
The black-footed cat belongs to the ''Felis'' group. Based on DNA found in the cell’s [[simple:Cell nucleus|nucleus]], it split from the common ancestor of all ''Felis'' species about {{mya|4.44|2.16}}.<ref name="Johnson2006" /><ref name="Werdelin2010" /> Studies of mitochondrial DNA suggest it split earlier, about {{Ma|6.52|1.03|million years ago|round=2}}.<ref name="Li_al2016" /> Both studies agree that the [[simple:jungle cat]] (''F. chaus'') split first, and the black-footed cat split after that.<ref name="Johnson2006" /><ref name="Li_al2016" />
No fossils of the black-footed cat have been found.<ref name="Werdelin2010" /> It probably moved into Africa during the [[simple:Pleistocene]].<ref name="Johnson2006" /> This may have been possible because sea levels were lower between Asia and Africa.<ref name="Li_al2016" />
The following [[simple:cladogram]] shows its relationships based on nuclear DNA:
<ref name="Johnson2006" /><ref name="Werdelin2010" />
{{cladogram |align=left |style=font-size:90%;line-height:100%;width:500px;
|cladogram={{clade |label1=Felidae
|1={{clade |label1=[[simple:Felinae]]
|1={{clade
|1={{clade
|1={{clade |label1=''Felis''
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1=[[simple:Domestic cat]] (''F. catus'')
|2=[[simple:European wildcat]] (''F. silvestris'')
}}
|2={{clade
|1=[[simple:African wildcat]] (''F. lybica'')
|2=[[simple:Chinese mountain cat]] (''F. bieti'')
}}}}
|2=[[simple:Sand cat]] (''F. margarita'')
}}
|2='''Black-footed cat'''
}}
|2=[[simple:Jungle cat]] (''F. chaus'')
}}}}
|2=other Felinae lineages
}}}}}}
|2=[[simple:Pantherinae]]
}}}}}}
{{clear}}
==Distribution and habitat==
The black-footed cat is found only in Southern Africa, and it has a smaller range than other small cats in the area.<ref>{{cite book |last1=Nowell |first1=K. |last2=Jackson |first2=P. |date=1996 |title=Wild Cats Status Survey and Conservation Action Plan |publisher=IUCN Cat Specialist Group |pages=8–10 |chapter=Black-footed cat, ''Felis nigripes'' Burchell, 1824}}</ref> It lives in South Africa and southern Botswana, where it was first recorded in the 1960s.<ref name=Smithers1971/> It has also been recorded in Namibia, southern Angola, and southern Zimbabwe, but it is unlikely to live in Lesotho or Eswatini.<ref name=iucn/>
Its habitat includes open, dry [[simple:savanna]]s and semi-dry [[simple:shrubland]] in the Karoo and the southwestern Kalahari. These places have short grass, low bushes, and some taller grasses.<ref name=Smithers1971/> Rainfall in these areas is about {{cvt|100|to|500|mm}} each year.<ref name=skinner/><ref name=Sliwa2013/> In the [[simple:Drakensberg]], it has been found at heights of up to {{cvt|2000|m|ft|-2}} above sea level.<ref name=skinner/>
==Behaviour==
[[File:Black-footed cat, Zoo Wuppertal, July 2009.jpg|thumb|An adult black-footed cat resting]]
[[File:Suspicious Black-Footed Cat.jpg|thumb|A black-footed cat under cover]]
The black-footed cat is [[simple:nocturnal]] and usually lives alone, except when females are raising kittens.<ref name=Smithers1971/><ref name=Sliwa2004/> During the day, it rests in hollow [[simple:termite]] mounds or empty burrows made by animals such as the springhare, [[simple:aardvark]], or Cape porcupine. It often digs to make these burrows larger.<ref name=Olbricht97/> At night, it comes out to hunt. When disturbed, it hides quickly, often in termite mounds. If it cannot escape, it fights aggressively. This behaviour gave it the Afrikaans name {{lang|af|miershooptier}} (“anthill tiger”). A [[simple:San people|San]] legend even says it can kill a [[simple:giraffe]], showing how brave it is believed to be.<ref>{{cite magazine |last=Sliwa |first=A. |date=November 2006 |title=Atomic Kitten: the secrets of Africa's black-footed cat |magazine=BBC Wildlife |volume=24 |issue=12 |pages=36–40}}</ref>
It is not a good climber because of its short, strong body and short tail,<ref name=Armstrong1977>{{cite book |last=Armstrong |first=J. |date=1977 |chapter=The development and hand-rearing of black-footed cats |pages=71–80 |title=The World's Cats: The Proceedings of an International Symposium |volume=3 |editor-last=Eaton |editor-first=R. L. |publisher=Winston Wildlife Safari |location=Oregon}}</ref> although one was once seen resting in a camelthorn tree.<ref>{{cite magazine |last=Sliwa |first=A. |date=2013 |title=Black-footed Lightning |magazine=Africa Geographic |issue=June |pages=27–31}}</ref>
Females usually live in areas of {{cvt|6.23|-|15.53|km2}}, while males range over larger areas of {{cvt|19.44|-|23.61|km2}}. A male’s range often overlaps with those of several females. They mark their territories using scent, such as spraying urine, rubbing, claw marks, and leaving faeces in easy-to-see places.<ref name=Sliwa2004/> Their calls are louder than those of most other small cats, but they also purr and make gurgling sounds when close together. When threatened, they hiss and growl.<ref name=Sliwa2004/> Adults usually travel about {{cvt|8.42|±|2.09|km}} each night while hunting.<ref>{{cite book |last1=Sliwa |first1=A. |last2=Herbst |first2=M. |last3=Mills |first3=M. |date=2010 |chapter=Black-footed cats (''Felis nigripes'') and African wild cats (''Felis silvestris''): A comparison of two small felids from South African arid lands |pages=537–558 |title=The Biology and Conservation of Wild Felids |publisher=Oxford University Press |isbn=978-0-19-959283-8}}</ref>
Because it is shy and moves fast without using roads, the black-footed cat is difficult to study. In South Africa, its population density was estimated at {{cvt|0.17|/sqkm}} near [[simple:Kimberley, Northern Cape|Kimberley]] in 1998–1999, but this dropped to {{cvt|0.08|/sqkm|1}} between 2005 and 2014. In Nuwejaarsfontein, densities from 2009 to 2014 were about {{cvt|0.06|/sqkm}}. In poorer habitats, numbers may be as low as {{cvt|0.03|/sqkm|2}}.<ref name=iucn/>
===Reproduction and life cycle===
In captivity, males become able to reproduce at about nine months old, and females at about seven months.<ref name="Olbricht97"/> A female’s [[simple:oestrus]] lasts around 36 hours, and [[simple:gestation]] lasts 63–68 days.<ref name=Leyhausen>{{cite journal |last1=Leyhausen |first1=P. |last2=Tonkin |first2=B. |name-list-style=amp |date=1966 |title=Breeding the black-footed cat (''Felis nigripes'') in captivity |journal=International Zoo Yearbook |volume=6 |issue=6 |pages=178–182 |doi=10.1111/j.1748-1090.1966.tb01744.x}}</ref> She can give birth to one or two [[simple:Litter|litters]] each year between October and March. Litters usually have one or two kittens, but can have up to four.<ref name=Olbricht97/>
In the wild, females are ready to mate for only five to ten hours, so males must find them quickly. Males may fight each other, and [[simple:Sexual intercourse|copulation]] happens every 20–50 minutes.<ref name=Sliwa2004/>
Kittens weigh {{cvt|60|to|93|g}} at birth. They are born blind but can crawl within a few hours. Their eyes open after 3–10 days, and their teeth appear at 2–3 weeks. They start eating solid food at about one month old and are [[simple:Weaning|weaned]] at two months. Adult teeth appear at 148–158 days.<ref name=Olbricht97/>
In captivity, mothers move their kittens to new hiding places every 6–10 days, which is more often than in other small cats.<ref name=Leyhausen/> In the wild, kittens are born in springhare burrows or termite mounds. From four days old, the mother may leave them alone at night for up to 10 hours. By six weeks, the kittens move quickly and leave the den often. Young cats may be killed by predators such as the [[simple:black-backed jackal]], [[simple:caracal]], and nocturnal [[simple:raptor]]s.<ref>{{cite journal |author=Sliwa, A. |year=1996 |title=Pleasures and Worries of a Black-Footed Cat Field Study in South Africa |journal=Cat Times |volume=23 |pages=1–3}}</ref> They become independent at 3–4 months old but usually stay close to their mother’s area. In captivity, they can live for up to 15 years.<ref name=Sliwa2013/>
===Diseases===
Black-footed cats often suffer from AA amyloidosis, a disease that causes long-term [[simple:inflammation]] and usually leads to [[simple:kidney failure]] and death.<ref name=Olbricht97/><ref>{{cite journal |last1=Terio |first1=K.A. |last2=O'Brien |first2=T. |last3=Lamberski |first3=N. |last4=Famula |first4=T. R. |last5=Munson |first5=L. |date=2008 |title=Amyloidosis in black-footed cats (''Felis nigripes'') |journal=Veterinary Pathology Online |volume=45 |issue=3 |pages=393–400 |doi=10.1354/vp.45-3-393 |pmid=18487501 |s2cid=43387363 |doi-access=free}}</ref> Wild cats are also at risk from diseases spread by domestic dogs and cats.<ref>{{cite book |last1=Lamberski |first1=N. |last2=Sliwa |first2=A. |last3=Wilson |first3=B. |last4=Herrick |first4=J. |last5=Lawrenz |first5=A. |date=2009 |chapter=Conservation of black-footed cats (''Felis nigripes'') and prevalence of infectious diseases in sympatric carnivores in the Northern Cape Province, South Africa |pages=243–245 |title=Proceedings of the International Conference on Diseases of Zoo and Wild Animals 2009 |publisher=Leibniz-Institut für Zoo- und Wildtierforschung |location=Berlin}}</ref>
==Threats==
Threats include accidental killing during predator control, such as poison bait and steel traps, [[simple:habitat destruction]] caused by [[simple:overgrazing]], fewer springhares, attacks by other carnivores, diseases, and poor farming methods. Some cats are killed by herding [[simple:dog]]s. Many protected areas are too small to support healthy populations.<ref name=iucn />
==Conservation==
The black-footed cat is listed on CITES Appendix I and is protected in most of its range, including [[simple:Botswana]] and [[simple:South Africa]], where hunting is not allowed.<ref name=iucn />
===Field research===
The Black-footed Cat Working Group runs a long-term study at Benfontein Nature Reserve and Nuwejaarsfontein Farm near [[simple:Kimberley, Northern Cape]].<ref>{{cite report |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lawrenz |first3=A. |date=2010 |title=Report on surveying and catching black-footed cats (''Felis nigripes'') on Nuwejaarsfontein Farm / Benfontein Nature Reserve, 4–20 July 2010 |publisher=Black-footed Cat Working Group}}</ref> In 2012, the study expanded to Biesiesfontein Farm near Victoria West.<ref>{{cite report |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lamberski |first3=N. |last4=Lawrenz |first4=A. |date=2013 |title=Report on surveying, catching and monitoring black-footed cats (''Felis nigripes'') on Benfontein, Nuwejaarsfontein, and Biesiesfontein in 2012 |publisher=Black-footed Cat Working Group}}</ref> Between 1992 and 2018, 65 cats were fitted with radio collars to study their social behaviour, home ranges, hunting, and diet.<ref>{{cite book |author=Sliwa, A. |year=2018 |pages=7–8 |chapter=25 years of Black-footed Cat ''Felis nigripes'' field research and conservation |title=Proceedings of the First International Small Wild Cat Conservation Summit |publisher=Wild Cat Network}}</ref> Camera traps are also used to study their behaviour and interactions with [[simple:aardwolf]]s.<ref>{{cite journal |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lawrenz |first3=A. |last4=Lamberski |first4=N. |last5=Herrick |first5=J. |last6=Küsters |first6=M. |title=Camera trap use in the study of black-footed cats (''Felis nigripes'') |journal=African Journal of Ecology |date=2018 |volume=56 |issue=4 |pages=895–897 |doi=10.1111/aje.12564 |bibcode=2018AfJEc..56..895S }}</ref>
=== In captivity ===
The Wuppertal Zoo in Germany obtained black-footed cats in 1957 and successfully bred them in 1963. In 1993, the European Endangered Species Programme was set up to manage breeding and reduce inbreeding. The ''International Studbook for the Black-footed Cat'' was maintained at Wuppertal Zoo.<ref>{{cite book |last1=Olbricht |first1=G. |last2=Schürer |first2=U. |date=1994 |title=International Studbook for the Black-footed Cat 1994 |publisher=Zoologischer Garten der Stadt Wuppertal}}</ref> {{as of|2011|July}}, records showed that 726 black-footed cats had been kept in captivity since 1964. At that time, 74 cats were living in 23 institutions in Germany, the United Arab Emirates, the US, the UK, and South Africa.<ref>{{cite book |last=Stadler |first=A. |date=2011 |title=International studbook for the black-footed cat (''Felis nigripes'') |volume=15 |publisher=Zoologischer Garten der Stadt Wuppertal}}</ref>
Several zoos have reported successful breeding, including Cleveland Metroparks Zoo,<ref>{{cite web|title=Press Release: Animal News: Second Litter of Black-footed Cats|publisher=Cleveland Metroparks Zoo|date=2012|url=http://www.clemetzoo.com/whats_new/new_animals.asp|archive-date=20 September 2013|archive-url=https://web.archive.org/web/20130920181420/http://www.clemetzoo.com/whats_new/new_animals.asp}}</ref> Fresno Chaffee Zoo,<ref>{{cite web|last=Condoian|first=L.|date=2011|title=General Meeting of the Board of Directors|publisher=Fresno Chaffee Zoo Corporation|website=FresnoChaffeeZoo.org|url=http://www.fresnochaffeezoo.org/pdf/2011-0609minutes.pdf|archive-date=14 November 2011|archive-url=https://web.archive.org/web/20111114152306/http://www.fresnochaffeezoo.org/pdf/2011-0609minutes.pdf}}</ref> Brookfield Zoo,<ref>{{cite web|author=Katzen, S.|date=2012|title=Black-footed cats born: A first at Brookfield Zoo|website=Chicago Zoological Society|url=http://www.czs.org/CZS/About-CZS/Press-Room/Press-Releases/Black-Footed-Cats-Born-A-First-at-Brookfield-Zoo|archive-date=31 March 2012|archive-url=https://web.archive.org/web/20120331001315/http://www.czs.org/CZS/About-CZS/Press-Room/Press-Releases/Black-Footed-Cats-Born-A-First-at-Brookfield-Zoo}}</ref> and Philadelphia Zoo.<ref>{{cite news|url=http://www.nj.com/indulge/index.ssf/2014/06/philadelphia_zoo_visitors_paws_to_gush_over_black-footed_cat_kittens.html|title=Philadelphia Zoo visitors 'paws' to gush over black-footed cat kittens|last=Rearick|first=K.|work=South Jersey Times|date=2014|access-date=10 June 2014|archive-date=14 July 2014|archive-url=https://web.archive.org/web/20140714112337/http://www.nj.com/indulge/index.ssf/2014/06/philadelphia_zoo_visitors_paws_to_gush_over_black-footed_cat_kittens.html|url-status=live}}</ref>
The Audubon Nature Institute’s Center for Research of Endangered Species in New Orleans has carried out advanced genetic research on cats.<ref>{{cite web|last=Jeffries|first=A.|date=2013|title=Where cats glow green: Weird feline science in New Orleans|website=The Verge|url=https://www.theverge.com/2013/11/6/4841714/where-cats-glow-green-weird-feline-science-acres-in-new-orleans|access-date=30 August 2017|archive-date=21 January 2018|archive-url=https://web.archive.org/web/20180121071408/https://www.theverge.com/2013/11/6/4841714/where-cats-glow-green-weird-feline-science-acres-in-new-orleans|url-status=live}}</ref> In February 2011, a female there gave birth to two male kittens. These were the first black-footed cats born using [[simple:In-vitro fertilisation|{{lang|la|nocat=y|in vitro}} fertilization]] with frozen sperm and embryos. The sperm was collected in 2003, used to fertilize an egg in 2005, and the embryos were frozen. In 2010, the embryos were thawed and placed into a surrogate female, who carried the pregnancy and gave birth to the kittens.<ref>{{cite web|last=Burnette|first=S.|date=2011|title=Rare cats born through amazing science at Audubon Center for Research of Endangered Species|publisher=Audubon Nature Institute|website=AudubonInstitute.org|url=http://www.auduboninstitute.org/media/audubonheadlines/rare-cats-born-through-amazing-science|archive-date=18 February 2014|archive-url=https://web.archive.org/web/20140218041529/http://www.auduboninstitute.org/media/releases/rare-cats-born-through-amazing-science}}</ref>
On 6 February 2012, the same center reported another success. A female kitten named Crystal was born to a domestic cat [[simple:Surrogacy|surrogate]] after interspecies [[simple:embryo transfer]].<ref>{{cite news|last=Waller|first=M.|date=2012|title=Audubon center in Algiers logs another breakthrough in genetic engineering of endangered cats|work=NOLA.com|publisher=New Orleans Net|url=http://www.nola.com/education/index.ssf/2012/03/audubon_center_in_algiers_logs.html|archive-date=20 January 2018|archive-url=https://web.archive.org/web/20180120235949/http://www.nola.com/education/index.ssf/2012/03/audubon_center_in_algiers_logs.html}}</ref>
== References ==
{{reflist}}
==Other websites==
{{Wikispecies|Felis nigripes}}
{{Commons category|Felis nigripes
}}
*{{cite web |url=http://www.black-footed-cat.wild-cat.org/ |title=Black-footed Cat Working Group |work=Wild Cat Network}}
*{{cite web |url=http://www.catsg.org/index.php?id=105 |work=IUCN/SSC Cat Specialist Group |title=''Felis nigripes'' |access-date=2026-01-02 |archive-date=2017-10-31 |archive-url=https://web.archive.org/web/20171031040815/http://www.catsg.org/index.php?id=105 |url-status=dead }}
*{{cite web |url=http://www.animalinfo.org/species/carnivor/felinigr.htm |work=Animal Info |title=''Felis nigripes''}}
*{{cite web |url=https://www.youtube.com/watch?v=nl8o9PsJPAQ&ab_channel=NatureonPBS |work=Nature on PBS |title=''Documentary: Meet the Deadliest Cat on the Planet''|date=25 October 2018 }}
*{{cite web |url=https://www.youtube.com/watch?v=pxFGkfYeUeA |work=Wildlife Wonder |title=''Documentary: Black-footed cats''|date=18 May 2015 }}
{{Carnivora|Fe.}}
{{Taxonbar|from=Q204814}}
[[simple:Category:Felines]]
[[simple:Category:Mammals of Africa]]
dtz7rjrd4rqsdxgvbklt0fqgfymttf3
737134
737132
2026-04-08T00:13:31Z
PieWriter
72123
737134
wikitext
text/x-wiki
{{complex|date=February 2026}}
{{pgood}}
{{Speciesbox
| image = Zoo Wuppertal Schwarzfusskatze.jpg
| status = VU
| status_ref =<ref name ="IUCN">{{cite IUCN|url=https://www.iucnredlist.org/species/8542/50652196|author1=Sliwa, A.|author2= Wilson, B.|author3=Küsters, M.|author4=Tordiffe, A.|year=2016|title=Felis nigripes|publisher=The IUCN Red List of Threatened Species|volume= 2016|page=e.T8542A50652196|doi=10.2305/IUCN.UK.2016-1.RLTS.T8542A50652196.en}}</ref>
| status_system = IUCN3.1
| taxon = Felis nigripes
| authority =
| synonyms =
| range_map = Black-footedCat distribution.jpg
}}
The '''black-footed cat''' ('''''Felis nigripes'''''), also called the '''small-spotted cat''', is the smallest wild [[simple:Felis|cat]] species in Africa.<ref name=Guggisberg1975>{{Cite book |last=Guggisberg |first=C. A. W. |title=Wild Cats of the World |url=https://archive.org/details/wildcatsofworld00gugg |year=1975 |publisher=Taplinger Publishing |location=New York |isbn=978-0-8008-8324-9 |chapter=Black-footed Cat ''Felis nigripes'' (Burchell, 1842) |pages=[https://archive.org/details/wildcatsofworld00gugg/page/40 40]–42}}</ref><ref name="skinner">{{cite book|last=Mills|first=M. G. L.|editor1-last=Skinner|editor1-first=J. D.|editor2-last=Chimimba|editor2-first=C. T.|title=The Mammals of the Southern African Subregion|edition=3rd|date=2005|publisher=Cambridge University Press|location=Cape Town|isbn=978-0-521-84418-5|chapter=Felis nigripes Burchell, 1824 (Black-footed cat)|pages=405–408|chapter-url=https://books.google.com/books?id=iqwEYkTDZf4C&pg=PA405}}</ref> Its body is about {{cvt|35|-|52|cm}} long.<ref name=ms>{{cite journal |last1=Renard |first1=A. |last2=Lavoie |first2=M. |last3=Pitt |first3=J. A. |last4=Larivière |first4=S. |title=''Felis nigripes'' (Carnivora: Felidae) |journal=Mammalian Species |date=2015 |volume=47 |issue=925 |pages=78–83 |doi=10.1093/mspecies/sev008 |doi-access=free}}</ref> Only the bottoms of its feet are black or dark brown.<ref name="Burchell1824">{{cite book|last=Burchell|first=W. J.|author-link=William John Burchell|title=Travels in the Interior of Southern Africa|volume=II|date=1824|publisher=Longman, Hurst, Rees, Orme, Brown, and Green|location=London|chapter=Felis nigripes|page=592|chapter-url=https://books.google.com/books?id=AGpdAAAAcAAJ&pg=PA592|access-date=29 April 2020|archive-date=19 July 2023|archive-url=https://web.archive.org/web/20230719111228/https://books.google.com/books?id=AGpdAAAAcAAJ&pg=PA592|url-status=live}}</ref> Its yellow-brown fur has small dark spots and stripes that help it stay hidden at night.<ref name="Sliwa2013">{{cite book|last=Sliwa|first=A.|date=2013|chapter=Felis nigripes Black-footed cat|pages=203–206|title=Mammals of Africa|volume=V. Carnivores, Pangolins, Equids and Rhinoceroses|publisher=Bloomsbury|location=London; New Delhi; New York; Sydney|isbn=978-1-4081-8994-8|editor1-last=Kingdon|editor1-first=J.|editor2-last=Happold|editor2-first=D.|editor3-last=Hoffmann|editor3-first=M.|editor4-last=Butynski|editor4-first=T.|editor5-last=Happold|editor5-first=M.|editor6-last=Kalina|editor6-first=J.|chapter-url=https://books.google.com/books?id=B_07noCPc4kC&pg=RA4-PA205}}</ref> It has black lines running from its eyes to its cheeks, and its tail has dark bands with a black tip.<ref name=skinner/>
Scientists discovered the first black-footed cat in the northern Karoo of South Africa and described it in 1824.<ref name=Burchell1824/> It lives only in the dry steppes and grasslands of [[simple:Southern Africa]].<ref name="iucn">{{cite journal|last1=Sliwa|first1=A.|last2=Wilson|first2=B.|last3=Lawrenz|first3=A.|name-list-style=amp|date=2016|title=Felis nigripes|journal=The IUCN Red List of Threatened Species|volume=2016|issue=e.T8540A50657483|doi=10.2305/IUCN.UK.2016-1.RLTS.T8540A50657483.en|doi-broken-date=6 February 2026 |url=https://www.iucnredlist.org/species/8540/50657483|access-date=30 April 2020}}</ref> It has been seen in southern Botswana,<ref name=Smithers1971>{{cite book |last=Smithers |first=R. N. H. |date=1971 |title=The Mammals of Botswana |location=Pretoria |publisher=University of Pretoria |chapter=''Felis nigripes'' Blackfooted Cat |pages=128–130}}</ref> and is rarely found in Namibia, southern Angola, and southern Zimbabwe.<ref name=iucn/> Because it lives in only a few places, it has been listed as [[simple:vulnerable species|Vulnerable]] on the [[simple:IUCN Red List]] since 2002.<ref name=iucn/> Its population is believed to be decreasing due to the [[simple:poaching]] of its prey for [[simple:bushmeat]], people killing it, [[simple:road kill]]s, and attacks by [[simple:herding dog]]s.<ref name=iucn/>
Since 1993, scientists have studied the black-footed cat using radio [[simple:telemetry]].<ref name=Olbricht97>{{cite journal |last1=Olbricht |first1=G. |last2=Sliwa |first2=A. |date=1997 |title=''In situ'' and ''ex situ'' observations and management of black-footed cats ''Felis nigripes'' |journal=International Zoo Yearbook |volume=35 |issue=35 |pages=81–89 |doi=10.1111/j.1748-1090.1997.tb01194.x}}</ref> This research showed how it lives in the wild: it rests in burrows during the day and hunts at night.<ref name=Sliwa2004>{{cite journal |last=Sliwa |first=A. |date=2004 |title=Home range size and social organization of black-footed cats (''Felis nigripes'') |journal=Mammalian Biology |volume=69 |issue=2 |pages=96–107 |doi=10.1078/1616-5047-00124}}</ref> It usually travels {{cvt|5|to|16|km|0}} each night to search for small rodents and birds.<ref name=Sliwa2004/> It eats around 40 types of [[simple:vertebrate]]s and may kill up to 14 small animals in one night.<ref name=Sliwa94>{{cite journal |last=Sliwa |first=A. |date=1994 |title=Diet and feeding behaviour of the Black-footed Cat (''Felis nigripes'' Burchell, 1824) in the Kimberley Region, South Africa |journal=Der Zoologische Garten N.F. |volume=64 |issue=2 |pages=83–96}}</ref> It can jump up to {{cvt|1.4|m|ft|0}} high to catch birds in the air,<ref name=Sliwa94/> and it sometimes attacks prey that is heavier than itself.<ref name=Sliwa94/> Females usually give birth to two kittens between October and March.<ref name=Sliwa2004/> The kittens stop drinking milk at about two months old and live on their own by four months of age.<ref name=Olbricht97/>
==Taxonomy==
The [[simple:binomial nomenclature|scientific name]] ''Felis nigripes'' was given in 1824 by British explorer William John Burchell. He named it after seeing skins of small spotted cats near Litákun (now Dithakong) in South Africa.<ref name=Burchell1824/>
In 1931, South African mammalogist Guy C. Shortridge suggested a [[simple:subspecies]] called ''Felis nigripes thomasi''. He based this idea on darker skins found in Griqualand West.<ref>{{cite journal |last1=Shortridge |first1=G. C. |author-link=Guy C. Shortridge |date=1931 |title=''Felis nigripes thomasi'' subsp. nov. |journal=Records of the Albany Museum |volume=4 |issue=1 |pages=119–120}}</ref>
Later, British zoologist Reginald Innes Pocock confirmed that the black-footed cat belongs to the genus ''[[simple:Felis]]''.<ref name="Pocock1951">{{cite book|last=Pocock|first=R. I.|date=1951|title=Catalogue of the genus Felis|publisher=British Museum (Natural History)|location=London|chapter=Felis nigripes Burchell|pages=145–150|chapter-url=https://archive.org/stream/catalogueofgenus00brit#page/145/mode/1up}}</ref>
The idea that the black-footed cat has subspecies was later questioned because there are no clear natural barriers, like a mountain, river, or desert, between its populations.<ref name=Olbricht97/> In 2017, the [[simple:IUCN]] Cat Specialist Group reviewed cat classification and said the black-footed cat is most likely a [[simple:monotype|monotypic]] species, meaning it has no subspecies.<ref name="CatSG2017">{{cite journal|last1=Kitchener|first1=A. C.|last2=Breitenmoser-Würsten|first2=C.|last3=Eizirik|first3=E.|last4=Gentry|first4=A.|last5=Werdelin|first5=L.|last6=Wilting|first6=A.|last7=Yamaguchi|first7=N.|last8=Abramov|first8=A. V.|last9=Christiansen|first9=P.|last10=Driscoll|first10=C.|last11=Duckworth|first11=J. W.|last12=Johnson|first12=W.|last13=Luo|first13=S.-J.|last14=Meijaard|first14=E.|last15=O'Donoghue|first15=P.|last16=Sanderson|first16=J.|last17=Seymour|first17=K.|last18=Bruford|first18=M.|last19=Groves|first19=C.|last20=Hoffmann|first20=M.|last21=Nowell|first21=K.|last22=Timmons|first22=Z.|last23=Tobe|first23=S.|name-list-style=amp|date=2017|title=A revised taxonomy of the Felidae: The final report of the Cat Classification Task Force of the IUCN Cat Specialist Group|journal=Cat News|issue=Special Issue 11|page=13|url=https://repository.si.edu/bitstream/handle/10088/32616/A_revised_Felidae_Taxonomy_CatNews.pdf?sequence=1&isAllowed=y|access-date=16 March 2019|archive-date=30 July 2018|archive-url=https://web.archive.org/web/20180730142355/https://repository.si.edu/bitstream/handle/10088/32616/A_revised_Felidae_Taxonomy_CatNews.pdf?sequence=1&isAllowed=y|url-status=live}}</ref>
=== Evolution ===
Studies of [[simple:DNA]] show that cats ([[simple:Felidae]]) first spread across Asia during the [[simple:Miocene]], about {{Ma|14.45|8.38|million years ago|round=2}}. <ref name="Johnson2006">{{cite journal |last1=Johnson |first1=W. E. |last2=Eizirik |first2=E. |last3=Pecon-Slattery |first3=J. |last4=Murphy |first4=W. J. |last5=Antunes |first5=A. |last6=Teeling |first6=E. |last7=O'Brien |first7=S. J. |name-list-style=amp |title=The late miocene radiation of modern Felidae: A genetic assessment |journal=[[Science (journal)|Science]] |volume=311 |issue=5757 |pages=73–77 |date=2006 |pmid=16400146 |doi=10.1126/science.1122277 |bibcode=2006Sci...311...73J |s2cid=41672825 |url=https://zenodo.org/record/1230866 |access-date=29 August 2020 |archive-date=4 October 2020 |archive-url=https://web.archive.org/web/20201004075725/https://zenodo.org/record/1230866 |url-status=live }}</ref><ref name="Werdelin2010">{{cite book |last1=Werdelin |first1=L. |last2=Yamaguchi |first2=N. |last3=Johnson |first3=W. E. |last4=O'Brien |first4=S. J. |name-list-style=amp |chapter=Phylogeny and evolution of cats (Felidae) |date=2010 |pages=59–82 |publisher=Oxford University Press |location=Oxford, UK |isbn=978-0-19-923445-5 |chapter-url=https://www.researchgate.net/publication/266755142 |editor1-last=Macdonald |editor1-first=D. W. |editor2-last=Loveridge |editor2-first=A. J. |title=Biology and Conservation of Wild Felids |access-date=3 May 2020 |archive-date=25 September 2018 |archive-url=https://web.archive.org/web/20180925141956/https://www.researchgate.net/publication/266755142 |url-status=live }}</ref> Studies of [[simple:mitochondrial DNA]] suggest this may have happened about {{mya|16.76|6.46}}.<ref name="Li_al2016">{{cite journal |last1=Li |first1=G. |last2=Davis |first2=B. W. |last3=Eizirik |first3=E. |last4=Murphy |first4=W. J. |name-list-style=amp |date=2016 |title=Phylogenomic evidence for ancient hybridization in the genomes of living cats (Felidae) |journal=Genome Research |volume=26 |issue=1 |pages=1–11 |doi=10.1101/gr.186668.114 |pmid=26518481 |pmc=4691742}}</ref>
The black-footed cat belongs to the ''Felis'' group. Based on DNA found in the cell’s [[simple:Cell nucleus|nucleus]], it split from the common ancestor of all ''Felis'' species about {{mya|4.44|2.16}}.<ref name="Johnson2006" /><ref name="Werdelin2010" /> Studies of mitochondrial DNA suggest it split earlier, about {{Ma|6.52|1.03|million years ago|round=2}}.<ref name="Li_al2016" /> Both studies agree that the [[simple:jungle cat]] (''F. chaus'') split first, and the black-footed cat split after that.<ref name="Johnson2006" /><ref name="Li_al2016" />
No fossils of the black-footed cat have been found.<ref name="Werdelin2010" /> It probably moved into Africa during the [[simple:Pleistocene]].<ref name="Johnson2006" /> This may have been possible because sea levels were lower between Asia and Africa.<ref name="Li_al2016" />
The following [[simple:cladogram]] shows its relationships based on nuclear DNA:
<ref name="Johnson2006" /><ref name="Werdelin2010" />
{{cladogram |align=left |style=font-size:90%;line-height:100%;width:500px;
|cladogram={{clade |label1=Felidae
|1={{clade |label1=[[simple:Felinae]]
|1={{clade
|1={{clade
|1={{clade |label1=''Felis''
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1={{clade
|1=[[simple:Domestic cat]] (''F. catus'')
|2=[[simple:European wildcat]] (''F. silvestris'')
}}
|2={{clade
|1=[[simple:African wildcat]] (''F. lybica'')
|2=[[simple:Chinese mountain cat]] (''F. bieti'')
}}}}
|2=[[simple:Sand cat]] (''F. margarita'')
}}
|2='''Black-footed cat'''
}}
|2=[[simple:Jungle cat]] (''F. chaus'')
}}}}
|2=other Felinae lineages
}}}}}}
|2=[[simple:Pantherinae]]
}}}}}}
{{clear}}
==Distribution and habitat==
The black-footed cat is found only in Southern Africa, and it has a smaller range than other small cats in the area.<ref>{{cite book |last1=Nowell |first1=K. |last2=Jackson |first2=P. |date=1996 |title=Wild Cats Status Survey and Conservation Action Plan |publisher=IUCN Cat Specialist Group |pages=8–10 |chapter=Black-footed cat, ''Felis nigripes'' Burchell, 1824}}</ref> It lives in South Africa and southern Botswana, where it was first recorded in the 1960s.<ref name=Smithers1971/> It has also been recorded in Namibia, southern Angola, and southern Zimbabwe, but it is unlikely to live in Lesotho or Eswatini.<ref name=iucn/>
Its habitat includes open, dry [[simple:savanna]]s and semi-dry [[simple:shrubland]] in the Karoo and the southwestern Kalahari. These places have short grass, low bushes, and some taller grasses.<ref name=Smithers1971/> Rainfall in these areas is about {{cvt|100|to|500|mm}} each year.<ref name=skinner/><ref name=Sliwa2013/> In the [[simple:Drakensberg]], it has been found at heights of up to {{cvt|2000|m|ft|-2}} above sea level.<ref name=skinner/>
==Behaviour==
[[File:Black-footed cat, Zoo Wuppertal, July 2009.jpg|thumb|An adult black-footed cat resting]]
[[File:Suspicious Black-Footed Cat.jpg|thumb|A black-footed cat under cover]]
The black-footed cat is [[simple:nocturnal]] and usually lives alone, except when females are raising kittens.<ref name=Smithers1971/><ref name=Sliwa2004/> During the day, it rests in hollow [[simple:termite]] mounds or empty burrows made by animals such as the springhare, [[simple:aardvark]], or Cape porcupine. It often digs to make these burrows larger.<ref name=Olbricht97/> At night, it comes out to hunt. When disturbed, it hides quickly, often in termite mounds. If it cannot escape, it fights aggressively. This behaviour gave it the Afrikaans name {{lang|af|miershooptier}} (“anthill tiger”). A [[simple:San people|San]] legend even says it can kill a [[simple:giraffe]], showing how brave it is believed to be.<ref>{{cite magazine |last=Sliwa |first=A. |date=November 2006 |title=Atomic Kitten: the secrets of Africa's black-footed cat |magazine=BBC Wildlife |volume=24 |issue=12 |pages=36–40}}</ref>
It is not a good climber because of its short, strong body and short tail,<ref name=Armstrong1977>{{cite book |last=Armstrong |first=J. |date=1977 |chapter=The development and hand-rearing of black-footed cats |pages=71–80 |title=The World's Cats: The Proceedings of an International Symposium |volume=3 |editor-last=Eaton |editor-first=R. L. |publisher=Winston Wildlife Safari |location=Oregon}}</ref> although one was once seen resting in a camelthorn tree.<ref>{{cite magazine |last=Sliwa |first=A. |date=2013 |title=Black-footed Lightning |magazine=Africa Geographic |issue=June |pages=27–31}}</ref>
Females usually live in areas of {{cvt|6.23|-|15.53|km2}}, while males range over larger areas of {{cvt|19.44|-|23.61|km2}}. A male’s range often overlaps with those of several females. They mark their territories using scent, such as spraying urine, rubbing, claw marks, and leaving faeces in easy-to-see places.<ref name=Sliwa2004/> Their calls are louder than those of most other small cats, but they also purr and make gurgling sounds when close together. When threatened, they hiss and growl.<ref name=Sliwa2004/> Adults usually travel about {{cvt|8.42|±|2.09|km}} each night while hunting.<ref>{{cite book |last1=Sliwa |first1=A. |last2=Herbst |first2=M. |last3=Mills |first3=M. |date=2010 |chapter=Black-footed cats (''Felis nigripes'') and African wild cats (''Felis silvestris''): A comparison of two small felids from South African arid lands |pages=537–558 |title=The Biology and Conservation of Wild Felids |publisher=Oxford University Press |isbn=978-0-19-959283-8}}</ref>
Because it is shy and moves fast without using roads, the black-footed cat is difficult to study. In South Africa, its population density was estimated at {{cvt|0.17|/sqkm}} near [[simple:Kimberley, Northern Cape|Kimberley]] in 1998–1999, but this dropped to {{cvt|0.08|/sqkm|1}} between 2005 and 2014. In Nuwejaarsfontein, densities from 2009 to 2014 were about {{cvt|0.06|/sqkm}}. In poorer habitats, numbers may be as low as {{cvt|0.03|/sqkm|2}}.<ref name=iucn/>
===Reproduction and life cycle===
In captivity, males become able to reproduce at about nine months old, and females at about seven months.<ref name="Olbricht97"/> A female’s [[simple:oestrus]] lasts around 36 hours, and [[simple:gestation]] lasts 63–68 days.<ref name=Leyhausen>{{cite journal |last1=Leyhausen |first1=P. |last2=Tonkin |first2=B. |name-list-style=amp |date=1966 |title=Breeding the black-footed cat (''Felis nigripes'') in captivity |journal=International Zoo Yearbook |volume=6 |issue=6 |pages=178–182 |doi=10.1111/j.1748-1090.1966.tb01744.x}}</ref> She can give birth to one or two [[simple:Litter|litters]] each year between October and March. Litters usually have one or two kittens, but can have up to four.<ref name=Olbricht97/>
In the wild, females are ready to mate for only five to ten hours, so males must find them quickly. Males may fight each other, and [[simple:Sexual intercourse|copulation]] happens every 20–50 minutes.<ref name=Sliwa2004/>
Kittens weigh {{cvt|60|to|93|g}} at birth. They are born blind but can crawl within a few hours. Their eyes open after 3–10 days, and their teeth appear at 2–3 weeks. They start eating solid food at about one month old and are [[simple:Weaning|weaned]] at two months. Adult teeth appear at 148–158 days.<ref name=Olbricht97/>
In captivity, mothers move their kittens to new hiding places every 6–10 days, which is more often than in other small cats.<ref name=Leyhausen/> In the wild, kittens are born in springhare burrows or termite mounds. From four days old, the mother may leave them alone at night for up to 10 hours. By six weeks, the kittens move quickly and leave the den often. Young cats may be killed by predators such as the [[simple:black-backed jackal]], [[simple:caracal]], and nocturnal [[simple:raptor]]s.<ref>{{cite journal |author=Sliwa, A. |year=1996 |title=Pleasures and Worries of a Black-Footed Cat Field Study in South Africa |journal=Cat Times |volume=23 |pages=1–3}}</ref> They become independent at 3–4 months old but usually stay close to their mother’s area. In captivity, they can live for up to 15 years.<ref name=Sliwa2013/>
===Diseases===
Black-footed cats often suffer from AA amyloidosis, a disease that causes long-term [[simple:inflammation]] and usually leads to [[simple:kidney failure]] and death.<ref name=Olbricht97/><ref>{{cite journal |last1=Terio |first1=K.A. |last2=O'Brien |first2=T. |last3=Lamberski |first3=N. |last4=Famula |first4=T. R. |last5=Munson |first5=L. |date=2008 |title=Amyloidosis in black-footed cats (''Felis nigripes'') |journal=Veterinary Pathology Online |volume=45 |issue=3 |pages=393–400 |doi=10.1354/vp.45-3-393 |pmid=18487501 |s2cid=43387363 |doi-access=free}}</ref> Wild cats are also at risk from diseases spread by domestic dogs and cats.<ref>{{cite book |last1=Lamberski |first1=N. |last2=Sliwa |first2=A. |last3=Wilson |first3=B. |last4=Herrick |first4=J. |last5=Lawrenz |first5=A. |date=2009 |chapter=Conservation of black-footed cats (''Felis nigripes'') and prevalence of infectious diseases in sympatric carnivores in the Northern Cape Province, South Africa |pages=243–245 |title=Proceedings of the International Conference on Diseases of Zoo and Wild Animals 2009 |publisher=Leibniz-Institut für Zoo- und Wildtierforschung |location=Berlin}}</ref>
==Threats==
Threats include accidental killing during predator control, such as poison bait and steel traps, [[simple:habitat destruction]] caused by [[simple:overgrazing]], fewer springhares, attacks by other carnivores, diseases, and poor farming methods. Some cats are killed by herding [[simple:dog]]s. Many protected areas are too small to support healthy populations.<ref name=iucn />
==Conservation==
The black-footed cat is listed on CITES Appendix I and is protected in most of its range, including [[simple:Botswana]] and [[simple:South Africa]], where hunting is not allowed.<ref name=iucn />
===Field research===
The Black-footed Cat Working Group runs a long-term study at Benfontein Nature Reserve and Nuwejaarsfontein Farm near [[simple:Kimberley, Northern Cape]].<ref>{{cite report |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lawrenz |first3=A. |date=2010 |title=Report on surveying and catching black-footed cats (''Felis nigripes'') on Nuwejaarsfontein Farm / Benfontein Nature Reserve, 4–20 July 2010 |publisher=Black-footed Cat Working Group}}</ref> In 2012, the study expanded to Biesiesfontein Farm near Victoria West.<ref>{{cite report |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lamberski |first3=N. |last4=Lawrenz |first4=A. |date=2013 |title=Report on surveying, catching and monitoring black-footed cats (''Felis nigripes'') on Benfontein, Nuwejaarsfontein, and Biesiesfontein in 2012 |publisher=Black-footed Cat Working Group}}</ref> Between 1992 and 2018, 65 cats were fitted with radio collars to study their social behaviour, home ranges, hunting, and diet.<ref>{{cite book |author=Sliwa, A. |year=2018 |pages=7–8 |chapter=25 years of Black-footed Cat ''Felis nigripes'' field research and conservation |title=Proceedings of the First International Small Wild Cat Conservation Summit |publisher=Wild Cat Network}}</ref> Camera traps are also used to study their behaviour and interactions with [[simple:aardwolf]]s.<ref>{{cite journal |last1=Sliwa |first1=A. |last2=Wilson |first2=B. |last3=Lawrenz |first3=A. |last4=Lamberski |first4=N. |last5=Herrick |first5=J. |last6=Küsters |first6=M. |title=Camera trap use in the study of black-footed cats (''Felis nigripes'') |journal=African Journal of Ecology |date=2018 |volume=56 |issue=4 |pages=895–897 |doi=10.1111/aje.12564 |bibcode=2018AfJEc..56..895S }}</ref>
=== In captivity ===
The Wuppertal Zoo in Germany obtained black-footed cats in 1957 and successfully bred them in 1963. In 1993, the European Endangered Species Programme was set up to manage breeding and reduce inbreeding. The ''International Studbook for the Black-footed Cat'' was maintained at Wuppertal Zoo.<ref>{{cite book |last1=Olbricht |first1=G. |last2=Schürer |first2=U. |date=1994 |title=International Studbook for the Black-footed Cat 1994 |publisher=Zoologischer Garten der Stadt Wuppertal}}</ref> {{as of|2011|July}}, records showed that 726 black-footed cats had been kept in captivity since 1964. At that time, 74 cats were living in 23 institutions in Germany, the United Arab Emirates, the US, the UK, and South Africa.<ref>{{cite book |last=Stadler |first=A. |date=2011 |title=International studbook for the black-footed cat (''Felis nigripes'') |volume=15 |publisher=Zoologischer Garten der Stadt Wuppertal}}</ref>
Several zoos have reported successful breeding, including Cleveland Metroparks Zoo,<ref>{{cite web|title=Press Release: Animal News: Second Litter of Black-footed Cats|publisher=Cleveland Metroparks Zoo|date=2012|url=http://www.clemetzoo.com/whats_new/new_animals.asp|archive-date=20 September 2013|archive-url=https://web.archive.org/web/20130920181420/http://www.clemetzoo.com/whats_new/new_animals.asp}}</ref> Fresno Chaffee Zoo,<ref>{{cite web|last=Condoian|first=L.|date=2011|title=General Meeting of the Board of Directors|publisher=Fresno Chaffee Zoo Corporation|website=FresnoChaffeeZoo.org|url=http://www.fresnochaffeezoo.org/pdf/2011-0609minutes.pdf|archive-date=14 November 2011|archive-url=https://web.archive.org/web/20111114152306/http://www.fresnochaffeezoo.org/pdf/2011-0609minutes.pdf}}</ref> Brookfield Zoo,<ref>{{cite web|author=Katzen, S.|date=2012|title=Black-footed cats born: A first at Brookfield Zoo|website=Chicago Zoological Society|url=http://www.czs.org/CZS/About-CZS/Press-Room/Press-Releases/Black-Footed-Cats-Born-A-First-at-Brookfield-Zoo|archive-date=31 March 2012|archive-url=https://web.archive.org/web/20120331001315/http://www.czs.org/CZS/About-CZS/Press-Room/Press-Releases/Black-Footed-Cats-Born-A-First-at-Brookfield-Zoo}}</ref> and Philadelphia Zoo.<ref>{{cite news|url=http://www.nj.com/indulge/index.ssf/2014/06/philadelphia_zoo_visitors_paws_to_gush_over_black-footed_cat_kittens.html|title=Philadelphia Zoo visitors 'paws' to gush over black-footed cat kittens|last=Rearick|first=K.|work=South Jersey Times|date=2014|access-date=10 June 2014|archive-date=14 July 2014|archive-url=https://web.archive.org/web/20140714112337/http://www.nj.com/indulge/index.ssf/2014/06/philadelphia_zoo_visitors_paws_to_gush_over_black-footed_cat_kittens.html|url-status=live}}</ref>
The Audubon Nature Institute’s Center for Research of Endangered Species in New Orleans has carried out advanced genetic research on cats.<ref>{{cite web|last=Jeffries|first=A.|date=2013|title=Where cats glow green: Weird feline science in New Orleans|website=The Verge|url=https://www.theverge.com/2013/11/6/4841714/where-cats-glow-green-weird-feline-science-acres-in-new-orleans|access-date=30 August 2017|archive-date=21 January 2018|archive-url=https://web.archive.org/web/20180121071408/https://www.theverge.com/2013/11/6/4841714/where-cats-glow-green-weird-feline-science-acres-in-new-orleans|url-status=live}}</ref> In February 2011, a female there gave birth to two male kittens. These were the first black-footed cats born using [[simple:In-vitro fertilisation|{{lang|la|nocat=y|in vitro}} fertilization]] with frozen sperm and embryos. The sperm was collected in 2003, used to fertilize an egg in 2005, and the embryos were frozen. In 2010, the embryos were thawed and placed into a surrogate female, who carried the pregnancy and gave birth to the kittens.<ref>{{cite web|last=Burnette|first=S.|date=2011|title=Rare cats born through amazing science at Audubon Center for Research of Endangered Species|publisher=Audubon Nature Institute|website=AudubonInstitute.org|url=http://www.auduboninstitute.org/media/audubonheadlines/rare-cats-born-through-amazing-science|archive-date=18 February 2014|archive-url=https://web.archive.org/web/20140218041529/http://www.auduboninstitute.org/media/releases/rare-cats-born-through-amazing-science}}</ref>
On 6 February 2012, the same center reported another success. A female kitten named Crystal was born to a domestic cat [[simple:Surrogacy|surrogate]] after interspecies [[simple:embryo transfer]].<ref>{{cite news|last=Waller|first=M.|date=2012|title=Audubon center in Algiers logs another breakthrough in genetic engineering of endangered cats|work=NOLA.com|publisher=New Orleans Net|url=http://www.nola.com/education/index.ssf/2012/03/audubon_center_in_algiers_logs.html|archive-date=20 January 2018|archive-url=https://web.archive.org/web/20180120235949/http://www.nola.com/education/index.ssf/2012/03/audubon_center_in_algiers_logs.html}}</ref>
== References ==
{{reflist}}
==Other websites==
{{Wikispecies|Felis nigripes}}
{{Commons category|Felis nigripes
}}
*{{cite web |url=http://www.black-footed-cat.wild-cat.org/ |title=Black-footed Cat Working Group |work=Wild Cat Network}}
*{{cite web |url=http://www.catsg.org/index.php?id=105 |work=IUCN/SSC Cat Specialist Group |title=''Felis nigripes'' |access-date=2026-01-02 |archive-date=2017-10-31 |archive-url=https://web.archive.org/web/20171031040815/http://www.catsg.org/index.php?id=105 |url-status=dead }}
*{{cite web |url=http://www.animalinfo.org/species/carnivor/felinigr.htm |work=Animal Info |title=''Felis nigripes''}}
*{{cite web |url=https://www.youtube.com/watch?v=nl8o9PsJPAQ&ab_channel=NatureonPBS |work=Nature on PBS |title=''Documentary: Meet the Deadliest Cat on the Planet''|date=25 October 2018 }}
*{{cite web |url=https://www.youtube.com/watch?v=pxFGkfYeUeA |work=Wildlife Wonder |title=''Documentary: Black-footed cats''|date=18 May 2015 }}
{{Carnivora|Fe.}}
{{Taxonbar|from=Q204814}}
[[simple:Category:Felines]]
[[simple:Category:Mammals of Africa]]
peqjhfxi2d3g5jyy81zj6qoniyhx6oa
Wikipedia:Requests/Permissions/Aqurs1 3
4
174740
737144
2026-04-08T08:47:30Z
Aqurs1
41128
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:''' Testing abusefilter..."
737144
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:Aqurs1|Aqurs1]] ===
* {{User3|Aqurs1}}, [[Special:CentralAuth/Aqurs1|global contribs]] 08:47, 8 April 2026 (UTC)
* '''Motive for request:''' Testing abusefilter rules and own scripts.
* '''Requested rights:''' Administrator
* '''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__
ipcdmpenmy9fecv6leuqjl1gd7q1oz1
User:Plantaest/sandbox
2
174741
737149
2026-04-08T09:03:48Z
Plantaest
37055
+
737149
wikitext
text/x-wiki
A '''chloroplast''' ({{IPAc-en|ˈ|k|l|ɔːr|ə|ˌ|p|l|æ|s|t|,_|-|p|l|ɑː|s|t|}} {{respell|KLOR|ə|plast|,_|-plahst}}){{refn|{{cite book |last=Jones |first=Daniel |author-link=Daniel Jones (phonetician) |title=English Pronouncing Dictionary | veditors=Roach P, Hartmann J, Setter J |place=Cambridge |publisher=Cambridge University Press |orig-date=1917 |year=2003 |isbn=3-12-539683-2 }}}}{{refn|{{MerriamWebsterDictionary|Chloroplast}}}} is a type of [[membrane-bound organelle|organelle]] known as a [[plastid]] that conducts [[photosynthesis]] mostly in [[plant cell|plant]] and [[algae|algal cells]]. Chloroplasts have a high concentration of [[chlorophyll]] pigments which capture the [[Radiant energy|energy from sunlight]] and convert it to [[chemical energy]] and release [[oxygen]]. The chemical energy created is then used to make sugar and other organic molecules from carbon dioxide in a process called the [[Calvin cycle]]. Chloroplasts carry out a number of other functions, including [[fatty acid synthesis]], [[amino acid]] synthesis, and the [[immune response]] in plants. The number of chloroplasts per cell varies from one, in some unicellular algae, up to 100 in plants like ''[[Arabidopsis]]'' and [[wheat]].
<references />
<div id="app"></div>
35sslf3w8a3xb7rs9eqgqv9sv4ytgx3
Wikipedia:Requests/Permissions/Plantaest
4
174742
737151
2026-04-08T09:08:24Z
Plantaest
37055
+
737151
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:Plantaest|Plantaest]] ===
* {{User3|Plantaest}}, [[Special:CentralAuth/Plantaest|global contribs]] 09:07, 8 April 2026 (UTC)
* '''Motive for request:''' Hi, I am Plantaest, an administrator on Vietnamese Wikipedia. I want to have administrator right on testwiki for testing some stuffs. Many thanks! [[User:Plantaest|Plantaest]] ([[User talk:Plantaest|talk]]) 09:07, 8 April 2026 (UTC)
* '''Requested rights:''' Administrator
* '''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__
39yedangvyyblaubjhvgglnrbpobdll
737154
737151
2026-04-08T09:15:01Z
Plantaest
37055
/* Plantaest */ reply ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
737154
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:Plantaest|Plantaest]] ===
* {{User3|Plantaest}}, [[Special:CentralAuth/Plantaest|global contribs]] 09:07, 8 April 2026 (UTC)
* '''Motive for request:''' Hi, I am Plantaest, an administrator on Vietnamese Wikipedia. I want to have administrator right on testwiki for testing some stuffs. Many thanks! [[User:Plantaest|Plantaest]] ([[User talk:Plantaest|talk]]) 09:07, 8 April 2026 (UTC)
* '''Requested rights:''' Administrator
* '''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__
* @[[User:Barras|Barras]]: Hello, I see you below. Can you help grant the right for me? Thanks! [[User:Plantaest|Plantaest]] ([[User talk:Plantaest|talk]]) 09:15, 8 April 2026 (UTC)
9pec7gzpl8yzaj4qhee3x8tx4rzuc5v