Skip to content

Commit 88c708b

Browse files
committed
feat: update title capitalization rules
- Capitalize 'a' and 'the' after sentence-ending punctuation (. \! ? :) - Always format 'iPhone' correctly regardless of input case - Always lowercase 'vs.' in titles - Exclude 'as' from capitalization in title case - Add comprehensive tests for all new capitalization rules
1 parent 7faf25c commit 88c708b

File tree

2 files changed

+134
-4
lines changed

2 files changed

+134
-4
lines changed

packages/content-common/src/index.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,90 @@ describe('content-common', () => {
130130
expect(applyApTitleCase(result)).toEqual(expected);
131131
});
132132
});
133+
134+
it('should capitalize "a" and "the" after sentence-ending punctuation', () => {
135+
const testCases = [
136+
{
137+
result: 'Nazi Persecution Scattered My Family. a Lost Archive Brought Us Together',
138+
expected: 'Nazi Persecution Scattered My Family. A Lost Archive Brought Us Together',
139+
},
140+
{
141+
result: 'This is the end! the beginning starts now',
142+
expected: 'This Is the End! The Beginning Starts Now',
143+
},
144+
{
145+
result: 'What happened? a miracle occurred',
146+
expected: 'What Happened? A Miracle Occurred',
147+
},
148+
{
149+
result: 'She said "Hello." the crowd cheered',
150+
expected: 'She Said "Hello." The Crowd Cheered',
151+
},
152+
];
153+
testCases.forEach(({ result, expected }) => {
154+
expect(applyApTitleCase(result)).toEqual(expected);
155+
});
156+
});
157+
158+
it('should always format iPhone correctly', () => {
159+
const testCases = [
160+
{
161+
result: 'the new Iphone is amazing',
162+
expected: 'The New iPhone Is Amazing',
163+
},
164+
{
165+
result: 'IPHONE users love their devices',
166+
expected: 'iPhone Users Love Their Devices',
167+
},
168+
{
169+
result: 'my iphone broke yesterday',
170+
expected: 'My iPhone Broke Yesterday',
171+
},
172+
];
173+
testCases.forEach(({ result, expected }) => {
174+
expect(applyApTitleCase(result)).toEqual(expected);
175+
});
176+
});
177+
178+
it('should always lowercase "vs."', () => {
179+
const testCases = [
180+
{
181+
result: 'Apple Vs. Samsung: the battle continues',
182+
expected: 'Apple vs. Samsung: The Battle Continues',
183+
},
184+
{
185+
result: 'Batman VS. Superman was a movie',
186+
expected: 'Batman vs. Superman Was a Movie',
187+
},
188+
{
189+
result: 'Good vs Evil: a timeless struggle',
190+
expected: 'Good vs. Evil: A Timeless Struggle',
191+
},
192+
];
193+
testCases.forEach(({ result, expected }) => {
194+
expect(applyApTitleCase(result)).toEqual(expected);
195+
});
196+
});
197+
198+
it('should not capitalize "as" in title case', () => {
199+
const testCases = [
200+
{
201+
result: 'Working As a Team Is Important',
202+
expected: 'Working as a Team Is Important',
203+
},
204+
{
205+
result: 'As The Sun Sets',
206+
expected: 'As the Sun Sets',
207+
},
208+
{
209+
result: 'She Sees It As An Opportunity',
210+
expected: 'She Sees It as an Opportunity',
211+
},
212+
];
213+
testCases.forEach(({ result, expected }) => {
214+
expect(applyApTitleCase(result)).toEqual(expected);
215+
});
216+
});
133217
});
134218
describe('lowercaseAfterApostrophe', () => {
135219
it('lowercase letter after apostrophe & return new string', () => {

packages/content-common/src/index.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ export const sanitizeText = (input: string, maxLength: number): string => {
5656
export const lowercaseAfterApostrophe = (input: string): string => {
5757
// Match either an ASCII or curly apostrophe followed by a letter, after a word character.
5858
const regex = /(?<=\w)(['\u2018\u2019])(\w)/g;
59-
return input.replace(regex, (_, apostrophe, letter) => `${apostrophe}${letter.toLowerCase()}`);
59+
return input.replace(
60+
regex,
61+
(_, apostrophe, letter) => `${apostrophe}${letter.toLowerCase()}`,
62+
);
6063
};
6164

6265
/**
@@ -89,22 +92,65 @@ export const applyApTitleCase = (value: string): string => {
8992

9093
const result = allWords
9194
.map((word, index, all) => {
95+
// Check if the previous non-empty element is a sentence-ending punctuation
96+
const isAfterSentenceEnd =
97+
index > 0 &&
98+
(() => {
99+
// Look for the previous non-empty element
100+
for (let i = index - 1; i >= 0; i--) {
101+
const prev = all[i].trim();
102+
if (prev) {
103+
// Check if it ends with sentence-ending punctuation
104+
return (
105+
/[.!?]$/.test(prev) ||
106+
// Or if it's a closing quote after sentence-ending punctuation
107+
(i > 0 &&
108+
(prev === '"' ||
109+
prev === '\u201D' ||
110+
prev === "'" ||
111+
prev === '\u2019') &&
112+
/[.!?]$/.test(all[i - 1].trim()))
113+
);
114+
}
115+
}
116+
return false;
117+
})();
118+
92119
const isAfterColon = index > 0 && all[index - 1].trim() === ':';
93120

94121
const isAfterQuote =
95122
index > 0 &&
96123
(allWords[index - 1] === "'" ||
97124
allWords[index - 1] === '"' ||
98-
allWords[index - 1] === '\u2018' || // Opening single quote ’
99-
allWords[index - 1] === '\u201C'); // Opening double quote “
125+
allWords[index - 1] === '\u2018' || // Opening single quote '
126+
allWords[index - 1] === '\u201C'); // Opening double quote "
127+
128+
// Special case handling for specific words
129+
const lowerWord = word.toLowerCase();
130+
131+
// Handle iPhone
132+
if (lowerWord === 'iphone') {
133+
return 'iPhone';
134+
}
135+
136+
// Handle vs.
137+
if (lowerWord === 'vs.' || lowerWord === 'vs') {
138+
return 'vs.';
139+
}
100140

141+
// Check if we should capitalize this word
101142
if (
102143
index === 0 || // first word
103144
index === all.length - 1 || // last word
104145
isAfterColon || // capitalize the first word after a colon
105146
isAfterQuote || // capitalize the first word after a quote
106-
!stop.includes(word.toLowerCase()) // not a stop word
147+
isAfterSentenceEnd || // capitalize after sentence-ending punctuation
148+
(!stop.includes(lowerWord) && lowerWord !== 'as') // not a stop word and not 'as'
107149
) {
150+
// Special handling for 'a' and 'the' after sentence-ending punctuation
151+
if (isAfterSentenceEnd && (lowerWord === 'a' || lowerWord === 'the')) {
152+
return capitalize(word);
153+
}
108154
return capitalize(word);
109155
}
110156

0 commit comments

Comments
 (0)