MediaWiki:Gadget-verify.js: Difference between revisions
From PickiPedia: A knowledge base of bluegrass, old time, and other traditional and public domain music
Jump to navigationJump to search
Make source URL optional - text source is sufficient (via update-page on MediaWiki MCP Server) |
Fix matching - strip wikitext formatting (bold/italic) when comparing to DOM text (via update-page on MediaWiki MCP Server) |
||
| (4 intermediate revisions by 2 users not shown) | |||
| Line 2: | Line 2: | ||
* Gadget-verify.js - Verification gadget for Pickipedia claims | * Gadget-verify.js - Verification gadget for Pickipedia claims | ||
* | * | ||
* Adds "Verify" buttons to {{claim}} and {{ | * Adds "Verify" buttons to {{claim}}, {{bot_proposes}}, and {{Show}} templates, | ||
* allowing logged-in users to verify content and record attestations. | * allowing logged-in users to verify content and record attestations. | ||
*/ | */ | ||
| Line 15: | Line 15: | ||
// Wait for page to be ready | // Wait for page to be ready | ||
mw.hook('wikipage.content').add(function($content) { | mw.hook('wikipage.content').add(function($content) { | ||
// Find all claim and bot_proposes elements | // Find all claim and bot_proposes elements (inline) | ||
var $claims = $content.find('.claim-unverified, .bot-proposal'); | var $claims = $content.find('.claim-unverified, span.bot-proposal'); | ||
$claims.each(function() { | $claims.each(function() { | ||
| Line 42: | Line 42: | ||
e.preventDefault(); | e.preventDefault(); | ||
e.stopPropagation(); | e.stopPropagation(); | ||
openVerifyDialog($claim); | openVerifyDialog($claim, false); | ||
}); | }); | ||
$claim.append(' ').append($btn); | $claim.append(' ').append($btn); | ||
}); | |||
// Find Show infoboxes that need verification | |||
var $shows = $content.find('.show-infobox.bot-proposal'); | |||
$shows.each(function() { | |||
var $show = $(this); | |||
// Don't add button twice | |||
if ($show.find('.verify-show-btn').length > 0) { | |||
return; | |||
} | |||
var $btn = $('<button>') | |||
.addClass('verify-show-btn') | |||
.text('✓ Verify This Show') | |||
.css({ | |||
'display': 'block', | |||
'width': '100%', | |||
'margin-top': '0.5em', | |||
'padding': '6px 12px', | |||
'cursor': 'pointer', | |||
'background': '#f8f9fa', | |||
'border': '1px solid #a2a9b1', | |||
'border-radius': '3px', | |||
'font-size': '0.9em' | |||
}) | |||
.on('click', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
openVerifyDialog($show, true); | |||
}); | |||
$show.append($btn); | |||
}); | }); | ||
}); | }); | ||
function openVerifyDialog($ | function openVerifyDialog($element, isShow) { | ||
var claimText = $ | var claimText; | ||
var existingSource = $ | if (isShow) { | ||
var isBotProposal = $ | // For shows, build a summary from the infobox content | ||
var artists = $element.find('div:contains("Artists:")').text().replace('Artists:', '').trim(); | |||
var venue = $element.find('div:contains("Venue:")').text().replace('Venue:', '').trim(); | |||
var block = $element.find('div:contains("Block:")').text().replace('Block:', '').trim(); | |||
claimText = artists + ' @ ' + venue + ' (block ' + block + ')'; | |||
} else { | |||
claimText = $element.clone().children('.verify-btn').remove().end().text().trim(); | |||
} | |||
var existingSource = $element.data('source') || ''; | |||
var isBotProposal = $element.hasClass('bot-proposal'); | |||
// Create dialog content | // Create dialog content | ||
| Line 59: | Line 103: | ||
var $form = $('<form>').on('submit', function(e) { | var $form = $('<form>').on('submit', function(e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
submitVerification($ | if (isShow) { | ||
submitShowVerification($element, $form, claimText, $overlay, $dialog); | |||
} else { | |||
submitVerification($element, $form, claimText, $overlay, $dialog); | |||
} | |||
}); | }); | ||
| Line 67: | Line 115: | ||
$form.append( | $form.append( | ||
$('<label>').attr('for', 'verify-source').text('Source ( | $('<label>').attr('for', 'verify-source').text('Source (optional):'), | ||
$('<br>'), | $('<br>'), | ||
$('<input>') | $('<input>') | ||
| Line 74: | Line 122: | ||
id: 'verify-source', | id: 'verify-source', | ||
name: 'source', | name: 'source', | ||
placeholder: 'https://... or "I was there" or | placeholder: 'https://... or "I was there" or leave blank' | ||
}) | }) | ||
.val(existingSource || '') | .val(existingSource || '') | ||
| Line 164: | Line 212: | ||
} | } | ||
function | function submitShowVerification($show, $form, claimText, $overlay, $dialog) { | ||
var source = $form.find('#verify-source').val().trim(); | var source = $form.find('#verify-source').val().trim(); | ||
var note = $form.find('#verify-note').val().trim(); | var note = $form.find('#verify-note').val().trim(); | ||
| Line 171: | Line 219: | ||
var timestamp = new Date().toISOString().split('T')[0]; | var timestamp = new Date().toISOString().split('T')[0]; | ||
// | // Disable form while processing | ||
$form.find('button').prop('disabled', true); | |||
mw.notify(' | $form.find('button[type="submit"]').text('Verifying...'); | ||
return; | |||
var api = new mw.Api(); | |||
api.get({ | |||
action: 'query', | |||
titles: pageName, | |||
prop: 'revisions', | |||
rvprop: 'content', | |||
rvslots: 'main', | |||
formatversion: 2 | |||
}).then(function(data) { | |||
var page = data.query.pages[0]; | |||
var content = page.revisions[0].slots.main.content; | |||
// For Show template, change status=proposed to status=verified | |||
var newContent = content | |||
.replace(/\|status=proposed/g, '|status=verified') | |||
.replace(/\|by=[^\n|}]*/g, '|by=' + userName) | |||
.replace(/\|source=[^\n|}]*/g, '|source=' + (source || 'verified by ' + userName)); | |||
if (newContent === content) { | |||
mw.notify('Could not update show status. It may have already been verified.', { type: 'error' }); | |||
$form.find('button').prop('disabled', false); | |||
$form.find('button[type="submit"]').text('Confirm Verification'); | |||
return; | |||
} | |||
var editSummary = 'Verified show: ' + claimText.substring(0, 50); | |||
return api.postWithToken('csrf', { | |||
action: 'edit', | |||
title: pageName, | |||
text: newContent, | |||
summary: editSummary | |||
}).then(function() { | |||
// Add to talk page | |||
return addTalkPageEntry(api, pageName, claimText, source, note, userName, timestamp); | |||
}).then(function() { | |||
mw.notify('Show verified!', { type: 'success' }); | |||
$overlay.remove(); | |||
$dialog.remove(); | |||
// Reload to show changes | |||
location.reload(); | |||
}); | |||
}).catch(function(err) { | |||
mw.notify('Error: ' + err, { type: 'error' }); | |||
$form.find('button').prop('disabled', false); | |||
$form.find('button[type="submit"]').text('Confirm Verification'); | |||
}); | |||
} | |||
/** | |||
* Strip wikitext formatting for comparison purposes. | |||
* Removes bold ('''), italic (''), and common markup. | |||
*/ | |||
function stripWikitext(text) { | |||
return text | |||
.replace(/'{2,}/g, '') // Remove '' and ''' | |||
.replace(/\[\[([^\]|]+\|)?([^\]]+)\]\]/g, '$2') // [[link|text]] -> text | |||
.replace(/\{\{!\}\}/g, '|') // {{!}} -> | | |||
.replace(/\s+/g, ' ') | |||
.trim() | |||
.toLowerCase(); | |||
} | |||
/** | |||
* Find a Bot_proposes or claim template in wikitext that contains the given text. | |||
* Returns {start, end, content} or null if not found. | |||
* Properly handles nested templates like {{!}}. | |||
*/ | |||
function findTemplateContaining(wikitext, searchText) { | |||
// Normalize searchText for comparison - collapse whitespace | |||
var normalizedSearch = stripWikitext(searchText).substring(0, 40); | |||
// Find all {{Bot_proposes or {{claim occurrences | |||
var templateRegex = /\{\{(Bot_proposes|claim)\s*\|/gi; | |||
var match; | |||
while ((match = templateRegex.exec(wikitext)) !== null) { | |||
var startIndex = match.index; | |||
var depth = 0; | |||
var i = startIndex; | |||
// Find the matching closing }} | |||
while (i < wikitext.length - 1) { | |||
if (wikitext[i] === '{' && wikitext[i + 1] === '{') { | |||
depth++; | |||
i += 2; | |||
} else if (wikitext[i] === '}' && wikitext[i + 1] === '}') { | |||
depth--; | |||
if (depth === 0) { | |||
var endIndex = i + 2; | |||
var templateContent = wikitext.substring(startIndex, endIndex); | |||
// Extract the actual content (first parameter, before |by= or |source=) | |||
var inner = templateContent.replace(/^\{\{(Bot_proposes|claim)\s*\|\s*/i, '').replace(/\}\}$/, ''); | |||
// Remove |by=... and |source=... parameters | |||
inner = inner.replace(/\|by=[^|}]*/gi, '').replace(/\|source=[^|}]*/gi, ''); | |||
// Strip wikitext formatting for comparison | |||
var normalizedInner = stripWikitext(inner); | |||
// Check if this template contains our search text | |||
if (normalizedInner.indexOf(normalizedSearch) !== -1) { | |||
return { | |||
start: startIndex, | |||
end: endIndex, | |||
fullMatch: templateContent, | |||
content: inner.trim() | |||
}; | |||
} | |||
break; | |||
} | |||
i += 2; | |||
} else { | |||
i++; | |||
} | |||
} | |||
} | } | ||
return null; | |||
} | |||
function submitVerification($claim, $form, claimText, $overlay, $dialog) { | |||
var source = $form.find('#verify-source').val().trim(); | |||
var note = $form.find('#verify-note').val().trim(); | |||
var pageName = mw.config.get('wgPageName'); | |||
var userName = mw.config.get('wgUserName'); | |||
var timestamp = new Date().toISOString().split('T')[0]; | |||
// Disable form while processing | // Disable form while processing | ||
| Line 194: | Line 368: | ||
var content = page.revisions[0].slots.main.content; | var content = page.revisions[0].slots.main.content; | ||
// | // Find the template containing this claim text | ||
var | var found = findTemplateContaining(content, claimText); | ||
if (!found) { | |||
mw.notify('Could not find the claim in page source. It may have been modified.', { type: 'error' }); | |||
$form.find('button').prop('disabled', false); | |||
$form.find('button[type="submit"]').text('Confirm Verification'); | |||
return; | |||
} | |||
// Build replacement - use {{source|...}} for URLs, {{verified|...}} for text | // Build replacement - use {{source|...}} for URLs, {{verified|...}} for text | ||
var replacement; | var replacement; | ||
var cleanContent = found.content; | |||
if (source && source.match(/^https?:\/\//)) { | if (source && source.match(/^https?:\/\//)) { | ||
replacement = | replacement = cleanContent + '{{source|' + source + '}}'; | ||
} else if (source) { | } else if (source) { | ||
replacement = | replacement = cleanContent + '{{verified|' + source + '}}'; | ||
} else { | } else { | ||
replacement = | replacement = cleanContent + '{{verified|by ' + userName + '}}'; | ||
} | } | ||
var newContent = content. | var newContent = content.substring(0, found.start) + replacement + content.substring(found.end); | ||
var editSummary = 'Verified: "' + claimText.substring(0, 50) + (claimText.length > 50 ? '...' : '') + '"'; | var editSummary = 'Verified: "' + claimText.substring(0, 50) + (claimText.length > 50 ? '...' : '') + '"'; | ||
Latest revision as of 21:17, 7 January 2026
/**
* Gadget-verify.js - Verification gadget for Pickipedia claims
*
* Adds "Verify" buttons to {{claim}}, {{bot_proposes}}, and {{Show}} templates,
* allowing logged-in users to verify content and record attestations.
*/
(function() {
'use strict';
// Only run for logged-in users
if (mw.config.get('wgUserName') === null) {
return;
}
// Wait for page to be ready
mw.hook('wikipage.content').add(function($content) {
// Find all claim and bot_proposes elements (inline)
var $claims = $content.find('.claim-unverified, span.bot-proposal');
$claims.each(function() {
var $claim = $(this);
// Don't add button twice
if ($claim.find('.verify-btn').length > 0) {
return;
}
var $btn = $('<button>')
.addClass('verify-btn')
.text('✓ Verify')
.css({
'margin-left': '0.5em',
'font-size': '0.8em',
'padding': '1px 6px',
'cursor': 'pointer',
'background': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '3px',
'vertical-align': 'middle'
})
.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
openVerifyDialog($claim, false);
});
$claim.append(' ').append($btn);
});
// Find Show infoboxes that need verification
var $shows = $content.find('.show-infobox.bot-proposal');
$shows.each(function() {
var $show = $(this);
// Don't add button twice
if ($show.find('.verify-show-btn').length > 0) {
return;
}
var $btn = $('<button>')
.addClass('verify-show-btn')
.text('✓ Verify This Show')
.css({
'display': 'block',
'width': '100%',
'margin-top': '0.5em',
'padding': '6px 12px',
'cursor': 'pointer',
'background': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '3px',
'font-size': '0.9em'
})
.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
openVerifyDialog($show, true);
});
$show.append($btn);
});
});
function openVerifyDialog($element, isShow) {
var claimText;
if (isShow) {
// For shows, build a summary from the infobox content
var artists = $element.find('div:contains("Artists:")').text().replace('Artists:', '').trim();
var venue = $element.find('div:contains("Venue:")').text().replace('Venue:', '').trim();
var block = $element.find('div:contains("Block:")').text().replace('Block:', '').trim();
claimText = artists + ' @ ' + venue + ' (block ' + block + ')';
} else {
claimText = $element.clone().children('.verify-btn').remove().end().text().trim();
}
var existingSource = $element.data('source') || '';
var isBotProposal = $element.hasClass('bot-proposal');
// Create dialog content
var $dialog = $('<div>').addClass('verify-dialog');
var $form = $('<form>').on('submit', function(e) {
e.preventDefault();
if (isShow) {
submitShowVerification($element, $form, claimText, $overlay, $dialog);
} else {
submitVerification($element, $form, claimText, $overlay, $dialog);
}
});
$form.append(
$('<p>').css('margin-top', '0').html('<strong>Verifying:</strong> ' + mw.html.escape(claimText))
);
$form.append(
$('<label>').attr('for', 'verify-source').text('Source (optional):'),
$('<br>'),
$('<input>')
.attr({
type: 'text',
id: 'verify-source',
name: 'source',
placeholder: 'https://... or "I was there" or leave blank'
})
.val(existingSource || '')
.css({ width: '100%', marginBottom: '1em', padding: '6px', boxSizing: 'border-box' })
);
if (isBotProposal && existingSource) {
$form.find('label[for="verify-source"]').after(
$('<div>').css({ fontSize: '0.9em', color: '#666', marginBottom: '0.5em' })
.text('Bot suggested source: ' + existingSource)
);
}
$form.append(
$('<label>').attr('for', 'verify-note').text('Note (optional):'),
$('<br>'),
$('<textarea>')
.attr({
id: 'verify-note',
name: 'note',
rows: 3,
placeholder: 'Additional context about your verification...'
})
.css({ width: '100%', marginBottom: '1em', padding: '6px', boxSizing: 'border-box' })
);
$form.append(
$('<button>')
.attr('type', 'submit')
.text('Confirm Verification')
.css({
padding: '8px 16px',
background: '#36c',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
marginRight: '8px'
}),
$('<button>')
.attr('type', 'button')
.text('Cancel')
.css({
padding: '8px 16px',
background: '#f8f9fa',
border: '1px solid #a2a9b1',
borderRadius: '3px',
cursor: 'pointer'
})
.on('click', function() {
$overlay.remove();
$dialog.remove();
})
);
$dialog.append($form);
// Style the dialog
$dialog.css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0,0,0,0.3)',
zIndex: 10000,
minWidth: '400px',
maxWidth: '90vw'
});
// Add overlay
var $overlay = $('<div>').css({
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
zIndex: 9999
}).on('click', function() {
$overlay.remove();
$dialog.remove();
});
$('body').append($overlay).append($dialog);
$dialog.find('#verify-source').focus();
}
function submitShowVerification($show, $form, claimText, $overlay, $dialog) {
var source = $form.find('#verify-source').val().trim();
var note = $form.find('#verify-note').val().trim();
var pageName = mw.config.get('wgPageName');
var userName = mw.config.get('wgUserName');
var timestamp = new Date().toISOString().split('T')[0];
// Disable form while processing
$form.find('button').prop('disabled', true);
$form.find('button[type="submit"]').text('Verifying...');
var api = new mw.Api();
api.get({
action: 'query',
titles: pageName,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var page = data.query.pages[0];
var content = page.revisions[0].slots.main.content;
// For Show template, change status=proposed to status=verified
var newContent = content
.replace(/\|status=proposed/g, '|status=verified')
.replace(/\|by=[^\n|}]*/g, '|by=' + userName)
.replace(/\|source=[^\n|}]*/g, '|source=' + (source || 'verified by ' + userName));
if (newContent === content) {
mw.notify('Could not update show status. It may have already been verified.', { type: 'error' });
$form.find('button').prop('disabled', false);
$form.find('button[type="submit"]').text('Confirm Verification');
return;
}
var editSummary = 'Verified show: ' + claimText.substring(0, 50);
return api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: newContent,
summary: editSummary
}).then(function() {
// Add to talk page
return addTalkPageEntry(api, pageName, claimText, source, note, userName, timestamp);
}).then(function() {
mw.notify('Show verified!', { type: 'success' });
$overlay.remove();
$dialog.remove();
// Reload to show changes
location.reload();
});
}).catch(function(err) {
mw.notify('Error: ' + err, { type: 'error' });
$form.find('button').prop('disabled', false);
$form.find('button[type="submit"]').text('Confirm Verification');
});
}
/**
* Strip wikitext formatting for comparison purposes.
* Removes bold ('''), italic (''), and common markup.
*/
function stripWikitext(text) {
return text
.replace(/'{2,}/g, '') // Remove '' and '''
.replace(/\[\[([^\]|]+\|)?([^\]]+)\]\]/g, '$2') // [[link|text]] -> text
.replace(/\{\{!\}\}/g, '|') // {{!}} -> |
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
/**
* Find a Bot_proposes or claim template in wikitext that contains the given text.
* Returns {start, end, content} or null if not found.
* Properly handles nested templates like {{!}}.
*/
function findTemplateContaining(wikitext, searchText) {
// Normalize searchText for comparison - collapse whitespace
var normalizedSearch = stripWikitext(searchText).substring(0, 40);
// Find all {{Bot_proposes or {{claim occurrences
var templateRegex = /\{\{(Bot_proposes|claim)\s*\|/gi;
var match;
while ((match = templateRegex.exec(wikitext)) !== null) {
var startIndex = match.index;
var depth = 0;
var i = startIndex;
// Find the matching closing }}
while (i < wikitext.length - 1) {
if (wikitext[i] === '{' && wikitext[i + 1] === '{') {
depth++;
i += 2;
} else if (wikitext[i] === '}' && wikitext[i + 1] === '}') {
depth--;
if (depth === 0) {
var endIndex = i + 2;
var templateContent = wikitext.substring(startIndex, endIndex);
// Extract the actual content (first parameter, before |by= or |source=)
var inner = templateContent.replace(/^\{\{(Bot_proposes|claim)\s*\|\s*/i, '').replace(/\}\}$/, '');
// Remove |by=... and |source=... parameters
inner = inner.replace(/\|by=[^|}]*/gi, '').replace(/\|source=[^|}]*/gi, '');
// Strip wikitext formatting for comparison
var normalizedInner = stripWikitext(inner);
// Check if this template contains our search text
if (normalizedInner.indexOf(normalizedSearch) !== -1) {
return {
start: startIndex,
end: endIndex,
fullMatch: templateContent,
content: inner.trim()
};
}
break;
}
i += 2;
} else {
i++;
}
}
}
return null;
}
function submitVerification($claim, $form, claimText, $overlay, $dialog) {
var source = $form.find('#verify-source').val().trim();
var note = $form.find('#verify-note').val().trim();
var pageName = mw.config.get('wgPageName');
var userName = mw.config.get('wgUserName');
var timestamp = new Date().toISOString().split('T')[0];
// Disable form while processing
$form.find('button').prop('disabled', true);
$form.find('button[type="submit"]').text('Verifying...');
var api = new mw.Api();
api.get({
action: 'query',
titles: pageName,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var page = data.query.pages[0];
var content = page.revisions[0].slots.main.content;
// Find the template containing this claim text
var found = findTemplateContaining(content, claimText);
if (!found) {
mw.notify('Could not find the claim in page source. It may have been modified.', { type: 'error' });
$form.find('button').prop('disabled', false);
$form.find('button[type="submit"]').text('Confirm Verification');
return;
}
// Build replacement - use {{source|...}} for URLs, {{verified|...}} for text
var replacement;
var cleanContent = found.content;
if (source && source.match(/^https?:\/\//)) {
replacement = cleanContent + '{{source|' + source + '}}';
} else if (source) {
replacement = cleanContent + '{{verified|' + source + '}}';
} else {
replacement = cleanContent + '{{verified|by ' + userName + '}}';
}
var newContent = content.substring(0, found.start) + replacement + content.substring(found.end);
var editSummary = 'Verified: "' + claimText.substring(0, 50) + (claimText.length > 50 ? '...' : '') + '"';
return api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: newContent,
summary: editSummary
}).then(function() {
// Add to talk page
return addTalkPageEntry(api, pageName, claimText, source, note, userName, timestamp);
}).then(function() {
mw.notify('Verification recorded!', { type: 'success' });
$overlay.remove();
$dialog.remove();
// Reload to show changes
location.reload();
});
}).catch(function(err) {
mw.notify('Error: ' + err, { type: 'error' });
$form.find('button').prop('disabled', false);
$form.find('button[type="submit"]').text('Confirm Verification');
});
}
function addTalkPageEntry(api, pageName, claimText, source, note, userName, timestamp) {
var talkPage = 'Talk:' + pageName.replace(/^Talk:/, '').replace(/_/g, ' ');
var entry = '\n== Verification by ' + userName + ' (' + timestamp + ') ==\n';
entry += "'''Claim:''' " + claimText + '\n\n';
if (source) {
entry += "'''Source:''' " + source + '\n';
}
if (note) {
entry += "\n'''Note:''' " + note + '\n';
}
return api.postWithToken('csrf', {
action: 'edit',
title: talkPage,
appendtext: entry,
summary: 'Recorded verification of claim'
});
}
})();