// titlecaseconverter.js
/* eslint-disable no-alert, no-console */
$( () => {
// Expanded list of common abbreviations and special cases
const SPECIAL_CASES = [
// Common abbreviations
'MRT', 'LTA', 'S$', 'US$', 'NASA', 'FBI', 'CIA', 'MP3', 'PDF', 'HTML',
'HTTP', 'URL', 'CEO', 'GPS', 'DVD', 'WiFi', 'PhD', 'ATM', 'ASAP', 'DIY',
'FAQ', 'ID', 'IQ', 'OK', 'PC', 'TV', 'UK', 'USA', 'USB', 'VIP',
// Singapore-specific
'HDB', 'URA', 'NUS', 'NTU', 'SBS', 'SMRT', 'COE', 'ERP', 'CPF',
// Technical terms
'iOS', 'macOS', 'iPhone', 'iPad', 'iMac', 'eBay', 'DataMall'
];
// Words that should always be capitalized
const ALWAYS_CAPITALIZE = [
'Me', 'It', 'His', 'If', 'Be', 'Am', 'Is', 'Are', 'Being', 'Was',
'Were', 'Been', 'During', 'Through', 'About', 'Until', 'Below', 'Under'
];
// Words that shouldn't be capitalized (unless first/last word)
const DO_NOT_CAPITALIZE = [
'a', 'an', 'the', 'and', 'by', 'at', 'but', 'or', 'nor', 'for',
'yet', 'so', 'as', 'in', 'of', 'on', 'to', 'from', 'into', 'like',
'over', 'with', 'till', 'upon', 'off', 'per', 'up', 'out', 'via'
];
/**
* Convert titles to title case with options
*/
function toTitleCase(title, options = {}) {
const {
changeExistingCapitalization = true,
keepAllCaps = false
} = options;
const isAllCaps = title.toUpperCase() === title;
if (isAllCaps && !keepAllCaps) {
title = title.toLowerCase();
}
title = title.split(' ').map((word, index, array) => {
// Retain words that are already in uppercase or are special cases
if ((keepAllCaps && word.toUpperCase() === word) || isSpecialCase(word)) {
return word;
}
// Retain capitalization for words following certain punctuation marks
if (index > 0 && /[/;\-,]/.test(array[index - 1])) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
// Check for mixed capitalization
const hasMixedCase = /[a-z]/.test(word) && /[A-Z]/.test(word);
if (hasMixedCase && !changeExistingCapitalization) {
return word;
}
// If there's already a capital letter in the word, we probably don't want to change it
const hasUpperCaseLetter = /[A-Z]/.test(word);
if (!changeExistingCapitalization && hasUpperCaseLetter) {
return word;
} else if (shouldCapitalize(word, index, array)) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
} else {
return word.toLowerCase();
}
}).join(' ');
// Capitalize first letters that occur after punctuation
title = title.replace(/ [^A-Za-z][a-z]/g, (match) => ' ' + match.slice(1, 2) + match.slice(2).toUpperCase());
// Capitalize anything after a semicolon
title = title.replace(/;[a-z]/g, (match) => ';' + match.slice(1).toUpperCase());
// Capitalize letters mid-word that occur after hyphens or slashes
title = title.replace(/-[a-z]/g, (match) => '-' + match.slice(1).toUpperCase());
title = title.replace(/\/[a-z]/g, (match) => '/' + match.slice(1).toUpperCase());
return title;
}
/**
* Check if a word is an abbreviation or an exception
*/
function isSpecialCase(word) {
// Check against our list of special cases
if (SPECIAL_CASES.includes(word)) {
return true;
}
// Check for patterns like S$50, US$100
if (/^[A-Z]+\$[\d,]+$/.test(word)) {
return true;
}
// Check for Roman numerals (basic check)
if (/^[IVXLCDM]+$/.test(word)) {
return true;
}
// Check for all-caps acronyms
return /^[A-Z0-9]+$/.test(word);
}
function shouldCapitalize(word, index, array) {
const punctuationMarks = ['.', ',', ';', ':', '?', '!'];
const isAbbr = isSpecialCase(word);
const isProperNoun = ALWAYS_CAPITALIZE.includes(word);
const isShortWord = DO_NOT_CAPITALIZE.includes(word.toLowerCase());
const isFirstOrLastWord = index === 0 || index === array.length - 1;
const isLongPreposition = word.length >= 5;
const isVerb = ['be', 'am', 'is', 'are', 'being', 'was', 'were', 'been'].includes(word.toLowerCase());
// Preserve capitalization after punctuation marks
if (index > 0) {
const prevWord = array[index - 1];
const lastChar = prevWord.charAt(prevWord.length - 1);
if (punctuationMarks.includes(lastChar)) {
return true;
}
}
return isAbbr || isFirstOrLastWord || isProperNoun || isLongPreposition || !isShortWord || isVerb;
}
/**
* Convert reference titles in the HTML content
*/
function convertReferenceTitles(htmlString, options = {}) {
const citationRegex = /<ref[^>]*>.*?<\/ref>/gi;
const titleRegex = /(\|title=)([^|]+)(\|)/i;
return htmlString.replace(citationRegex, (match) => match.replace(titleRegex, (titleMatch, p1, p2, p3) => {
const originalTitle = p2.trim();
const titleCaseTitle = toTitleCase(originalTitle, options);
// Ensure space is retained at the end
const endingSpace = p2.endsWith(' ') ? ' ' : '';
return `${p1}${titleCaseTitle}${endingSpace}${p3}`;
}));
}
/**
* Show preview of changes in a modal dialog
*/
function showPreview(originalText, convertedText) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 80%;
max-width: 800px;
background: white;
padding: 20px;
border: 1px solid #aaa;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1000;
max-height: 80vh;
overflow: auto;
`;
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
`;
closeButton.onclick = () => modal.remove();
const title = document.createElement('h3');
title.textContent = 'Title Case Conversion Preview';
title.style.marginTop = '0';
const originalTitle = document.createElement('h4');
originalTitle.textContent = 'Original:';
const originalContent = document.createElement('div');
originalContent.style.cssText = `
background: #f8f9fa;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #eaecf0;
white-space: pre-wrap;
`;
originalContent.textContent = originalText;
const convertedTitle = document.createElement('h4');
convertedTitle.textContent = 'Converted:';
const convertedContent = document.createElement('div');
convertedContent.style.cssText = `
background: #f8f9fa;
padding: 10px;
border: 1px solid #eaecf0;
white-space: pre-wrap;
`;
convertedContent.textContent = convertedText;
const applyButton = document.createElement('button');
applyButton.textContent = 'Apply Changes';
applyButton.style.cssText = `
margin-top: 20px;
padding: 8px 16px;
background: #36c;
color: white;
border: none;
border-radius: 2px;
cursor: pointer;
`;
applyButton.onclick = () => {
const textArea = document.querySelector('#wpTextbox1');
if (textArea) {
textArea.value = convertedText;
const summaryInput = document.querySelector('#wpSummary');
if (summaryInput && !summaryInput.value.trim()) {
summaryInput.value = 'Converted reference titles to title case per [[MOS:CT]] using [[User:ZKang123/TitleCaseConverter|TitleCaseConverter]]';
}
}
modal.remove();
};
modal.appendChild(closeButton);
modal.appendChild(title);
modal.appendChild(originalTitle);
modal.appendChild(originalContent);
modal.appendChild(convertedTitle);
modal.appendChild(convertedContent);
modal.appendChild(applyButton);
document.body.appendChild(modal);
}
/**
* Load the script and add the sidebar links
*/
function loadTitleCaseConverter() {
const sidebar = document.getElementById('p-tb');
if (!sidebar) {
alert('Error: Sidebar section not found!');
return;
}
const ul = sidebar.querySelector('ul') || document.createElement('ul');
// Create menu item for each option
const options = [
{
text: 'Convert Ref Titles (Preserve Capitals)',
options: { changeExistingCapitalization: false, keepAllCaps: false }
},
{
text: 'Convert Ref Titles (Change Capitals)',
options: { changeExistingCapitalization: true, keepAllCaps: false }
},
{
text: 'Convert Ref Titles (Keep ALL CAPS)',
options: { changeExistingCapitalization: true, keepAllCaps: true }
}
];
options.forEach((option) => {
const sidebarLink = document.createElement('li');
const link = document.createElement('a');
link.innerText = option.text;
link.href = '#';
link.style.cssText = 'cursor: pointer; color: #0645ad; display: block; padding: 2px 0;';
link.addEventListener('click', (event) => {
event.preventDefault();
const textArea = document.querySelector('#wpTextbox1');
if (!textArea) {
alert('Error: Editing area not found!');
return;
}
const convertedText = convertReferenceTitles(textArea.value, option.options);
showPreview(textArea.value, convertedText);
});
sidebarLink.appendChild(link);
ul.appendChild(sidebarLink);
});
if (!sidebar.querySelector('ul')) {
sidebar.appendChild(ul);
}
}
// Load the script when the page is ready
if (document.readyState !== 'loading') {
loadTitleCaseConverter();
} else {
document.addEventListener('DOMContentLoaded', loadTitleCaseConverter);
}
// Unit tests can be run from console with: window.TitleCaseConverterUnitTests = true;
if (window.TitleCaseConverterUnitTests) {
runUnitTests();
}
function runUnitTests() {
const tests = [
// Normal cases
{ old: 'The South and West lines', new: 'The South and West Lines' },
{ old: 'Work on second phase of MRT system ahead of schedule', new: 'Work on Second Phase of MRT System Ahead of Schedule' },
// Abbreviations and special cases
{ old: 'NASA and FBI report on UFOs', new: 'NASA and FBI Report on UFOs' },
{ old: 'New iPhone and iPad releases', new: 'New iPhone and iPad Releases' },
{ old: 'Payment via ATM and online banking', new: 'Payment via ATM and Online Banking' },
// ALL CAPS handling
{
old: 'PHASE 2 GETS GO-AHEAD TO ENSURE CONTINUITY',
new: 'Phase 2 Gets Go-Ahead To Ensure Continuity',
options: { keepAllCaps: false }
},
{
old: 'PHASE 2 GETS GO-AHEAD TO ENSURE CONTINUITY',
new: 'PHASE 2 GETS GO-AHEAD TO ENSURE CONTINUITY',
options: { keepAllCaps: true }
},
// Mixed case and existing capitalization
{
old: 'DataMall and eBay sales figures',
new: 'DataMall and eBay Sales Figures',
options: { changeExistingCapitalization: false }
},
// Punctuation
{ old: 'Revived, re-opened, newly appreciated', new: 'Revived, Re-Opened, Newly Appreciated' },
{ old: "Streetscapes/eldridge street Synagogue;a prayer-filled time capsule", new: "Streetscapes/Eldridge Street Synagogue;A Prayer-Filled Time Capsule" }
];
let failures = 0;
tests.forEach((test, i) => {
const actual = toTitleCase(test.old, test.options || {});
if (actual !== test.new) {
console.log(`[Titlecaseconverter.js] Failed test ${i + 1}. Input: "${test.old}"`);
console.log(` Expected: "${test.new}"`);
console.log(` Actual: "${actual}"`);
failures++;
}
});
if (!failures) {
console.log('[Titlecaseconverter.js] All unit tests passed successfully.');
} else {
console.log(`[Titlecaseconverter.js] ${failures} test(s) failed.`);
}
}
});