User:Polygnotus/DuplicateReferences.js: Difference between revisions

Content deleted Content added
No edit summary
Tag: Reverted
No edit summary
 
(87 intermediate revisions by the same user not shown)
Line 1:
//Testpage: https://en.wikipedia.org/wiki/User:Polygnotus/DuplicateReferencesTest
 
// <nowiki>
mw.loader.using(['mediawiki.util'], function () {
$(document).ready(function () {
 
if ((mw.config.get('wgNamespaceNumber') !== 0 && mw.config.get('wgPageName') !== 'User:Polygnotus/dupreftest') || mw.config.get('wgAction') !== 'view') {
const DEBUG = false;
 
function debug(...args) {
if (DEBUG) {
console.log('[DuplicateReferences]', ...args);
}
}
 
if (
mw.config.get('wgAction') !== 'view' ||
mw.config.get('wgDiffNewId') ||
mw.config.get('wgDiffOldId') ||
(mw.config.get('wgNamespaceNumber') !== 0 && mw.config.get('wgPageName') !== 'User:Polygnotus/DuplicateReferencesTest')
) {
debug("Not the correct page or action, script terminated");
return;
}
 
debug("Page title:", document.title);
debug("URL:", window.___location.href);
 
function findNextReflistDiv(element) {
let nextElement = element.nextElementSibling;
while (nextElement) {
if (nextElement.tagName.toLowerCase() === 'div' &&
(nextElement.classList.contains('reflist') || nextElement.classList.contains('mw-references-wrap'))) {
return nextElement;
}
nextElement = nextElement.nextElementSibling;
}
return null;
}
 
const referencesHeader = document.querySelector("h2#References");
if (!referencesHeader) {
debug("References heading not found, script terminated");
return;
}
 
letconst referencesHeadingcontainerDiv = documentreferencesHeader.getElementByIdclosest("Referencesdiv");
if (!referencesHeadingcontainerDiv) {
debug("Container div not found, script terminated");
return;
}
 
const reflistDiv = findNextReflistDiv(containerDiv);
if (!reflistDiv) {
debug("Reflist div not found, script terminated");
return;
}
 
const referencesList = reflistDiv.querySelector('ol.references');
if (!referencesList) {
debug("ol.references not found within reflist div");
return;
}
 
const style = document.createElement('style');
style.textContent = `
li:target { border: 1px dotted red; padding: 2px; background-color: #ffcccc !important;}
.duplicate-citation-highlight { background-color: #ffe6e6e1eeff; }
.duplicate-citation-hover { background-color: #ffcccccce0ff; border: 1px dotted blue; }
.duplicate-citation-clicked { border: 1px dotted red; padding: 2px; background-color: #ffe6e6; }
.groupedmw-referencescollapsible-toggle { marginfont-leftweight: normal; float: 20pxright; }
.groupedduplicate-references-toggletable { cursorwidth: pointer; color: blue100%; }
.grouped-references-content@media {only displayscreen and (max-width: none;768px) }{
.groupedduplicate-references-content.expandedtable { display: blocknone; }
}
`;
document.head.appendChild(style);
 
function addDuplicateCitationsTemplate(linkElement) {
let parentDiv = referencesHeading.closest("div");
debug("Adding duplicate citations template");
let newParagraph = document.createElement("p");
showLoading(linkElement);
newParagraph.style.color = "red";
function addDuplicateCitationsTemplate() {
var api = new mw.Api();
var pageTitle = mw.config.get('wgPageName');
 
let duplicateInfo = getDuplicateInfo();
 
// Get current date
const currentDate = new Date();
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
const currentMonth = monthNames[currentDate.getMonth()];
const currentYear = currentDate.getFullYear();
const dateParam = `|date=${currentMonth} ${currentYear}`;
 
api.get({
Line 40 ⟶ 100:
rvslots: 'main',
formatversion: 2
}).then(function (data) {
var page = data.query.pages[0];
var content = page.revisions[0].slots.main.content;
 
// AddCreate the templatereason at the top of the pagestring
varlet newContentreason = '{{DuplicateDuplicateReferences citations}}script detected:\n\n' + content;
 
// Create the edit summary
let summary = '+{{Duplicate citations}}';
if (duplicateInfo.length > 0) {
summaryduplicateInfo.forEach((info) +=> ': ';{
duplicateInfo reason += `* ${info.forEach(url} (refs: ${info, index).refs.map(r => {r.number).join(', ')})\n\n`;
summary += `${info.url} (refs: ${info.refs.join(', ')})`;
if (index < duplicateInfo.length - 1) {
summary += '; ';
}
});
}
 
// MakeCreate the edittemplate to insert
const templateToInsert = `{{Duplicated citations|reason=${reason}${dateParam}}}\n`;
 
// Use Morebits to handle the template insertion
const wikitextPage = new Morebits.wikitext.page(content);
// Define templates that should come before the duplicated citations template
const precedingTemplates = [
'short description',
'displaytitle',
'lowercase title',
'italic title',
'about',
'redirect',
'distinguish',
'for',
'Featured list',
'Featured article',
'Good article',
'Other uses',
'Redirect2',
'Use mdy dates',
'Use dmy dates',
'Use American English',
'Use British English'
];
 
// Insert the template after the specified templates
// The third parameter is flags (default 'i' for case-insensitive)
// The fourth parameter can include pre-template content like HTML comments
wikitextPage.insertAfterTemplates(templateToInsert, precedingTemplates, 'i', ['<!--[\\s\\S]*?-->']);
var newContent = wikitextPage.getText();
 
let summary = `Tagged [[WP:DUPREF|duplicate citations]] using [[User:Polygnotus/DuplicateReferences|DuplicateReferences]]`;
 
return api.postWithToken('csrf', {
action: 'edit',
Line 66 ⟶ 154:
summary: summary
});
}).then(function () {
mw.notifyshowSuccess('Successfully added the Duplicate citations template!'linkElement);
//setTimeout(function Reload() the page to show the changes{
___location.reload();
}, 100); // Reload after 0.catch(function(error)1 {second
}).catch(function (error) {
console.error('Error:', error);
showError(linkElement);
mw.notify('Failed to add the template. See console for details.', {type: 'error'});
});
}
 
function getDuplicateInfoshowLoading(element) {
element.innerHTML = '<sup><small>[ Working... ]</small></sup>';
const referenceSpans = document.querySelectorAll('span.reference-text');
const urlMap = new Map();}
const duplicates = [];
 
function referenceSpans.forEachshowSuccess((spanelement) => {
element.innerHTML = '<sup><small>[ Done ]</small></sup>';
const links = span.querySelectorAll('a');
}
const refNumber = span.closest('li')?.id.split('-').pop() || 'Unknown';
 
function showError(element) {
let validLink = null;
element.innerHTML = '<sup><small>[ Error for (let link of links) {]</small></sup>';
}
const url = link.href;
const linkText = link.textContent.trim();
if (
linkText !== "Archived" &&
(!url.includes("wikipedia.org/wiki/") || url.includes("Special:BookSources")) &&
!url.includes("_(identifier)")
) {
validLink = link;
break;
}
}
 
function if getVisibleText(validLinkelement) {
// Recursively get the visible text content of constan url = validLink.href;element
let text = if (urlMap.has(url)) {'';
for (let node of urlMapelement.get(urlchildNodes).push(refNumber); {
if (node.nodeType === } elseNode.TEXT_NODE) {
text += urlMapnode.settextContent.trim(url, [refNumber]) + ' ';
} else if (node.nodeType === Node.ELEMENT_NODE) {
// Skip hidden elements
const style = window.getComputedStyle(node);
if (style.display !== 'none' && style.visibility !== 'hidden') {
text += getVisibleText(node) + ' ';
}
}
});
return text.trim();
}
 
function urlMap.forEachcalculateLevenshteinDistance((refsa, urlb) => {
if debug(refs.length > 1"Comparing:") {;
duplicates.pushdebug({"Text url1:", refs }a);
debug("Text 2:", }b);
});
 
if (a.length === 0) return duplicatesb.length;
if (b.length === 0) return a.length;
}
 
const matrix = [];
function calculateSimilarity(str1, str2) {
 
const longer = str1.length > str2.length ? str1 : str2;
const// shorterIncrement =along str1.lengththe >first str2.lengthcolumn ?of str2each : str1;row
constfor longerLength(let i = longer0; i <= b.length; i++) {
if (longerLength === 0) {matrix[i] = [i];
return 1.0;
}
return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength);
}
 
// Increment each column in the first row
function editDistance(s1, s2) {
s1for (let j = s10; j <= a.toLowerCase()length; j++) {
s2 matrix[0][j] = s2.toLowerCase()j;
}
 
const// costsFill =in newthe Array();rest of the matrix
for (let i = 01; i <= s1b.length; i++) {
for (let lastValuej = i1; j <= a.length; j++) {
for (let j = 0;if j(b.charAt(i <- 1) === s2a.length; charAt(j++ - 1)) {
if ( matrix[i][j] == 0)matrix[i - 1][j - 1];
} else costs[j] = j;{
else { matrix[i][j] = Math.min(
if ( matrix[i - 1][j >- 1] + 1, 0)// {substitution
let newValue = costs[j - 1];Math.min(
if (s1.charAt( matrix[i][j - 1)] !=+ s2.charAt(j1, -// 1))insertion
newValuematrix[i =- Math.min(Math.min(newValue,1][j] lastValue),+ 1 // deletion
costs[j]) + 1;
costs[j - 1] = lastValue);
lastValue = newValue;
}
}
}
if (i > 0)
costs[s2.length] = lastValue;
}
return costs[s2.length];
}
 
debug("Levenshtein distance:", matrix[b.length][a.length]);
function getAllVisibleText(element) {
ifreturn (elementmatrix[b.nodeType === Nodelength][a.TEXT_NODE) {length];
return element.textContent.trim();
}
if (element.nodeType === Node.ELEMENT_NODE) {
if (window.getComputedStyle(element).display === 'none') {
return '';
}
return Array.from(element.childNodes)
.map(child => getAllVisibleText(child))
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
return '';
}
 
function extractVisibleTextcalculateSimilarityPercentage(htmlStringdistance, maxLength) {
const parsersimilarity = new DOMParser((maxLength - distance) / maxLength) * 100;
debug("Similarity percentage:", similarity.toFixed(2) + "%");
const doc = parser.parseFromString(htmlString, 'text/html');
return getAllVisibleText(docMath.bodyround(similarity) + '%';
}
 
function checkDuplicateReferenceLinksgetDuplicateInfo() {
debug("Getting duplicate info");
const referenceSpans = document.querySelectorAll('span.reference-text');
 
const duplicates = [];
const urlMap = new Map();
const duplicatesreferenceItems = new MapArray.from(referencesList.children);
 
const contentMap = new Map();
letdebug("Number totalLinksof =reference 0items:", referenceItems.length);
 
referenceItems.forEach((item, index) => {
// Create a map of cite_note ids to their correct reference numbers
const citeNoteMap = new Mapif (item.tagName.toLowerCase(); === 'li') {
const refId = item.id;
document.querySelectorAll('.reference').forEach(ref => {
const linkrefNumber = ref.querySelector('a')index + 1;
if debug(link)`Processing reference item ${refNumber} (${refId})`);
 
const linkHref = link.getAttribute('href');
if// (linkHref)Get {the visible text of the entire reference item
const citeNoteIdrefText = linkHref.substringgetVisibleText(1item); // Remove the leading '#'
debug(` Reference text: const refNumber = citeNoteId.split('-').pop(${refText}`);
 
citeNoteMap.set(citeNoteId, refNumber);
}// Find the first valid link in the reference
} const links = item.querySelectorAll('a');
}) let validLink = null;
for (let link of links) {
referenceSpans.forEach((span) const url => {link.href;
 
const links = span.querySelectorAll('a');
const citeNote = span.closest( // Skip this reference if the URL doesn't contain 'lihttp')?.id;
const refNumber = citeNoteMap.get(citeNote) || if (!url.includes('Unknownhttp';)) {
debug(` Skipping reference ${refNumber} - URL does not contain 'http'`);
const refText = extractVisibleText(span.outerHTML); // Extract visible text content
return; // This 'return' is equivalent to 'continue' in a regular for loop
let validLink = null;
for (let link of links) {
const url = link.href;
const linkText = link.textContent.trim();
if (
linkText !== "Archived" &&
(!url.includes("wikipedia.org/wiki/") || url.includes("Special:BookSources")) &&
!url.includes("_(identifier)")
) {
validLink = link;
break;
}
}
if (validLink) {
const url = validLink.href;
totalLinks++;
if (urlMap.has(url)) {
if (duplicates.has(url)) {
duplicates.get(url).push({ refNumber, citeNote, refText });
} else {
duplicates.set(url, [urlMap.get(url), { refNumber, citeNote, refText }]);
}
} else { const linkText = link.textContent.trim();
urlMap.set(url, { refNumber, citeNote, refText });
}
 
// Check for similar contentif (
// (!url.includes("wikipedia.org/wiki/") || url.includes("Special:BookSources")) &&
let isDuplicate = false;
for (let [content, info] of contentMap) { linkText !== "Archived" &&
if (calculateSimilarity(refText, content) > 0!url.9includes("wikipedia.org") { // 90% similarity threshold&&
!url.includes("_(identifier)") && // Templates like ISBN and ISSN and OCLC and S2CID contain (identifier)
info.push({ refNumber, citeNote, refText, url });
isDuplicate!url.startsWith("https://search.worldcat.org/") && // |issn= true;parameter in cite news
!url.startsWith("https://www.bbc.co.uk/news/live/") && // live articles get frequent updates
!url.startsWith("https://www.aljazeera.com/news/liveblog/") &&
!url.startsWith("https://www.nbcnews.com/news/world/live-blog/") &&
!url.startsWith("https://www.theguardian.com/world/live/") &&
!url.startsWith("https://www.nytimes.com/live/") &&
!url.startsWith("https://edition.cnn.com/world/live-news/") &&
!url.startsWith("https://www.timesofisrael.com/liveblog") &&
!url.startsWith("https://www.france24.com/en/live-news/") &&
!url.startsWith("https://books.google.com/") && //may be 2 different pages of the same book
!url.startsWith("https://archive.org/details/isbn_")
) {
validLink = link;
debug(` Valid link found: ${url}`);
break;
}
}
 
if (!isDuplicate) {
if contentMap.set(refText,validLink) [{ refNumber, citeNote, refText, url }]);
const url = validLink.href;
if (urlMap.has(url)) {
urlMap.get(url).push({id: refId, number: refNumber, text: refText});
debug(` Duplicate found for URL: ${url}`);
} else {
urlMap.set(url, [{id: refId, number: refNumber, text: refText}]);
debug(` New URL added to map: ${url}`);
}
} else {
debug(` No valid link found in this item`);
}
}
});
 
urlMap.forEach((refs, url) => {
if (duplicates.size > 0 || Array.from(contentMap.values()).some(group => group.length > 1)) {
if (documentrefs.querySelector('table.box-Duplicated_citations')length ===> null1) {
const// editSectionsCalculate =Levenshtein parentDiv.querySelectorAll('span.mw-editsection');distance for each pair of refs
for (let i = 0; i < refs.length - 1; i++) {
editSections.forEach for (editSectionlet j => i + 1; j < refs.length; j++) {
let spanBefore = document.createElement debug('span'`Comparing references ${refs[i].number} and ${refs[j].number}:`);
spanBefore.className const distance = 'mw-editsection-bracket'calculateLevenshteinDistance(refs[i].text, refs[j].text);
spanBefore.textContent const maxLength = 'Math.max(refs['i].text.length, refs[j].text.length);
const similarity = calculateSimilarityPercentage(distance, maxLength);
let addTemplateLink refs[i].similarity = documentrefs[i].createElement('a')similarity || {};
addTemplateLink.textContent = ' add {{duplicatedrefs[i].similarity[refs[j].id] citations}}= 'similarity;
addTemplateLink.href = '#';
addTemplateLink.addEventListener('click', function(e) {
e.preventDefault();
addDuplicateCitationsTemplate();
});
let spanAfter = document.createElement('span');
spanAfter.className = 'mw-editsection-bracket';
spanAfter.textContent = ']';
editSection.appendChild(spanBefore);
editSection.appendChild(addTemplateLink);
editSection.appendChild(spanAfter);
});
}
// Group and collapse duplicate references
groupAndCollapseDuplicates(duplicates, contentMap);
// Display information about duplicates
displayDuplicateInfo(duplicates, contentMap);
parentDiv.after(newParagraph);
}
}
function groupAndCollapseDuplicates(duplicates, contentMap) {
const referencesSection = document.querySelector('ol.references');
if (!referencesSection) return;
// Process URL-based duplicates
duplicates.forEach((refInfo, url) => {
if (refInfo.length > 1) {
const groupContainer = document.createElement('li');
groupContainer.className = 'grouped-references';
const toggle = document.createElement('span');
toggle.className = 'grouped-references-toggle';
toggle.textContent = `[+] ${refInfo.length} references to ${url}`;
groupContainer.appendChild(toggle);
const content = document.createElement('ol');
content.className = 'grouped-references-content';
refInfo.forEach(ref => {
const originalRef = document.getElementById(ref.citeNote);
if (originalRef) {
content.appendChild(originalRef.cloneNode(true));
originalRef.remove();
}
});
groupContainerduplicates.appendChildpush(content{url, refs});
toggle.addEventListener('click', () => {
content.classList.toggle('expanded');
toggle.textContent = content.classList.contains('expanded') ?
`[-] ${refInfo.length} references to ${url}` :
`[+] ${refInfo.length} references to ${url}`;
});
referencesSection.appendChild(groupContainer);
}
});
// Process content-based duplicates
contentMap.forEach((group, content) => {
if (group.length > 1) {
const groupContainer = document.createElement('li');
groupContainer.className = 'grouped-references';
const toggle = document.createElement('span');
toggle.className = 'grouped-references-toggle';
toggle.textContent = `[+] ${group.length} similar references`;
groupContainer.appendChild(toggle);
const contentDiv = document.createElement('ol');
contentDiv.className = 'grouped-references-content';
group.forEach(ref => {
const originalRef = document.getElementById(ref.citeNote);
if (originalRef) {
contentDiv.appendChild(originalRef.cloneNode(true));
originalRef.remove();
}
});
groupContainer.appendChild(contentDiv);
toggle.addEventListener('click', () => {
contentDiv.classList.toggle('expanded');
toggle.textContent = contentDiv.classList.contains('expanded') ?
`[-] ${group.length} similar references` :
`[+] ${group.length} similar references`;
});
referencesSection.appendChild(groupContainer);
}
});
 
debug("Number of duplicate sets found:", duplicates.length);
debug("Duplicate sets:", duplicates);
return duplicates;
}
 
function displayDuplicateInfocreateCollapsibleTable(duplicates, contentMapduplicateInfo) {
const table = document.createElement('table');
duplicates.forEach((refInfo, url) => {
table.className = 'wikitable mw-collapsible duplicate-references-table';
let paragraphInfo = document.createElement('span');
table.setAttribute('role', 'presentation');
 
const tbody = document.createElement('tbody');
table.appendChild(tbody);
 
const headerRow = document.createElement('tr');
const headerCell = document.createElement('td');
headerCell.innerHTML = '<strong>Duplicate References</strong>';
 
const toggleSpan = document.createElement('span');
toggleSpan.className = 'mw-collapsible-toggle';
toggleSpan.innerHTML = '[<a href="#" class="mw-collapsible-text">hide</a>]';
headerCell.appendChild(toggleSpan);
 
 
// Check if the {{Duplicated citations}} template is already present
const duplicatedCitationsTemplate = document.querySelector('table.box-Duplicated_citations');
 
// Only add the link if the template is not present
if (!duplicatedCitationsTemplate) {
 
// Add the "add {{duplicated citations}}" link to the header
const addTemplateLink = document.createElement('a');
addTemplateLink.textContent = ' add {{duplicated citations}} ';
addTemplateLink.href = '#';
addTemplateLink.addEventListener('click', function (e) {
e.preventDefault();
addDuplicateCitationsTemplate(this);
});
//headerCell.appendChild(document.createTextNode(' ['));
headerCell.appendChild(addTemplateLink);
//headerCell.appendChild(document.createTextNode(']'));
}
headerRow.appendChild(headerCell);
tbody.appendChild(headerRow);
 
const pageTitle = mw.config.get('wgPageName').replace(/_/g, ' ');
 
duplicateInfo.forEach(({url, refs}) => {
const row = document.createElement('tr');
const cell = document.createElement('td');
 
// Create report icon
const reportIcon = document.createElement('a');
reportIcon.href = `https://en.wikipedia.org/wiki/User_talk:Polygnotus?action=edit&section=new&preloadtitle=Reporting%20%5B%5BUser%3APolygnotus%2FDuplicateReferences%7CDuplicateReferences%5D%5D%20false-positive&preload=User:Polygnotus/$1&preloadparams%5b%5d=${encodeURIComponent(`[[${pageTitle}]] ${url}`)}%20~~~~`;
reportIcon.innerHTML = '<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Cross_CSS_Red.svg/15px-Cross_CSS_Red.svg.png" width="15" height="15" alt="Report false positive" title="Report false positive" />';
reportIcon.style.marginRight = '5px';
cell.appendChild(reportIcon);
 
let urlLink = document.createElement('a');
urlLink.href = url;
Line 367 ⟶ 391:
urlLink.target = "_blank";
urlLink.rel = "noopener noreferrer";
paragraphInfo.appendChild(document.createTextNode('Duplicate URL: '));
paragraphInfo.appendChild(urlLink);
paragraphInfo.appendChild(document.createTextNode(' in refs: '));
refInfo.forEach((ref, index) => {
if (ref.citeNote) {
let link = document.createElement('a');
link.href = `#${ref.citeNote}`;
link.textContent = ref.refNumber;
paragraphInfo.appendChild(link);
 
// Highlight only the specific duplicates on hovercell.appendChild(urlLink);
linkcell.addEventListenerappendChild(document.createTextNode('mouseover', ()in =>refs: {'));
 
refInfo.forEach(duplicate => {
const citationElementoriginalRef = document.getElementById(duplicate.citeNote)refs[0];
refs.forEach((ref, index) => {
if (citationElement) {
let if (duplicate.citeNotelink === refdocument.citeNotecreateElement('a') {;
link.href = citationElement.classList`#${ref.add('duplicate-citation-hover')id}`;
link.textContent = } else {ref.number;
citationElementcell.classList.addappendChild('duplicate-citation-highlight'link);
 
}
// Add similarity information
if (index > 0) {
const similarity = calculateSimilarityPercentage(
calculateLevenshteinDistance(originalRef.text, ref.text),
Math.max(originalRef.text.length, ref.text.length)
);
let similarityInfo = document.createElement('span');
similarityInfo.textContent = ` (${similarity})`;
cell.appendChild(similarityInfo);
}
link.addEventListener('mouseover', () => {
refs.forEach(r => {
const citationElement = document.getElementById(r.id);
if (citationElement) {
if (r.id === ref.id) {
citationElement.classList.add('duplicate-citation-hover');
} else {
citationElement.classList.add('duplicate-citation-highlight');
}
});
});
});
link.addEventListener('mouseout', () => {
refInforefs.forEach(duplicater => {
const citationElement = document.getElementById(duplicater.citeNoteid);
if (citationElement) {
citationElement.classList.remove('duplicate-citation-hover');
citationElement.classList.remove('duplicate-citation-highlight');
}
});
});
});
 
// Highlight duplicates on link.addEventListener('click', and() allow=> navigation{
linkdocument.addEventListenerquerySelectorAll('click.duplicate-citation-clicked', ().forEach(el => {
// Remove previous click highlightsel.classList.remove('duplicate-citation-clicked');
document.querySelectorAll('.duplicate-citation-clicked').forEach(el => {
el.classList.remove('duplicate-citation-clicked');
});
// Add new click highlights
refInfo.forEach(duplicate => {
const citationElement = document.getElementById(duplicate.citeNote);
if (citationElement) {
citationElement.classList.add('duplicate-citation-clicked');
}
});
// The default behavior (navigation) will now occur
});
} else refs.forEach(r => {
paragraphInfo.appendChild( const citationElement = document.createTextNodegetElementById(refr.refNumber)id);
} if (citationElement) {
citationElement.classList.add('duplicate-citation-clicked');
// Calculate similarity with the next reference }
if (index < refInfo.length - 1}) {;
const similarity = calculateSimilarity(ref.refText, refInfo[index + 1].refText});
 
const similarityPercentage = Math.round(similarity * 100);
if (index < paragraphInforefs.appendChild(document.createTextNode(`length - 1) (${similarityPercentage}%)`));
} cell.appendChild(document.createTextNode(', '));
if (index < refInfo.length - 1) {
paragraphInfo.appendChild(document.createTextNode(', '));
}
});
 
paragraphInforow.appendChild(document.createElement('br')cell);
newParagraphtbody.appendChild(paragraphInforow);
});
 
return table;
// Add information about similar content duplicates
}
contentMap.forEach((group, content) => {
if (group.length > 1) {
let paragraphInfo = document.createElement('span');
paragraphInfo.appendChild(document.createTextNode('Similar content in refs: '));
group.forEach((ref, index) => {
if (ref.citeNote) {
let link = document.createElement('a');
link.href = `#${ref.citeNote}`;
link.textContent = ref.refNumber;
paragraphInfo.appendChild(link);
 
function checkDuplicateReferenceLinks() {
// Highlight similar content on hover and click (same as URL duplicates)
debug("Checking for duplicate reference links");
link.addEventListener('mouseover', () => {
const duplicateInfo = group.forEachgetDuplicateInfo(duplicate => {);
const citationElement = document.getElementById(duplicate.citeNote);
if (citationElement) {
if (duplicate.citeNote === ref.citeNote) {
citationElement.classList.add('duplicate-citation-hover');
} else {
citationElement.classList.add('duplicate-citation-highlight');
}
}
});
});
link.addEventListener('mouseout', () => {
group.forEach(duplicate => {
const citationElement = document.getElementById(duplicate.citeNote);
if (citationElement) {
citationElement.classList.remove('duplicate-citation-hover');
citationElement.classList.remove('duplicate-citation-highlight');
}
});
});
 
if (duplicateInfo.length > 0) {
link.addEventListener('click', () => {
debug("Duplicates found, creating collapsible table");
document.querySelectorAll('.duplicate-citation-clicked').forEach(el => {
 
el.classList.remove('duplicate-citation-clicked');
const table = }createCollapsibleTable(duplicateInfo);
groupcontainerDiv.forEachafter(duplicate => {table);
 
const citationElement = document.getElementById(duplicate.citeNote);
// Set up collapsible if (citationElement) {functionality
const toggleLink = citationElementtable.classList.addquerySelector('duplicate.mw-citationcollapsible-clickedtoggle a');
const tableBody = }$(table).find('tr:not(:first-child)');
const storageKey = })'duplicateReferencesTableState';
 
});
function } elsesetTableState(isCollapsed) {
if paragraphInfo.appendChild(document.createTextNode(ref.refNumberisCollapsed)); {
}tableBody.hide();
toggleLink.textContent = 'show';
} if (index < group.length - 1)else {
paragraphInfotableBody.appendChildshow(document.createTextNode(', '));
}toggleLink.textContent = 'hide';
});
localStorage.setItem(storageKey, isCollapsed);
paragraphInfo.appendChild(document.createElement('br'));
newParagraph.appendChild(paragraphInfo);
}
 
});
// Initialize state from localStorage
const initialState = localStorage.getItem(storageKey) === 'true';
setTableState(initialState);
 
toggleLink.addEventListener('click', function (e) {
e.preventDefault();
const isCurrentlyCollapsed = tableBody.is(':hidden');
setTableState(!isCurrentlyCollapsed);
});
} else {
debug("No duplicates found");
}
}
 
checkDuplicateReferenceLinks();
debug("Script execution completed");
});
});
// </nowiki>