Jump to content

User:ZKang123/TitleCaseConverter.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// titlecaseconverter.js

/* eslint-disable no-alert, no-console */

$( () => {
	/**
	 * Convert titles to title case
	 */
	function toTitleCase( title, changeExistingCapitalization = true ) {
		const isAllCaps = title.toUpperCase() === title;
		if ( isAllCaps ) {
			title = title.toLowerCase();
		}

		title = title.split( ' ' ).map( ( word, index, array ) => {
			// Retain words that are already in uppercase or are special cases
			if ( 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) {
				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 ) {
		// Define custom exceptions for abbreviations and specific titles
		const exceptions = [ 'MRT', 'LTA', 'S$', 'US$', 'NASA', 'FBI', 'MP3' ]; // Add more exceptions as needed
		return exceptions.includes( word ) || /^[A-Z0-9]+$/.test( word );
	}

	function shouldCapitalize( word, index, array ) {
		const alwaysCapitalize = [ 'Me', 'It', 'His', 'If', 'Be', 'Am', 'Is', 'Are', 'Being', 'Was', 'Were', 'Been', 'During', 'Through', 'About', 'Until', 'Below', 'Under' ];
		const doNotCapitalize = [ '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' ];
		const punctuationMarks = [ '.', ',', ';', ':', '?', '!' ];

		const isAbbr = isSpecialCase( word );
		const isProperNoun = alwaysCapitalize.includes( word );
		const isShortWord = doNotCapitalize.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, changeExistingCapitalization = true ) {
		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, changeExistingCapitalization );
			// Ensure space is retained at the end
			const endingSpace = p2.endsWith(' ') ? ' ' : '';
			return `${ p1 }${ titleCaseTitle }${ endingSpace }${ p3 }`;
		} ) );
	}

	/**
	 * Load the script and add the sidebar links
	 */
	function loadTitleCaseConverter() {
		// Create the first sidebar link
		const sidebarLink1 = document.createElement( 'li' );
		const link1 = document.createElement( 'a' );
		link1.innerText = 'Convert Ref Titles to Title Case (Preserve Capitalization)';
		link1.href = '#';
		link1.style.cssText = 'cursor: pointer; color: #0645ad;';

		// Add click event listener to the first link
		link1.addEventListener( 'click', ( event ) => {
			event.preventDefault();
			const textArea = document.querySelector( '#wpTextbox1' );
			if ( textArea ) {
				const summary = 'Converted reference titles to title case per [[MOS:CT]] using [[User:ZKang123/TitleCaseConverter|TitleCaseConverter]]';
				textArea.value = convertReferenceTitles( textArea.value, false );
				// Set default editing summary
				const summaryInput = document.querySelector( '#wpSummary' );
				if ( summaryInput && !summaryInput.value.trim() ) {
					summaryInput.value = summary;
				}
			} else {
				alert( 'Error: Editing area not found!' );
			}
		} );

		sidebarLink1.appendChild( link1 );

		// Create the second sidebar link
		const sidebarLink2 = document.createElement( 'li' );
		const link2 = document.createElement( 'a' );
		link2.innerText = 'Convert Ref Titles to Title Case (Change Capitalization)';
		link2.href = '#';
		link2.style.cssText = 'cursor: pointer; color: #0645ad;';

		// Add click event listener to the second link
		link2.addEventListener( 'click', ( event ) => {
			event.preventDefault();
			const textArea = document.querySelector( '#wpTextbox1' );
			if ( textArea ) {
				const summary = 'Converted reference titles to title case per [[MOS:CT]] using [[User:ZKang123/TitleCaseConverter|TitleCaseConverter]]';
				textArea.value = convertReferenceTitles( textArea.value, true );
				// Set default editing summary
				const summaryInput = document.querySelector( '#wpSummary' );
				if ( summaryInput && !summaryInput.value.trim() ) {
					summaryInput.value = summary;
				}
			} else {
				alert( 'Error: Editing area not found!' );
			}
		} );

		sidebarLink2.appendChild( link2 );

		// Add the links to the sidebar (p-tb section)
		const sidebar = document.getElementById( 'p-tb' );
		const ul = sidebar ? sidebar.querySelector( 'ul' ) : null;
		if ( ul ) {
			ul.appendChild( sidebarLink1 );
			ul.appendChild( sidebarLink2 );
		} else {
			alert( 'Error: Sidebar section not found!' );
		}
	}

	function runUnitTests() {
		const tests = [
			// normal
			{
				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'
			},
			{
				old: 'Earlier target date for Phase II MRT',
				new: 'Earlier Target Date for Phase II MRT'
			},
			{
				old: 'MRT System to be Implemented in Eight Stages',
				new: 'MRT System to Be Implemented in Eight Stages'
			},
			{
				old: 'MRT to Bt Batok, Bt Gombak and Choa Chu Kang on Mar 10',
				new: 'MRT to Bt Batok, Bt Gombak and Choa Chu Kang on Mar 10'
			},
			// mid-word hyphens and slashes
			{
				old: 'Revived, re-opened, newly appreciated',
				new: 'Revived, Re-Opened, Newly Appreciated'
			},
			{
				old: "Streetscapes/eldridge street Synagogue;a prayer-filled time capsule from the 1880's",
				new: "Streetscapes/Eldridge Street Synagogue;A Prayer-Filled Time Capsule from the 1880's"
			},
			{
				old: 'Phase 2 gets go-ahead to ensure continuity',
				new: 'Phase 2 Gets Go-Ahead To Ensure Continuity'
			},
			// weird mid-word capitalization
			{
				old: 'Phase 2 gets go-ahead to build iPad',
				new: 'Phase 2 Gets Go-Ahead To Build iPad'
			},
			{
				old: 'Phase 2 gets go-ahead to build DataMall',
				new: 'Phase 2 Gets Go-Ahead To Build DataMall'
			},
			// all caps
			{
				old: 'PHASE 2 GETS GO-AHEAD TO ENSURE CONTINUITY',
				new: 'Phase 2 Gets Go-Ahead To Ensure Continuity'
			},
			// punctuation at beginning of word
			{
				old: 'She was "amazingly spectacular"',
				new: 'She Was "Amazingly Spectacular"'
			}
		];

		let i = 1;
		let failures = 0;
		for ( const test of tests ) {
			const actual = toTitleCase( test.old );
			if ( actual !== test.new ) {
				console.log( `[Titlecaseconverter.js] Failed unit test ${ i }. Received "${ actual }" instead of "${ test.new }".` );
				failures++;
			}
			i++;
		}
		if ( !failures ) {
			console.log( '[Titlecaseconverter.js] All unit tests passed. Yay.' );
		}
	}

	// Load the script when the page is ready
	if ( document.readyState !== 'loading' ) {
		loadTitleCaseConverter();
	} else {
		document.addEventListener( 'DOMContentLoaded', loadTitleCaseConverter );
	}

	// Put this at the top of your common.js file to run unit tests in the browser devtools console:
	// window.TitleCaseConverterUnitTests = true;
	if ( window.TitleCaseConverterUnitTests ) {
		runUnitTests();
	}
} );