User:ZKang123/TitleCaseConverter.js
Appearance
(Redirected from User:ZKang123/Titlecaseconverter.js)
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 user script seems to have a documentation page at User:ZKang123/TitleCaseConverter. |
// 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.`);
}
}
});