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