@@ -59,6 +59,37 @@ class JSONTraversalStep extends VisitNodeStep {
5959 }
6060}
6161
62+ /**
63+ * Processes tokens to extract comments and their starting tokens.
64+ * @param {Array<Token> } tokens The tokens to process.
65+ * @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>} }
66+ * An object containing an array of comments, a map of starting token range to token index, and
67+ * a map of ending token range to token index.
68+ */
69+ function processTokens ( tokens ) {
70+ /** @type {Array<Token> } */
71+ const comments = [ ] ;
72+
73+ /** @type {Map<number, number> } */
74+ const starts = new Map ( ) ;
75+
76+ /** @type {Map<number, number> } */
77+ const ends = new Map ( ) ;
78+
79+ for ( let i = 0 ; i < tokens . length ; i ++ ) {
80+ const token = tokens [ i ] ;
81+
82+ if ( token . type . endsWith ( "Comment" ) ) {
83+ comments . push ( token ) ;
84+ }
85+
86+ starts . set ( token . range [ 0 ] , i ) ;
87+ ends . set ( token . range [ 1 ] , i ) ;
88+ }
89+
90+ return { comments, starts, ends } ;
91+ }
92+
6293//-----------------------------------------------------------------------------
6394// Exports
6495//-----------------------------------------------------------------------------
@@ -93,11 +124,23 @@ export class JSONSourceCode extends TextSourceCodeBase {
93124 ast = undefined ;
94125
95126 /**
96- * The comment node in the source code.
127+ * The comment tokens in the source code.
97128 * @type {Array<Token>|undefined }
98129 */
99130 comments ;
100131
132+ /**
133+ * A map of token start positions to their corresponding index.
134+ * @type {Map<number, number> }
135+ */
136+ #tokenStarts;
137+
138+ /**
139+ * A map of token end positions to their corresponding index.
140+ * @type {Map<number, number> }
141+ */
142+ #tokenEnds;
143+
101144 /**
102145 * Creates a new instance.
103146 * @param {Object } options The options for the instance.
@@ -107,9 +150,11 @@ export class JSONSourceCode extends TextSourceCodeBase {
107150 constructor ( { text, ast } ) {
108151 super ( { text, ast } ) ;
109152 this . ast = ast ;
110- this . comments = ast . tokens
111- ? ast . tokens . filter ( token => token . type . endsWith ( "Comment" ) )
112- : [ ] ;
153+
154+ const { comments, starts, ends } = processTokens ( this . ast . tokens ?? [ ] ) ;
155+ this . comments = comments ;
156+ this . #tokenStarts = starts ;
157+ this . #tokenEnds = ends ;
113158 }
114159
115160 /**
@@ -283,4 +328,83 @@ export class JSONSourceCode extends TextSourceCodeBase {
283328
284329 return steps ;
285330 }
331+
332+ /**
333+ * Gets the token before the given node or token, optionally including comments.
334+ * @param {AnyNode|Token } nodeOrToken The node or token to get the previous token for.
335+ * @param {Object } [options] Options object.
336+ * @param {boolean } [options.includeComments] If true, return comments when they are present.
337+ * @returns {Token|null } The previous token or comment, or null if there is none.
338+ */
339+ getTokenBefore ( nodeOrToken , { includeComments = false } = { } ) {
340+ const index = this . #tokenStarts. get ( nodeOrToken . range [ 0 ] ) ;
341+
342+ if ( index === undefined ) {
343+ return null ;
344+ }
345+
346+ let previousIndex = index - 1 ;
347+ if ( previousIndex < 0 ) {
348+ return null ;
349+ }
350+
351+ const tokens = this . ast . tokens ;
352+ let tokenOrComment = tokens [ previousIndex ] ;
353+
354+ if ( includeComments ) {
355+ return tokenOrComment ;
356+ }
357+
358+ // skip comments
359+ while ( tokenOrComment ?. type . endsWith ( "Comment" ) ) {
360+ previousIndex -- ;
361+
362+ if ( previousIndex < 0 ) {
363+ return null ;
364+ }
365+
366+ tokenOrComment = tokens [ previousIndex ] ;
367+ }
368+ return tokenOrComment ;
369+ }
370+
371+ /**
372+ * Gets the token after the given node or token, skipping any comments unless includeComments is true.
373+ * @param {AnyNode|Token } nodeOrToken The node or token to get the next token for.
374+ * @param {Object } [options] Options object.
375+ * @param {boolean } [options.includeComments=false] If true, return comments when they are present.
376+ * @returns {Token|null } The next token or comment, or null if there is none.
377+ */
378+ getTokenAfter ( nodeOrToken , { includeComments = false } = { } ) {
379+ const index = this . #tokenEnds. get ( nodeOrToken . range [ 1 ] ) ;
380+
381+ if ( index === undefined ) {
382+ return null ;
383+ }
384+
385+ let nextIndex = index + 1 ;
386+ const tokens = this . ast . tokens ;
387+ if ( nextIndex >= tokens . length ) {
388+ return null ;
389+ }
390+
391+ let tokenOrComment = tokens [ nextIndex ] ;
392+
393+ if ( includeComments ) {
394+ return tokenOrComment ;
395+ }
396+
397+ // skip comments
398+ while ( tokenOrComment ?. type . endsWith ( "Comment" ) ) {
399+ nextIndex ++ ;
400+
401+ if ( nextIndex >= tokens . length ) {
402+ return null ;
403+ }
404+
405+ tokenOrComment = tokens [ nextIndex ] ;
406+ }
407+
408+ return tokenOrComment ;
409+ }
286410}
0 commit comments