001package gudusoft.gsqlparser.pp.print; 002 003import gudusoft.gsqlparser.ETokenStatus; 004import gudusoft.gsqlparser.ETokenType; 005import gudusoft.gsqlparser.TSourceToken; 006import gudusoft.gsqlparser.TSourceTokenList; 007import gudusoft.gsqlparser.pp.output.OutputConfig; 008import gudusoft.gsqlparser.pp.utils.SourceTokenNameConstant; 009 010import java.io.IOException; 011import java.io.OutputStream; 012import java.util.ArrayList; 013import java.util.List; 014 015/** 016 * the text print 017 * 018 * @author zhoujun 019 * 020 */ 021public class TextPrinter implements IPrinter 022{ 023 024 private OutputStream out = System.out; 025 026 private OutputConfig outputConfig = null; 027 028 /* 029 * (non-Javadoc) 030 * 031 * @see gudusoft.gsqlparser.pp.print.IPrinter#print(gudusoft.gsqlparser. 032 * TSourceTokenList) 033 */ 034 public void print( TSourceTokenList tl ) 035 { 036 List<TSourceToken> flat = new ArrayList<TSourceToken>( tl.size( ) ); 037 flatten( tl, flat ); 038 boolean inNoFormat = false; 039 for ( int i = 0; i < flat.size( ); i++ ) 040 { 041 TSourceToken t = flat.get( i ); 042 if ( isComment( t ) ) 043 { 044 String c = t.toString( ).toLowerCase( java.util.Locale.ROOT ); 045 if ( c.contains( SourceTokenNameConstant.BEGIN_NO_FORMAT ) ) 046 { 047 inNoFormat = true; 048 } 049 else if ( c.contains( SourceTokenNameConstant.END_NO_FORMAT ) ) 050 { 051 inNoFormat = false; 052 } 053 } 054 // Trailing blanks before a line break are never part of a token 055 // (string literals and comments are single tokens), so dropping 056 // them cannot change any token's text. Without this, a whitespace 057 // token left behind a "--" comment became part of that comment 058 // when the output was lexed again, and many lines ended in blanks 059 // (PrettyScore TRAILING_WHITESPACE / COMMENTS_PRESERVED). A 060 // no-format zone is reproduced verbatim, blanks included. 061 if ( !inNoFormat && isWhitespaceToken( t ) ) 062 { 063 // the first non-blank token after this one (blank tokens may come in runs) 064 int j = i + 1; 065 while ( j < flat.size( ) && isBlankToken( flat.get( j ) ) ) 066 { 067 j++; 068 } 069 TSourceToken next = j < flat.size( ) ? flat.get( j ) : null; 070 TSourceToken prev = i > 0 ? flat.get( i - 1 ) : null; 071 // keep one blank after a MySQL "$" delimiter: "END$" directly 072 // followed by a line break lexes as one identifier 073 boolean keepOne = prev != null && endsWithDollar( prev ); 074 String text = t.toString( ); 075 // blanks inside a merged whitespace token (" \n ") that sit 076 // before a line break 077 String stripped = stripBlanksBeforeLineBreaks( text, keepOne ); 078 if ( isBlankToken( t ) && ( next == null || startsWithLineBreak( next ) ) ) 079 { 080 stripped = keepOne ? " " : ""; 081 } 082 if ( !stripped.equals( text ) ) 083 { 084 if ( !stripped.isEmpty( ) ) 085 { 086 printText( stripped ); 087 } 088 continue; 089 } 090 } 091 print( t ); 092 } 093 } 094 095 /** 096 * Expand tokensBefore / replaceToken / tokensAfter in print order, 097 * skipping deleted tokens. 098 */ 099 private static void flatten( TSourceTokenList tl, List<TSourceToken> out ) 100 { 101 TSourceToken activeSplit = null; 102 for ( int i = 0; i < tl.size( ); i++ ) 103 { 104 TSourceToken t = tl.get( i ); 105 if ( t.tokenstatus == ETokenStatus.tsdeleted ) 106 { 107 continue; 108 } 109 // A run of tokens the vendor tokenizer split from one source token 110 // (BigQuery `p.d.t`) is printed as that source token, once: the 111 // user's spelling is not the formatter's to change. 112 if ( t.splitFrom != null ) 113 { 114 boolean first = activeSplit != t.splitFrom; 115 boolean last = true; 116 for ( int j = i + 1; j < tl.size( ); j++ ) 117 { 118 TSourceToken n = tl.get( j ); 119 if ( n.tokenstatus == ETokenStatus.tsdeleted ) 120 { 121 continue; 122 } 123 last = n.splitFrom != t.splitFrom; 124 break; 125 } 126 if ( first ) 127 { 128 activeSplit = t.splitFrom; 129 if ( t.getTokensBefore( ) != null && t.getTokensBefore( ).size( ) > 0 ) 130 { 131 flatten( t.getTokensBefore( ), out ); 132 } 133 out.add( t.splitFrom ); 134 } 135 if ( last && t.getTokensAfter( ) != null && t.getTokensAfter( ).size( ) > 0 ) 136 { 137 flatten( t.getTokensAfter( ), out ); 138 } 139 continue; 140 } 141 activeSplit = null; 142 if ( t.getTokensBefore( ) != null 143 && t.getTokensBefore( ).size( ) > 0 ) 144 { 145 flatten( t.getTokensBefore( ), out ); 146 } 147 if ( t.getReplaceToken( ) != null ) 148 { 149 out.add( t.getReplaceToken( ) ); 150 } 151 else 152 { 153 out.add( t ); 154 } 155 if ( t.getTokensAfter( ) != null && t.getTokensAfter( ).size( ) > 0 ) 156 { 157 flatten( t.getTokensAfter( ), out ); 158 } 159 } 160 } 161 162 /** 163 * Drops the blanks (spaces, tabs) that sit directly before a line break ({@code \n} or {@code \r\n}), 164 * leaving one blank when {@code keepOne}. The result is that of 165 * {@code text.replaceAll("[ \t]+(\r?\n)", keepOne ? " $1" : "$1")}, in one linear pass: the regex 166 * backtracks quadratically over a long run of blanks that no line break follows. 167 */ 168 static String stripBlanksBeforeLineBreaks( String text, boolean keepOne ) 169 { 170 StringBuilder out = null; 171 int n = text.length( ); 172 int i = 0; 173 while ( i < n ) 174 { 175 char c = text.charAt( i ); 176 if ( c != ' ' && c != '\t' ) 177 { 178 if ( out != null ) 179 { 180 out.append( c ); 181 } 182 i++; 183 continue; 184 } 185 int end = i; 186 while ( end < n && ( text.charAt( end ) == ' ' || text.charAt( end ) == '\t' ) ) 187 { 188 end++; 189 } 190 boolean beforeLineBreak = end < n && ( text.charAt( end ) == '\n' 191 || ( text.charAt( end ) == '\r' && end + 1 < n && text.charAt( end + 1 ) == '\n' ) ); 192 if ( beforeLineBreak ) 193 { 194 if ( out == null ) 195 { 196 out = new StringBuilder( n ).append( text, 0, i ); 197 } 198 if ( keepOne ) 199 { 200 out.append( ' ' ); 201 } 202 } 203 else if ( out != null ) 204 { 205 out.append( text, i, end ); 206 } 207 i = end; 208 } 209 return out == null ? text : out.toString( ); 210 } 211 212 /** Whitespace / return tokens, including formatter-made ones that carry no type. */ 213 private static boolean isWhitespaceToken( TSourceToken t ) 214 { 215 if ( t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn ) 216 { 217 return true; 218 } 219 if ( t.tokentype != null ) 220 { 221 return false; 222 } 223 String s = t.toString( ); 224 if ( s == null || s.isEmpty( ) ) 225 { 226 return false; 227 } 228 for ( int i = 0; i < s.length( ); i++ ) 229 { 230 char c = s.charAt( i ); 231 if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' ) 232 { 233 return false; 234 } 235 } 236 return true; 237 } 238 239 private void printText( String text ) 240 { 241 try 242 { 243 out.write( text.getBytes( ) ); 244 } 245 catch ( IOException e ) 246 { 247 e.printStackTrace( ); 248 } 249 } 250 251 private static boolean isComment( TSourceToken t ) 252 { 253 return t.tokentype == ETokenType.ttsimplecomment 254 || t.tokentype == ETokenType.ttbracketedcomment 255 || t.tokentype == ETokenType.ttCPPComment; 256 } 257 258 /** 259 * A token made only of blanks (spaces / tabs), never a return; formatter-made 260 * blanks may carry no type, and an empty placeholder token counts as blank. 261 */ 262 private static boolean isBlankToken( TSourceToken t ) 263 { 264 if ( t.tokentype != null && t.tokentype != ETokenType.ttwhitespace ) 265 { 266 return false; 267 } 268 String s = t.toString( ); 269 if ( s == null || s.isEmpty( ) ) 270 { 271 return true; 272 } 273 for ( int i = 0; i < s.length( ); i++ ) 274 { 275 char c = s.charAt( i ); 276 if ( c != ' ' && c != '\t' ) 277 { 278 return false; 279 } 280 } 281 return true; 282 } 283 284 /** 285 * A token ending in "$" (MySQL delimiter, {@code END$}, PostgreSQL {@code $$}): 286 * the blank after it is kept because "$" directly followed by a line break 287 * fuses with the preceding word when the output is lexed again. 288 */ 289 private static boolean endsWithDollar( TSourceToken t ) 290 { 291 String s = t.toString( ); 292 return s != null && !s.isEmpty( ) && s.charAt( s.length( ) - 1 ) == '$'; 293 } 294 295 private static boolean startsWithLineBreak( TSourceToken t ) 296 { 297 if ( t.tokentype == ETokenType.ttreturn ) 298 { 299 return true; 300 } 301 String s = t.toString( ); 302 return s != null && !s.isEmpty( ) 303 && ( s.charAt( 0 ) == '\n' || s.charAt( 0 ) == '\r' ); 304 } 305 306 /* 307 * (non-Javadoc) 308 * 309 * @see 310 * gudusoft.gsqlparser.pp.print.IPrinter#print(gudusoft.gsqlparser.TSourceToken 311 * ) 312 */ 313 public void print( TSourceToken t ) 314 { 315 try 316 { 317 if ( outputConfig != null ) 318 { 319 out.write( outputConfig.renderHighlightingElement( t ) 320 .toString( ) 321 .getBytes( ) ); 322 } 323 else 324 { 325 out.write( t.toString( ).getBytes( ) ); 326 } 327 } 328 catch ( IOException e ) 329 { 330 // TODO Auto-generated catch block 331 e.printStackTrace( ); 332 } 333 } 334 335 public OutputStream getOut( ) 336 { 337 return out; 338 } 339 340 public void setOut( OutputStream out ) 341 { 342 this.out = out; 343 } 344 345 public void setOutputConfig( OutputConfig outputConfig ) 346 { 347 this.outputConfig = outputConfig; 348 } 349 350}