| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library source_writer; | |
| 6 | |
| 7 import 'dart:math' as math; | |
| 8 | |
| 9 class Line { | |
| 10 final List<LineToken> tokens = <LineToken>[]; | |
| 11 final bool useTabs; | |
| 12 final int spacesPerIndent; | |
| 13 final int indentLevel; | |
| 14 final LinePrinter printer; | |
| 15 | |
| 16 Line({this.indentLevel: 0, this.useTabs: false, this.spacesPerIndent: 2, | |
| 17 this.printer: const SimpleLinePrinter()}) { | |
| 18 if (indentLevel > 0) { | |
| 19 indent(indentLevel); | |
| 20 } | |
| 21 } | |
| 22 | |
| 23 void addSpace() { | |
| 24 addSpaces(1); | |
| 25 } | |
| 26 | |
| 27 void addSpaces(int n, {breakWeight: DEFAULT_SPACE_WEIGHT}) { | |
| 28 tokens.add(new SpaceToken(n, breakWeight: breakWeight)); | |
| 29 } | |
| 30 | |
| 31 void addToken(LineToken token) { | |
| 32 tokens.add(token); | |
| 33 } | |
| 34 | |
| 35 void clear() { | |
| 36 tokens.clear(); | |
| 37 } | |
| 38 | |
| 39 bool isEmpty() => tokens.isEmpty; | |
| 40 | |
| 41 bool isWhitespace() => | |
| 42 tokens.every((tok) => tok is SpaceToken || tok is TabToken); | |
| 43 | |
| 44 void indent(int n) { | |
| 45 tokens.insert( | |
| 46 0, useTabs ? new TabToken(n) : new SpaceToken(n * spacesPerIndent)); | |
| 47 } | |
| 48 | |
| 49 String toString() => printer.printLine(this); | |
| 50 } | |
| 51 | |
| 52 /// Base class for line printers | |
| 53 abstract class LinePrinter { | |
| 54 const LinePrinter(); | |
| 55 | |
| 56 /// Convert this [line] to a [String] representation. | |
| 57 String printLine(Line line); | |
| 58 } | |
| 59 | |
| 60 typedef String Indenter(int n); | |
| 61 | |
| 62 /// A simple line breaking [LinePrinter] | |
| 63 class SimpleLineBreaker extends LinePrinter { | |
| 64 static final NO_OP_INDENTER = (n) => ''; | |
| 65 | |
| 66 final chunks = <Chunk>[]; | |
| 67 final int maxLength; | |
| 68 Indenter indenter; | |
| 69 | |
| 70 SimpleLineBreaker(this.maxLength, [this.indenter]) { | |
| 71 if (indenter == null) { | |
| 72 indenter = NO_OP_INDENTER; | |
| 73 } | |
| 74 } | |
| 75 | |
| 76 String printLine(Line line) { | |
| 77 var buf = new StringBuffer(); | |
| 78 var chunks = breakLine(line); | |
| 79 for (var i = 0; i < chunks.length; ++i) { | |
| 80 var chunk = chunks[i]; | |
| 81 if (i > 0) { | |
| 82 buf.write(indent(chunk, chunk.indent)); | |
| 83 } else { | |
| 84 buf.write(chunk); | |
| 85 } | |
| 86 } | |
| 87 return buf.toString(); | |
| 88 } | |
| 89 | |
| 90 String indent(Chunk chunk, int level) { | |
| 91 return '\n' + indenter(level) + chunk.toString(); | |
| 92 } | |
| 93 | |
| 94 List<Chunk> breakLine(Line line) { | |
| 95 List<LineToken> tokens = preprocess(line.tokens); | |
| 96 List<Chunk> chunks = <Chunk>[ | |
| 97 new Chunk(line.indentLevel, maxLength, tokens) | |
| 98 ]; | |
| 99 // try SINGLE_SPACE_WEIGHT | |
| 100 { | |
| 101 Chunk chunk = chunks[0]; | |
| 102 if (chunk.length > maxLength) { | |
| 103 for (int i = 0; i < tokens.length; i++) { | |
| 104 LineToken token = tokens[i]; | |
| 105 if (token is SpaceToken && token.breakWeight == SINGLE_SPACE_WEIGHT) { | |
| 106 var beforeChunk = chunk.subChunk(chunk.indent, 0, i); | |
| 107 var restChunk = chunk.subChunk(chunk.indent + 2, i + 1); | |
| 108 // check if 'init' in 'var v = init;' fits a line | |
| 109 if (restChunk.length < maxLength) { | |
| 110 return [beforeChunk, restChunk]; | |
| 111 } | |
| 112 // check if 'var v = method(' in 'var v = method(args)' does not fit | |
| 113 int weight = chunk.findMinSpaceWeight(); | |
| 114 if (chunk.getLengthToSpaceWithWeight(weight) > maxLength) { | |
| 115 chunks = [beforeChunk, restChunk]; | |
| 116 } | |
| 117 // done anyway | |
| 118 break; | |
| 119 } | |
| 120 } | |
| 121 } | |
| 122 } | |
| 123 // other spaces | |
| 124 while (true) { | |
| 125 List<Chunk> newChunks = <Chunk>[]; | |
| 126 bool hasChanges = false; | |
| 127 for (Chunk chunk in chunks) { | |
| 128 tokens = chunk.tokens; | |
| 129 if (chunk.length > maxLength) { | |
| 130 if (chunk.hasAnySpace()) { | |
| 131 int weight = chunk.findMinSpaceWeight(); | |
| 132 int newIndent = chunk.indent; | |
| 133 if (weight == DEFAULT_SPACE_WEIGHT) { | |
| 134 int start = 0; | |
| 135 int length = 0; | |
| 136 for (int i = 0; i < tokens.length; i++) { | |
| 137 LineToken token = tokens[i]; | |
| 138 if (token is SpaceToken && | |
| 139 token.breakWeight == weight && | |
| 140 i < tokens.length - 1) { | |
| 141 LineToken nextToken = tokens[i + 1]; | |
| 142 if (length + token.length + nextToken.length > maxLength) { | |
| 143 newChunks.add(chunk.subChunk(newIndent, start, i)); | |
| 144 newIndent = chunk.indent + 2; | |
| 145 start = i + 1; | |
| 146 length = 0; | |
| 147 continue; | |
| 148 } | |
| 149 } | |
| 150 length += token.length; | |
| 151 } | |
| 152 if (start < tokens.length) { | |
| 153 newChunks.add(chunk.subChunk(newIndent, start)); | |
| 154 } | |
| 155 } else { | |
| 156 int start = 0; | |
| 157 for (int i = 0; i < tokens.length; i++) { | |
| 158 LineToken token = tokens[i]; | |
| 159 if (token is SpaceToken && token.breakWeight == weight) { | |
| 160 newChunks.add(chunk.subChunk(newIndent, start, i)); | |
| 161 newIndent = chunk.indent + 2; | |
| 162 start = i + 1; | |
| 163 } | |
| 164 } | |
| 165 if (start < tokens.length) { | |
| 166 newChunks.add(chunk.subChunk(newIndent, start)); | |
| 167 } | |
| 168 } | |
| 169 } else { | |
| 170 newChunks.add(chunk); | |
| 171 } | |
| 172 } else { | |
| 173 newChunks.add(chunk); | |
| 174 } | |
| 175 if (newChunks.length > chunks.length) { | |
| 176 hasChanges = true; | |
| 177 } | |
| 178 } | |
| 179 if (!hasChanges) { | |
| 180 break; | |
| 181 } | |
| 182 chunks = newChunks; | |
| 183 } | |
| 184 return chunks; | |
| 185 } | |
| 186 | |
| 187 static List<LineToken> preprocess(List<LineToken> tok) { | |
| 188 var tokens = <LineToken>[]; | |
| 189 var curr; | |
| 190 | |
| 191 tok.forEach((token) { | |
| 192 if (token is! SpaceToken) { | |
| 193 if (curr == null) { | |
| 194 curr = token; | |
| 195 } else { | |
| 196 curr = merge(curr, token); | |
| 197 } | |
| 198 } else { | |
| 199 if (isNonbreaking(token)) { | |
| 200 curr = merge(curr, token); | |
| 201 } else { | |
| 202 if (curr != null) { | |
| 203 tokens.add(curr); | |
| 204 curr = null; | |
| 205 } | |
| 206 tokens.add(token); | |
| 207 } | |
| 208 } | |
| 209 }); | |
| 210 | |
| 211 if (curr != null) { | |
| 212 tokens.add(curr); | |
| 213 } | |
| 214 | |
| 215 return tokens; | |
| 216 } | |
| 217 | |
| 218 static bool isNonbreaking(SpaceToken token) => | |
| 219 token.breakWeight == UNBREAKABLE_SPACE_WEIGHT; | |
| 220 | |
| 221 static LineToken merge(LineToken first, LineToken second) => | |
| 222 new LineToken(first.value + second.value); | |
| 223 } | |
| 224 | |
| 225 /// Test if this [string] contains only whitespace characters | |
| 226 bool isWhitespace(String string) => string.codeUnits | |
| 227 .every((c) => c == 0x09 || c == 0x20 || c == 0x0A || c == 0x0D); | |
| 228 | |
| 229 /// Special token indicating a line start | |
| 230 final LINE_START = new SpaceToken(0); | |
| 231 | |
| 232 const DEFAULT_SPACE_WEIGHT = UNBREAKABLE_SPACE_WEIGHT - 1; | |
| 233 /// The weight of a space after '=' in variable declaration or assignment | |
| 234 const SINGLE_SPACE_WEIGHT = UNBREAKABLE_SPACE_WEIGHT - 2; | |
| 235 const UNBREAKABLE_SPACE_WEIGHT = 100000000; | |
| 236 | |
| 237 /// Simple non-breaking printer | |
| 238 class SimpleLinePrinter extends LinePrinter { | |
| 239 const SimpleLinePrinter(); | |
| 240 | |
| 241 String printLine(Line line) { | |
| 242 var buffer = new StringBuffer(); | |
| 243 line.tokens.forEach((tok) => buffer.write(tok.toString())); | |
| 244 return buffer.toString(); | |
| 245 } | |
| 246 } | |
| 247 | |
| 248 /// Describes a piece of text in a [Line]. | |
| 249 abstract class LineText { | |
| 250 int get length; | |
| 251 } | |
| 252 | |
| 253 /// A working piece of text used in calculating line breaks | |
| 254 class Chunk { | |
| 255 final int indent; | |
| 256 final int maxLength; | |
| 257 final List<LineToken> tokens = <LineToken>[]; | |
| 258 | |
| 259 Chunk(this.indent, this.maxLength, [List<LineToken> tokens]) { | |
| 260 this.tokens.addAll(tokens); | |
| 261 } | |
| 262 | |
| 263 int get length { | |
| 264 return tokens.fold(0, (len, token) => len + token.length); | |
| 265 } | |
| 266 | |
| 267 int getLengthToSpaceWithWeight(int weight) { | |
| 268 int length = 0; | |
| 269 for (LineToken token in tokens) { | |
| 270 if (token is SpaceToken && token.breakWeight == weight) { | |
| 271 break; | |
| 272 } | |
| 273 length += token.length; | |
| 274 } | |
| 275 return length; | |
| 276 } | |
| 277 | |
| 278 bool fits(LineToken a, LineToken b) { | |
| 279 return length + a.length + a.length <= maxLength; | |
| 280 } | |
| 281 | |
| 282 void add(LineToken token) { | |
| 283 tokens.add(token); | |
| 284 } | |
| 285 | |
| 286 bool hasInitializerSpace() { | |
| 287 return tokens.any((token) { | |
| 288 return token is SpaceToken && token.breakWeight == SINGLE_SPACE_WEIGHT; | |
| 289 }); | |
| 290 } | |
| 291 | |
| 292 bool hasAnySpace() { | |
| 293 return tokens.any((token) => token is SpaceToken); | |
| 294 } | |
| 295 | |
| 296 int findMinSpaceWeight() { | |
| 297 int minWeight = UNBREAKABLE_SPACE_WEIGHT; | |
| 298 for (var token in tokens) { | |
| 299 if (token is SpaceToken) { | |
| 300 minWeight = math.min(minWeight, token.breakWeight); | |
| 301 } | |
| 302 } | |
| 303 return minWeight; | |
| 304 } | |
| 305 | |
| 306 Chunk subChunk(int indentLevel, int start, [int end]) { | |
| 307 List<LineToken> subTokens = tokens.sublist(start, end); | |
| 308 return new Chunk(indentLevel, maxLength, subTokens); | |
| 309 } | |
| 310 | |
| 311 String toString() => tokens.join(); | |
| 312 } | |
| 313 | |
| 314 class LineToken implements LineText { | |
| 315 final String value; | |
| 316 | |
| 317 LineToken(this.value); | |
| 318 | |
| 319 String toString() => value; | |
| 320 | |
| 321 int get length => lengthLessNewlines(value); | |
| 322 | |
| 323 int lengthLessNewlines(String str) => | |
| 324 str.endsWith('\n') ? str.length - 1 : str.length; | |
| 325 } | |
| 326 | |
| 327 class SpaceToken extends LineToken { | |
| 328 final int breakWeight; | |
| 329 | |
| 330 SpaceToken(int n, {this.breakWeight: DEFAULT_SPACE_WEIGHT}) | |
| 331 : super(getSpaces(n)); | |
| 332 } | |
| 333 | |
| 334 class TabToken extends LineToken { | |
| 335 TabToken(int n) : super(getTabs(n)); | |
| 336 } | |
| 337 | |
| 338 class NewlineToken extends LineToken { | |
| 339 NewlineToken(String value) : super(value); | |
| 340 } | |
| 341 | |
| 342 class SourceWriter { | |
| 343 final StringBuffer buffer = new StringBuffer(); | |
| 344 Line currentLine; | |
| 345 | |
| 346 final String lineSeparator; | |
| 347 int indentCount = 0; | |
| 348 final int spacesPerIndent; | |
| 349 final bool useTabs; | |
| 350 | |
| 351 LinePrinter linePrinter; | |
| 352 LineToken _lastToken; | |
| 353 | |
| 354 SourceWriter({this.indentCount: 0, this.lineSeparator: NEW_LINE, | |
| 355 this.useTabs: false, this.spacesPerIndent: 2, int maxLineLength: 80}) { | |
| 356 if (maxLineLength > 0) { | |
| 357 linePrinter = new SimpleLineBreaker(maxLineLength, (n) => getIndentString( | |
| 358 n, useTabs: useTabs, spacesPerIndent: spacesPerIndent)); | |
| 359 } else { | |
| 360 linePrinter = new SimpleLinePrinter(); | |
| 361 } | |
| 362 currentLine = newLine(); | |
| 363 } | |
| 364 | |
| 365 LineToken get lastToken => _lastToken; | |
| 366 | |
| 367 _addToken(LineToken token) { | |
| 368 _lastToken = token; | |
| 369 currentLine.addToken(token); | |
| 370 } | |
| 371 | |
| 372 void indent() { | |
| 373 ++indentCount; | |
| 374 // Rather than fiddle with deletions/insertions just start fresh | |
| 375 if (currentLine.isWhitespace()) { | |
| 376 currentLine = newLine(); | |
| 377 } | |
| 378 } | |
| 379 | |
| 380 void newline() { | |
| 381 if (currentLine.isWhitespace()) { | |
| 382 currentLine.tokens.clear(); | |
| 383 } | |
| 384 _addToken(new NewlineToken(this.lineSeparator)); | |
| 385 buffer.write(currentLine.toString()); | |
| 386 currentLine = newLine(); | |
| 387 } | |
| 388 | |
| 389 void newlines(int num) { | |
| 390 while (num-- > 0) { | |
| 391 newline(); | |
| 392 } | |
| 393 } | |
| 394 | |
| 395 void write(String string) { | |
| 396 var lines = string.split(lineSeparator); | |
| 397 var length = lines.length; | |
| 398 for (int i = 0; i < length; i++) { | |
| 399 var line = lines[i]; | |
| 400 _addToken(new LineToken(line)); | |
| 401 if (i != length - 1) { | |
| 402 newline(); | |
| 403 // no indentation for multi-line strings | |
| 404 currentLine.clear(); | |
| 405 } | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 void writeln(String s) { | |
| 410 write(s); | |
| 411 newline(); | |
| 412 } | |
| 413 | |
| 414 void space() { | |
| 415 spaces(1); | |
| 416 } | |
| 417 | |
| 418 void spaces(n, {breakWeight: DEFAULT_SPACE_WEIGHT}) { | |
| 419 currentLine.addSpaces(n, breakWeight: breakWeight); | |
| 420 } | |
| 421 | |
| 422 void unindent() { | |
| 423 --indentCount; | |
| 424 // Rather than fiddle with deletions/insertions just start fresh | |
| 425 if (currentLine.isWhitespace()) { | |
| 426 currentLine = newLine(); | |
| 427 } | |
| 428 } | |
| 429 | |
| 430 Line newLine() => new Line( | |
| 431 indentLevel: indentCount, | |
| 432 useTabs: useTabs, | |
| 433 spacesPerIndent: spacesPerIndent, | |
| 434 printer: linePrinter); | |
| 435 | |
| 436 String toString() { | |
| 437 var source = new StringBuffer(buffer.toString()); | |
| 438 if (!currentLine.isWhitespace()) { | |
| 439 source.write(currentLine); | |
| 440 } | |
| 441 return source.toString(); | |
| 442 } | |
| 443 } | |
| 444 | |
| 445 const NEW_LINE = '\n'; | |
| 446 const SPACE = ' '; | |
| 447 const SPACES = const [ | |
| 448 '', | |
| 449 ' ', | |
| 450 ' ', | |
| 451 ' ', | |
| 452 ' ', | |
| 453 ' ', | |
| 454 ' ', | |
| 455 ' ', | |
| 456 ' ', | |
| 457 ' ', | |
| 458 ' ', | |
| 459 ' ', | |
| 460 ' ', | |
| 461 ' ', | |
| 462 ' ', | |
| 463 ' ', | |
| 464 ' ', | |
| 465 ]; | |
| 466 const TABS = const [ | |
| 467 '', | |
| 468 '\t', | |
| 469 '\t\t', | |
| 470 '\t\t\t', | |
| 471 '\t\t\t\t', | |
| 472 '\t\t\t\t\t', | |
| 473 '\t\t\t\t\t\t', | |
| 474 '\t\t\t\t\t\t\t', | |
| 475 '\t\t\t\t\t\t\t\t', | |
| 476 '\t\t\t\t\t\t\t\t\t', | |
| 477 '\t\t\t\t\t\t\t\t\t\t', | |
| 478 '\t\t\t\t\t\t\t\t\t\t\t', | |
| 479 '\t\t\t\t\t\t\t\t\t\t\t\t', | |
| 480 '\t\t\t\t\t\t\t\t\t\t\t\t\t', | |
| 481 '\t\t\t\t\t\t\t\t\t\t\t\t\t\t', | |
| 482 ]; | |
| 483 | |
| 484 String getIndentString(int indentWidth, | |
| 485 {bool useTabs: false, int spacesPerIndent: 2}) => | |
| 486 useTabs ? getTabs(indentWidth) : getSpaces(indentWidth * spacesPerIndent); | |
| 487 | |
| 488 String getSpaces(int n) => n < SPACES.length ? SPACES[n] : repeat(' ', n); | |
| 489 | |
| 490 String getTabs(int n) => n < TABS.length ? TABS[n] : repeat('\t', n); | |
| 491 | |
| 492 String repeat(String ch, int times) { | |
| 493 var sb = new StringBuffer(); | |
| 494 for (var i = 0; i < times; ++i) { | |
| 495 sb.write(ch); | |
| 496 } | |
| 497 return sb.toString(); | |
| 498 } | |
| OLD | NEW |