punctilio 3.13.2 → 4.1.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 +1 -1
- package/README.md +30 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +190 -60
- package/dist/cli.js.map +1 -1
- package/dist/dashes.js +4 -4
- package/dist/dashes.js.map +1 -1
- package/dist/html.d.ts +5 -0
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +8 -2
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +9 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +7 -1
- package/dist/markdown.js.map +1 -1
- package/dist/prettier-plugin.d.ts.map +1 -1
- package/dist/prettier-plugin.js +13 -1
- package/dist/prettier-plugin.js.map +1 -1
- package/dist/rehype.d.ts +15 -1
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +41 -12
- package/dist/rehype.js.map +1 -1
- package/dist/remark.d.ts +5 -0
- package/dist/remark.d.ts.map +1 -1
- package/dist/remark.js +7 -6
- package/dist/remark.js.map +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +2 -3
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +18 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +40 -3
- package/dist/utils.js.map +1 -1
- package/package.json +2 -3
- package/src/cli.ts +200 -59
- package/src/dashes.ts +4 -4
- package/src/html.ts +11 -2
- package/src/index.ts +15 -4
- package/src/markdown.ts +15 -1
- package/src/prettier-plugin.ts +13 -1
- package/src/rehype.ts +60 -11
- package/src/remark.ts +13 -6
- package/src/symbols.ts +2 -3
- package/src/utils.ts +48 -3
package/src/markdown.ts
CHANGED
|
@@ -4,9 +4,15 @@ import remarkGfm from "remark-gfm"
|
|
|
4
4
|
import remarkStringify from "remark-stringify"
|
|
5
5
|
import QuickLRU from "quick-lru"
|
|
6
6
|
|
|
7
|
+
import { TRANSFORM_OPTION_KEYS } from "./index.js"
|
|
7
8
|
import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
|
|
8
|
-
import { stableStringify } from "./utils.js"
|
|
9
|
+
import { assertKnownOptionKeys, stableStringify } from "./utils.js"
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Options for `transformMarkdown`. Inherits the Markdown-sink default of
|
|
13
|
+
* `nbsp: false` from {@link RemarkPunctilioOptions}; pass `nbsp: true` to
|
|
14
|
+
* opt in to non-breaking space insertion.
|
|
15
|
+
*/
|
|
10
16
|
export interface MarkdownOptions extends RemarkPunctilioOptions {
|
|
11
17
|
/**
|
|
12
18
|
* Character for emphasis (`*text*` vs `_text_`) and strong (`**text**` vs `__text__`).
|
|
@@ -27,6 +33,12 @@ export interface MarkdownOptions extends RemarkPunctilioOptions {
|
|
|
27
33
|
ruleMarker?: "-" | "*" | "_"
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/** Option keys handled by `transformMarkdown` itself rather than `transform()`. */
|
|
37
|
+
export const MARKDOWN_ONLY_OPTION_KEYS: readonly string[] = ["emphasisMarker", "strongMarker", "bulletMarker", "ruleMarker"]
|
|
38
|
+
|
|
39
|
+
/** Runtime list of valid `transformMarkdown` option keys. */
|
|
40
|
+
export const MARKDOWN_OPTION_KEYS: readonly string[] = [...TRANSFORM_OPTION_KEYS, ...MARKDOWN_ONLY_OPTION_KEYS]
|
|
41
|
+
|
|
30
42
|
function createProcessor(options: MarkdownOptions) {
|
|
31
43
|
const {
|
|
32
44
|
emphasisMarker,
|
|
@@ -70,6 +82,8 @@ export async function transformMarkdown(
|
|
|
70
82
|
input: string,
|
|
71
83
|
options: MarkdownOptions = {}
|
|
72
84
|
): Promise<string> {
|
|
85
|
+
assertKnownOptionKeys(options, MARKDOWN_OPTION_KEYS, "transformMarkdown")
|
|
86
|
+
|
|
73
87
|
const optionsKey = stableStringify(options)
|
|
74
88
|
|
|
75
89
|
let processor = processorCache.get(optionsKey)
|
package/src/prettier-plugin.ts
CHANGED
|
@@ -5,14 +5,26 @@ import type { Parser, Plugin } from "prettier"
|
|
|
5
5
|
import type { Root } from "mdast"
|
|
6
6
|
import { cosmiconfig } from "cosmiconfig"
|
|
7
7
|
|
|
8
|
+
import { HTML_ONLY_OPTION_KEYS } from "./html.js"
|
|
9
|
+
import { MARKDOWN_ONLY_OPTION_KEYS } from "./markdown.js"
|
|
8
10
|
import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
|
|
11
|
+
import { omitKeys } from "./utils.js"
|
|
9
12
|
|
|
10
13
|
const upstreamMarkdown = prettierMarkdown.parsers.markdown as Parser<Root>
|
|
11
14
|
const explorer = cosmiconfig("punctilio")
|
|
12
15
|
|
|
16
|
+
// A config file may be shared with the CLI, so it can carry markdown-only
|
|
17
|
+
// keys (prettier controls stringification itself) and HTML-only keys. Strip
|
|
18
|
+
// those; unknown keys still fail loudly inside remarkPunctilio.
|
|
19
|
+
const INAPPLICABLE_CONFIG_KEYS: readonly string[] = [
|
|
20
|
+
...MARKDOWN_ONLY_OPTION_KEYS,
|
|
21
|
+
...HTML_ONLY_OPTION_KEYS,
|
|
22
|
+
]
|
|
23
|
+
|
|
13
24
|
async function loadPunctilioConfig(searchFrom: string): Promise<RemarkPunctilioOptions> {
|
|
14
25
|
const result = await explorer.search(searchFrom)
|
|
15
|
-
|
|
26
|
+
if (!result || result.isEmpty) return {}
|
|
27
|
+
return omitKeys(result.config as RemarkPunctilioOptions, INAPPLICABLE_CONFIG_KEYS)
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
async function runRemarkPunctilio(opts: RemarkPunctilioOptions, ast: Root): Promise<void> {
|
package/src/rehype.ts
CHANGED
|
@@ -3,9 +3,9 @@ import type { Transformer } from "unified"
|
|
|
3
3
|
|
|
4
4
|
import { SKIP, visitParents } from "unist-util-visit-parents"
|
|
5
5
|
|
|
6
|
-
import { transform, type TransformOptions } from "./index.js"
|
|
6
|
+
import { transform, TRANSFORM_OPTION_KEYS, type TransformOptions } from "./index.js"
|
|
7
7
|
import { DEFAULT_SEPARATOR, MAX_RECURSION_DEPTH } from "./constants.js"
|
|
8
|
-
import { formatErrorString, transformTextNodes } from "./utils.js"
|
|
8
|
+
import { assertKnownOptionKeys, formatErrorString, transformTextNodes } from "./utils.js"
|
|
9
9
|
|
|
10
10
|
type ElementPredicate = (node: Element) => boolean
|
|
11
11
|
|
|
@@ -46,10 +46,40 @@ export interface RehypePunctilioOptions
|
|
|
46
46
|
* Default: []
|
|
47
47
|
*/
|
|
48
48
|
skipClasses?: string[]
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Invert the element model: transform text inside every element except the
|
|
52
|
+
* skip-list (`skipTags`/`skipClasses`) plus `textarea`/`input`/`select`,
|
|
53
|
+
* whose text is a literal form-control value rather than prose. The default
|
|
54
|
+
* (`false`) keeps the `TRANSFORMABLE_ELEMENTS` allowlist, transforming only
|
|
55
|
+
* known prose-bearing tags (and custom elements).
|
|
56
|
+
*
|
|
57
|
+
* Default: false
|
|
58
|
+
*/
|
|
59
|
+
transformAllElements?: boolean
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
const DEFAULT_SKIP_TAGS = ["code", "pre", "script", "style", "kbd", "var", "samp", "template", "math", "svg"]
|
|
52
63
|
|
|
64
|
+
// Form elements whose text content is a literal control value, not prose.
|
|
65
|
+
// Hard-skipped (whole subtree) under `transformAllElements`, on top of the
|
|
66
|
+
// user skip-list, so their values are protected even when nested inside a
|
|
67
|
+
// transformable element.
|
|
68
|
+
const FORM_VALUE_TAGS = ["textarea", "input"]
|
|
69
|
+
|
|
70
|
+
// `<select>` is not prose itself, but its `<option>` children are (and are
|
|
71
|
+
// transformed under the default allowlist). Under `transformAllElements` we
|
|
72
|
+
// exclude `select` as a transform *unit* while still recursing into — and
|
|
73
|
+
// transforming — its options, so the inverted mode never transforms less than
|
|
74
|
+
// the allowlist.
|
|
75
|
+
const NON_PROSE_CONTAINER_TAGS = new Set(["select"])
|
|
76
|
+
|
|
77
|
+
/** Option keys handled by `rehypePunctilio` itself rather than `transform()`. */
|
|
78
|
+
export const REHYPE_ONLY_OPTION_KEYS: readonly string[] = ["skipTags", "skipClasses", "shouldSkipText", "transformAllElements"]
|
|
79
|
+
|
|
80
|
+
/** Runtime list of valid `rehypePunctilio` option keys. */
|
|
81
|
+
export const REHYPE_OPTION_KEYS: readonly string[] = [...TRANSFORM_OPTION_KEYS, ...REHYPE_ONLY_OPTION_KEYS]
|
|
82
|
+
|
|
53
83
|
export function flattenTextNodes(
|
|
54
84
|
node: Element | ElementContent,
|
|
55
85
|
shouldSkip: ElementPredicate,
|
|
@@ -266,8 +296,18 @@ const TRANSFORMABLE_ELEMENTS = new Set([
|
|
|
266
296
|
"ruby",
|
|
267
297
|
"rt",
|
|
268
298
|
"rp",
|
|
299
|
+
"title",
|
|
300
|
+
"button",
|
|
301
|
+
"option",
|
|
302
|
+
"output",
|
|
269
303
|
])
|
|
270
304
|
|
|
305
|
+
// A tag name containing "-" is a custom element per the HTML custom-element
|
|
306
|
+
// naming rule; assume those hold transformable prose like built-in elements.
|
|
307
|
+
function isTransformableElement(tagName: string): boolean {
|
|
308
|
+
return TRANSFORMABLE_ELEMENTS.has(tagName) || tagName.includes("-")
|
|
309
|
+
}
|
|
310
|
+
|
|
271
311
|
function hasTextDescendant(
|
|
272
312
|
node: Element | ElementContent,
|
|
273
313
|
shouldSkip: ElementPredicate,
|
|
@@ -296,7 +336,8 @@ export function collectTransformableElements(
|
|
|
296
336
|
node: Element,
|
|
297
337
|
shouldSkip: ElementPredicate,
|
|
298
338
|
depth: number = 0,
|
|
299
|
-
alreadyTransformed?: ReadonlySet<Element
|
|
339
|
+
alreadyTransformed?: ReadonlySet<Element>,
|
|
340
|
+
isTransformable: (tagName: string) => boolean = isTransformableElement
|
|
300
341
|
): Element[] {
|
|
301
342
|
/* istanbul ignore if -- defensive: prevents stack overflow from malicious HTML */
|
|
302
343
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
@@ -319,7 +360,7 @@ export function collectTransformableElements(
|
|
|
319
360
|
hasTextDescendant(node, shouldSkip)
|
|
320
361
|
|
|
321
362
|
if (
|
|
322
|
-
|
|
363
|
+
isTransformable(node.tagName) &&
|
|
323
364
|
(hasDirectText || hasTextDescendants)
|
|
324
365
|
) {
|
|
325
366
|
results.push(node)
|
|
@@ -328,7 +369,7 @@ export function collectTransformableElements(
|
|
|
328
369
|
// children that should be independent, or has no text to transform.
|
|
329
370
|
for (const child of node.children) {
|
|
330
371
|
if (child.type === "element") {
|
|
331
|
-
const childResults = collectTransformableElements(child, shouldSkip, depth + 1, alreadyTransformed)
|
|
372
|
+
const childResults = collectTransformableElements(child, shouldSkip, depth + 1, alreadyTransformed, isTransformable)
|
|
332
373
|
for (const r of childResults) results.push(r)
|
|
333
374
|
}
|
|
334
375
|
}
|
|
@@ -354,17 +395,29 @@ function markDescendants(node: Element, set: Set<Element>, depth: number = 0): v
|
|
|
354
395
|
export function rehypePunctilio(
|
|
355
396
|
options: RehypePunctilioOptions = {}
|
|
356
397
|
): Transformer<Root, Root> {
|
|
398
|
+
assertKnownOptionKeys(options, REHYPE_OPTION_KEYS, "rehypePunctilio")
|
|
399
|
+
|
|
357
400
|
const {
|
|
358
401
|
skipTags = DEFAULT_SKIP_TAGS,
|
|
359
402
|
skipClasses = [],
|
|
360
403
|
separator = DEFAULT_SEPARATOR,
|
|
361
404
|
shouldSkipText,
|
|
405
|
+
transformAllElements = false,
|
|
362
406
|
...transformOptions
|
|
363
407
|
} = options
|
|
364
408
|
|
|
365
|
-
|
|
409
|
+
// In inverted mode, form-value elements join the skip-list so their literal
|
|
410
|
+
// values are neither transformed nor flattened into a transformable ancestor.
|
|
411
|
+
const skipTagSet = new Set(transformAllElements ? [...skipTags, ...FORM_VALUE_TAGS] : skipTags)
|
|
366
412
|
const skipClassSet = new Set(skipClasses)
|
|
367
413
|
|
|
414
|
+
// Inverted mode transforms every non-skipped element except non-prose
|
|
415
|
+
// containers; the default keeps the `TRANSFORMABLE_ELEMENTS` allowlist (plus
|
|
416
|
+
// custom elements).
|
|
417
|
+
const isTransformable = transformAllElements
|
|
418
|
+
? (tagName: string) => !NON_PROSE_CONTAINER_TAGS.has(tagName)
|
|
419
|
+
: isTransformableElement
|
|
420
|
+
|
|
368
421
|
const hasSkipClass = (node: Element): boolean => {
|
|
369
422
|
if (skipClassSet.size === 0) return false
|
|
370
423
|
const classNames = node.properties?.className
|
|
@@ -377,11 +430,7 @@ export function rehypePunctilio(
|
|
|
377
430
|
const shouldSkip = (node: Element): boolean =>
|
|
378
431
|
skipTagSet.has(node.tagName) || hasSkipClass(node)
|
|
379
432
|
|
|
380
|
-
// Default idempotency check to false in plugin context — the separator
|
|
381
|
-
// count check already guards against corruption, and the double-pass
|
|
382
|
-
// penalty compounds across every block-level element.
|
|
383
433
|
const pluginOptions = {
|
|
384
|
-
checkIdempotency: false,
|
|
385
434
|
...transformOptions,
|
|
386
435
|
separator,
|
|
387
436
|
}
|
|
@@ -412,7 +461,7 @@ export function rehypePunctilio(
|
|
|
412
461
|
}
|
|
413
462
|
|
|
414
463
|
// Collect and transform elements with text content
|
|
415
|
-
const elementsToTransform = collectTransformableElements(node, shouldSkip, 0, transformed)
|
|
464
|
+
const elementsToTransform = collectTransformableElements(node, shouldSkip, 0, transformed, isTransformable)
|
|
416
465
|
for (const elt of elementsToTransform) {
|
|
417
466
|
if (!transformed.has(elt)) {
|
|
418
467
|
transformElement(elt, transformFn, shouldSkip, separator, false, elementOptions)
|
package/src/remark.ts
CHANGED
|
@@ -3,10 +3,15 @@ import type { Transformer } from "unified"
|
|
|
3
3
|
|
|
4
4
|
import { visitParents } from "unist-util-visit-parents"
|
|
5
5
|
|
|
6
|
-
import { transform, type TransformOptions } from "./index.js"
|
|
6
|
+
import { transform, TRANSFORM_OPTION_KEYS, type TransformOptions } from "./index.js"
|
|
7
7
|
import { DEFAULT_SEPARATOR, MAX_RECURSION_DEPTH } from "./constants.js"
|
|
8
|
-
import { transformTextNodes } from "./utils.js"
|
|
8
|
+
import { assertKnownOptionKeys, transformTextNodes } from "./utils.js"
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Same options as `transform()`, except `nbsp` defaults to `false`:
|
|
12
|
+
* invisible U+00A0 characters written into Markdown source files break
|
|
13
|
+
* grep/Ctrl+F matching. Pass `nbsp: true` explicitly to opt in.
|
|
14
|
+
*/
|
|
10
15
|
export type RemarkPunctilioOptions = TransformOptions
|
|
11
16
|
|
|
12
17
|
const PHRASING_CONTAINERS = new Set(["paragraph", "heading", "tableCell"])
|
|
@@ -42,14 +47,16 @@ function flattenTextNodes(
|
|
|
42
47
|
export function remarkPunctilio(
|
|
43
48
|
options: RemarkPunctilioOptions = {}
|
|
44
49
|
): Transformer<Root, Root> {
|
|
50
|
+
assertKnownOptionKeys(options, TRANSFORM_OPTION_KEYS, "remarkPunctilio")
|
|
51
|
+
|
|
45
52
|
const separator = options.separator ?? DEFAULT_SEPARATOR
|
|
46
53
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
54
|
+
// Markdown is a source format, so default `nbsp` to false (`?? false`
|
|
55
|
+
// rather than spread defaults so an explicit `nbsp: undefined` also gets
|
|
56
|
+
// the Markdown default instead of falling through to transform's).
|
|
50
57
|
const pluginOptions = {
|
|
51
|
-
checkIdempotency: false,
|
|
52
58
|
...options,
|
|
59
|
+
nbsp: options.nbsp ?? false,
|
|
53
60
|
separator,
|
|
54
61
|
}
|
|
55
62
|
|
package/src/symbols.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cachedRegExp, DEFAULT_SEPARATOR, getEscapedSeparator, LATIN_LETTERS, SPACE_CHARS, UNICODE_SYMBOLS, wordBoundaryEnd } from "./constants.js"
|
|
2
|
-
import { namedGroups } from "./utils.js"
|
|
2
|
+
import { namedGroups, replaceCallbackContext } from "./utils.js"
|
|
3
3
|
|
|
4
4
|
export interface SymbolOptions {
|
|
5
5
|
/** Boundary marker for HTML element boundaries. Default: "\uE000\uE001" */
|
|
@@ -337,8 +337,7 @@ function balancedPrimeReplacer(primeChar: string, escapedSeparator: string) {
|
|
|
337
337
|
// Inside balanced quotes: check if this is feet-inches notation
|
|
338
338
|
// (e.g., 5′10" where ″ is inches, not a closing quote)
|
|
339
339
|
if (feetInchesContext) {
|
|
340
|
-
const matchOffset = args
|
|
341
|
-
const fullString = args.at(-2) as string
|
|
340
|
+
const { offset: matchOffset, input: fullString } = replaceCallbackContext(args)
|
|
342
341
|
if (feetInchesContext.test(fullString.substring(0, matchOffset))) {
|
|
343
342
|
return `${digit}${sep}${primeChar}${afterSep}`
|
|
344
343
|
}
|
package/src/utils.ts
CHANGED
|
@@ -4,16 +4,18 @@ const ERROR_STRING_THRESHOLD = 2000
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Formats a string for error messages, truncating above the threshold.
|
|
7
|
-
*
|
|
7
|
+
* When the PUNCTILIO_DEBUG environment variable is set to a non-empty
|
|
8
|
+
* value, also writes the full content to stderr in Node.js so build logs
|
|
9
|
+
* capture it.
|
|
8
10
|
*/
|
|
9
11
|
export function formatErrorString(content: string, label: string): string {
|
|
10
12
|
if (content.length <= ERROR_STRING_THRESHOLD) {
|
|
11
13
|
return JSON.stringify(content)
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
// In Node.js, write full content to stderr for debugging
|
|
16
|
+
// In Node.js, write full content to stderr for debugging when opted in
|
|
15
17
|
try {
|
|
16
|
-
if (typeof globalThis.process?.stderr?.write === "function") {
|
|
18
|
+
if (globalThis.process?.env?.PUNCTILIO_DEBUG && typeof globalThis.process?.stderr?.write === "function") {
|
|
17
19
|
globalThis.process.stderr.write(
|
|
18
20
|
`\n[punctilio ${label} full content (${content.length} chars)]:\n${content}\n\n`
|
|
19
21
|
)
|
|
@@ -106,6 +108,30 @@ export function assertSeparatorCountPreserved(
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/** Throws if `options` contains a key not present in `validKeys`. @internal */
|
|
112
|
+
export function assertKnownOptionKeys(
|
|
113
|
+
options: object,
|
|
114
|
+
validKeys: readonly string[],
|
|
115
|
+
context: string,
|
|
116
|
+
): void {
|
|
117
|
+
for (const key of Object.keys(options)) {
|
|
118
|
+
if (!validKeys.includes(key)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unknown option "${key}" for ${context}. Valid options: ${[...validKeys].sort().join(", ")}.`
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Returns a copy of `obj` without the listed keys. @internal */
|
|
127
|
+
export function omitKeys<T extends object>(obj: T, keys: readonly string[]): Partial<T> {
|
|
128
|
+
const result: Record<string, unknown> = {}
|
|
129
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
130
|
+
if (!keys.includes(k)) result[k] = v
|
|
131
|
+
}
|
|
132
|
+
return result as Partial<T>
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
/** Returns a copy of `obj` with `undefined`-valued keys removed. @internal */
|
|
110
136
|
export function filterUndefined<T extends object>(obj: T): Partial<T> {
|
|
111
137
|
const result: Record<string, unknown> = {}
|
|
@@ -128,3 +154,22 @@ export function stableStringify(obj: object): string {
|
|
|
128
154
|
export function namedGroups<G>(args: unknown[]): G {
|
|
129
155
|
return args[args.length - 1] as G
|
|
130
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extracts the match offset and input string from a `.replace()` callback's
|
|
160
|
+
* arguments, independent of capture-group count. The trailing arguments are
|
|
161
|
+
* `..., offset, input[, namedGroups]`: the input is the last string (the
|
|
162
|
+
* named-groups object, when present, follows it) and the offset is the
|
|
163
|
+
* number immediately before the input. @internal
|
|
164
|
+
*/
|
|
165
|
+
export function replaceCallbackContext(args: unknown[]): { offset: number; input: string } {
|
|
166
|
+
let inputIndex = args.length - 1
|
|
167
|
+
while (inputIndex >= 0 && typeof args[inputIndex] !== "string") {
|
|
168
|
+
inputIndex--
|
|
169
|
+
}
|
|
170
|
+
const offset = args[inputIndex - 1]
|
|
171
|
+
if (inputIndex < 1 || typeof offset !== "number") {
|
|
172
|
+
throw new Error("Could not locate offset and input in replace-callback arguments.")
|
|
173
|
+
}
|
|
174
|
+
return { offset, input: args[inputIndex] as string }
|
|
175
|
+
}
|