blob: 4bf182e43cb5daaeaa3e59ec756c8123b4d56f5d [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library source_writer;
import 'dart:math' as math;
class Line {
final List<LineToken> tokens = <LineToken>[];
final bool useTabs;
final int spacesPerIndent;
final int indentLevel;
final LinePrinter printer;
Line({this.indentLevel: 0, this.useTabs: false, this.spacesPerIndent: 2,
this.printer: const SimpleLinePrinter()}) {
if (indentLevel > 0) {
indent(indentLevel);
}
}
void addSpace() {
addSpaces(1);
}
void addSpaces(int n, {breakWeight: DEFAULT_SPACE_WEIGHT}) {
tokens.add(new SpaceToken(n, breakWeight: breakWeight));
}
void addToken(LineToken token) {
tokens.add(token);
}
void clear() {
tokens.clear();
}
bool isEmpty() => tokens.isEmpty;
bool isWhitespace() =>
tokens.every((tok) => tok is SpaceToken || tok is TabToken);
void indent(int n) {
tokens.insert(
0, useTabs ? new TabToken(n) : new SpaceToken(n * spacesPerIndent));
}
String toString() => printer.printLine(this);
}
/// Base class for line printers
abstract class LinePrinter {
const LinePrinter();
/// Convert this [line] to a [String] representation.
String printLine(Line line);
}
typedef String Indenter(int n);
/// A simple line breaking [LinePrinter]
class SimpleLineBreaker extends LinePrinter {
static final NO_OP_INDENTER = (n) => '';
final chunks = <Chunk>[];
final int maxLength;
Indenter indenter;
SimpleLineBreaker(this.maxLength, [this.indenter]) {
if (indenter == null) {
indenter = NO_OP_INDENTER;
}
}
String printLine(Line line) {
var buf = new StringBuffer();
var chunks = breakLine(line);
for (var i = 0; i < chunks.length; ++i) {
var chunk = chunks[i];
if (i > 0) {
buf.write(indent(chunk, chunk.indent));
} else {
buf.write(chunk);
}
}
return buf.toString();
}
String indent(Chunk chunk, int level) {
return '\n' + indenter(level) + chunk.toString();
}
List<Chunk> breakLine(Line line) {
List<LineToken> tokens = preprocess(line.tokens);
List<Chunk> chunks = <Chunk>[
new Chunk(line.indentLevel, maxLength, tokens)
];
// try SINGLE_SPACE_WEIGHT
{
Chunk chunk = chunks[0];
if (chunk.length > maxLength) {
for (int i = 0; i < tokens.length; i++) {
LineToken token = tokens[i];
if (token is SpaceToken && token.breakWeight == SINGLE_SPACE_WEIGHT) {
var beforeChunk = chunk.subChunk(chunk.indent, 0, i);
var restChunk = chunk.subChunk(chunk.indent + 2, i + 1);
// check if 'init' in 'var v = init;' fits a line
if (restChunk.length < maxLength) {
return [beforeChunk, restChunk];
}
// check if 'var v = method(' in 'var v = method(args)' does not fit
int weight = chunk.findMinSpaceWeight();
if (chunk.getLengthToSpaceWithWeight(weight) > maxLength) {
chunks = [beforeChunk, restChunk];
}
// done anyway
break;
}
}
}
}
// other spaces
while (true) {
List<Chunk> newChunks = <Chunk>[];
bool hasChanges = false;
for (Chunk chunk in chunks) {
tokens = chunk.tokens;
if (chunk.length > maxLength) {
if (chunk.hasAnySpace()) {
int weight = chunk.findMinSpaceWeight();
int newIndent = chunk.indent;
if (weight == DEFAULT_SPACE_WEIGHT) {
int start = 0;
int length = 0;
for (int i = 0; i < tokens.length; i++) {
LineToken token = tokens[i];
if (token is SpaceToken &&
token.breakWeight == weight &&
i < tokens.length - 1) {
LineToken nextToken = tokens[i + 1];
if (length + token.length + nextToken.length > maxLength) {
newChunks.add(chunk.subChunk(newIndent, start, i));
newIndent = chunk.indent + 2;
start = i + 1;
length = 0;
continue;
}
}
length += token.length;
}
if (start < tokens.length) {
newChunks.add(chunk.subChunk(newIndent, start));
}
} else {
int start = 0;
for (int i = 0; i < tokens.length; i++) {
LineToken token = tokens[i];
if (token is SpaceToken && token.breakWeight == weight) {
newChunks.add(chunk.subChunk(newIndent, start, i));
newIndent = chunk.indent + 2;
start = i + 1;
}
}
if (start < tokens.length) {
newChunks.add(chunk.subChunk(newIndent, start));
}
}
} else {
newChunks.add(chunk);
}
} else {
newChunks.add(chunk);
}
if (newChunks.length > chunks.length) {
hasChanges = true;
}
}
if (!hasChanges) {
break;
}
chunks = newChunks;
}
return chunks;
}
static List<LineToken> preprocess(List<LineToken> tok) {
var tokens = <LineToken>[];
var curr;
tok.forEach((token) {
if (token is! SpaceToken) {
if (curr == null) {
curr = token;
} else {
curr = merge(curr, token);
}
} else {
if (isNonbreaking(token)) {
curr = merge(curr, token);
} else {
if (curr != null) {
tokens.add(curr);
curr = null;
}
tokens.add(token);
}
}
});
if (curr != null) {
tokens.add(curr);
}
return tokens;
}
static bool isNonbreaking(SpaceToken token) =>
token.breakWeight == UNBREAKABLE_SPACE_WEIGHT;
static LineToken merge(LineToken first, LineToken second) =>
new LineToken(first.value + second.value);
}
/// Test if this [string] contains only whitespace characters
bool isWhitespace(String string) => string.codeUnits
.every((c) => c == 0x09 || c == 0x20 || c == 0x0A || c == 0x0D);
/// Special token indicating a line start
final LINE_START = new SpaceToken(0);
const DEFAULT_SPACE_WEIGHT = UNBREAKABLE_SPACE_WEIGHT - 1;
/// The weight of a space after '=' in variable declaration or assignment
const SINGLE_SPACE_WEIGHT = UNBREAKABLE_SPACE_WEIGHT - 2;
const UNBREAKABLE_SPACE_WEIGHT = 100000000;
/// Simple non-breaking printer
class SimpleLinePrinter extends LinePrinter {
const SimpleLinePrinter();
String printLine(Line line) {
var buffer = new StringBuffer();
line.tokens.forEach((tok) => buffer.write(tok.toString()));
return buffer.toString();
}
}
/// Describes a piece of text in a [Line].
abstract class LineText {
int get length;
}
/// A working piece of text used in calculating line breaks
class Chunk {
final int indent;
final int maxLength;
final List<LineToken> tokens = <LineToken>[];
Chunk(this.indent, this.maxLength, [List<LineToken> tokens]) {
this.tokens.addAll(tokens);
}
int get length {
return tokens.fold(0, (len, token) => len + token.length);
}
int getLengthToSpaceWithWeight(int weight) {
int length = 0;
for (LineToken token in tokens) {
if (token is SpaceToken && token.breakWeight == weight) {
break;
}
length += token.length;
}
return length;
}
bool fits(LineToken a, LineToken b) {
return length + a.length + a.length <= maxLength;
}
void add(LineToken token) {
tokens.add(token);
}
bool hasInitializerSpace() {
return tokens.any((token) {
return token is SpaceToken && token.breakWeight == SINGLE_SPACE_WEIGHT;
});
}
bool hasAnySpace() {
return tokens.any((token) => token is SpaceToken);
}
int findMinSpaceWeight() {
int minWeight = UNBREAKABLE_SPACE_WEIGHT;
for (var token in tokens) {
if (token is SpaceToken) {
minWeight = math.min(minWeight, token.breakWeight);
}
}
return minWeight;
}
Chunk subChunk(int indentLevel, int start, [int end]) {
List<LineToken> subTokens = tokens.sublist(start, end);
return new Chunk(indentLevel, maxLength, subTokens);
}
String toString() => tokens.join();
}
class LineToken implements LineText {
final String value;
LineToken(this.value);
String toString() => value;
int get length => lengthLessNewlines(value);
int lengthLessNewlines(String str) =>
str.endsWith('\n') ? str.length - 1 : str.length;
}
class SpaceToken extends LineToken {
final int breakWeight;
SpaceToken(int n, {this.breakWeight: DEFAULT_SPACE_WEIGHT})
: super(getSpaces(n));
}
class TabToken extends LineToken {
TabToken(int n) : super(getTabs(n));
}
class NewlineToken extends LineToken {
NewlineToken(String value) : super(value);
}
class SourceWriter {
final StringBuffer buffer = new StringBuffer();
Line currentLine;
final String lineSeparator;
int indentCount = 0;
final int spacesPerIndent;
final bool useTabs;
LinePrinter linePrinter;
LineToken _lastToken;
SourceWriter({this.indentCount: 0, this.lineSeparator: NEW_LINE,
this.useTabs: false, this.spacesPerIndent: 2, int maxLineLength: 80}) {
if (maxLineLength > 0) {
linePrinter = new SimpleLineBreaker(maxLineLength, (n) => getIndentString(
n, useTabs: useTabs, spacesPerIndent: spacesPerIndent));
} else {
linePrinter = new SimpleLinePrinter();
}
currentLine = newLine();
}
LineToken get lastToken => _lastToken;
_addToken(LineToken token) {
_lastToken = token;
currentLine.addToken(token);
}
void indent() {
++indentCount;
// Rather than fiddle with deletions/insertions just start fresh
if (currentLine.isWhitespace()) {
currentLine = newLine();
}
}
void newline() {
if (currentLine.isWhitespace()) {
currentLine.tokens.clear();
}
_addToken(new NewlineToken(this.lineSeparator));
buffer.write(currentLine.toString());
currentLine = newLine();
}
void newlines(int num) {
while (num-- > 0) {
newline();
}
}
void write(String string) {
var lines = string.split(lineSeparator);
var length = lines.length;
for (int i = 0; i < length; i++) {
var line = lines[i];
_addToken(new LineToken(line));
if (i != length - 1) {
newline();
// no indentation for multi-line strings
currentLine.clear();
}
}
}
void writeln(String s) {
write(s);
newline();
}
void space() {
spaces(1);
}
void spaces(n, {breakWeight: DEFAULT_SPACE_WEIGHT}) {
currentLine.addSpaces(n, breakWeight: breakWeight);
}
void unindent() {
--indentCount;
// Rather than fiddle with deletions/insertions just start fresh
if (currentLine.isWhitespace()) {
currentLine = newLine();
}
}
Line newLine() => new Line(
indentLevel: indentCount,
useTabs: useTabs,
spacesPerIndent: spacesPerIndent,
printer: linePrinter);
String toString() {
var source = new StringBuffer(buffer.toString());
if (!currentLine.isWhitespace()) {
source.write(currentLine);
}
return source.toString();
}
}
const NEW_LINE = '\n';
const SPACE = ' ';
const SPACES = const [
'',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
];
const TABS = const [
'',
'\t',
'\t\t',
'\t\t\t',
'\t\t\t\t',
'\t\t\t\t\t',
'\t\t\t\t\t\t',
'\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t\t\t\t\t\t',
'\t\t\t\t\t\t\t\t\t\t\t\t\t\t',
];
String getIndentString(int indentWidth,
{bool useTabs: false, int spacesPerIndent: 2}) =>
useTabs ? getTabs(indentWidth) : getSpaces(indentWidth * spacesPerIndent);
String getSpaces(int n) => n < SPACES.length ? SPACES[n] : repeat(' ', n);
String getTabs(int n) => n < TABS.length ? TABS[n] : repeat('\t', n);
String repeat(String ch, int times) {
var sb = new StringBuffer();
for (var i = 0; i < times; ++i) {
sb.write(ch);
}
return sb.toString();
}