Skip to content

Commit

Permalink
MC-1583:"Fix Title” for English titles not capitalizing first word af…
Browse files Browse the repository at this point in the history
…ter single quote or colon (#1223)

* MC-1583:"Fix Title” for English headlines not capitalizing first word after single quote or colon
  • Loading branch information
katerinachinnappan authored Nov 5, 2024
1 parent 1ee961e commit 7d4c610
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 11 deletions.
37 changes: 36 additions & 1 deletion src/_shared/utils/applyApTitleCase.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { applyApTitleCase } from './applyApTitleCase';
import { applyApTitleCase, lowercaseAfterApostrophe } from './applyApTitleCase';

// examples taken from https://www.grammarly.com/blog/capitalization-in-the-titles/
// tested at https://headlinecapitalization.com/ (AP style)
Expand Down Expand Up @@ -113,4 +113,39 @@ describe('applyApTitleCase', () => {
expect(applyApTitleCase(swc.result)).toEqual(swc.expected);
});
});

it('should differentiate between strings in quotes and apostrophe', () => {
const sentencesWithContractions = [
{
result: "Here's what you haven't noticed 'foo bar' foo'S",
expected: "Here's What You Haven't Noticed 'Foo Bar' Foo's",
},
];
sentencesWithContractions.forEach((swc) => {
expect(applyApTitleCase(swc.result)).toEqual(swc.expected);
});
});

it('should capitalize after a colon (:)', () => {
const sentencesWithContractions = [
{
result: "Here's what you haven't noticed 'foo bar' foo'S: foo Bar",
expected: "Here's What You Haven't Noticed 'Foo Bar' Foo'S: Foo Bar",
},
];
sentencesWithContractions.forEach((swc) => {
expect(applyApTitleCase(swc.result)).toEqual(swc.expected);
});
});
});

describe('lowercaseAfterApostrophe', () => {
it('lowercase letter after apostrophe & return new string', () => {
const result = lowercaseAfterApostrophe("foo'S");
expect(result).toEqual("foo's");
});
it('lowercase letter after apostrophe, ignore string in quotes, & return new string', () => {
const result = lowercaseAfterApostrophe("'Foo' foo'S DaY's");
expect(result).toEqual("'Foo' foo's DaY's");
});
});
58 changes: 48 additions & 10 deletions src/_shared/utils/applyApTitleCase.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
export const STOP_WORDS =
'a an and at but by for in nor of on or so the to up yet';
'a an and at but by for in nor of on or the to up yet';

export const SEPARATORS = /(\s+|[-,:;!?()"])/;
// Matches a colon (:) and 0+ white spaces following after
// Matches 1+ white spaces
// Matches special chars (i.e. hyphens, quotes, etc)
export const SEPARATORS = /(:\s*|\s+|[-,:;!?()'"])/; // Include curly quotes as separators

export const stop = STOP_WORDS.split(' ');

/**
* Format a string: Capture the letter after an apostrophe at the end of a
* sentence (without requiring a space) or with a white space following the letter.
* Lowercase the captured letter & return the formatted string.
* @param input
* @returns {string}
*/
export const lowercaseAfterApostrophe = (input: string): string => {
// matches a char (num or letter) right after an apostrophe,
// only if the apostrophe is preceded by a char & is followed
// by a space or end of the str.
const regex = /(?<=\w)'(\w)(?=\s|$)/g;

return input.replace(regex, (match, p1) => {
return `'${p1.toLowerCase()}`; // Replace with the apostrophe and the lowercase letter
});
};

/**
* Capitalize first character for string
*
Expand All @@ -30,19 +51,36 @@ export const applyApTitleCase = (value: string): string => {
if (!value) {
return '';
}
// split by separators, check if word is first or last
// or not blacklisted, then capitalize
return value
.split(SEPARATORS)

// Split and filter empty strings
// Boolean here acts as a callback, evaluates each word:
// If it's a non-empty string, keep the word in the array;
// If it's an empty string (or falsy), remove from array.
const allWords = value.split(SEPARATORS).filter(Boolean); // Split and filter empty strings

const result = allWords
.map((word, index, all) => {
const isAfterColon = index > 0 && all[index - 1].trim() === ':';

const isAfterQuote =
index > 0 &&
(allWords[index - 1] === "'" ||
allWords[index - 1] === '"' ||
allWords[index - 1] === '\u2018' || // Opening single quote ’
allWords[index - 1] === '\u201C'); // Opening double quote “

if (
index === 0 ||
index === all.length - 1 ||
!stop.includes(word.toLowerCase())
index === 0 || // first word
index === all.length - 1 || // last word
isAfterColon || // capitalize the first word after a colon
isAfterQuote || // capitalize the first word after a quote
!stop.includes(word.toLowerCase()) // not a stop word
) {
return capitalize(word);
}

return word.toLowerCase();
})
.join('');
.join(''); // join without additional spaces
return lowercaseAfterApostrophe(result);
};

0 comments on commit 7d4c610

Please sign in to comment.