punctilio 3.5.0 → 3.6.0
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/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +44 -6
- package/dist/quotes.js.map +1 -1
- package/package.json +1 -1
- package/src/quotes.ts +53 -13
- package/src/tests/quotes.test.ts +17 -0
package/dist/quotes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quotes.d.ts","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"quotes.d.ts","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAyBH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEpF,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;CACpC;AAySD,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAK3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAEpF"}
|
package/dist/quotes.js
CHANGED
|
@@ -6,6 +6,13 @@ import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, LATIN_LETTERS, TERMINAL_PUNCTUATION
|
|
|
6
6
|
const { EM_DASH, LEFT_DOUBLE_QUOTE, RIGHT_DOUBLE_QUOTE, LEFT_SINGLE_QUOTE, RIGHT_SINGLE_QUOTE, MODIFIER_LETTER_APOSTROPHE, } = UNICODE_SYMBOLS;
|
|
7
7
|
/** Joined string of all terminal punctuation characters for use in regex character classes. */
|
|
8
8
|
const TERMINAL_PUNCTUATION_CLASS = TERMINAL_PUNCTUATION.join("");
|
|
9
|
+
/**
|
|
10
|
+
* Maximum consecutive closing quotes the punctuation-placement regexes will
|
|
11
|
+
* traverse in a single match (e.g., '". for two-deep nesting). Bounded to keep
|
|
12
|
+
* the regex linear on pathological inputs that contain many closing quotes
|
|
13
|
+
* without any following period or comma.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_NESTED_QUOTES = 4;
|
|
9
16
|
/** Convert straight single quotes to curly quotes and apostrophes */
|
|
10
17
|
function convertSingleQuotes(text, sep) {
|
|
11
18
|
const escapedSep = getEscapedSeparator({ separator: sep });
|
|
@@ -107,16 +114,47 @@ function convertDoubleQuotes(text, sep) {
|
|
|
107
114
|
text = text.replace(cachedRegExp(`'(?=${RIGHT_DOUBLE_QUOTE})`, "gu"), RIGHT_SINGLE_QUOTE);
|
|
108
115
|
return text;
|
|
109
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Build a regex that matches a punctuation mark sitting OUTSIDE one or more
|
|
119
|
+
* consecutive closing quotes, for American-style inside-the-quotes placement.
|
|
120
|
+
*
|
|
121
|
+
* Shape: `<non-terminal>(sepBefore)(quotes)(sepAfter)<punctuation>`
|
|
122
|
+
*
|
|
123
|
+
* - The lookbehind ensures the char just before the match isn't already terminal
|
|
124
|
+
* punctuation (so we don't rewrite, e.g., "Stop!".).
|
|
125
|
+
* - `quotes` matches 1 to MAX_NESTED_QUOTES closing quotes (possibly separated
|
|
126
|
+
* by element-boundary markers). The bound keeps the regex linear on
|
|
127
|
+
* pathological inputs — unbounded `*` causes catastrophic backtracking when
|
|
128
|
+
* many closing quotes appear with no following punctuation.
|
|
129
|
+
* - `punctuationPattern` is the trailing pattern (e.g. `,` or `(?!\.\.\.)\.`).
|
|
130
|
+
*/
|
|
131
|
+
function buildOutsidePunctuationRegex(escapedSep, punctuationPattern) {
|
|
132
|
+
const closingQuoteClass = `[${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}]`;
|
|
133
|
+
const additionalQuotes = `(?:${escapedSep}?${closingQuoteClass}){0,${MAX_NESTED_QUOTES - 1}}`;
|
|
134
|
+
const nonTerminalLookbehind = `(?<![${TERMINAL_PUNCTUATION_CLASS}])`;
|
|
135
|
+
const pattern = `${nonTerminalLookbehind}` +
|
|
136
|
+
`(?<sepBefore>${escapedSep}?)` +
|
|
137
|
+
`(?<quotes>${closingQuoteClass}${additionalQuotes})` +
|
|
138
|
+
`(?<sepAfter>${escapedSep}?)` +
|
|
139
|
+
punctuationPattern;
|
|
140
|
+
return cachedRegExp(pattern, "g");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Move a punctuation mark sitting outside one or more consecutive closing
|
|
144
|
+
* quotes to the inside (American style): `Hello'".` → `Hello.'"`.
|
|
145
|
+
*/
|
|
146
|
+
function moveOutsideToInside(text, escapedSep, replacementChar, punctuationPattern) {
|
|
147
|
+
const regex = buildOutsidePunctuationRegex(escapedSep, punctuationPattern);
|
|
148
|
+
return text.replace(regex, `$<sepBefore>${replacementChar}$<quotes>$<sepAfter>`);
|
|
149
|
+
}
|
|
110
150
|
/** Apply American or British punctuation style */
|
|
111
151
|
function applyPunctuationStyle(text, sep, style) {
|
|
112
152
|
const escapedSep = getEscapedSeparator({ separator: sep });
|
|
113
153
|
if (style === "american") {
|
|
114
|
-
// Period outside → inside:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const commaOutsideRegex = cachedRegExp(`(?<![${TERMINAL_PUNCTUATION_CLASS}])(?<sepBefore>${escapedSep}?)(?<quote>[${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}])(?<sepAfter>${escapedSep}?),`, "g");
|
|
119
|
-
text = text.replace(commaOutsideRegex, "$<sepBefore>,$<quote>$<sepAfter>");
|
|
154
|
+
// Period outside → inside: Hello". → Hello." (and Hello'". → Hello.'")
|
|
155
|
+
text = moveOutsideToInside(text, escapedSep, ".", `(?!\\.\\.\\.)\\.`);
|
|
156
|
+
// Comma outside → inside: Hello", → Hello,"
|
|
157
|
+
text = moveOutsideToInside(text, escapedSep, ",", `,`);
|
|
120
158
|
}
|
|
121
159
|
else if (style === "british" || style === "german" || style === "french") {
|
|
122
160
|
// Period inside → outside: "Hello." → "Hello".
|
package/dist/quotes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,kBAAkB,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE3I,MAAM,EACJ,OAAO,EACP,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,GAC3B,GAAG,eAAe,CAAA;AAEnB,+FAA+F;AAC/F,MAAM,0BAA0B,GAAG,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/quotes.ts
CHANGED
|
@@ -17,6 +17,14 @@ const {
|
|
|
17
17
|
/** Joined string of all terminal punctuation characters for use in regex character classes. */
|
|
18
18
|
const TERMINAL_PUNCTUATION_CLASS = TERMINAL_PUNCTUATION.join("")
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Maximum consecutive closing quotes the punctuation-placement regexes will
|
|
22
|
+
* traverse in a single match (e.g., '". for two-deep nesting). Bounded to keep
|
|
23
|
+
* the regex linear on pathological inputs that contain many closing quotes
|
|
24
|
+
* without any following period or comma.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_NESTED_QUOTES = 4
|
|
27
|
+
|
|
20
28
|
export type PunctuationStyle = "american" | "british" | "german" | "french" | "none"
|
|
21
29
|
|
|
22
30
|
export interface QuoteOptions {
|
|
@@ -156,24 +164,56 @@ function convertDoubleQuotes(text: string, sep: string): string {
|
|
|
156
164
|
return text
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Build a regex that matches a punctuation mark sitting OUTSIDE one or more
|
|
169
|
+
* consecutive closing quotes, for American-style inside-the-quotes placement.
|
|
170
|
+
*
|
|
171
|
+
* Shape: `<non-terminal>(sepBefore)(quotes)(sepAfter)<punctuation>`
|
|
172
|
+
*
|
|
173
|
+
* - The lookbehind ensures the char just before the match isn't already terminal
|
|
174
|
+
* punctuation (so we don't rewrite, e.g., "Stop!".).
|
|
175
|
+
* - `quotes` matches 1 to MAX_NESTED_QUOTES closing quotes (possibly separated
|
|
176
|
+
* by element-boundary markers). The bound keeps the regex linear on
|
|
177
|
+
* pathological inputs — unbounded `*` causes catastrophic backtracking when
|
|
178
|
+
* many closing quotes appear with no following punctuation.
|
|
179
|
+
* - `punctuationPattern` is the trailing pattern (e.g. `,` or `(?!\.\.\.)\.`).
|
|
180
|
+
*/
|
|
181
|
+
function buildOutsidePunctuationRegex(escapedSep: string, punctuationPattern: string): RegExp {
|
|
182
|
+
const closingQuoteClass = `[${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}]`
|
|
183
|
+
const additionalQuotes = `(?:${escapedSep}?${closingQuoteClass}){0,${MAX_NESTED_QUOTES - 1}}`
|
|
184
|
+
const nonTerminalLookbehind = `(?<![${TERMINAL_PUNCTUATION_CLASS}])`
|
|
185
|
+
const pattern =
|
|
186
|
+
`${nonTerminalLookbehind}` +
|
|
187
|
+
`(?<sepBefore>${escapedSep}?)` +
|
|
188
|
+
`(?<quotes>${closingQuoteClass}${additionalQuotes})` +
|
|
189
|
+
`(?<sepAfter>${escapedSep}?)` +
|
|
190
|
+
punctuationPattern
|
|
191
|
+
return cachedRegExp(pattern, "g")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Move a punctuation mark sitting outside one or more consecutive closing
|
|
196
|
+
* quotes to the inside (American style): `Hello'".` → `Hello.'"`.
|
|
197
|
+
*/
|
|
198
|
+
function moveOutsideToInside(
|
|
199
|
+
text: string,
|
|
200
|
+
escapedSep: string,
|
|
201
|
+
replacementChar: string,
|
|
202
|
+
punctuationPattern: string,
|
|
203
|
+
): string {
|
|
204
|
+
const regex = buildOutsidePunctuationRegex(escapedSep, punctuationPattern)
|
|
205
|
+
return text.replace(regex, `$<sepBefore>${replacementChar}$<quotes>$<sepAfter>`)
|
|
206
|
+
}
|
|
207
|
+
|
|
159
208
|
/** Apply American or British punctuation style */
|
|
160
209
|
function applyPunctuationStyle(text: string, sep: string, style: PunctuationStyle): string {
|
|
161
210
|
const escapedSep = getEscapedSeparator({ separator: sep })
|
|
162
211
|
|
|
163
212
|
if (style === "american") {
|
|
164
|
-
// Period outside → inside:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
text = text.replace(periodOutsideRegex, "$<sepBefore>.$<quote>$<sepAfter>")
|
|
170
|
-
|
|
171
|
-
// Comma outside → inside: "Hello", → "Hello,"
|
|
172
|
-
const commaOutsideRegex = cachedRegExp(
|
|
173
|
-
`(?<![${TERMINAL_PUNCTUATION_CLASS}])(?<sepBefore>${escapedSep}?)(?<quote>[${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}])(?<sepAfter>${escapedSep}?),`,
|
|
174
|
-
"g"
|
|
175
|
-
)
|
|
176
|
-
text = text.replace(commaOutsideRegex, "$<sepBefore>,$<quote>$<sepAfter>")
|
|
213
|
+
// Period outside → inside: Hello". → Hello." (and Hello'". → Hello.'")
|
|
214
|
+
text = moveOutsideToInside(text, escapedSep, ".", `(?!\\.\\.\\.)\\.`)
|
|
215
|
+
// Comma outside → inside: Hello", → Hello,"
|
|
216
|
+
text = moveOutsideToInside(text, escapedSep, ",", `,`)
|
|
177
217
|
} else if (style === "british" || style === "german" || style === "french") {
|
|
178
218
|
// Period inside → outside: "Hello." → "Hello".
|
|
179
219
|
// No terminal punctuation guard — "Stop!." inside is always wrong; move the period out.
|
package/src/tests/quotes.test.ts
CHANGED
|
@@ -719,6 +719,23 @@ describe("niceQuotes", () => {
|
|
|
719
719
|
const first = niceQuotes(`"He said, 'Hello.'"`, { punctuationStyle: style })
|
|
720
720
|
expect(niceQuotes(first, { punctuationStyle: style })).toBe(first)
|
|
721
721
|
})
|
|
722
|
+
|
|
723
|
+
it.each([
|
|
724
|
+
// Period moves past all closing quotes in one pass
|
|
725
|
+
[
|
|
726
|
+
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}.`,
|
|
727
|
+
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello.${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`,
|
|
728
|
+
],
|
|
729
|
+
// Comma moves past all closing quotes in one pass
|
|
730
|
+
[
|
|
731
|
+
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}, she replied.`,
|
|
732
|
+
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello,${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE} she replied.`,
|
|
733
|
+
],
|
|
734
|
+
])("american moves past all closing quotes: %s → %s", (input, expected) => {
|
|
735
|
+
const result = niceQuotes(input, { punctuationStyle: "american" })
|
|
736
|
+
expect(result).toBe(expected)
|
|
737
|
+
expect(niceQuotes(result, { punctuationStyle: "american" })).toBe(result)
|
|
738
|
+
})
|
|
722
739
|
})
|
|
723
740
|
|
|
724
741
|
describe("pathological inputs", () => {
|