(function() {
'use strict';
class WikipediaSourceVerifier {
constructor() {
this.providers = {
claude: {
name: 'Claude',
storageKey: 'claude_api_key',
color: '#0645ad',
model: 'claude-sonnet-4-20250514'
},
gemini: {
name: 'Gemini',
storageKey: 'gemini_api_key',
color: '#4285F4',
model: 'gemini-2.5-flash-preview-05-20'
},
openai: {
name: 'ChatGPT',
storageKey: 'openai_api_key',
color: '#10a37f',
model: 'gpt-4o'
}
};
this.currentProvider = localStorage.getItem('source_verifier_provider') || 'claude';
this.sidebarWidth = localStorage.getItem('verifier_sidebar_width') || '400px';
this.isVisible = localStorage.getItem('verifier_sidebar_visible') !== 'false';
this.currentResults = localStorage.getItem('verifier_current_results') || '';
this.buttons = {};
this.activeClaim = null;
this.activeSource = null;
this.init();
}
init() {
this.loadOOUI().then(() => {
this.createUI();
this.attachEventListeners();
this.attachReferenceClickHandlers();
this.adjustMainContent();
});
}
async loadOOUI() {
await mw.loader.using(['oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows']);
}
getCurrentApiKey() {
return localStorage.getItem(this.providers[this.currentProvider].storageKey);
}
setCurrentApiKey(key) {
localStorage.setItem(this.providers[this.currentProvider].storageKey, key);
}
removeCurrentApiKey() {
localStorage.removeItem(this.providers[this.currentProvider].storageKey);
}
getCurrentColor() {
return this.providers[this.currentProvider].color;
}
createUI() {
const sidebar = document.createElement('div');
sidebar.id = 'source-verifier-sidebar';
this.createOOUIButtons();
sidebar.innerHTML = `
<div id="verifier-sidebar-header">
<h3>Source Verifier</h3>
<div id="verifier-sidebar-controls">
<div id="verifier-close-btn-container"></div>
</div>
</div>
<div id="verifier-sidebar-content">
<div id="verifier-controls">
<div id="verifier-provider-container"></div>
<div id="verifier-buttons-container"></div>
</div>
<div id="verifier-claim-section">
<h4>Selected Claim</h4>
<div id="verifier-claim-text">Click on a reference number [1] next to a claim to verify it against its source.</div>
</div>
<div id="verifier-source-section">
<h4>Source Content</h4>
<div id="verifier-source-text">No source loaded yet.</div>
</div>
<div id="verifier-results">
<h4>Verification Result</h4>
<div id="verifier-output">${this.currentResults}</div>
</div>
</div>
<div id="verifier-resize-handle"></div>
`;
this.createVerifierTab();
this.createStyles();
document.body.append(sidebar);
this.appendOOUIButtons();
if (!this.isVisible) {
this.hideSidebar();
}
this.makeResizable();
}
createStyles() {
const style = document.createElement('style');
style.textContent = `
#source-verifier-sidebar {
position: fixed;
top: 0;
right: 0;
width: ${this.sidebarWidth};
height: 100vh;
background: #fff;
border-left: 2px solid ${this.getCurrentColor()};
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
#verifier-sidebar-header {
background: ${this.getCurrentColor()};
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
#verifier-sidebar-header h3 {
margin: 0;
font-size: 16px;
}
#verifier-sidebar-controls {
display: flex;
gap: 8px;
}
#verifier-sidebar-content {
padding: 15px;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
}
#verifier-controls {
flex-shrink: 0;
}
#verifier-provider-container {
margin-bottom: 10px;
}
#verifier-buttons-container {
display: flex;
flex-direction: column;
gap: 8px;
}
#verifier-buttons-container .oo-ui-buttonElement {
width: 100%;
}
#verifier-buttons-container .oo-ui-buttonElement-button {
width: 100%;
justify-content: center;
}
#verifier-claim-section, #verifier-source-section, #verifier-results {
flex-shrink: 0;
}
#verifier-claim-section h4, #verifier-source-section h4, #verifier-results h4 {
margin: 0 0 8px 0;
color: ${this.getCurrentColor()};
font-size: 14px;
font-weight: bold;
}
#verifier-claim-text, #verifier-source-text {
padding: 10px;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
max-height: 120px;
overflow-y: auto;
}
#verifier-output {
padding: 10px;
background: #fafafa;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
line-height: 1.5;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
}
#verifier-output h1, #verifier-output h2, #verifier-output h3 {
color: ${this.getCurrentColor()};
margin-top: 16px;
margin-bottom: 8px;
}
#verifier-output h1 { font-size: 1.3em; }
#verifier-output h2 { font-size: 1.2em; }
#verifier-output h3 { font-size: 1.1em; }
#verifier-output ul, #verifier-output ol {
padding-left: 18px;
}
#verifier-output p {
margin-bottom: 10px;
}
#verifier-output strong {
color: #d33;
}
#verifier-resize-handle {
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
background: transparent;
cursor: ew-resize;
z-index: 10001;
}
#verifier-resize-handle:hover {
background: ${this.getCurrentColor()};
opacity: 0.5;
}
#ca-verifier {
display: none;
}
#ca-verifier a {
color: ${this.getCurrentColor()} !important;
text-decoration: none !important;
padding: 0.5em !important;
}
#ca-verifier a:hover {
text-decoration: underline !important;
}
body {
margin-right: ${this.isVisible ? this.sidebarWidth : '0'};
transition: margin-right 0.3s ease;
}
.verifier-error {
color: #d33;
background: #fef2f2;
border: 1px solid #fecaca;
padding: 8px;
border-radius: 4px;
}
.verifier-sidebar-hidden body {
margin-right: 0 !important;
}
.verifier-sidebar-hidden #source-verifier-sidebar {
display: none;
}
.verifier-sidebar-hidden #ca-verifier {
display: list-item !important;
}
.reference:hover {
background-color: #e6f3ff;
cursor: pointer;
}
.reference.verifier-active {
background-color: ${this.getCurrentColor()};
color: white;
}
.claim-highlight {
background-color: #fff3cd;
border-left: 3px solid ${this.getCurrentColor()};
padding-left: 5px;
margin-left: -8px;
}
`;
document.head.appendChild(style);
}
createOOUIButtons() {
this.buttons.close = new OO.ui.ButtonWidget({
icon: 'close',
title: 'Close',
framed: false,
classes: ['verifier-close-button']
});
// Provider selector
this.buttons.providerSelect = new OO.ui.DropdownWidget({
menu: {
items: Object.keys(this.providers).map(key =>
new OO.ui.MenuOptionWidget({
data: key,
label: this.providers[key].name
})
)
}
});
this.buttons.providerSelect.getMenu().selectItemByData(this.currentProvider);
this.buttons.setKey = new OO.ui.ButtonWidget({
label: 'Set API Key',
flags: ['primary', 'progressive'],
disabled: false
});
this.buttons.verify = new OO.ui.ButtonWidget({
label: 'Verify Claim',
flags: ['primary', 'progressive'],
icon: 'check',
disabled: true
});
this.buttons.changeKey = new OO.ui.ButtonWidget({
label: 'Change Key',
flags: ['safe'],
icon: 'edit',
disabled: false
});
this.buttons.removeKey = new OO.ui.ButtonWidget({
label: 'Remove API Key',
flags: ['destructive'],
icon: 'trash',
disabled: false
});
this.updateButtonVisibility();
}
appendOOUIButtons() {
document.getElementById('verifier-close-btn-container').appendChild(this.buttons.close.$element[0]);
document.getElementById('verifier-provider-container').appendChild(this.buttons.providerSelect.$element[0]);
const container = document.getElementById('verifier-buttons-container');
if (this.getCurrentApiKey()) {
container.appendChild(this.buttons.verify.$element[0]);
container.appendChild(this.buttons.changeKey.$element[0]);
container.appendChild(this.buttons.removeKey.$element[0]);
} else {
container.appendChild(this.buttons.setKey.$element[0]);
}
}
updateButtonVisibility() {
const container = document.getElementById('verifier-buttons-container');
if (!container) return;
container.innerHTML = '';
if (this.getCurrentApiKey()) {
this.buttons.verify.setDisabled(!this.activeClaim || !this.activeSource);
container.appendChild(this.buttons.verify.$element[0]);
container.appendChild(this.buttons.changeKey.$element[0]);
container.appendChild(this.buttons.removeKey.$element[0]);
} else {
this.buttons.verify.setDisabled(true);
container.appendChild(this.buttons.setKey.$element[0]);
}
}
createVerifierTab() {
if (typeof mw !== 'undefined' && mw.config.get('wgNamespaceNumber') === 0) {
let portletId = 'p-namespaces';
if (mw.config.get('skin') === 'vector-2022') {
portletId = 'p-associated-pages';
}
const verifierLink = mw.util.addPortletLink(
portletId,
'#',
'Verify',
't-verifier',
'Verify claims against sources',
'v',
);
verifierLink.addEventListener('click', (e) => {
e.preventDefault();
this.showSidebar();
});
}
}
attachReferenceClickHandlers() {
// Attach click handlers to all reference links
const references = document.querySelectorAll('.reference a');
references.forEach(ref => {
ref.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.handleReferenceClick(ref);
});
});
}
async handleReferenceClick(refElement) {
try {
// Clear previous highlights
this.clearHighlights();
// Show sidebar if hidden
this.showSidebar();
// Extract claim text (sentence before the reference)
const claim = this.extractClaimText(refElement);
if (!claim) {
this.updateStatus('Could not extract claim text', true);
return;
}
// Highlight the claim in the article
this.highlightClaim(refElement, claim);
// Mark this reference as active
refElement.parentElement.classList.add('verifier-active');
// Extract reference URL
const refUrl = this.extractReferenceUrl(refElement);
if (!refUrl) {
this.updateStatus('Could not extract reference URL', true);
return;
}
// Update UI with claim
this.activeClaim = claim;
document.getElementById('verifier-claim-text').textContent = claim;
// Fetch source content
this.updateStatus('Fetching source content...');
const sourceContent = await this.fetchSourceContent(refUrl);
if (!sourceContent) {
this.updateStatus('Could not fetch source content', true);
return;
}
// Update UI with source
this.activeSource = sourceContent;
const sourceElement = document.getElementById('verifier-source-text');
sourceElement.textContent = sourceContent.substring(0, 1000) + (sourceContent.length > 1000 ? '...' : '');
// Enable verify button
this.updateButtonVisibility();
this.updateStatus('Ready to verify claim against source');
} catch (error) {
console.error('Error handling reference click:', error);
this.updateStatus(`Error: ${error.message}`, true);
}
}
extractClaimText(refElement) {
// Find the text node containing the reference
let currentNode = refElement.parentElement.previousSibling;
let claim = '';
// Look backwards to find the start of the sentence
while (currentNode) {
if (currentNode.nodeType === Node.TEXT_NODE) {
const text = currentNode.textContent;
claim = text + claim;
// Stop at sentence boundaries
const sentenceEnd = text.match(/[.!?]\s*$/);
if (sentenceEnd) {
break;
}
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
// Include text from elements but stop at certain boundaries
if (currentNode.tagName && ['BR', 'P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(currentNode.tagName)) {
break;
}
claim = currentNode.textContent + claim;
}
currentNode = currentNode.previousSibling;
}
// Clean up the claim text
claim = claim.trim();
// If we didn't find a sentence boundary, try to extract from current element
if (!claim || claim.length < 10) {
const parentElement = refElement.closest('p, li, td, div');
if (parentElement) {
const fullText = parentElement.textContent;
const refIndex = fullText.indexOf('[');
if (refIndex > 0) {
claim = fullText.substring(0, refIndex).trim();
// Find the last sentence before the reference
const sentences = claim.split(/[.!?]/);
if (sentences.length > 1) {
claim = sentences[sentences.length - 2] + '.';
}
}
}
}
return claim.trim();
}
extractReferenceUrl(refElement) {
// Get the reference ID from the href
const href = refElement.getAttribute('href');
if (!href || !href.startsWith('#')) {
return null;
}
const refId = href.substring(1);
const refTarget = document.getElementById(refId);
if (!refTarget) {
return null;
}
// Look for URLs in the reference
const links = refTarget.querySelectorAll('a[href^="http"]');
if (links.length === 0) {
return null;
}
// Return the first external URL found
return links[0].href;
}
async fetchSourceContent(url) {
try {
// Use CORS proxy for external URLs
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(url)}`;
const response = await fetch(proxyUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; WikipediaSourceVerifier/1.0)'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
// Extract text content from HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Remove script and style elements
const unwanted = doc.querySelectorAll('script, style, nav, header, footer, aside, .ad, .advertisement');
unwanted.forEach(el => el.remove());
// Try to find main content
let mainContent = doc.querySelector('main, article, .content, .post-content, .article-content, .story-body')
|| doc.querySelector('body');
if (!mainContent) {
mainContent = doc.body;
}
let text = mainContent.textContent || mainContent.innerText || '';
// Clean up the text
text = text.replace(/\s+/g, ' ').trim();
// Limit length to avoid token limits
if (text.length > 10000) {
text = text.substring(0, 10000);
}
return text;
} catch (error) {
console.error('Error fetching source content:', error);
throw new Error(`Failed to fetch source: ${error.message}`);
}
}
highlightClaim(refElement, claim) {
const parentElement = refElement.closest('p, li, td, div');
if (parentElement && !parentElement.classList.contains('claim-highlight')) {
parentElement.classList.add('claim-highlight');
}
}
clearHighlights() {
// Remove active reference highlighting
document.querySelectorAll('.reference.verifier-active').forEach(el => {
el.classList.remove('verifier-active');
});
// Remove claim highlighting
document.querySelectorAll('.claim-highlight').forEach(el => {
el.classList.remove('claim-highlight');
});
}
makeResizable() {
const handle = document.getElementById('verifier-resize-handle');
const sidebar = document.getElementById('source-verifier-sidebar');
if (!handle || !sidebar) return;
let isResizing = false;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isResizing) return;
const newWidth = window.innerWidth - e.clientX;
const minWidth = 300;
const maxWidth = window.innerWidth * 0.8;
if (newWidth >= minWidth && newWidth <= maxWidth) {
const widthPx = newWidth + 'px';
sidebar.style.width = widthPx;
document.body.style.marginRight = widthPx;
this.sidebarWidth = widthPx;
localStorage.setItem('verifier_sidebar_width', widthPx);
}
};
const handleMouseUp = () => {
isResizing = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
showSidebar() {
const verifierTab = document.getElementById('ca-verifier');
document.body.classList.remove('verifier-sidebar-hidden');
if (verifierTab) verifierTab.style.display = 'none';
document.body.style.marginRight = this.sidebarWidth;
this.isVisible = true;
localStorage.setItem('verifier_sidebar_visible', 'true');
}
hideSidebar() {
const verifierTab = document.getElementById('ca-verifier');
document.body.classList.add('verifier-sidebar-hidden');
if (verifierTab) verifierTab.style.display = 'list-item';
document.body.style.marginRight = '0';
this.clearHighlights();
this.isVisible = false;
localStorage.setItem('verifier_sidebar_visible', 'false');
}
adjustMainContent() {
if (this.isVisible) {
document.body.style.marginRight = this.sidebarWidth;
} else {
document.body.style.marginRight = '0';
}
}
attachEventListeners() {
this.buttons.close.on('click', () => {
this.hideSidebar();
});
this.buttons.providerSelect.getMenu().on('select', (item) => {
this.currentProvider = item.getData();
localStorage.setItem('source_verifier_provider', this.currentProvider);
this.updateButtonVisibility();
this.updateTheme();
this.updateStatus(`Switched to ${this.providers[this.currentProvider].name}`);
});
this.buttons.setKey.on('click', () => {
this.setApiKey();
});
this.buttons.changeKey.on('click', () => {
this.setApiKey();
});
this.buttons.verify.on('click', () => {
this.verifyClaim();
});
this.buttons.removeKey.on('click', () => {
this.removeApiKey();
});
}
updateTheme() {
const color = this.getCurrentColor();
// Update theme colors similar to the original implementation
// This would update sidebar border, header background, etc.
}
setApiKey() {
const provider = this.providers[this.currentProvider];
const dialog = new OO.ui.MessageDialog();
const textInput = new OO.ui.TextInputWidget({
placeholder: `Enter your ${provider.name} API Key...`,
type: 'password',
value: this.getCurrentApiKey() || ''
});
const windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog, {
title: `Set ${provider.name} API Key`,
message: $('<div>').append(
$('<p>').text(`Enter your ${provider.name} API Key to enable source verification:`),
textInput.$element
),
actions: [
{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
action: 'cancel',
label: 'Cancel',
flags: ['safe']
}
]
}).closed.then((data) => {
if (data && data.action === 'save') {
const key = textInput.getValue().trim();
if (key) {
this.setCurrentApiKey(key);
this.updateButtonVisibility();
this.updateStatus('API key set successfully!');
}
}
windowManager.destroy();
});
}
removeApiKey() {
OO.ui.confirm('Are you sure you want to remove the stored API key?').done((confirmed) => {
if (confirmed) {
this.removeCurrentApiKey();
this.updateButtonVisibility();
this.updateStatus('API key removed successfully!');
}
});
}
updateStatus(message, isError = false) {
// For now, we'll update the claim section to show status
// In a full implementation, you might want a dedicated status area
if (isError) {
console.error('Verifier Error:', message);
} else {
console.log('Verifier Status:', message);
}
}
async verifyClaim() {
if (!this.getCurrentApiKey() || !this.activeClaim || !this.activeSource) {
this.updateStatus('Missing API key, claim, or source content', true);
return;
}
try {
this.buttons.verify.setDisabled(true);
this.updateStatus('Verifying claim against source...');
const provider = this.providers[this.currentProvider];
let result;
switch (this.currentProvider) {
case 'claude':
result = await this.callClaudeAPI(this.activeClaim, this.activeSource);
break;
case 'gemini':
result = await this.callGeminiAPI(this.activeClaim, this.activeSource);
break;
case 'openai':
result = await this.callOpenAIAPI(this.activeClaim, this.activeSource);
break;
}
this.updateStatus('Verification complete!');
this.updateOutput(result, true);
} catch (error) {
console.error('Verification error:', error);
this.updateStatus(`Error: ${error.message}`, true);
} finally {
this.buttons.verify.setDisabled(false);
}
}
async callClaudeAPI(claim, source) {
const requestBody = {
model: this.providers.claude.model,
max_tokens: 2000,
system: `You are a fact-checking assistant. Your task is to verify whether a claim from a Wikipedia article is supported by the provided source content.
Instructions:
1. Analyze the claim and determine what specific facts it asserts
2. Search through the source content for information that supports or contradicts the claim
3. Provide a clear verdict: SUPPORTED, PARTIALLY SUPPORTED, NOT SUPPORTED, or UNCLEAR
4. Quote the specific sentences from the source that support your verdict
5. Explain any discrepancies or missing information
Be precise and objective in your analysis.`,
messages: [{
role: "user",
content: `Claim to verify: "${claim}"
Source content: "${source}"`
}]
};
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.getCurrentApiKey(),
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API request failed (${response.status}): ${errorText}`);
}
const data = await response.json();
return data.content[0].text;
}
async callGeminiAPI(claim, source) {
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${this.providers.gemini.model}:generateContent?key=${this.getCurrentApiKey()}`;
const systemPrompt = `You are a fact-checking assistant. Verify whether a Wikipedia claim is supported by the provided source content.
Provide:
1. A clear verdict: SUPPORTED, PARTIALLY SUPPORTED, NOT SUPPORTED, or UNCLEAR
2. Specific quotes from the source that support your verdict
3. Explanation of any discrepancies or missing information
Be precise and objective in your analysis.`;
const requestBody = {
contents: [{
parts: [{ "text": `Claim to verify: "${claim}"\n\nSource content: "${source}"` }],
}],
systemInstruction: {
parts: [{ "text": systemPrompt }]
},
generationConfig: {
maxOutputTokens: 2048,
temperature: 0.0,
},
};
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
const responseData = await response.json();
if (!response.ok) {
const errorDetail = responseData.error ? responseData.error.message : response.statusText;
throw new Error(`API request failed (${response.status}): ${errorDetail}`);
}
if (!responseData.candidates || !responseData.candidates[0] ||
!responseData.candidates[0].content || !responseData.candidates[0].content.parts ||
!responseData.candidates[0].content.parts[0] || !responseData.candidates[0].content.parts[0].text) {
throw new Error('Invalid API response format or no content generated.');
}
return responseData.candidates[0].content.parts[0].text;
}
async callOpenAIAPI(claim, source) {
const requestBody = {
model: this.providers.openai.model,
max_tokens: 2000,
messages: [
{
role: "system",
content: `You are a fact-checking assistant. Your task is to verify whether a claim from a Wikipedia article is supported by the provided source content.
Instructions:
1. Analyze the claim and determine what specific facts it asserts
2. Search through the source content for information that supports or contradicts the claim
3. Provide a clear verdict: SUPPORTED, PARTIALLY SUPPORTED, NOT SUPPORTED, or UNCLEAR
4. Quote the specific sentences from the source that support your verdict
5. Explain any discrepancies or missing information
Be precise and objective in your analysis.`
},
{
role: "user",
content: `Claim to verify: "${claim}"\n\nSource content: "${source}"`
}
],
temperature: 0.1
};
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getCurrentApiKey()}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error?.message || errorText;
} catch {
errorMessage = errorText;
}
throw new Error(`API request failed (${response.status}): ${errorMessage}`);
}
const data = await response.json();
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
throw new Error('Invalid API response format');
}
return data.choices[0].message.content;
}
updateOutput(content, isMarkdown = false) {
const outputEl = document.getElementById('verifier-output');
let processedContent = content;
if (isMarkdown) {
processedContent = this.markdownToHtml(content);
outputEl.innerHTML = processedContent;
} else {
outputEl.textContent = content;
}
if (content) {
this.currentResults = processedContent;
localStorage.setItem('verifier_current_results', this.currentResults);
} else {
this.currentResults = '';
localStorage.removeItem('verifier_current_results');
}
}
markdownToHtml(markdown) {
let html = markdown;
// Convert headers
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
// Convert bold and italic
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
// Convert lists
html = html.replace(/^\s*[\*\-] (.*$)/gim, '<li>$1</li>');
html = html.replace(/^\s*\d+\. (.*$)/gim, '<li>$1</li>');
// Wrap consecutive list items in ul tags
html = html.replace(/((<li>.*<\/li>\s*)+)/g, (match, p1) => {
return `<ul>${p1.replace(/\s*<li>/g,'<li>')}</ul>`;
});
// Convert paragraphs
html = html.split(/\n\s*\n/).map(paragraph => {
paragraph = paragraph.trim();
if (!paragraph) return '';
if (paragraph.startsWith('<h') || paragraph.startsWith('<ul') || paragraph.startsWith('<ol') || paragraph.startsWith('<li')) {
return paragraph;
}
return `<p>${paragraph.replace(/\n/g, '<br>')}</p>`;
}).join('');
// Clean up
html = html.replace(/<p>\s*(<(?:ul|ol|h[1-6])[^>]*>[\s\S]*?<\/(?:ul|ol|h[1-6])>)\s*<\/p>/gi, '$1');
html = html.replace(/<p>\s*<\/p>/gi, '');
return html;
}
}
// Initialize the source verifier when MediaWiki is ready
if (typeof mw !== 'undefined' && mw.config.get('wgNamespaceNumber') === 0) {
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows']).then(function() {
$(function() {
new WikipediaSourceVerifier();
});
});
}
})();