@@ -168,52 +168,166 @@ function ensureJsExt(filePath) {
168168 return filePath . replace ( extensionEnsureRegEx , '.js' ) ;
169169}
170170
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+
171196exports . 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 ) ,
205281 ) ;
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+ ] ) ;
206293 }
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 ;
212325 }
213- throw new Error ( "Missing closing '}'" ) ;
214- } ;
215- } ,
216- ) ;
326+ ++ i ;
327+ }
328+ throw new Error ( "Missing closing '}'" ) ;
329+ } ;
330+ } ) ;
217331} ;
218332
219333exports . astNodeVisitor = {
0 commit comments