@danielhaim/titlecaser 1.2.53 → 1.2.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -7
- package/package.json +1 -1
- package/src/TitleCaser.js +122 -116
- package/src/TitleCaserConsts.js +71 -71
- package/src/TitleCaserUtils.js +318 -282
package/src/TitleCaserUtils.js
CHANGED
|
@@ -7,205 +7,206 @@ import {
|
|
|
7
7
|
} from "./TitleCaserConsts.js";
|
|
8
8
|
|
|
9
9
|
export class TitleCaserUtils {
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
// Validate the option key
|
|
12
|
-
static validateOption
|
|
12
|
+
static validateOption(key, value) {
|
|
13
13
|
// Check if value is an array
|
|
14
|
-
if (
|
|
15
|
-
throw new TypeError
|
|
14
|
+
if (!Array.isArray(value)) {
|
|
15
|
+
throw new TypeError(`Invalid option: ${key} must be an array`);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
// Check if all values in the array are strings
|
|
19
|
-
if (
|
|
20
|
-
throw new TypeError
|
|
19
|
+
if (!value.every((word) => typeof word === "string")) {
|
|
20
|
+
throw new TypeError(`Invalid option: ${key} must be an array of strings`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
// Validate the option object
|
|
25
25
|
static TitleCaseValidator;
|
|
26
|
-
|
|
27
|
-
static validateOptions
|
|
28
|
-
for (
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
if (
|
|
32
|
-
throw new TypeError
|
|
33
|
-
} else if (
|
|
34
|
-
throw new TypeError
|
|
26
|
+
|
|
27
|
+
static validateOptions(options) {
|
|
28
|
+
for (const key of Object.keys(options)) {
|
|
29
|
+
|
|
30
|
+
if (key === 'style') {
|
|
31
|
+
if (typeof options.style !== 'string') {
|
|
32
|
+
throw new TypeError(`Invalid option: ${key} must be a string`);
|
|
33
|
+
} else if (!allowedTitleCaseStylesList.includes(options.style)) {
|
|
34
|
+
throw new TypeError(`Invalid option: ${key} must be a string`);
|
|
35
35
|
}
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
if (
|
|
41
|
-
throw new TypeError
|
|
38
|
+
|
|
39
|
+
if (key === 'wordReplacementsList') {
|
|
40
|
+
if (!Array.isArray(options.wordReplacementsList)) {
|
|
41
|
+
throw new TypeError(`Invalid option: ${key} must be an array`);
|
|
42
42
|
} else {
|
|
43
|
-
for (
|
|
44
|
-
if (
|
|
45
|
-
throw new TypeError
|
|
43
|
+
for (const term of options.wordReplacementsList) {
|
|
44
|
+
if (typeof term !== 'string') {
|
|
45
|
+
throw new TypeError(`Invalid option: ${key} must contain only strings`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
throw new TypeError
|
|
51
|
+
|
|
52
|
+
if (!titleCaseDefaultOptionsList.hasOwnProperty(key)) {
|
|
53
|
+
throw new TypeError(`Invalid option: ${key}`);
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
this.TitleCaseValidator.validateOption
|
|
55
|
+
|
|
56
|
+
this.TitleCaseValidator.validateOption(key, options[key]);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
static titleCaseOptionsCache = new Map
|
|
61
|
-
|
|
62
|
-
static getTitleCaseOptions
|
|
59
|
+
|
|
60
|
+
static titleCaseOptionsCache = new Map();
|
|
61
|
+
|
|
62
|
+
static getTitleCaseOptions(options = {}, lowercaseWords = []) {
|
|
63
63
|
// Create a unique key for the cache that combines the options and the lowercase words
|
|
64
|
-
const cacheKey = JSON.stringify
|
|
64
|
+
const cacheKey = JSON.stringify({
|
|
65
65
|
options,
|
|
66
66
|
lowercaseWords
|
|
67
|
-
}
|
|
68
|
-
|
|
67
|
+
});
|
|
68
|
+
|
|
69
69
|
// If the cache already has an entry for this key, return the cached options
|
|
70
|
-
if (
|
|
71
|
-
return TitleCaserUtils.titleCaseOptionsCache.get
|
|
70
|
+
if (TitleCaserUtils.titleCaseOptionsCache.has(cacheKey)) {
|
|
71
|
+
return TitleCaserUtils.titleCaseOptionsCache.get(cacheKey);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
// Merge the default options with the user-provided options
|
|
73
|
+
|
|
75
74
|
const mergedOptions = {
|
|
76
75
|
...titleCaseDefaultOptionsList[options.style || "ap"],
|
|
77
|
-
...options
|
|
76
|
+
...options,
|
|
77
|
+
smartQuotes: options.hasOwnProperty('smartQuotes') ? options.smartQuotes : false
|
|
78
78
|
};
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
// Merge the default articles with user-provided articles and lowercase words
|
|
81
|
-
const mergedArticles = mergedOptions.articlesList.concat
|
|
82
|
-
.filter
|
|
83
|
-
|
|
81
|
+
const mergedArticles = mergedOptions.articlesList.concat(lowercaseWords)
|
|
82
|
+
.filter((word, index, array) => array.indexOf(word) === index);
|
|
83
|
+
|
|
84
84
|
// Merge the default short conjunctions with user-provided conjunctions and lowercase words
|
|
85
|
-
const mergedShortConjunctions = mergedOptions.shortConjunctionsList.concat
|
|
86
|
-
.filter
|
|
87
|
-
|
|
85
|
+
const mergedShortConjunctions = mergedOptions.shortConjunctionsList.concat(lowercaseWords)
|
|
86
|
+
.filter((word, index, array) => array.indexOf(word) === index);
|
|
87
|
+
|
|
88
88
|
// Merge the default short prepositions with user-provided prepositions and lowercase words
|
|
89
|
-
const mergedShortPrepositions = mergedOptions.shortPrepositionsList.concat
|
|
90
|
-
.filter
|
|
91
|
-
|
|
89
|
+
const mergedShortPrepositions = mergedOptions.shortPrepositionsList.concat(lowercaseWords)
|
|
90
|
+
.filter((word, index, array) => array.indexOf(word) === index);
|
|
91
|
+
|
|
92
92
|
// Merge the default word replacements with the user-provided replacements
|
|
93
93
|
const mergedReplaceTerms = [
|
|
94
94
|
...(mergedOptions.replaceTerms || [])
|
|
95
|
-
.map
|
|
95
|
+
.map(([key, value]) => [key.toLowerCase(), value]),
|
|
96
96
|
...wordReplacementsList,
|
|
97
97
|
];
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Return the merged options
|
|
100
100
|
const result = {
|
|
101
101
|
articlesList: mergedArticles,
|
|
102
102
|
shortConjunctionsList: mergedShortConjunctions,
|
|
103
103
|
shortPrepositionsList: mergedShortPrepositions,
|
|
104
|
-
neverCapitalizedList: [
|
|
104
|
+
neverCapitalizedList: [...mergedOptions.neverCapitalizedList],
|
|
105
105
|
replaceTerms: mergedReplaceTerms,
|
|
106
|
+
smartQuotes: mergedOptions.smartQuotes // Add smartQuotes option to result
|
|
106
107
|
};
|
|
107
|
-
|
|
108
|
+
|
|
108
109
|
// Add the merged options to the cache and return them
|
|
109
|
-
TitleCaserUtils.titleCaseOptionsCache.set
|
|
110
|
+
TitleCaserUtils.titleCaseOptionsCache.set(cacheKey, result);
|
|
110
111
|
return result;
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
static isNeverCapitalizedCache = new Map
|
|
114
|
-
|
|
113
|
+
|
|
114
|
+
static isNeverCapitalizedCache = new Map();
|
|
115
|
+
|
|
115
116
|
// Check if the word is a short conjunction
|
|
116
|
-
static isShortConjunction
|
|
117
|
+
static isShortConjunction(word, style) {
|
|
117
118
|
// Get the list of short conjunctions from the TitleCaseHelper
|
|
118
|
-
const shortConjunctionsList = [
|
|
119
|
+
const shortConjunctionsList = [...TitleCaserUtils.getTitleCaseOptions({
|
|
119
120
|
style: style
|
|
120
|
-
}
|
|
121
|
+
})
|
|
121
122
|
.shortConjunctionsList
|
|
122
123
|
];
|
|
123
|
-
|
|
124
|
+
|
|
124
125
|
// Convert the word to lowercase
|
|
125
|
-
const wordLowerCase = word.toLowerCase
|
|
126
|
-
|
|
126
|
+
const wordLowerCase = word.toLowerCase();
|
|
127
|
+
|
|
127
128
|
// Return true if the word is in the list of short conjunctions
|
|
128
|
-
return shortConjunctionsList.includes
|
|
129
|
+
return shortConjunctionsList.includes(wordLowerCase);
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
// Check if the word is an article
|
|
132
|
-
static isArticle
|
|
133
|
+
static isArticle(word, style) {
|
|
133
134
|
// Get the list of articles for the language
|
|
134
|
-
const articlesList = TitleCaserUtils.getTitleCaseOptions
|
|
135
|
+
const articlesList = TitleCaserUtils.getTitleCaseOptions({
|
|
135
136
|
style: style
|
|
136
|
-
}
|
|
137
|
+
})
|
|
137
138
|
.articlesList;
|
|
138
139
|
// Return true if the word matches an article
|
|
139
|
-
return articlesList.includes
|
|
140
|
+
return articlesList.includes(word.toLowerCase());
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
+
|
|
142
143
|
// Check if the word is a short preposition
|
|
143
|
-
static isShortPreposition
|
|
144
|
+
static isShortPreposition(word, style) {
|
|
144
145
|
// Get the list of short prepositions from the Title Case Helper.
|
|
145
146
|
const {
|
|
146
147
|
shortPrepositionsList
|
|
147
|
-
} = TitleCaserUtils.getTitleCaseOptions
|
|
148
|
+
} = TitleCaserUtils.getTitleCaseOptions({
|
|
148
149
|
style: style
|
|
149
|
-
}
|
|
150
|
+
});
|
|
150
151
|
// Check if the word is in the list of short prepositions.
|
|
151
|
-
return shortPrepositionsList.includes
|
|
152
|
+
return shortPrepositionsList.includes(word.toLowerCase());
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
+
|
|
154
155
|
// This function is only ever called once per word per style, since the result is cached.
|
|
155
156
|
// The cache key is a combination of the style and the lowercase word.
|
|
156
|
-
static isNeverCapitalized
|
|
157
|
+
static isNeverCapitalized(word, style) {
|
|
157
158
|
// Check if the word is in the cache. If it is, return it.
|
|
158
|
-
const cacheKey = `${
|
|
159
|
-
if (
|
|
160
|
-
return TitleCaserUtils.isNeverCapitalizedCache.get
|
|
159
|
+
const cacheKey = `${style}_${word.toLowerCase()}`;
|
|
160
|
+
if (TitleCaserUtils.isNeverCapitalizedCache.has(cacheKey)) {
|
|
161
|
+
return TitleCaserUtils.isNeverCapitalizedCache.get(cacheKey);
|
|
161
162
|
}
|
|
162
|
-
|
|
163
|
+
|
|
163
164
|
// If the word is not in the cache, then check if it is in the word list for the given style.
|
|
164
165
|
const {
|
|
165
166
|
neverCapitalizedList
|
|
166
|
-
} = TitleCaserUtils.getTitleCaseOptions
|
|
167
|
+
} = TitleCaserUtils.getTitleCaseOptions({
|
|
167
168
|
style
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const result = neverCapitalizedList.includes
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = neverCapitalizedList.includes(word.toLowerCase());
|
|
171
172
|
// Store the result in the cache so it can be used again
|
|
172
|
-
TitleCaserUtils.isNeverCapitalizedCache.set
|
|
173
|
-
|
|
173
|
+
TitleCaserUtils.isNeverCapitalizedCache.set(cacheKey, result);
|
|
174
|
+
|
|
174
175
|
return result;
|
|
175
176
|
}
|
|
176
|
-
|
|
177
|
-
static isShortWord
|
|
177
|
+
|
|
178
|
+
static isShortWord(word, style) {
|
|
178
179
|
// If the word is not a string, throw a TypeError.
|
|
179
|
-
if (
|
|
180
|
-
throw new TypeError
|
|
180
|
+
if (typeof word !== "string") {
|
|
181
|
+
throw new TypeError(`Invalid input: word must be a string. Received ${typeof word}.`);
|
|
181
182
|
}
|
|
182
|
-
|
|
183
|
+
|
|
183
184
|
// If the style is not one of the allowed styles, throw an Error.
|
|
184
|
-
if (
|
|
185
|
-
throw new Error
|
|
185
|
+
if (!allowedTitleCaseStylesList.includes(style)) {
|
|
186
|
+
throw new Error(`Invalid option: style must be one of ${allowedTitleCaseStylesList.join(", ")}.`);
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
+
|
|
188
189
|
// If the word is a short conjunction, article, preposition, or is in the never-capitalized list, return true.
|
|
189
190
|
// Otherwise, return false.
|
|
190
|
-
return TitleCaserUtils.isShortConjunction
|
|
191
|
-
TitleCaserUtils.isArticle
|
|
192
|
-
TitleCaserUtils.isShortPreposition
|
|
193
|
-
TitleCaserUtils.isNeverCapitalized
|
|
191
|
+
return TitleCaserUtils.isShortConjunction(word, style) ||
|
|
192
|
+
TitleCaserUtils.isArticle(word, style) ||
|
|
193
|
+
TitleCaserUtils.isShortPreposition(word, style) ||
|
|
194
|
+
TitleCaserUtils.isNeverCapitalized(word, style);
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
+
|
|
196
197
|
// Check if a word has a number
|
|
197
|
-
static hasNumbers
|
|
198
|
-
return /\d/.test
|
|
198
|
+
static hasNumbers(word) {
|
|
199
|
+
return /\d/.test(word);
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
+
|
|
201
202
|
// Check if a word has multiple uppercase letters
|
|
202
|
-
static hasUppercaseMultiple
|
|
203
|
+
static hasUppercaseMultiple(word) {
|
|
203
204
|
// initialize count to 0
|
|
204
205
|
let count = 0;
|
|
205
206
|
// loop through each character of the word
|
|
206
|
-
for (
|
|
207
|
+
for (let i = 0; i < word.length && count < 2; i++) {
|
|
207
208
|
// if the character is an uppercase letter
|
|
208
|
-
if (
|
|
209
|
+
if (/[A-Z]/.test(word[i])) {
|
|
209
210
|
// increment count by 1
|
|
210
211
|
count++;
|
|
211
212
|
}
|
|
@@ -213,104 +214,104 @@ export class TitleCaserUtils {
|
|
|
213
214
|
// return true if count is greater or equal to 2, false otherwise
|
|
214
215
|
return count >= 2;
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
+
|
|
217
218
|
// Check if a word has an intentional uppercase letter
|
|
218
219
|
// (i.e. not the first letter of the word)
|
|
219
|
-
static hasUppercaseIntentional
|
|
220
|
+
static hasUppercaseIntentional(word) {
|
|
220
221
|
// Only check for uppercase letters after the first letter
|
|
221
222
|
// and only check for lowercase letters before the last letter
|
|
222
|
-
return /[A-Z]/.test
|
|
223
|
+
return /[A-Z]/.test(word.slice(1)) && /[a-z]/.test(word.slice(0, -1));
|
|
223
224
|
}
|
|
224
|
-
|
|
225
|
+
|
|
225
226
|
// Check if a word has a suffix
|
|
226
|
-
static hasSuffix
|
|
227
|
+
static hasSuffix(word) {
|
|
227
228
|
// Test if word is longer than suffix
|
|
228
229
|
const suffix = "'s";
|
|
229
230
|
// Test if word ends with suffix
|
|
230
|
-
return word.length > suffix.length && word.endsWith
|
|
231
|
+
return word.length > suffix.length && word.endsWith(suffix);
|
|
231
232
|
}
|
|
232
|
-
|
|
233
|
+
|
|
233
234
|
// Check if a word has an apostrophe
|
|
234
|
-
static hasApostrophe
|
|
235
|
-
return word.indexOf
|
|
235
|
+
static hasApostrophe(word) {
|
|
236
|
+
return word.indexOf("'") !== -1;
|
|
236
237
|
}
|
|
237
|
-
|
|
238
|
+
|
|
238
239
|
// Check if a word has a hyphen
|
|
239
|
-
static hasHyphen
|
|
240
|
-
return word.indexOf
|
|
240
|
+
static hasHyphen(word) {
|
|
241
|
+
return word.indexOf('-') !== -1 || word.indexOf('–') !== -1 || word.indexOf('—') !== -1;
|
|
241
242
|
}
|
|
242
|
-
|
|
243
|
+
|
|
243
244
|
// Check if a word is a Roman numeral
|
|
244
|
-
static hasRomanNumeral
|
|
245
|
+
static hasRomanNumeral(word) {
|
|
245
246
|
// Check if the input is a string
|
|
246
|
-
if (
|
|
247
|
+
if (typeof word !== 'string' || word === '') {
|
|
247
248
|
// Throw an error if the input is not a string
|
|
248
|
-
throw new TypeError
|
|
249
|
+
throw new TypeError('Invalid input: word must be a non-empty string.');
|
|
249
250
|
}
|
|
250
|
-
|
|
251
|
+
|
|
251
252
|
// Define a regular expression that matches a roman numeral
|
|
252
253
|
const romanNumeralRegex = /^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
|
|
253
254
|
// Check if the input word matches the regular expression
|
|
254
|
-
return romanNumeralRegex.test
|
|
255
|
+
return romanNumeralRegex.test(word);
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
+
|
|
257
258
|
// Check if a word is a hyphenated Roman numeral
|
|
258
|
-
static hasHyphenRomanNumeral
|
|
259
|
-
if (
|
|
260
|
-
throw new TypeError
|
|
259
|
+
static hasHyphenRomanNumeral(word) {
|
|
260
|
+
if (typeof word !== "string" || word === "") {
|
|
261
|
+
throw new TypeError("Invalid input: word must be a non-empty string.");
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
-
const parts = word.split
|
|
264
|
-
for (
|
|
265
|
-
if (
|
|
263
|
+
|
|
264
|
+
const parts = word.split("-");
|
|
265
|
+
for (let i = 0; i < parts.length; i++) {
|
|
266
|
+
if (!TitleCaserUtils.hasRomanNumeral(parts[i])) {
|
|
266
267
|
return false;
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
return true;
|
|
270
271
|
}
|
|
271
|
-
|
|
272
|
+
|
|
272
273
|
// Check if a word has `nl2br` in it
|
|
273
|
-
static hasHtmlBreak
|
|
274
|
+
static hasHtmlBreak(word) {
|
|
274
275
|
return word === "nl2br";
|
|
275
276
|
}
|
|
276
|
-
|
|
277
|
+
|
|
277
278
|
// Check if a string has Unicode symbols.
|
|
278
|
-
static hasUnicodeSymbols
|
|
279
|
-
return /[^\x00-\x7F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u02B0-\u02FF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0800-\u083F\u0840-\u085F\u0860-\u087F\u0880-\u08AF\u08B0-\u08FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF]/.test
|
|
279
|
+
static hasUnicodeSymbols(str) {
|
|
280
|
+
return /[^\x00-\x7F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u02B0-\u02FF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0800-\u083F\u0840-\u085F\u0860-\u087F\u0880-\u08AF\u08B0-\u08FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF]/.test(str);
|
|
280
281
|
}
|
|
281
|
-
|
|
282
|
+
|
|
282
283
|
// Checks whether a string contains any currency symbols
|
|
283
|
-
static hasCurrencySymbols
|
|
284
|
-
return /[^\x00-\x7F\u00A0-\u00FF\u20AC\u20A0-\u20B9\u20BD\u20A1-\u20A2\u00A3-\u00A5\u058F\u060B\u09F2-\u09F3\u0AF1\u0BF9\u0E3F\u17DB\u20A6\u20A8\u20B1\u2113\u20AA-\u20AB\u20AA\u20AC-\u20AD\u20B9]/.test
|
|
284
|
+
static hasCurrencySymbols(str) {
|
|
285
|
+
return /[^\x00-\x7F\u00A0-\u00FF\u20AC\u20A0-\u20B9\u20BD\u20A1-\u20A2\u00A3-\u00A5\u058F\u060B\u09F2-\u09F3\u0AF1\u0BF9\u0E3F\u17DB\u20A6\u20A8\u20B1\u2113\u20AA-\u20AB\u20AA\u20AC-\u20AD\u20B9]/.test(str);
|
|
285
286
|
}
|
|
286
|
-
|
|
287
|
+
|
|
287
288
|
// Check if a word is ampersand
|
|
288
|
-
static isWordAmpersand
|
|
289
|
-
return /&|&/.test
|
|
289
|
+
static isWordAmpersand(str) {
|
|
290
|
+
return /&|&/.test(str);
|
|
290
291
|
}
|
|
291
|
-
|
|
292
|
-
|
|
292
|
+
|
|
293
|
+
|
|
293
294
|
// Check if a word starts with a symbol
|
|
294
|
-
static startsWithSymbol
|
|
295
|
-
if (
|
|
296
|
-
throw new Error
|
|
295
|
+
static startsWithSymbol(word) {
|
|
296
|
+
if (typeof word !== 'string') {
|
|
297
|
+
throw new Error(`Parameter 'word' must be a string. Received '${typeof word}' instead.`);
|
|
297
298
|
}
|
|
298
|
-
|
|
299
|
-
if (
|
|
299
|
+
|
|
300
|
+
if (word.length === 0) {
|
|
300
301
|
return false;
|
|
301
302
|
}
|
|
302
|
-
|
|
303
|
-
const firstChar = word.charAt
|
|
304
|
-
|
|
303
|
+
|
|
304
|
+
const firstChar = word.charAt(0);
|
|
305
|
+
|
|
305
306
|
return (
|
|
306
307
|
firstChar === '#' ||
|
|
307
308
|
firstChar === '@' ||
|
|
308
309
|
firstChar === '.'
|
|
309
310
|
);
|
|
310
311
|
}
|
|
311
|
-
|
|
312
|
-
static escapeSpecialCharacters
|
|
313
|
-
return str.replace
|
|
312
|
+
|
|
313
|
+
static escapeSpecialCharacters(str) {
|
|
314
|
+
return str.replace(/[&<>"']/g, function (match) {
|
|
314
315
|
switch (match) {
|
|
315
316
|
case "&":
|
|
316
317
|
return "&";
|
|
@@ -325,11 +326,11 @@ export class TitleCaserUtils {
|
|
|
325
326
|
default:
|
|
326
327
|
return match;
|
|
327
328
|
}
|
|
328
|
-
}
|
|
329
|
+
});
|
|
329
330
|
}
|
|
330
|
-
|
|
331
|
-
static unescapeSpecialCharacters
|
|
332
|
-
return str.replace
|
|
331
|
+
|
|
332
|
+
static unescapeSpecialCharacters(str) {
|
|
333
|
+
return str.replace(/&|<|>|"|'/g, function (match) {
|
|
333
334
|
switch (match) {
|
|
334
335
|
case "&":
|
|
335
336
|
return "&";
|
|
@@ -344,227 +345,262 @@ export class TitleCaserUtils {
|
|
|
344
345
|
default:
|
|
345
346
|
return match;
|
|
346
347
|
}
|
|
347
|
-
}
|
|
348
|
+
});
|
|
348
349
|
}
|
|
349
|
-
|
|
350
|
-
|
|
350
|
+
|
|
351
|
+
|
|
351
352
|
// Check if a word ends with a symbol
|
|
352
|
-
static endsWithSymbol
|
|
353
|
+
static endsWithSymbol(word, symbols = [".", ",", ";", ":", "?", "!"]) {
|
|
353
354
|
// Check if the word is a string and the symbols is an array
|
|
354
|
-
if (
|
|
355
|
-
throw new Error
|
|
355
|
+
if (typeof word !== "string" || !Array.isArray(symbols))
|
|
356
|
+
throw new Error("Invalid arguments");
|
|
356
357
|
// Check if the word ends with a symbol or two symbols
|
|
357
|
-
return symbols.some
|
|
358
|
+
return symbols.some(symbol => word.endsWith(symbol)) || symbols.includes(word.slice(-2));
|
|
358
359
|
}
|
|
359
|
-
|
|
360
|
+
|
|
360
361
|
// This function accepts two arguments: a word and an array of ignored words.
|
|
361
|
-
static isWordIgnored
|
|
362
|
+
static isWordIgnored(word, ignoredWords = ignoredWordList) {
|
|
362
363
|
// If the ignoredWords argument is not an array, throw an error.
|
|
363
|
-
if (
|
|
364
|
-
throw new TypeError
|
|
364
|
+
if (!Array.isArray(ignoredWords)) {
|
|
365
|
+
throw new TypeError("Invalid input: ignoredWords must be an array.");
|
|
365
366
|
}
|
|
366
|
-
|
|
367
|
+
|
|
367
368
|
// If the word argument is not a non-empty string, throw an error.
|
|
368
|
-
if (
|
|
369
|
-
throw new TypeError
|
|
369
|
+
if (typeof word !== "string" || word.trim() === "") {
|
|
370
|
+
throw new TypeError("Invalid input: word must be a non-empty string.");
|
|
370
371
|
}
|
|
371
|
-
|
|
372
|
+
|
|
372
373
|
// Convert the word to lowercase and trim any space.
|
|
373
374
|
let lowercasedWord;
|
|
374
|
-
lowercasedWord = word.toLowerCase
|
|
375
|
-
.trim
|
|
376
|
-
|
|
375
|
+
lowercasedWord = word.toLowerCase()
|
|
376
|
+
.trim();
|
|
377
|
+
|
|
377
378
|
// If the word is in the ignoredWords array, return true. Otherwise, return false.
|
|
378
|
-
return ignoredWords.includes
|
|
379
|
+
return ignoredWords.includes(lowercasedWord);
|
|
379
380
|
}
|
|
380
|
-
|
|
381
|
+
|
|
381
382
|
// Check if the wordList is a valid array
|
|
382
|
-
static isWordInArray
|
|
383
|
-
if (
|
|
383
|
+
static isWordInArray(targetWord, wordList) {
|
|
384
|
+
if (!Array.isArray(wordList)) {
|
|
384
385
|
return false;
|
|
385
386
|
}
|
|
386
|
-
|
|
387
|
+
|
|
387
388
|
// Check if the targetWord is in the wordList
|
|
388
|
-
return wordList.some
|
|
389
|
+
return wordList.some((word) => word.toLowerCase() === targetWord.toLowerCase());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
static convertQuotesToCurly(input) {
|
|
393
|
+
const curlyQuotes = {
|
|
394
|
+
"'": ['\u2018', '\u2019'],
|
|
395
|
+
'"': ['\u201C', '\u201D'],
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
let replacedText = '';
|
|
399
|
+
|
|
400
|
+
for (let i = 0; i < input.length; i++) {
|
|
401
|
+
const char = input[i];
|
|
402
|
+
const curlyQuotePair = curlyQuotes[char];
|
|
403
|
+
|
|
404
|
+
if (curlyQuotePair) {
|
|
405
|
+
const prevChar = input[i - 1];
|
|
406
|
+
const nextChar = input[i + 1];
|
|
407
|
+
|
|
408
|
+
// Determine whether to use left or right curly quote
|
|
409
|
+
const isLeftAligned = (!prevChar || prevChar === ' ' || prevChar === '\n');
|
|
410
|
+
const curlyQuote = isLeftAligned ? curlyQuotePair[0] : curlyQuotePair[1];
|
|
411
|
+
replacedText += curlyQuote;
|
|
412
|
+
|
|
413
|
+
// Handle cases where right curly quote is followed by punctuation or space
|
|
414
|
+
if (curlyQuote === curlyQuotePair[1] && /[.,;!?()\[\]{}:]/.test(nextChar)) {
|
|
415
|
+
replacedText += nextChar;
|
|
416
|
+
i++; // Skip the next character
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
replacedText += char;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return replacedText;
|
|
389
424
|
}
|
|
390
|
-
|
|
425
|
+
|
|
426
|
+
|
|
391
427
|
// This function is used to replace a word with a term in the replaceTerms object
|
|
392
|
-
static replaceTerm
|
|
428
|
+
static replaceTerm(word, replaceTermObj) {
|
|
393
429
|
// Validate input
|
|
394
|
-
if (
|
|
395
|
-
throw new TypeError
|
|
430
|
+
if (typeof word !== "string" || word === "") {
|
|
431
|
+
throw new TypeError("Invalid input: word must be a non-empty string.");
|
|
396
432
|
}
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
throw new TypeError
|
|
433
|
+
|
|
434
|
+
if (!replaceTermObj || typeof replaceTermObj !== "object") {
|
|
435
|
+
throw new TypeError("Invalid input: replaceTermObj must be a non-null object.");
|
|
400
436
|
}
|
|
401
|
-
|
|
437
|
+
|
|
402
438
|
// Convert the word to lowercase
|
|
403
439
|
let lowercasedWord;
|
|
404
|
-
lowercasedWord = word.toLowerCase
|
|
405
|
-
|
|
440
|
+
lowercasedWord = word.toLowerCase();
|
|
441
|
+
|
|
406
442
|
// Check if the word is in the object with lowercase key
|
|
407
|
-
if (
|
|
443
|
+
if (replaceTermObj.hasOwnProperty(lowercasedWord)) {
|
|
408
444
|
return replaceTermObj[lowercasedWord];
|
|
409
445
|
}
|
|
410
|
-
|
|
446
|
+
|
|
411
447
|
// Check if the word is in the object with original case key
|
|
412
|
-
if (
|
|
448
|
+
if (replaceTermObj.hasOwnProperty(word)) {
|
|
413
449
|
return replaceTermObj[word];
|
|
414
450
|
}
|
|
415
|
-
|
|
451
|
+
|
|
416
452
|
// Check if the word is in the object with uppercase key
|
|
417
|
-
const uppercasedWord = word.toUpperCase
|
|
418
|
-
if (
|
|
453
|
+
const uppercasedWord = word.toUpperCase();
|
|
454
|
+
if (replaceTermObj.hasOwnProperty(uppercasedWord)) {
|
|
419
455
|
return replaceTermObj[uppercasedWord];
|
|
420
456
|
}
|
|
421
|
-
|
|
457
|
+
|
|
422
458
|
// If the word is not in the object, return the original word
|
|
423
459
|
return word;
|
|
424
460
|
}
|
|
425
|
-
|
|
461
|
+
|
|
426
462
|
// This function is used to check if a suffix is present in a word that is in the correct terms list
|
|
427
|
-
static correctSuffix
|
|
463
|
+
static correctSuffix(word, correctTerms) {
|
|
428
464
|
// Validate input
|
|
429
|
-
if (
|
|
430
|
-
throw new TypeError
|
|
465
|
+
if (typeof word !== "string" || word === "") {
|
|
466
|
+
throw new TypeError("Invalid input: word must be a non-empty string.");
|
|
431
467
|
}
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
throw new TypeError
|
|
468
|
+
|
|
469
|
+
if (!correctTerms || !Array.isArray(correctTerms) || correctTerms.some((term) => typeof term !== "string")) {
|
|
470
|
+
throw new TypeError("Invalid input: correctTerms must be an array of strings.");
|
|
435
471
|
}
|
|
436
|
-
|
|
472
|
+
|
|
437
473
|
// Define the regular expression for the suffix
|
|
438
474
|
const suffixRegex = /'s$/i;
|
|
439
|
-
|
|
475
|
+
|
|
440
476
|
// If the word ends with the suffix
|
|
441
|
-
if (
|
|
477
|
+
if (suffixRegex.test(word)) {
|
|
442
478
|
// Remove the suffix from the word
|
|
443
|
-
const wordWithoutSuffix = word.slice
|
|
479
|
+
const wordWithoutSuffix = word.slice(0, -2);
|
|
444
480
|
// Check if the word without the suffix matches any of the correct terms
|
|
445
|
-
const matchingIndex = correctTerms.findIndex
|
|
446
|
-
|
|
447
|
-
if (
|
|
481
|
+
const matchingIndex = correctTerms.findIndex((term) => term.toLowerCase() === wordWithoutSuffix.toLowerCase());
|
|
482
|
+
|
|
483
|
+
if (matchingIndex >= 0) {
|
|
448
484
|
// If it does, return the correct term with the suffix
|
|
449
485
|
const correctCase = correctTerms[matchingIndex];
|
|
450
|
-
return `${
|
|
486
|
+
return `${correctCase}'s`;
|
|
451
487
|
} else {
|
|
452
488
|
// If not, capitalize the first letter and append the suffix
|
|
453
|
-
const capitalizedWord = wordWithoutSuffix.charAt
|
|
454
|
-
return `${
|
|
489
|
+
const capitalizedWord = wordWithoutSuffix.charAt(0).toUpperCase() + wordWithoutSuffix.slice(1);
|
|
490
|
+
return `${capitalizedWord}'s`;
|
|
455
491
|
}
|
|
456
492
|
}
|
|
457
|
-
|
|
493
|
+
|
|
458
494
|
// If the word doesn't end with the suffix, return the word as-is
|
|
459
495
|
return word;
|
|
460
496
|
}
|
|
461
|
-
|
|
497
|
+
|
|
462
498
|
// This function is used to check if a word is in the correct terms list
|
|
463
|
-
static correctTerm
|
|
499
|
+
static correctTerm(word, correctTerms, delimiters = /[-']/) {
|
|
464
500
|
// Validate input
|
|
465
|
-
if (
|
|
466
|
-
throw new TypeError
|
|
501
|
+
if (typeof word !== "string" || word === "") {
|
|
502
|
+
throw new TypeError("Invalid input: word must be a non-empty string.");
|
|
467
503
|
}
|
|
468
|
-
|
|
469
|
-
if (
|
|
470
|
-
throw new TypeError
|
|
504
|
+
|
|
505
|
+
if (!correctTerms || !Array.isArray(correctTerms)) {
|
|
506
|
+
throw new TypeError("Invalid input: correctTerms must be an array.");
|
|
471
507
|
}
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
throw new TypeError
|
|
508
|
+
|
|
509
|
+
if (typeof delimiters !== "string" && !Array.isArray(delimiters) && !(delimiters instanceof RegExp)) {
|
|
510
|
+
throw new TypeError("Invalid input: delimiters must be a string, an array of strings, or a regular expression.");
|
|
475
511
|
}
|
|
476
|
-
|
|
512
|
+
|
|
477
513
|
// Convert delimiters to a regular expression if it is a string or an array
|
|
478
|
-
if (
|
|
479
|
-
delimiters = new RegExp
|
|
480
|
-
} else if (
|
|
481
|
-
delimiters = new RegExp
|
|
514
|
+
if (typeof delimiters === "string") {
|
|
515
|
+
delimiters = new RegExp(`[${delimiters}]`);
|
|
516
|
+
} else if (Array.isArray(delimiters)) {
|
|
517
|
+
delimiters = new RegExp(`[${delimiters.join("")}]`);
|
|
482
518
|
}
|
|
483
|
-
|
|
519
|
+
|
|
484
520
|
// Split the word into parts delimited by the specified delimiters
|
|
485
|
-
const parts = word.split
|
|
521
|
+
const parts = word.split(delimiters);
|
|
486
522
|
// Count the number of parts
|
|
487
523
|
const numParts = parts.length;
|
|
488
|
-
|
|
524
|
+
|
|
489
525
|
// For each part
|
|
490
|
-
for (
|
|
526
|
+
for (let i = 0; i < numParts; i++) {
|
|
491
527
|
// Lowercase the part
|
|
492
|
-
const lowercasedPart = parts[i].toLowerCase
|
|
528
|
+
const lowercasedPart = parts[i].toLowerCase();
|
|
493
529
|
// Search for the part in the list of correct terms
|
|
494
|
-
const index = correctTerms.findIndex
|
|
530
|
+
const index = correctTerms.findIndex((t) => t.toLowerCase() === lowercasedPart);
|
|
495
531
|
// If the part is found in the list of correct terms
|
|
496
|
-
if (
|
|
532
|
+
if (index >= 0) {
|
|
497
533
|
// Replace the part with the correct term
|
|
498
534
|
parts[i] = correctTerms[index];
|
|
499
535
|
}
|
|
500
536
|
}
|
|
501
|
-
|
|
537
|
+
|
|
502
538
|
// Join the parts back together using the first delimiter as the default delimiter
|
|
503
|
-
return parts.join
|
|
539
|
+
return parts.join(delimiters.source.charAt(0));
|
|
504
540
|
}
|
|
505
|
-
|
|
541
|
+
|
|
506
542
|
// This function is used to check if a word is in the correct terms list
|
|
507
|
-
static correctTermHyphenated
|
|
543
|
+
static correctTermHyphenated(word, style) {
|
|
508
544
|
// Split the word into an array of words
|
|
509
|
-
const hyphenatedWords = word.split
|
|
510
|
-
|
|
545
|
+
const hyphenatedWords = word.split("-");
|
|
546
|
+
|
|
511
547
|
// Define functions to process words
|
|
512
|
-
const capitalizeFirst = (
|
|
513
|
-
.toUpperCase
|
|
514
|
-
const lowercaseRest = (
|
|
515
|
-
.toLowerCase
|
|
516
|
-
|
|
548
|
+
const capitalizeFirst = (word) => word.charAt(0)
|
|
549
|
+
.toUpperCase() + word.slice(1);
|
|
550
|
+
const lowercaseRest = (word) => word.charAt(0) + word.slice(1)
|
|
551
|
+
.toLowerCase();
|
|
552
|
+
|
|
517
553
|
// Define the style-specific processing functions
|
|
518
554
|
const styleFunctions = {
|
|
519
|
-
ap: (
|
|
555
|
+
ap: (word, index) => (index === 0 ? capitalizeFirst(word) : lowercaseRest(word)),
|
|
520
556
|
chicago: capitalizeFirst,
|
|
521
|
-
apa: (
|
|
522
|
-
if (
|
|
523
|
-
return word.toLowerCase
|
|
557
|
+
apa: (word, index, length) => {
|
|
558
|
+
if (TitleCaserUtils.isShortWord(word, style) && index > 0 && index < length - 1) {
|
|
559
|
+
return word.toLowerCase();
|
|
524
560
|
} else {
|
|
525
|
-
return capitalizeFirst
|
|
561
|
+
return capitalizeFirst(word);
|
|
526
562
|
}
|
|
527
563
|
},
|
|
528
|
-
nyt: (
|
|
529
|
-
wikipedia: (
|
|
564
|
+
nyt: (word, index) => (index === 0 ? capitalizeFirst(word) : lowercaseRest(word)),
|
|
565
|
+
wikipedia: (word, index) => (index === 0 ? capitalizeFirst(word) : lowercaseRest(word)),
|
|
530
566
|
};
|
|
531
|
-
|
|
567
|
+
|
|
532
568
|
// Get the style-specific processing function
|
|
533
569
|
const processWord = styleFunctions[style] || lowercaseRest;
|
|
534
|
-
|
|
570
|
+
|
|
535
571
|
// Process each word
|
|
536
|
-
const processedWords = hyphenatedWords.map
|
|
537
|
-
|
|
572
|
+
const processedWords = hyphenatedWords.map((word, i) => {
|
|
573
|
+
|
|
538
574
|
// Check if the word is a Roman numeral
|
|
539
575
|
const romanNumeralRegex = /^(M{0,3})(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i;
|
|
540
|
-
if (
|
|
541
|
-
return word.toUpperCase
|
|
576
|
+
if (romanNumeralRegex.test(word)) {
|
|
577
|
+
return word.toUpperCase();
|
|
542
578
|
}
|
|
543
|
-
|
|
579
|
+
|
|
544
580
|
// Preserve the original word
|
|
545
581
|
let correctedWord = word;
|
|
546
|
-
|
|
582
|
+
|
|
547
583
|
// Check if the word is in the list of words to preserve
|
|
548
|
-
const lowerCaseWord = word.toLowerCase
|
|
549
|
-
const uniqueTermsIndex = correctTitleCasingList.findIndex
|
|
550
|
-
if (
|
|
584
|
+
const lowerCaseWord = word.toLowerCase();
|
|
585
|
+
const uniqueTermsIndex = correctTitleCasingList.findIndex((w) => w.toLowerCase() === lowerCaseWord);
|
|
586
|
+
if (uniqueTermsIndex >= 0) {
|
|
551
587
|
correctedWord = correctTitleCasingList[uniqueTermsIndex];
|
|
552
588
|
}
|
|
553
589
|
// Check if the word is a possessive form
|
|
554
|
-
else if (
|
|
555
|
-
const rootWord = lowerCaseWord.substring
|
|
556
|
-
const rootWordIndex = correctTitleCasingList.findIndex
|
|
557
|
-
if (
|
|
558
|
-
correctedWord = `${
|
|
590
|
+
else if (lowerCaseWord.endsWith("'s")) {
|
|
591
|
+
const rootWord = lowerCaseWord.substring(0, lowerCaseWord.length - 2);
|
|
592
|
+
const rootWordIndex = correctTitleCasingList.findIndex((w) => w.toLowerCase() === rootWord);
|
|
593
|
+
if (rootWordIndex >= 0) {
|
|
594
|
+
correctedWord = `${correctTitleCasingList[rootWordIndex]}'s`;
|
|
559
595
|
}
|
|
560
596
|
}
|
|
561
|
-
|
|
597
|
+
|
|
562
598
|
// Process the word
|
|
563
|
-
return processWord
|
|
564
|
-
}
|
|
565
|
-
|
|
599
|
+
return processWord(correctedWord, i, hyphenatedWords.length);
|
|
600
|
+
});
|
|
601
|
+
|
|
566
602
|
// Rejoin the words
|
|
567
|
-
return processedWords.join
|
|
603
|
+
return processedWords.join("-");
|
|
568
604
|
}
|
|
569
|
-
|
|
605
|
+
|
|
570
606
|
}
|