Skip to content

Commit 7d4c610

Browse files
MC-1583:"Fix Title” for English titles not capitalizing first word after single quote or colon (#1223)
* MC-1583:"Fix Title” for English headlines not capitalizing first word after single quote or colon
1 parent 1ee961e commit 7d4c610

File tree

2 files changed

+84
-11
lines changed

2 files changed

+84
-11
lines changed

src/_shared/utils/applyApTitleCase.test.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { applyApTitleCase } from './applyApTitleCase';
1+
import { applyApTitleCase, lowercaseAfterApostrophe } from './applyApTitleCase';
22

33
// examples taken from https://www.grammarly.com/blog/capitalization-in-the-titles/
44
// tested at https://headlinecapitalization.com/ (AP style)
@@ -113,4 +113,39 @@ describe('applyApTitleCase', () => {
113113
expect(applyApTitleCase(swc.result)).toEqual(swc.expected);
114114
});
115115
});
116+
117+
it('should differentiate between strings in quotes and apostrophe', () => {
118+
const sentencesWithContractions = [
119+
{
120+
result: "Here's what you haven't noticed 'foo bar' foo'S",
121+
expected: "Here's What You Haven't Noticed 'Foo Bar' Foo's",
122+
},
123+
];
124+
sentencesWithContractions.forEach((swc) => {
125+
expect(applyApTitleCase(swc.result)).toEqual(swc.expected);
126+
});
127+
});
128+
129+
it('should capitalize after a colon (:)', () => {
130+
const sentencesWithContractions = [
131+
{
132+
result: "Here's what you haven't noticed 'foo bar' foo'S: foo Bar",
133+
expected: "Here's What You Haven't Noticed 'Foo Bar' Foo'S: Foo Bar",
134+
},
135+
];
136+
sentencesWithContractions.forEach((swc) => {
137+
expect(applyApTitleCase(swc.result)).toEqual(swc.expected);
138+
});
139+
});
140+
});
141+
142+
describe('lowercaseAfterApostrophe', () => {
143+
it('lowercase letter after apostrophe & return new string', () => {
144+
const result = lowercaseAfterApostrophe("foo'S");
145+
expect(result).toEqual("foo's");
146+
});
147+
it('lowercase letter after apostrophe, ignore string in quotes, & return new string', () => {
148+
const result = lowercaseAfterApostrophe("'Foo' foo'S DaY's");
149+
expect(result).toEqual("'Foo' foo's DaY's");
150+
});
116151
});

src/_shared/utils/applyApTitleCase.ts

+48-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
export const STOP_WORDS =
2-
'a an and at but by for in nor of on or so the to up yet';
2+
'a an and at but by for in nor of on or the to up yet';
33

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

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

11+
/**
12+
* Format a string: Capture the letter after an apostrophe at the end of a
13+
* sentence (without requiring a space) or with a white space following the letter.
14+
* Lowercase the captured letter & return the formatted string.
15+
* @param input
16+
* @returns {string}
17+
*/
18+
export const lowercaseAfterApostrophe = (input: string): string => {
19+
// matches a char (num or letter) right after an apostrophe,
20+
// only if the apostrophe is preceded by a char & is followed
21+
// by a space or end of the str.
22+
const regex = /(?<=\w)'(\w)(?=\s|$)/g;
23+
24+
return input.replace(regex, (match, p1) => {
25+
return `'${p1.toLowerCase()}`; // Replace with the apostrophe and the lowercase letter
26+
});
27+
};
28+
829
/**
930
* Capitalize first character for string
1031
*
@@ -30,19 +51,36 @@ export const applyApTitleCase = (value: string): string => {
3051
if (!value) {
3152
return '';
3253
}
33-
// split by separators, check if word is first or last
34-
// or not blacklisted, then capitalize
35-
return value
36-
.split(SEPARATORS)
54+
55+
// Split and filter empty strings
56+
// Boolean here acts as a callback, evaluates each word:
57+
// If it's a non-empty string, keep the word in the array;
58+
// If it's an empty string (or falsy), remove from array.
59+
const allWords = value.split(SEPARATORS).filter(Boolean); // Split and filter empty strings
60+
61+
const result = allWords
3762
.map((word, index, all) => {
63+
const isAfterColon = index > 0 && all[index - 1].trim() === ':';
64+
65+
const isAfterQuote =
66+
index > 0 &&
67+
(allWords[index - 1] === "'" ||
68+
allWords[index - 1] === '"' ||
69+
allWords[index - 1] === '\u2018' || // Opening single quote ’
70+
allWords[index - 1] === '\u201C'); // Opening double quote “
71+
3872
if (
39-
index === 0 ||
40-
index === all.length - 1 ||
41-
!stop.includes(word.toLowerCase())
73+
index === 0 || // first word
74+
index === all.length - 1 || // last word
75+
isAfterColon || // capitalize the first word after a colon
76+
isAfterQuote || // capitalize the first word after a quote
77+
!stop.includes(word.toLowerCase()) // not a stop word
4278
) {
4379
return capitalize(word);
4480
}
81+
4582
return word.toLowerCase();
4683
})
47-
.join('');
84+
.join(''); // join without additional spaces
85+
return lowercaseAfterApostrophe(result);
4886
};

0 commit comments

Comments
 (0)