@@ -168,52 +168,166 @@ function ensureJsExt(filePath) {
168
168
return filePath . replace ( extensionEnsureRegEx , '.js' ) ;
169
169
}
170
170
171
+ /**
172
+ * Replaces text by indices where each element of `replacements` is `[startIndex, endIndex, replacement]`.
173
+ *
174
+ * Note: This function does not handle nested replacements.
175
+ *
176
+ * @param {string } text The text to replace
177
+ * @param {Array<[number, number, string]> } replacements The replacements to apply
178
+ * @return {string } The text with replacements applied
179
+ */
180
+ function replaceByIndices ( text , replacements ) {
181
+ let offset = 0 ;
182
+ let replacedText = text ;
183
+
184
+ replacements . forEach ( ( [ startIndex , endIndex , replacement ] , i ) => {
185
+ const head = replacedText . slice ( 0 , startIndex + offset ) ;
186
+ const tail = replacedText . slice ( endIndex + offset ) ;
187
+
188
+ replacedText = head + replacement + tail ;
189
+
190
+ offset += replacement . length - ( endIndex - startIndex ) ;
191
+ } ) ;
192
+
193
+ return replacedText ;
194
+ }
195
+
171
196
exports . defineTags = function ( dictionary ) {
172
- [ 'type' , 'typedef' , 'property' , 'return' , 'param' , 'template' ] . forEach (
173
- function ( tagName ) {
174
- const tag = dictionary . lookUp ( tagName ) ;
175
- const oldOnTagText = tag . onTagText ;
176
- tag . onTagText = function ( tagText ) {
177
- if ( oldOnTagText ) {
178
- tagText = oldOnTagText . apply ( this , arguments ) ;
179
- }
180
- // Replace `templateliteral` with 'templateliteral'
181
- const startIndex = tagText . search ( '{' ) ;
182
- if ( startIndex === - 1 ) {
183
- return tagText ;
184
- }
185
- const len = tagText . length ;
186
- let open = 0 ;
187
- let i = startIndex ;
188
- while ( i < len ) {
189
- switch ( tagText [ i ] ) {
190
- case '\\' :
191
- // Skip escaped character
192
- ++ i ;
193
- break ;
194
- case '{' :
195
- ++ open ;
196
- break ;
197
- case '}' :
198
- if ( ! -- open ) {
199
- return (
200
- tagText . slice ( 0 , startIndex ) +
201
- tagText
202
- . slice ( startIndex , i + 1 )
203
- . replace ( / ` ( [ ^ ` ] * ) ` / g, "'$1'" ) +
204
- tagText . slice ( i + 1 )
197
+ const tags = [
198
+ 'type' ,
199
+ 'typedef' ,
200
+ 'property' ,
201
+ 'return' ,
202
+ 'param' ,
203
+ 'template' ,
204
+ 'default' ,
205
+ 'member' ,
206
+ ] ;
207
+
208
+ tags . forEach ( function ( tagName ) {
209
+ const tag = dictionary . lookUp ( tagName ) ;
210
+ const oldOnTagText = tag . onTagText ;
211
+
212
+ /**
213
+ * @param {string } tagText The tag text
214
+ * @return {string } The modified tag text
215
+ */
216
+ tag . onTagText = function ( tagText ) {
217
+ if ( oldOnTagText ) {
218
+ tagText = oldOnTagText . apply ( this , arguments ) ;
219
+ }
220
+
221
+ const startIndex = tagText . search ( '{' ) ;
222
+ if ( startIndex === - 1 ) {
223
+ return tagText ;
224
+ }
225
+
226
+ const len = tagText . length ;
227
+
228
+ /** @type {Array<[number, number, string]> } */
229
+ let replacements = [ ] ;
230
+ let openCurly = 0 ;
231
+ let openRound = 0 ;
232
+ let isWithinString = false ;
233
+ let quoteChar = '' ;
234
+ let i = startIndex ;
235
+ let functionStartIndex ;
236
+
237
+ while ( i < len ) {
238
+ switch ( tagText [ i ] ) {
239
+ case '\\' :
240
+ // Skip escaped character
241
+ ++ i ;
242
+ break ;
243
+ case '"' :
244
+ case "'" :
245
+ if ( isWithinString && quoteChar === tagText [ i ] ) {
246
+ isWithinString = false ;
247
+ quoteChar = '' ;
248
+ } else if ( ! isWithinString ) {
249
+ isWithinString = true ;
250
+ quoteChar = tagText [ i ] ;
251
+ }
252
+
253
+ break ;
254
+ case ';' :
255
+ // Replace interface-style semi-colon separators with commas
256
+ if ( ! isWithinString && openCurly > 1 ) {
257
+ const isTrailingSemiColon = / ^ \s * } / . test ( tagText . slice ( i + 1 ) ) ;
258
+
259
+ replacements . push ( [ i , i + 1 , isTrailingSemiColon ? '' : ',' ] ) ;
260
+ }
261
+
262
+ break ;
263
+ case '(' :
264
+ if ( openRound === 0 ) {
265
+ functionStartIndex = i ;
266
+ }
267
+
268
+ ++ openRound ;
269
+
270
+ break ;
271
+ case ')' :
272
+ if ( ! -- openRound ) {
273
+ // If round brackets form a function
274
+ const returnMatch = tagText . slice ( i + 1 ) . match ( / ^ \s * ( : | = > ) / ) ;
275
+
276
+ // Replace TS inline function syntax with JSDoc
277
+ if ( returnMatch ) {
278
+ const functionEndIndex = i + returnMatch [ 0 ] . length + 1 ;
279
+ const hasFunctionKeyword = / \b f u n c t i o n \s * $ / . test (
280
+ tagText . slice ( 0 , functionStartIndex ) ,
205
281
) ;
282
+
283
+ // Filter out any replacements that are within the function
284
+ replacements = replacements . filter ( ( [ startIndex ] ) => {
285
+ return startIndex < functionStartIndex || startIndex > i ;
286
+ } ) ;
287
+
288
+ replacements . push ( [
289
+ functionStartIndex ,
290
+ functionEndIndex ,
291
+ hasFunctionKeyword ? '():' : 'function():' ,
292
+ ] ) ;
206
293
}
207
- break ;
208
- default :
209
- break ;
210
- }
211
- ++ i ;
294
+
295
+ functionStartIndex = null ;
296
+ }
297
+
298
+ break ;
299
+ case '{' :
300
+ ++ openCurly ;
301
+ break ;
302
+ case '}' :
303
+ if ( ! -- openCurly ) {
304
+ const head = tagText . slice ( 0 , startIndex ) ;
305
+ const tail = tagText . slice ( i + 1 ) ;
306
+
307
+ const replaced = replaceByIndices (
308
+ tagText . slice ( startIndex , i + 1 ) ,
309
+ replacements ,
310
+ )
311
+ // Replace `templateliteral` with 'templateliteral'
312
+ . replace ( / ` ( [ ^ ` ] * ) ` / g, "'$1'" )
313
+ // Bracket notation to dot notation
314
+ . replace (
315
+ / ( \w + | > | \) | \] ) \[ (?: ' ( [ ^ ' ] + ) ' | " ( [ ^ " ] + ) " ) \] / g,
316
+ '$1.$2$3' ,
317
+ ) ;
318
+
319
+ return head + replaced + tail ;
320
+ }
321
+
322
+ break ;
323
+ default :
324
+ break ;
212
325
}
213
- throw new Error ( "Missing closing '}'" ) ;
214
- } ;
215
- } ,
216
- ) ;
326
+ ++ i ;
327
+ }
328
+ throw new Error ( "Missing closing '}'" ) ;
329
+ } ;
330
+ } ) ;
217
331
} ;
218
332
219
333
exports . astNodeVisitor = {
0 commit comments