punctilio 3.9.3 → 3.11.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/.pre-commit-hooks.yaml +27 -0
- package/README.md +55 -2
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +346 -0
- package/dist/cli.js.map +1 -0
- package/dist/dashes.d.ts +2 -1
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +5 -3
- package/dist/dashes.js.map +1 -1
- package/dist/html.d.ts +44 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +67 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -26
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +0 -4
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +2 -5
- package/dist/markdown.js.map +1 -1
- package/dist/prettier-plugin.d.ts +18 -0
- package/dist/prettier-plugin.d.ts.map +1 -0
- package/dist/prettier-plugin.js +52 -0
- package/dist/prettier-plugin.js.map +1 -0
- package/dist/quotes.d.ts +2 -1
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +4 -1
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +14 -36
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +14 -36
- package/dist/rehype.js.map +1 -1
- package/dist/remark.d.ts +0 -3
- package/dist/remark.d.ts.map +1 -1
- package/dist/remark.js +0 -7
- package/dist/remark.js.map +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +6 -5
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +26 -9
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +40 -9
- package/dist/utils.js.map +1 -1
- package/package.json +25 -2
- package/src/cli.ts +417 -0
- package/src/dashes.ts +25 -4
- package/src/html.ts +91 -0
- package/src/index.ts +20 -30
- package/src/markdown.ts +2 -7
- package/src/prettier-plugin.ts +61 -0
- package/src/quotes.ts +5 -2
- package/src/rehype.ts +14 -36
- package/src/remark.ts +0 -7
- package/src/symbols.ts +18 -5
- package/src/utils.ts +42 -9
package/src/markdown.ts
CHANGED
|
@@ -16,6 +16,7 @@ import remarkStringify from "remark-stringify"
|
|
|
16
16
|
import QuickLRU from "quick-lru"
|
|
17
17
|
|
|
18
18
|
import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
|
|
19
|
+
import { stableStringify } from "./utils.js"
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Options for the Markdown transform pipeline.
|
|
@@ -94,10 +95,6 @@ export function clearProcessorCache(): void {
|
|
|
94
95
|
* The unified processor pipeline is cached and reused across calls with
|
|
95
96
|
* identical options, avoiding redundant pipeline construction.
|
|
96
97
|
*
|
|
97
|
-
* @param input - The Markdown text to transform
|
|
98
|
-
* @param options - Configuration options for both the transform and serializer
|
|
99
|
-
* @returns The typographically improved Markdown text
|
|
100
|
-
*
|
|
101
98
|
* @example
|
|
102
99
|
* ```ts
|
|
103
100
|
* import { transformMarkdown } from 'punctilio/markdown'
|
|
@@ -110,9 +107,7 @@ export async function transformMarkdown(
|
|
|
110
107
|
input: string,
|
|
111
108
|
options: MarkdownOptions = {}
|
|
112
109
|
): Promise<string> {
|
|
113
|
-
const optionsKey =
|
|
114
|
-
Object.entries(options).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b))
|
|
115
|
-
)
|
|
110
|
+
const optionsKey = stableStringify(options)
|
|
116
111
|
|
|
117
112
|
let processor = processorCache.get(optionsKey)
|
|
118
113
|
if (!processor) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prettier plugin applying punctilio's typographic transforms to Markdown.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```jsonc
|
|
6
|
+
* // .prettierrc
|
|
7
|
+
* { "plugins": ["punctilio/prettier-plugin"] }
|
|
8
|
+
*
|
|
9
|
+
* // .punctiliorc.json (optional)
|
|
10
|
+
* { "punctuationStyle": "british", "skipTags": ["pre", "code"] }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { dirname } from "node:path"
|
|
17
|
+
|
|
18
|
+
import * as prettierMarkdown from "prettier/plugins/markdown"
|
|
19
|
+
import type { Parser, Plugin } from "prettier"
|
|
20
|
+
import type { Root } from "mdast"
|
|
21
|
+
import { cosmiconfig } from "cosmiconfig"
|
|
22
|
+
|
|
23
|
+
import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
|
|
24
|
+
|
|
25
|
+
const upstreamMarkdown = prettierMarkdown.parsers.markdown as Parser<Root>
|
|
26
|
+
const explorer = cosmiconfig("punctilio")
|
|
27
|
+
|
|
28
|
+
async function loadPunctilioConfig(searchFrom: string): Promise<RemarkPunctilioOptions> {
|
|
29
|
+
const result = await explorer.search(searchFrom)
|
|
30
|
+
return !result || result.isEmpty ? {} : (result.config as RemarkPunctilioOptions)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Runs `remarkPunctilio`'s transformer against an mdast tree. The plugin
|
|
35
|
+
* mutates the tree in place; awaiting handles any future async variant
|
|
36
|
+
* without churning callers.
|
|
37
|
+
*/
|
|
38
|
+
async function runRemarkPunctilio(opts: RemarkPunctilioOptions, ast: Root): Promise<void> {
|
|
39
|
+
// The unified `Transformer` signature requires a VFile and a callback,
|
|
40
|
+
// but `remarkPunctilio` only ever reads the tree. Narrow to the actual
|
|
41
|
+
// shape here so the cast lives in one well-commented place.
|
|
42
|
+
const transformer = remarkPunctilio(opts) as (tree: Root) => void | Promise<void>
|
|
43
|
+
await transformer(ast)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const plugin: Plugin = {
|
|
47
|
+
parsers: {
|
|
48
|
+
markdown: {
|
|
49
|
+
...upstreamMarkdown,
|
|
50
|
+
async parse(text, options) {
|
|
51
|
+
const ast = await upstreamMarkdown.parse(text, options)
|
|
52
|
+
const searchFrom = typeof options.filepath === "string" ? dirname(options.filepath) : process.cwd()
|
|
53
|
+
const opts = await loadPunctilioConfig(searchFrom)
|
|
54
|
+
await runRemarkPunctilio(opts, ast)
|
|
55
|
+
return ast
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default plugin
|
package/src/quotes.ts
CHANGED
|
@@ -25,7 +25,8 @@ const TERMINAL_PUNCTUATION_CLASS = TERMINAL_PUNCTUATION.join("")
|
|
|
25
25
|
*/
|
|
26
26
|
const MAX_NESTED_QUOTES = 4
|
|
27
27
|
|
|
28
|
-
export
|
|
28
|
+
export const PUNCTUATION_STYLES = ["american", "british", "german", "french", "none"] as const
|
|
29
|
+
export type PunctuationStyle = (typeof PUNCTUATION_STYLES)[number]
|
|
29
30
|
|
|
30
31
|
export interface QuoteOptions {
|
|
31
32
|
/** Boundary marker for HTML element boundaries. Default: "\uE000\uE001" */
|
|
@@ -214,7 +215,9 @@ function applyPunctuationStyle(text: string, sep: string, style: PunctuationStyl
|
|
|
214
215
|
text = moveOutsideToInside(text, escapedSep, ".", `(?!\\.\\.\\.)\\.`)
|
|
215
216
|
// Comma outside → inside: Hello", → Hello,"
|
|
216
217
|
text = moveOutsideToInside(text, escapedSep, ",", `,`)
|
|
217
|
-
} else
|
|
218
|
+
} else {
|
|
219
|
+
// Every non-"american" non-"none" style (british/german/french) places
|
|
220
|
+
// punctuation outside the quotes.
|
|
218
221
|
// Period inside → outside: "Hello." → "Hello".
|
|
219
222
|
// No terminal punctuation guard — "Stop!." inside is always wrong; move the period out.
|
|
220
223
|
// Match ALL consecutive closing quotes so nested quotes like .'" become '". in one pass.
|
package/src/rehype.ts
CHANGED
|
@@ -78,13 +78,9 @@ const DEFAULT_SKIP_TAGS = ["code", "pre", "script", "style", "kbd", "var", "samp
|
|
|
78
78
|
/**
|
|
79
79
|
* Flattens text nodes from an element tree into a single array.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* exclude individual text nodes from the result. Applied after
|
|
85
|
-
* element-level `shouldSkip`, so the hook is never invoked for text
|
|
86
|
-
* inside an already-skipped element.
|
|
87
|
-
* @returns Array of Text nodes
|
|
81
|
+
* Pass `options.shouldSkipText` to exclude individual text nodes from the
|
|
82
|
+
* result. Applied after element-level `shouldSkip`, so the hook is never
|
|
83
|
+
* invoked for text inside an already-skipped element.
|
|
88
84
|
*
|
|
89
85
|
* @example
|
|
90
86
|
* ```ts
|
|
@@ -143,10 +139,6 @@ function flattenTextNodesImpl(
|
|
|
143
139
|
/**
|
|
144
140
|
* Extracts concatenated text content from an element.
|
|
145
141
|
*
|
|
146
|
-
* @param node - The element to extract text from
|
|
147
|
-
* @param shouldSkip - Optional function to determine which elements to skip
|
|
148
|
-
* @returns The combined text content
|
|
149
|
-
*
|
|
150
142
|
* @example
|
|
151
143
|
* ```ts
|
|
152
144
|
* const text = getTextContent(paragraphElement)
|
|
@@ -164,10 +156,7 @@ export function getTextContent(
|
|
|
164
156
|
|
|
165
157
|
/**
|
|
166
158
|
* Recursively finds the first text node in a tree of HTML elements.
|
|
167
|
-
*
|
|
168
|
-
* @param node - The root node to search from
|
|
169
|
-
* @param depth - Current recursion depth (internal use)
|
|
170
|
-
* @returns The first text node found, or null if no text nodes exist
|
|
159
|
+
* Returns null if no text nodes exist.
|
|
171
160
|
*
|
|
172
161
|
* @example
|
|
173
162
|
* ```ts
|
|
@@ -205,7 +194,6 @@ const CLOSER_TO_OPENER: Record<string, string> = { "\u201D": "\u201C" }
|
|
|
205
194
|
* excluded because \u2019 (right single quote) doubles as an apostrophe,
|
|
206
195
|
* making balance-checking unreliable.
|
|
207
196
|
*
|
|
208
|
-
* @param input - The text to validate
|
|
209
197
|
* @throws Error if double quotes are mismatched
|
|
210
198
|
*
|
|
211
199
|
* @example
|
|
@@ -245,20 +233,17 @@ export function assertSmartQuotesMatch(input: string): void {
|
|
|
245
233
|
* 2. Concatenates and transforms the whole paragraph
|
|
246
234
|
* 3. Splits the result back into the original text nodes
|
|
247
235
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* @param options - Optional transform options. Pass `shouldSkipText` here to
|
|
258
|
-
* opt individual text nodes out of transformation without skipping their
|
|
259
|
-
* enclosing element. Skipped text nodes keep their original `.value`.
|
|
236
|
+
* When `checkInvariance` is true, also verifies that the transform produces
|
|
237
|
+
* the same result with and without markers — i.e.,
|
|
238
|
+
* `stripMarkers(transform(textWithMarkers)) === transform(stripMarkers(text))`.
|
|
239
|
+
* Useful for debugging transforms that accidentally interact with markers.
|
|
240
|
+
*
|
|
241
|
+
* Pass `options.shouldSkipText` to opt individual text nodes out of
|
|
242
|
+
* transformation without skipping their enclosing element; skipped text
|
|
243
|
+
* nodes keep their original `.value`.
|
|
244
|
+
*
|
|
260
245
|
* @throws Error if transformation alters the number of text nodes
|
|
261
|
-
* @throws Error if checkInvariance is true and the invariance check fails
|
|
246
|
+
* @throws Error if `checkInvariance` is true and the invariance check fails
|
|
262
247
|
*
|
|
263
248
|
* @example
|
|
264
249
|
* ```ts
|
|
@@ -430,11 +415,7 @@ function hasTextDescendant(
|
|
|
430
415
|
* When an element has block-level children, we recurse so each block
|
|
431
416
|
* is processed independently (e.g., `<div><p>A</p><p>B</p></div>`).
|
|
432
417
|
*
|
|
433
|
-
* @param node - The root element to search from
|
|
434
|
-
* @param shouldSkip - Function to determine which elements to skip
|
|
435
|
-
* @param depth - Current recursion depth (internal use)
|
|
436
418
|
* @param alreadyTransformed - Elements already processed; skipped during traversal to avoid redundant work
|
|
437
|
-
* @returns Array of elements that contain transformable text
|
|
438
419
|
*/
|
|
439
420
|
export function collectTransformableElements(
|
|
440
421
|
node: Element,
|
|
@@ -503,9 +484,6 @@ function markDescendants(node: Element, set: Set<Element>, depth: number = 0): v
|
|
|
503
484
|
/**
|
|
504
485
|
* Rehype plugin that applies punctilio typography transformations to HTML.
|
|
505
486
|
*
|
|
506
|
-
* @param options - Plugin configuration options
|
|
507
|
-
* @returns Unified transformer function
|
|
508
|
-
*
|
|
509
487
|
* @example
|
|
510
488
|
* ```ts
|
|
511
489
|
* import { unified } from 'unified'
|
package/src/remark.ts
CHANGED
|
@@ -32,10 +32,6 @@ const SKIP_TYPES = new Set(["inlineCode", "html"])
|
|
|
32
32
|
/**
|
|
33
33
|
* Recursively collects text nodes from a phrasing content tree,
|
|
34
34
|
* skipping code and HTML nodes.
|
|
35
|
-
*
|
|
36
|
-
* @param node - The phrasing content node to collect from
|
|
37
|
-
* @param depth - Current recursion depth (internal use)
|
|
38
|
-
* @returns Array of Text nodes
|
|
39
35
|
*/
|
|
40
36
|
function flattenTextNodes(
|
|
41
37
|
node: PhrasingContent,
|
|
@@ -66,9 +62,6 @@ function flattenTextNodes(
|
|
|
66
62
|
/**
|
|
67
63
|
* Remark plugin that applies punctilio typography transformations to Markdown.
|
|
68
64
|
*
|
|
69
|
-
* @param options - Plugin configuration options
|
|
70
|
-
* @returns Unified transformer function
|
|
71
|
-
*
|
|
72
65
|
* @example
|
|
73
66
|
* ```ts
|
|
74
67
|
* import { unified } from 'unified'
|
package/src/symbols.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, LATIN_LETTERS, wordBoundaryEnd, SPACE_CHARS, cachedRegExp, getEscapedSeparator } from "./constants.js"
|
|
6
|
+
import { namedGroups } from "./utils.js"
|
|
6
7
|
|
|
7
8
|
export interface SymbolOptions {
|
|
8
9
|
/** Boundary marker for HTML element boundaries. Default: "\uE000\uE001" */
|
|
@@ -49,7 +50,7 @@ export function ellipsis(text: string, options: SymbolOptions = {}): string {
|
|
|
49
50
|
// into one ellipsis.
|
|
50
51
|
const pattern = cachedRegExp(`\\.(?:[${SPACE_CHARS}]|(?<sep1>${chr}))?\\.(?:[${SPACE_CHARS}]?|(?<sep2>${chr})?)\\.`, "g")
|
|
51
52
|
text = text.replace(pattern, (...args) => {
|
|
52
|
-
const { sep1, sep2 } =
|
|
53
|
+
const { sep1, sep2 } = namedGroups<{ sep1?: string; sep2?: string }>(args)
|
|
53
54
|
return ELLIPSIS + (sep1 ?? "") + (sep2 ?? "")
|
|
54
55
|
})
|
|
55
56
|
|
|
@@ -94,7 +95,7 @@ export function multiplication(text: string, options: SymbolOptions = {}): strin
|
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
text = text.replace(chainPattern, (...args) => {
|
|
97
|
-
const { firstNum, rest } =
|
|
98
|
+
const { firstNum, rest } = namedGroups<{ firstNum: string; rest: string }>(args)
|
|
98
99
|
// Skip hexadecimal: 0x... or 0X...
|
|
99
100
|
if (firstNum === "0" && /^x/i.test(rest)) return args[0] as string
|
|
100
101
|
|
|
@@ -111,7 +112,13 @@ export function multiplication(text: string, options: SymbolOptions = {}): strin
|
|
|
111
112
|
"gy"
|
|
112
113
|
),
|
|
113
114
|
(...innerArgs) => {
|
|
114
|
-
const groups =
|
|
115
|
+
const groups = namedGroups<{
|
|
116
|
+
pre: string
|
|
117
|
+
spaceBefore: string
|
|
118
|
+
spaceAfter: string
|
|
119
|
+
post: string
|
|
120
|
+
num: string
|
|
121
|
+
}>(innerArgs)
|
|
115
122
|
const space = groups.spaceBefore || groups.spaceAfter ? " " : ""
|
|
116
123
|
return `${groups.pre}${space}${MULTIPLICATION}${space}${groups.post}${groups.num}`
|
|
117
124
|
}
|
|
@@ -164,7 +171,7 @@ export function mathSymbols(text: string, options: SymbolOptions = {}): string {
|
|
|
164
171
|
// and preserves the text-node-count invariant in transformTextNodes.
|
|
165
172
|
const pattern = cachedRegExp(`${left}(?<sep>${chr})?${right}${lookahead(chr)}`, "g")
|
|
166
173
|
text = text.replace(pattern, (...args) => {
|
|
167
|
-
const { sep } =
|
|
174
|
+
const { sep } = namedGroups<{ sep?: string }>(args)
|
|
168
175
|
return `${replacement}${sep ?? ""}`
|
|
169
176
|
})
|
|
170
177
|
}
|
|
@@ -348,7 +355,13 @@ function balancedPrimeReplacer(primeChar: string, escapedSeparator: string) {
|
|
|
348
355
|
// Replace callback: (match, ...captures, offset, fullString, namedGroups)
|
|
349
356
|
return (...args: unknown[]): string => {
|
|
350
357
|
const fullMatch = args[0] as string
|
|
351
|
-
const groups =
|
|
358
|
+
const groups = namedGroups<{
|
|
359
|
+
digit?: string
|
|
360
|
+
sep?: string
|
|
361
|
+
afterSep?: string
|
|
362
|
+
contraction?: string
|
|
363
|
+
trailing?: string
|
|
364
|
+
}>(args)
|
|
352
365
|
|
|
353
366
|
// Contraction (letter + quote + letter): skip entirely
|
|
354
367
|
if (groups.contraction !== undefined) {
|
package/src/utils.ts
CHANGED
|
@@ -40,8 +40,6 @@ export function formatErrorString(content: string, label: string): string {
|
|
|
40
40
|
* Throws if any text node contains the separator character, which would
|
|
41
41
|
* corrupt the split/join mechanism used by the rehype and remark plugins.
|
|
42
42
|
*
|
|
43
|
-
* @param textValues - The raw text values to check
|
|
44
|
-
* @param separator - The separator character
|
|
45
43
|
* @throws Error if the separator is found in any text value
|
|
46
44
|
*/
|
|
47
45
|
export function assertSeparatorAbsent(textValues: string[], separator: string): void {
|
|
@@ -88,9 +86,8 @@ interface TextNode {
|
|
|
88
86
|
* 4. Splits the result back on the separator and writes each fragment
|
|
89
87
|
* back into the corresponding text node.
|
|
90
88
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @param separator - The marker string used to track node boundaries
|
|
89
|
+
* `textNodes` is mutated in place.
|
|
90
|
+
*
|
|
94
91
|
* @throws Error if the transformation alters the number of text nodes
|
|
95
92
|
*/
|
|
96
93
|
export function transformTextNodes(
|
|
@@ -122,10 +119,6 @@ export function transformTextNodes(
|
|
|
122
119
|
* Validates that a transformation preserved the separator count.
|
|
123
120
|
* Throws an error if separators were added or removed.
|
|
124
121
|
*
|
|
125
|
-
* @param original - The original text before transformation
|
|
126
|
-
* @param transformed - The text after transformation
|
|
127
|
-
* @param separator - The separator character to check (default: DEFAULT_SEPARATOR)
|
|
128
|
-
* @param transformName - Name of the transform for error messages (default: "transform")
|
|
129
122
|
* @throws Error if separator count changed
|
|
130
123
|
*/
|
|
131
124
|
export function assertSeparatorCountPreserved(
|
|
@@ -144,3 +137,43 @@ export function assertSeparatorCountPreserved(
|
|
|
144
137
|
)
|
|
145
138
|
}
|
|
146
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns a copy of `obj` with `undefined`-valued keys removed.
|
|
143
|
+
* Used so caller-supplied `{ foo: undefined }` doesn't override a default.
|
|
144
|
+
*
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export function filterUndefined<T extends object>(obj: T): Partial<T> {
|
|
148
|
+
const result: Record<string, unknown> = {}
|
|
149
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
150
|
+
if (v !== undefined) result[k] = v
|
|
151
|
+
}
|
|
152
|
+
return result as Partial<T>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns a deterministic JSON-style string for an options object: drops
|
|
157
|
+
* `undefined` values and sorts keys, so equivalent option sets always
|
|
158
|
+
* produce the same string (suitable as a cache or LRU key).
|
|
159
|
+
*
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
export function stableStringify(obj: object): string {
|
|
163
|
+
const entries = Object.entries(obj)
|
|
164
|
+
.filter(([, v]) => v !== undefined)
|
|
165
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
166
|
+
return JSON.stringify(entries)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extracts the named-groups object from a `String.prototype.replace`
|
|
171
|
+
* callback's argument tuple. The last element is always the groups
|
|
172
|
+
* object when the regex has named captures. Callers supply the group
|
|
173
|
+
* shape via the type parameter to localise the unavoidable type cast.
|
|
174
|
+
*
|
|
175
|
+
* @internal
|
|
176
|
+
*/
|
|
177
|
+
export function namedGroups<G>(args: unknown[]): G {
|
|
178
|
+
return args[args.length - 1] as G
|
|
179
|
+
}
|