User:Polygnotus/Scripts/SourceTable5.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/SourceTable5.
// <nowiki>
// AfD Source Assessment Tool
// Note: mw.Api() running on-wiki handles User-Agent automatically.
// The User-Agent requirement applies to off-wiki external API consumers.
// Only run on AfD edit pages
var currentAction = mw.config.get('wgAction');
if (mw.config.get('wgPageName').startsWith('Wikipedia:Articles_for_deletion/') &&
(currentAction === 'edit' || currentAction === 'submit')) {
$(function() {
console.log('AfD Source Assessment script running...');
// Fix #9: simplified button placement — try #firstHeading, fall back to #editform
var $target = $('#firstHeading');
if (!$target.length) {
$target = $('#editform');
}
if ($target.length) {
$('<button>')
.attr('id', 'assessSourcesButton')
.text('Assess Sources')
.css({
'margin-left': '10px',
'margin-bottom': '5px',
'display': 'inline-block',
'vertical-align': 'middle',
'font-size': '15px',
'padding': '4px 8px'
})
.click(function(e) {
e.preventDefault();
showSourceAssessmentPopup();
})
.appendTo($target);
console.log('Assess Sources button added to', $target.attr('id'));
} else {
console.error('Could not find a suitable element to attach the button to');
}
});
}
// Global state
var assessmentState = {
sources: [],
assessments: []
};
function showSourceAssessmentPopup() {
var afdPageName = mw.config.get('wgPageName');
var articleName = extractArticleName(afdPageName);
var editTextarea = document.getElementById('wpTextbox1');
if (!editTextarea) {
mw.notify('Error: Could not find edit textarea.', {type: 'error'});
return;
}
var existingState = parseExistingAssessment(editTextarea.value);
if (existingState) {
assessmentState = existingState;
console.log('Loaded existing state with', existingState.sources.length, 'sources');
displayPopup();
} else {
// Fix #4: loading indicator via button state
var $button = $('#assessSourcesButton');
$button.text('Loading...').prop('disabled', true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
titles: articleName,
formatversion: 2
}).done(function(data) {
$button.text('Assess Sources').prop('disabled', false);
try {
if (!data.query || !data.query.pages || data.query.pages.length === 0) {
mw.notify('Error: Could not retrieve article data.', {type: 'error'});
return;
}
var page = data.query.pages[0];
if (page.missing) {
mw.notify('The article "' + articleName + '" could not be found.', {type: 'error'});
return;
}
if (!page.revisions || page.revisions.length === 0) {
mw.notify('No revisions found for the article.', {type: 'error'});
return;
}
var pageContent = page.revisions[0].content;
var references = extractReferences(pageContent);
if (references.length === 0) {
mw.notify('No references found in the article.', {type: 'warning'});
return;
}
assessmentState.sources = references;
assessmentState.assessments = references.map(function() {
return {
independent: '',
reliable: '',
significant: '',
gng: '',
comments: ''
};
});
displayPopup();
} catch (error) {
mw.notify('An error occurred: ' + error.message, {type: 'error'});
console.error('Error:', error);
}
}).fail(function(error) {
$button.text('Assess Sources').prop('disabled', false);
mw.notify('Failed to fetch article content.', {type: 'error'});
console.error('API error:', error);
});
}
}
function displayPopup() {
$('#sourceAssessmentPopup').remove();
var popup = $('<div>')
.attr('id', 'sourceAssessmentPopup')
.css({
'position': 'fixed',
'top': '0',
'left': '0',
'width': '100%',
'height': '100%',
'background': 'rgba(0,0,0,0.5)',
'z-index': '10000',
'display': 'flex',
'align-items': 'center',
'justify-content': 'center',
'overflow': 'auto'
});
var content = $('<div>')
.css({
'background': 'white',
'padding': '20px',
'border-radius': '5px',
'width': '90%',
'max-height': '90%',
'overflow': 'auto',
'position': 'relative'
});
var closeButton = $('<button>')
.text('×')
.css({
'position': 'absolute',
'top': '10px',
'right': '10px',
'border': 'none',
'background': '#f0f0f0',
'font-size': '24px',
'width': '30px',
'height': '30px',
'border-radius': '50%',
'cursor': 'pointer',
'line-height': '1',
'padding': '0'
})
.hover(
function() { $(this).css('background', '#e0e0e0'); },
function() { $(this).css('background', '#f0f0f0'); }
)
.click(function() {
popup.remove();
});
content.append(closeButton);
content.append($('<h2>').text('Source Assessment for WP:GNG').css('margin-top', '0'));
var table = $('<table>')
.attr('id', 'sourceAssessmentTable')
.css({
'width': '100%',
'border-collapse': 'collapse',
'margin': '20px 0'
});
var thead = $('<thead>');
var headerRow = $('<tr>');
['#', 'Source', 'Independent', 'Reliable', 'Significant Coverage', 'Count Toward GNG', 'Comments'].forEach(function(header) {
var th = $('<th>').text(header).css({
'border': '1px solid #ccc',
'padding': '8px',
'background': '#f0f0f0',
'text-align': 'left'
});
if (header === 'Count Toward GNG') {
th.css('width', '20px');
}
headerRow.append(th);
});
thead.append(headerRow);
table.append(thead);
var tbody = $('<tbody>').attr('id', 'sourceAssessmentTbody');
assessmentState.sources.forEach(function(source, index) {
tbody.append(createAssessmentRow(source, index));
});
table.append(tbody);
content.append(table);
var buttonDiv = $('<div>').css({'text-align': 'right', 'margin-top': '10px'});
// Fix #7: button to manually add a source row
$('<button>')
.text('Add Source Manually')
.css({'margin-right': '10px', 'padding': '8px 16px'})
.click(function() {
var newIndex = assessmentState.sources.length;
assessmentState.sources.push('');
assessmentState.assessments.push({
independent: '',
reliable: '',
significant: '',
gng: '',
comments: ''
});
var row = createAssessmentRow('', newIndex, true);
$('#sourceAssessmentTbody').append(row);
})
.appendTo(buttonDiv);
$('<button>')
.text('Generate Wikicode')
.css({'margin-right': '10px', 'padding': '8px 16px'})
.click(function() {
generateWikicode();
})
.appendTo(buttonDiv);
$('<button>')
.text('Close')
.css({'padding': '8px 16px'})
.click(function() {
popup.remove();
})
.appendTo(buttonDiv);
content.append(buttonDiv);
popup.append(content);
$('body').append(popup);
}
// Fix #7: added optional isManual parameter — manual rows get an editable source textarea
function createAssessmentRow(source, index, isManual) {
var assessment = assessmentState.assessments[index];
var row = $('<tr>').attr('data-index', index);
// Number cell
var numberCell = $('<td>').css({
'border': '1px solid #ccc',
'padding': '8px',
'text-align': 'center',
'font-weight': 'bold'
}).text(index + 1);
row.append(numberCell);
// Source cell — editable textarea for manually added rows
var sourceCell = $('<td>').css({
'border': '1px solid #ccc',
'padding': '8px',
'max-width': '300px',
'overflow': 'auto'
});
if (isManual) {
var sourceTextarea = $('<textarea>')
.css({'width': '100%', 'min-height': '60px'})
.attr('placeholder', 'Paste citation wikicode here')
.on('input', function() {
assessmentState.sources[index] = $(this).val();
});
sourceCell.append(sourceTextarea);
} else {
sourceCell.html(source);
}
row.append(sourceCell);
// Radio button columns: Independent, Reliable, Significant Coverage
['independent', 'reliable', 'significant'].forEach(function(field) {
var cell = $('<td>').css({
'border': '1px solid #ccc',
'padding': '8px'
});
var radioGroup = $('<div>').css({'display': 'flex', 'flex-direction': 'column', 'gap': '4px'});
[{value: 'yes', text: 'Yes'},
{value: 'no', text: 'No'},
{value: 'unknown', text: 'Unknown'},
{value: 'partial', text: 'Partial'}
].forEach(function(option) {
var radioId = 'radio_' + index + '_' + field + '_' + option.value;
var label = $('<label>').css({'display': 'flex', 'align-items': 'center', 'cursor': 'pointer'});
var radio = $('<input>')
.attr('type', 'radio')
.attr('name', 'radio_' + index + '_' + field)
.attr('id', radioId)
.val(option.value)
.css({'margin-right': '4px'});
if (assessment[field] === option.value) {
radio.attr('checked', 'checked');
}
radio.change(function() {
assessmentState.assessments[index][field] = $(this).val().trim();
updateRowStatus(row, index);
});
label.append(radio);
label.append($('<span>').text(option.text));
radioGroup.append(label);
});
cell.append(radioGroup);
row.append(cell);
});
// Count Toward GNG cell
var gngCell = $('<td>').css({
'border': '1px solid #ccc',
'padding': '8px',
'text-align': 'center',
'width': '20px'
}).attr('data-gng', '');
row.append(gngCell);
// Comments cell
var commentsCell = $('<td>').css({
'border': '1px solid #ccc',
'padding': '8px'
});
var commentsTextarea = $('<textarea>')
.css({'width': '100%', 'min-height': '40px'})
.val(assessment.comments)
.on('input', function() {
assessmentState.assessments[index].comments = $(this).val().trim();
});
commentsCell.append(commentsTextarea);
row.append(commentsCell);
updateRowStatus(row, index);
return row;
}
function updateRowStatus(row, index) {
var assessment = assessmentState.assessments[index];
var gngCell = row.find('td[data-gng]');
var allYes = assessment.independent === 'yes' &&
assessment.reliable === 'yes' &&
assessment.significant === 'yes';
if (allYes) {
row.css('background-color', '#d4edda');
gngCell.html('<img alt="Yes" src="//upload.wikimedia.org/wikipedia/commons/thumb/2/26/Check-green.svg/20px-Check-green.svg.png" width="13" height="13">');
gngCell.attr('data-gng', 'yes');
assessmentState.assessments[index].gng = 'yes'; // Fix #1: write gng back to state
} else if (assessment.independent || assessment.reliable || assessment.significant) {
row.css('background-color', '#f8d7da');
gngCell.html('<img alt="No" src="//upload.wikimedia.org/wikipedia/commons/thumb/4/48/Dark_Red_x.svg/20px-Dark_Red_x.svg.png" width="13" height="13">');
gngCell.attr('data-gng', 'no');
assessmentState.assessments[index].gng = 'no'; // Fix #1
} else {
row.css('background-color', 'white');
gngCell.html('');
gngCell.attr('data-gng', '');
assessmentState.assessments[index].gng = ''; // Fix #1
}
}
function generateWikicode() {
var wikicode = '\n\n{{User:Polygnotus/Templates/SourceAssessTable\n';
assessmentState.sources.forEach(function(source, index) {
var assessment = assessmentState.assessments[index];
var num = index + 1;
wikicode += '|src' + num + '=' + source + '\n';
wikicode += '|num' + num + '=' + num + '\n';
wikicode += '|ind' + num + '=' + assessment.independent + '\n';
wikicode += '|rel' + num + '=' + assessment.reliable + '\n';
wikicode += '|sig' + num + '=' + assessment.significant + '\n';
wikicode += '|gng' + num + '=' + assessment.gng + '\n';
wikicode += '|comments' + num + '=' + assessment.comments + '\n';
});
wikicode += '}}\n';
var editTextarea = document.getElementById('wpTextbox1');
if (editTextarea) {
// Fix #8: use brace-counting removal instead of a lazy regex
var currentContent = removeExistingAssessmentBlock(editTextarea.value);
editTextarea.value = currentContent + wikicode;
$(editTextarea).trigger('change');
mw.notify('Source assessment added to edit area.', {type: 'success'});
$('#sourceAssessmentPopup').remove();
} else {
mw.notify('Failed to find edit textarea.', {type: 'error'});
}
}
// Fix #8: removes the existing assessment block using brace counting,
// so a }} inside a citation template cannot prematurely end the match
function removeExistingAssessmentBlock(content) {
var startPattern = /\{\{User:Polygnotus\/Templates\/SourceAssessTable/;
var startMatch = content.search(startPattern);
if (startMatch === -1) {
return content;
}
// Also strip any leading newlines before the template
var blockStart = startMatch;
while (blockStart > 0 && content[blockStart - 1] === '\n') {
blockStart--;
}
var braceCount = 0;
var templateEnd = -1;
for (var i = startMatch; i < content.length - 1; i++) {
if (content[i] === '{' && content[i + 1] === '{') {
braceCount++;
i++;
} else if (content[i] === '}' && content[i + 1] === '}') {
braceCount--;
if (braceCount === 0) {
templateEnd = i + 2;
break;
}
i++;
}
}
if (templateEnd === -1) {
console.warn('removeExistingAssessmentBlock: could not find end of template, leaving content unchanged');
return content;
}
// Also strip trailing newlines after the template
while (templateEnd < content.length && content[templateEnd] === '\n') {
templateEnd++;
}
return content.substring(0, blockStart) + content.substring(templateEnd);
}
function parseExistingAssessment(content) {
var startPattern = /\{\{User:Polygnotus\/Templates\/SourceAssessTable/;
var startMatch = content.search(startPattern);
if (startMatch === -1) {
console.log('No existing assessment template found');
return null;
}
var braceCount = 0;
var inTemplate = false;
var templateEnd = -1;
for (var i = startMatch; i < content.length - 1; i++) {
if (content[i] === '{' && content[i + 1] === '{') {
braceCount++;
inTemplate = true;
i++;
} else if (content[i] === '}' && content[i + 1] === '}') {
braceCount--;
if (braceCount === 0 && inTemplate) {
templateEnd = i + 2;
break;
}
i++;
}
}
if (templateEnd === -1) {
console.log('Could not find end of template');
return null;
}
var fullTemplate = content.substring(startMatch, templateEnd);
var templateNameEnd = fullTemplate.indexOf('\n');
if (templateNameEnd === -1) {
return null;
}
var templateContent = fullTemplate.substring(templateNameEnd + 1, fullTemplate.length - 2);
// Parse parameters, supporting multiline values
var params = {};
var currentParam = null;
var currentValue = '';
var lines = templateContent.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var paramMatch = line.match(/^\|([a-zA-Z]+\d+)=(.*)$/);
if (paramMatch) {
if (currentParam) {
params[currentParam] = currentValue;
}
currentParam = paramMatch[1];
currentValue = paramMatch[2];
} else if (currentParam && line.trim() !== '') {
currentValue += '\n' + line;
}
}
if (currentParam) {
params[currentParam] = currentValue;
}
// Determine number of sources from highest numbered parameter
var maxNum = 0;
Object.keys(params).forEach(function(key) {
var numMatch = key.match(/\d+$/);
if (numMatch) {
var num = parseInt(numMatch[0]);
if (num > maxNum) maxNum = num;
}
});
var sources = [];
var assessments = [];
for (var i = 1; i <= maxNum; i++) {
if (params['src' + i] !== undefined) {
sources.push(params['src' + i].trim());
assessments.push({
independent: (params['ind' + i] || '').trim(),
reliable: (params['rel' + i] || '').trim(),
significant: (params['sig' + i] || '').trim(),
gng: (params['gng' + i] || '').trim(),
comments: (params['comments' + i] || '').trim()
});
}
}
if (sources.length === 0) {
console.log('No sources found in template');
return null;
}
console.log('Successfully parsed', sources.length, 'sources with assessments');
return { sources: sources, assessments: assessments };
}
function extractArticleName(afdPageName) {
var articleName = afdPageName.replace('Wikipedia:Articles_for_deletion/', '');
var nominationMatch = articleName.match(/(.*) \((\d+)(st|nd|rd|th) nomination\)$/);
if (nominationMatch) {
articleName = nominationMatch[1];
}
return articleName;
}
function extractReferences(content) {
var references = [];
var seenNames = {}; // Fix #3: track named refs to avoid duplicates
var index = 0;
while (index < content.length) {
var startIndex = content.indexOf('<ref', index);
if (startIndex === -1) break;
var endIndex = content.indexOf('>', startIndex);
if (endIndex === -1) break;
var openingTag = content.substring(startIndex, endIndex + 1);
// Fix #6: use regex instead of endsWith to handle whitespace before />
if (/\/\s*>$/.test(openingTag)) {
index = endIndex + 1;
continue;
}
// Fix #5: skip refs with a group attribute — these are footnotes, not sources
if (/\bgroup\s*=/.test(openingTag)) {
var skipEnd = content.indexOf('</ref>', endIndex);
index = (skipEnd === -1) ? content.length : skipEnd + 6;
continue;
}
// Fix #3: deduplicate by named ref — if name= already seen, skip this definition
var nameMatch = openingTag.match(/\bname\s*=\s*["']?([^"'\s>\/]+)["']?/);
if (nameMatch) {
var refName = nameMatch[1];
if (seenNames[refName]) {
var skipEnd = content.indexOf('</ref>', endIndex);
index = (skipEnd === -1) ? content.length : skipEnd + 6;
continue;
}
seenNames[refName] = true;
}
var refEndIndex = content.indexOf('</ref>', endIndex);
if (refEndIndex === -1) break;
var refContent = content.substring(endIndex + 1, refEndIndex).trim();
// Fix #2: escape pipe characters from {{!}} so they don't break wikicode output
refContent = refContent.replace(/\{\{!\}\}/g, '|');
if (refContent) {
references.push(refContent);
}
index = refEndIndex + 6;
}
console.log('Extracted', references.length, 'references');
return references;
}
// </nowiki>