1- import { AttributeNode , ClassNode , SourceFileNode , TypeDeclarationNode , TypeReferenceNode } from "./ast" ;
1+ import { AttributeNode , ClassNode , CommentTrivia , SourceFileNode , TypeDeclarationNode , TypeReferenceNode } from "./ast" ;
22
33export interface GenerateOptions {
44 exportModule ?: boolean ;
@@ -8,6 +8,8 @@ export interface GenerateOptions {
88 quoteStyle ?: "double" | "single" ;
99 semicolons ?: boolean ;
1010 normalizeAcronyms ?: boolean ;
11+ preserveComments ?: boolean ;
12+ convertDocumentationComments ?: boolean ;
1113}
1214
1315export interface GeneratedFile {
@@ -41,7 +43,7 @@ export function generate(files: SourceFileNode[], options: GenerateOptions = {})
4143 const typeMap : TypeMap = { declarations : new Map ( declarations . map ( ( d ) => [ d . name , d ] ) ) } ;
4244 const output = declarations . map ( ( declaration ) => ( {
4345 name : `${ declaration . name } .ts` ,
44- text : declaration . kind === "enum" ? generateEnum ( declaration ) : generateClass ( declaration , typeMap , settings ) ,
46+ text : declaration . kind === "enum" ? generateEnum ( declaration , settings ) : generateClass ( declaration , typeMap , settings ) ,
4547 } ) ) ;
4648
4749 if ( settings . exportModule ) {
@@ -62,10 +64,13 @@ function shouldEmit(declaration: TypeDeclarationNode): boolean {
6264 return ! declaration . name . endsWith ( "Attribute" ) && declaration . properties . length > 0 ;
6365}
6466
65- function generateEnum ( node : Extract < TypeDeclarationNode , { kind : "enum" } > ) : string {
66- const lines = [ "" , `export enum ${ node . name } {` ] ;
67+ function generateEnum ( node : Extract < TypeDeclarationNode , { kind : "enum" } > , options : Required < GenerateOptions > ) : string {
68+ const lines = [ "" ] ;
69+ pushComments ( lines , node . leadingComments , "" , options ) ;
70+ lines . push ( `export enum ${ node . name } {` ) ;
6771 for ( const member of node . members ) {
68- lines . push ( ` ${ member . name } = ${ member . value ?? 0 } ,` ) ;
72+ pushComments ( lines , member . leadingComments , " " , options ) ;
73+ lines . push ( ` ${ member . name } = ${ member . value ?? 0 } ,${ trailingComment ( member . trailingComments , options ) } ` ) ;
6974 }
7075 lines . push ( "}" , "" ) ;
7176 return lines . join ( "\n" ) ;
@@ -85,14 +90,20 @@ function generateClass(node: ClassNode, typeMap: TypeMap, options: Required<Gene
8590 : "" ;
8691 const readonlyClass = options . readonlyProperties || hasAttribute ( node . attributes , "Readonly" ) ||
8792 hasAttribute ( node . attributes , "ReadOnly" ) || hasAttribute ( node . attributes , "ReadonlyProperties" ) ;
93+ pushComments ( lines , node . leadingComments , "" , options ) ;
8894 lines . push ( `export interface ${ node . name } ${ typeParams } ${ base } {` ) ;
8995 for ( const prop of node . properties ) {
96+ pushComments ( lines , prop . leadingComments , " " , options ) ;
9097 const union = typeUnion ( prop . attributes , options ) ;
9198 const readonly = readonlyClass || hasAttribute ( prop . attributes , "Readonly" ) ||
9299 hasAttribute ( prop . attributes , "ReadOnly" ) ;
93100 const prefix = readonly ? "readonly " : "" ;
94101 if ( union ) {
95- lines . push ( ` ${ prefix } ${ propertyName ( prop . name , options ) } : ${ union } ${ statementEnd ( options ) } ` ) ;
102+ lines . push (
103+ ` ${ prefix } ${ propertyName ( prop . name , options ) } : ${ union } ${ statementEnd ( options ) } ${
104+ trailingComment ( prop . trailingComments , options )
105+ } `,
106+ ) ;
96107 continue ;
97108 }
98109
@@ -105,13 +116,86 @@ function generateClass(node: ClassNode, typeMap: TypeMap, options: Required<Gene
105116 lines . push (
106117 ` ${ prefix } ${ propertyName ( prop . name , options ) } ${ optional ? "?" : "" } : ${ typeName } ${ nullable } ${
107118 statementEnd ( options )
108- } `,
119+ } ${ trailingComment ( prop . trailingComments , options ) } `,
109120 ) ;
110121 }
111122 lines . push ( "}" , "" ) ;
112123 return lines . join ( "\n" ) ;
113124}
114125
126+ function pushComments (
127+ lines : string [ ] ,
128+ comments : CommentTrivia [ ] ,
129+ indent : string ,
130+ options : Required < GenerateOptions > ,
131+ ) : void {
132+ let docComments : CommentTrivia [ ] = [ ] ;
133+ const flushDocComments = ( ) => {
134+ if ( docComments . length === 0 ) return ;
135+ if ( options . convertDocumentationComments ) {
136+ pushDocumentationComment ( lines , docComments , indent ) ;
137+ } else if ( options . preserveComments ) {
138+ for ( const comment of docComments ) lines . push ( `${ indent } ${ comment . text . trim ( ) } ` ) ;
139+ }
140+ docComments = [ ] ;
141+ } ;
142+
143+ for ( const comment of comments ) {
144+ if ( comment . kind === "doc" ) {
145+ docComments . push ( comment ) ;
146+ continue ;
147+ }
148+
149+ flushDocComments ( ) ;
150+ if ( comment . kind !== "doc" && ! options . preserveComments ) continue ;
151+ const text = comment . text . trim ( ) ;
152+ if ( comment . kind === "block" ) {
153+ for ( const line of text . split ( / \r ? \n / ) ) lines . push ( `${ indent } ${ line } ` ) ;
154+ } else {
155+ lines . push ( `${ indent } ${ text } ` ) ;
156+ }
157+ }
158+ flushDocComments ( ) ;
159+ }
160+
161+ function trailingComment ( comments : CommentTrivia [ ] , options : Required < GenerateOptions > ) : string {
162+ if ( ! options . preserveComments || comments . length === 0 ) return "" ;
163+ return ` ${ comments . map ( ( comment ) => comment . text . trim ( ) ) . join ( " " ) } ` ;
164+ }
165+
166+ function pushDocumentationComment ( lines : string [ ] , comments : CommentTrivia [ ] , indent : string ) : void {
167+ const text = comments
168+ . map ( ( comment ) => documentationText ( comment . text ) )
169+ . join ( "\n\n" )
170+ . trim ( ) ;
171+ if ( ! text ) return ;
172+
173+ lines . push ( `${ indent } /**` ) ;
174+ for ( const line of text . split ( / \r ? \n / ) ) {
175+ lines . push ( line ? `${ indent } * ${ line } ` : `${ indent } *` ) ;
176+ }
177+ lines . push ( `${ indent } */` ) ;
178+ }
179+
180+ function documentationText ( text : string ) : string {
181+ const xml = text
182+ . split ( / \r ? \n / )
183+ . map ( ( line ) => line . trim ( ) . replace ( / ^ \/ \/ \/ \s ? / , "" ) )
184+ . join ( "\n" )
185+ . trim ( ) ;
186+ return xml
187+ . replace ( / < s u m m a r y > \s * ( [ \s \S ] * ?) \s * < \/ s u m m a r y > / gi, "$1" )
188+ . replace ( / < r e m a r k s > \s * ( [ \s \S ] * ?) \s * < \/ r e m a r k s > / gi, "\n\n$1" )
189+ . replace ( / < r e t u r n s > \s * ( [ \s \S ] * ?) \s * < \/ r e t u r n s > / gi, "\n\n@returns $1" )
190+ . replace ( / < p a r a m \s + n a m e = " ( [ ^ " ] + ) " > \s * ( [ \s \S ] * ?) \s * < \/ p a r a m > / gi, "\n\n@param $1 $2" )
191+ . replace ( / < [ ^ > ] + > / g, "" )
192+ . split ( / \r ? \n / )
193+ . map ( ( line ) => line . trim ( ) )
194+ . join ( "\n" )
195+ . replace ( / \n { 3 , } / g, "\n\n" )
196+ . trim ( ) ;
197+ }
198+
115199function collectImports ( node : ClassNode , typeMap : TypeMap ) : string [ ] {
116200 const imports = new Set < string > ( ) ;
117201 const visit = ( type : TypeReferenceNode ) => {
@@ -220,6 +304,8 @@ function normalizeOptions(options: GenerateOptions): Required<GenerateOptions> {
220304 quoteStyle : options . quoteStyle ?? "double" ,
221305 semicolons : options . semicolons ?? true ,
222306 normalizeAcronyms : options . normalizeAcronyms ?? false ,
307+ preserveComments : options . preserveComments ?? false ,
308+ convertDocumentationComments : options . convertDocumentationComments ?? false ,
223309 } ;
224310}
225311
0 commit comments