1
1
/**
2
- * @typedef Options
3
- * Configuration (optional).
4
- * @property {Test } [ignore]
5
- * `unist-util-is` test used to assert parents
6
- *
2
+ * @typedef {import('mdast').Parent } MdastParent
7
3
* @typedef {import('mdast').Root } Root
8
4
* @typedef {import('mdast').Content } Content
9
5
* @typedef {import('mdast').PhrasingContent } PhrasingContent
10
6
* @typedef {import('mdast').Text } Text
11
- * @typedef {Content|Root } Node
12
- * @typedef {Exclude<Extract<Node, import('mdast').Parent>, Root> } Parent
13
- *
14
7
* @typedef {import('unist-util-visit-parents').Test } Test
15
8
* @typedef {import('unist-util-visit-parents').VisitorResult } VisitorResult
9
+ */
10
+
11
+ /**
12
+ * @typedef {Content | Root } Node
13
+ * @typedef {Extract<Node, MdastParent> } Parent
14
+ * @typedef {Exclude<Parent, Root> } ContentParent
16
15
*
17
16
* @typedef RegExpMatchObject
17
+ * Info on the match.
18
18
* @property {number } index
19
+ * The index of the search at which the result was found.
19
20
* @property {string } input
20
- * @property {[Root, ...Array<Parent>, Text] } stack
21
+ * A copy of the search string in the text node.
22
+ * @property {[Root, ...Array<ContentParent>, Text] } stack
23
+ * All ancestors of the text node, where the last node is the text itself.
21
24
*
22
- * @typedef {string|RegExp } Find
23
- * @typedef {string|ReplaceFunction } Replace
25
+ * @callback ReplaceFunction
26
+ * Callback called when a search matches.
27
+ * @param {...any } parameters
28
+ * The parameters are the result of corresponding search expression:
24
29
*
25
- * @typedef {[Find, Replace] } FindAndReplaceTuple
26
- * @typedef {Record<string, Replace> } FindAndReplaceSchema
27
- * @typedef {Array<FindAndReplaceTuple> } FindAndReplaceList
30
+ * * `value` (`string`) — whole match
31
+ * * `...capture` (`Array<string>`) — matches from regex capture groups
32
+ * * `match` (`RegExpMatchObject`) — info on the match
33
+ * @returns {Array<PhrasingContent> | PhrasingContent | string | false | undefined | null }
34
+ * Thing to replace with.
35
+ *
36
+ * * when `null`, `undefined`, `''`, remove the match
37
+ * * …or when `false`, do not replace at all
38
+ * * …or when `string`, replace with a text node of that value
39
+ * * …or when `Node` or `Array<Node>`, replace with those nodes
28
40
*
41
+ * @typedef {string | RegExp } Find
42
+ * Pattern to find.
43
+ *
44
+ * Strings are escaped and then turned into global expressions.
45
+ *
46
+ * @typedef {Array<FindAndReplaceTuple> } FindAndReplaceList
47
+ * Several find and replaces, in array form.
48
+ * @typedef {Record<string, Replace> } FindAndReplaceSchema
49
+ * Several find and replaces, in object form.
50
+ * @typedef {[Find, Replace] } FindAndReplaceTuple
51
+ * Find and replace in tuple form.
52
+ * @typedef {string | ReplaceFunction } Replace
53
+ * Thing to replace with.
29
54
* @typedef {[RegExp, ReplaceFunction] } Pair
55
+ * Normalized find and replace.
30
56
* @typedef {Array<Pair> } Pairs
31
- */
32
-
33
- /**
34
- * @callback ReplaceFunction
35
- * @param { ...any } parameters
36
- * @returns { Array<PhrasingContent>|PhrasingContent|string|false|undefined|null }
57
+ * All find and replaced.
58
+ *
59
+ * @typedef Options
60
+ * Configuration.
61
+ * @property { Test | null | undefined } [ignore]
62
+ * Test for which nodes to ignore.
37
63
*/
38
64
39
65
import escape from 'escape-string-regexp'
@@ -43,31 +69,42 @@ import {convert} from 'unist-util-is'
43
69
const own = { } . hasOwnProperty
44
70
45
71
/**
46
- * @param tree mdast tree
47
- * @param find Value to find and remove. When `string`, escaped and made into a global `RegExp`
48
- * @param [replace] Value to insert.
49
- * * When `string`, turned into a Text node.
50
- * * When `Function`, called with the results of calling `RegExp.exec` as
51
- * arguments, in which case it can return a single or a list of `Node`,
52
- * a `string` (which is wrapped in a `Text` node), or `false` to not replace
53
- * @param [options] Configuration.
72
+ * Find patterns in a tree and replace them.
73
+ *
74
+ * The algorithm searches the tree in *preorder* for complete values in `Text`
75
+ * nodes.
76
+ * Partial matches are not supported.
77
+ *
78
+ * @param tree
79
+ * Tree to change.
80
+ * @param find
81
+ * Patterns to find.
82
+ * @param replace
83
+ * Things to replace with (when `find` is `Find`) or configuration.
84
+ * @param options
85
+ * Configuration (when `find` is not `Find`).
86
+ * @returns
87
+ * Given, modified, tree.
54
88
*/
89
+ // To do: next major: remove `find` & `replace` combo, remove schema.
55
90
export const findAndReplace =
56
91
/**
57
92
* @type {(
58
- * ((tree: Node , find: Find, replace?: Replace, options?: Options) => Node ) &
59
- * ((tree: Node , schema: FindAndReplaceSchema| FindAndReplaceList, options?: Options) => Node )
93
+ * (<Tree extends Node> (tree: Tree , find: Find, replace?: Replace | null | undefined , options?: Options | null | undefined ) => Tree ) &
94
+ * (<Tree extends Node> (tree: Tree , schema: FindAndReplaceSchema | FindAndReplaceList, options?: Options | null | undefined ) => Tree )
60
95
* )}
61
96
**/
62
97
(
63
98
/**
64
- * @param {Node } tree
65
- * @param {Find|FindAndReplaceSchema|FindAndReplaceList } find
66
- * @param {Replace|Options } [replace]
67
- * @param {Options } [options]
99
+ * @template {Node} Tree
100
+ * @param {Tree } tree
101
+ * @param {Find | FindAndReplaceSchema | FindAndReplaceList } find
102
+ * @param {Replace | Options | null | undefined } [replace]
103
+ * @param {Options | null | undefined } [options]
104
+ * @returns {Tree }
68
105
*/
69
106
function ( tree , find , replace , options ) {
70
- /** @type {Options| undefined } */
107
+ /** @type {Options | null | undefined } */
71
108
let settings
72
109
/** @type {FindAndReplaceSchema|FindAndReplaceList } */
73
110
let schema
@@ -94,21 +131,22 @@ export const findAndReplace =
94
131
visitParents ( tree , 'text' , visitor )
95
132
}
96
133
134
+ // To do next major: don’t return the given tree.
97
135
return tree
98
136
99
137
/** @type {import('unist-util-visit-parents/complex-types.js').BuildVisitor<Root, 'text'> } */
100
138
function visitor ( node , parents ) {
101
139
let index = - 1
102
- /** @type {Parent| undefined } */
140
+ /** @type {Parent | undefined } */
103
141
let grandparent
104
142
105
143
while ( ++ index < parents . length ) {
106
- const parent = /** @type { Parent } */ ( parents [ index ] )
144
+ const parent = parents [ index ]
107
145
108
146
if (
109
147
ignored (
110
148
parent ,
111
- // @ts -expect-error mdast vs. unist parent .
149
+ // @ts -expect-error: TS doesn’t understand but it’s perfect .
112
150
grandparent ? grandparent . children . indexOf ( parent ) : undefined ,
113
151
grandparent
114
152
)
@@ -120,15 +158,19 @@ export const findAndReplace =
120
158
}
121
159
122
160
if ( grandparent ) {
123
- // @ts -expect-error: stack is fine.
124
161
return handler ( node , parents )
125
162
}
126
163
}
127
164
128
165
/**
166
+ * Handle a text node which is not in an ignored parent.
167
+ *
129
168
* @param {Text } node
130
- * @param {[Root, ...Array<Parent>] } parents
169
+ * Text node.
170
+ * @param {Array<Parent> } parents
171
+ * Parents.
131
172
* @returns {VisitorResult }
173
+ * Result.
132
174
*/
133
175
function handler ( node , parents ) {
134
176
const parent = parents [ parents . length - 1 ]
@@ -140,19 +182,18 @@ export const findAndReplace =
140
182
let change = false
141
183
/** @type {Array<PhrasingContent> } */
142
184
let nodes = [ ]
143
- /** @type {number|undefined } */
144
- let position
145
185
146
186
find . lastIndex = 0
147
187
148
188
let match = find . exec ( node . value )
149
189
150
190
while ( match ) {
151
- position = match . index
191
+ const position = match . index
152
192
/** @type {RegExpMatchObject } */
153
193
const matchObject = {
154
194
index : match . index ,
155
195
input : match . input ,
196
+ // @ts -expect-error: stack is fine.
156
197
stack : [ ...parents , node ]
157
198
}
158
199
let value = replace ( ...match , matchObject )
@@ -161,6 +202,7 @@ export const findAndReplace =
161
202
value = value . length > 0 ? { type : 'text' , value} : undefined
162
203
}
163
204
205
+ // It wasn’t a match after all.
164
206
if ( value !== false ) {
165
207
if ( start !== position ) {
166
208
nodes . push ( {
@@ -202,8 +244,12 @@ export const findAndReplace =
202
244
)
203
245
204
246
/**
205
- * @param {FindAndReplaceSchema|FindAndReplaceList } schema
247
+ * Turn a schema into pairs.
248
+ *
249
+ * @param {FindAndReplaceSchema | FindAndReplaceList } schema
250
+ * Schema.
206
251
* @returns {Pairs }
252
+ * Clean pairs.
207
253
*/
208
254
function toPairs ( schema ) {
209
255
/** @type {Pairs } */
@@ -237,16 +283,24 @@ function toPairs(schema) {
237
283
}
238
284
239
285
/**
286
+ * Turn a find into an expression.
287
+ *
240
288
* @param {Find } find
289
+ * Find.
241
290
* @returns {RegExp }
291
+ * Expression.
242
292
*/
243
293
function toExpression ( find ) {
244
294
return typeof find === 'string' ? new RegExp ( escape ( find ) , 'g' ) : find
245
295
}
246
296
247
297
/**
298
+ * Turn a replace into a function.
299
+ *
248
300
* @param {Replace } replace
301
+ * Replace.
249
302
* @returns {ReplaceFunction }
303
+ * Function.
250
304
*/
251
305
function toFunction ( replace ) {
252
306
return typeof replace === 'function' ? replace : ( ) => replace
0 commit comments