Add SourceSpan.highlight(). (#10)

This is useful for constructing a message with a non-standard
file/line/column display.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6da8487..afcc493 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 1.3.0
+
+* Add `SourceSpan.highlight()`, which returns just the highlighted text that
+  would be included in `SourceSpan.message()`.
+
 # 1.2.4
 
 * Fix a new strong mode error.
diff --git a/lib/src/span.dart b/lib/src/span.dart
index fe1ac39..599d668 100644
--- a/lib/src/span.dart
+++ b/lib/src/span.dart
@@ -53,6 +53,19 @@
   /// should be highlighted using the default color. If it's `false` or `null`,
   /// it indicates that the text shouldn't be highlighted.
   String message(String message, {color});
+
+  /// Prints the text associated with this span in a user-friendly way.
+  ///
+  /// This is identical to [message], except that it doesn't print the file
+  /// name, line number, column number, or message. If [length] is 0 and this
+  /// isn't a [SourceSpanWithContext], returns an empty string.
+  ///
+  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+  /// it indicates an ANSII terminal color escape that should be used to
+  /// highlight the span's text. If it's `true`, it indicates that the text
+  /// should be highlighted using the default color. If it's `false` or `null`,
+  /// it indicates that the text shouldn't be highlighted.
+  String highlight({color});
 }
 
 /// A base class for source spans with [start], [end], and [text] known at
diff --git a/lib/src/span_mixin.dart b/lib/src/span_mixin.dart
index a258cf5..8d84cea 100644
--- a/lib/src/span_mixin.dart
+++ b/lib/src/span_mixin.dart
@@ -46,20 +46,26 @@
   }
 
   String message(String message, {color}) {
-    if (color == true) color = colors.RED;
-    if (color == false) color = null;
-
-    var line = start.line;
-    var column = start.column;
-
     var buffer = new StringBuffer();
-    buffer.write('line ${line + 1}, column ${column + 1}');
+    buffer.write('line ${start.line + 1}, column ${start.column + 1}');
     if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
     buffer.write(': $message');
 
-    if (length == 0 && this is! SourceSpanWithContext) return buffer.toString();
-    buffer.write("\n");
+    var highlight = this.highlight(color: color);
+    if (!highlight.isEmpty) {
+      buffer.writeln();
+      buffer.write(highlight);
+    }
 
+    return buffer.toString();
+  }
+
+  String highlight({color}) {
+    if (color == true) color = colors.RED;
+    if (color == false) color = null;
+
+    var column = start.column;
+    var buffer = new StringBuffer();
     String textLine;
     if (this is SourceSpanWithContext) {
       var context = (this as SourceSpanWithContext).context;
@@ -71,6 +77,8 @@
       var endIndex = context.indexOf('\n');
       textLine = endIndex == -1 ? context : context.substring(0, endIndex + 1);
       column = math.min(column, textLine.length);
+    } else if (length == 0) {
+      return "";
     } else {
       textLine = text.split("\n").first;
       column = 0;
diff --git a/pubspec.yaml b/pubspec.yaml
index 9e41fdd..8fa8711 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_span
-version: 1.2.4
+version: 1.3.0
 author: Dart Team <misc@dartlang.org>
 description: A library for identifying source spans and locations.
 homepage: https://github.com/dart-lang/source_span
diff --git a/test/file_message_test.dart b/test/file_message_test.dart
deleted file mode 100644
index d78a651..0000000
--- a/test/file_message_test.dart
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright (c) 2014, 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.
-
-import 'package:test/test.dart';
-import 'package:source_span/source_span.dart';
-import 'package:source_span/src/colors.dart' as colors;
-
-main() {
-  var file;
-  setUp(() {
-    file = new SourceFile("""
-foo bar baz
-whiz bang boom
-zip zap zop
-""", url: "foo.dart");
-  });
-
-  test("points to the span in the source", () {
-    expect(file.span(4, 7).message("oh no"), equals("""
-line 1, column 5 of foo.dart: oh no
-foo bar baz
-    ^^^"""));
-  });
-
-  test("gracefully handles a missing source URL", () {
-    var span = new SourceFile("foo bar baz").span(4, 7);
-    expect(span.message("oh no"), equals("""
-line 1, column 5: oh no
-foo bar baz
-    ^^^"""));
-  });
-
-  test("highlights the first line of a multiline span", () {
-    expect(file.span(4, 20).message("oh no"), equals("""
-line 1, column 5 of foo.dart: oh no
-foo bar baz
-    ^^^^^^^^"""));
-  });
-
-  test("works for a point span", () {
-    expect(file.location(4).pointSpan().message("oh no"), equals("""
-line 1, column 5 of foo.dart: oh no
-foo bar baz
-    ^"""));
-  });
-
-  test("works for a point span at the end of a line", () {
-    expect(file.location(11).pointSpan().message("oh no"), equals("""
-line 1, column 12 of foo.dart: oh no
-foo bar baz
-           ^"""));
-  });
-
-  test("works for a point span at the end of the file", () {
-    expect(file.location(38).pointSpan().message("oh no"), equals("""
-line 3, column 12 of foo.dart: oh no
-zip zap zop
-           ^"""));
-  });
-
-  test("works for a point span at the end of the file with no trailing newline",
-      () {
-    file = new SourceFile("zip zap zop");
-    expect(file.location(11).pointSpan().message("oh no"), equals("""
-line 1, column 12: oh no
-zip zap zop
-           ^"""));
-  });
-
-  test("works for a point span in an empty file", () {
-    expect(new SourceFile("").location(0).pointSpan().message("oh no"),
-        equals("""
-line 1, column 1: oh no
-
-^"""));
-  });
-
-  test("works for a single-line file without a newline", () {
-    expect(new SourceFile("foo bar").span(0, 7).message("oh no"),
-        equals("""
-line 1, column 1: oh no
-foo bar
-^^^^^^^"""));
-  });
-
-  group("colors", () {
-    test("doesn't colorize if color is false", () {
-      expect(file.span(4, 7).message("oh no", color: false), equals("""
-line 1, column 5 of foo.dart: oh no
-foo bar baz
-    ^^^"""));
-    });
-
-    test("colorizes if color is true", () {
-      expect(file.span(4, 7).message("oh no", color: true), equals("""
-line 1, column 5 of foo.dart: oh no
-foo ${colors.RED}bar${colors.NONE} baz
-    ${colors.RED}^^^${colors.NONE}"""));
-    });
-
-    test("uses the given color if it's passed", () {
-      expect(file.span(4, 7).message("oh no", color: colors.YELLOW), equals("""
-line 1, column 5 of foo.dart: oh no
-foo ${colors.YELLOW}bar${colors.NONE} baz
-    ${colors.YELLOW}^^^${colors.NONE}"""));
-    });
-  });
-}
diff --git a/test/highlight_test.dart b/test/highlight_test.dart
new file mode 100644
index 0000000..32b09ff
--- /dev/null
+++ b/test/highlight_test.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2014, 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.
+
+import 'package:test/test.dart';
+import 'package:source_span/source_span.dart';
+import 'package:source_span/src/colors.dart' as colors;
+
+main() {
+  var file;
+  setUp(() {
+    file = new SourceFile("""
+foo bar baz
+whiz bang boom
+zip zap zop
+""");
+  });
+
+  test("points to the span in the source", () {
+    expect(file.span(4, 7).highlight(), equals("""
+foo bar baz
+    ^^^"""));
+  });
+
+  test("gracefully handles a missing source URL", () {
+    var span = new SourceFile("foo bar baz").span(4, 7);
+    expect(span.highlight(), equals("""
+foo bar baz
+    ^^^"""));
+  });
+
+  test("highlights the first line of a multiline span", () {
+    expect(file.span(4, 20).highlight(), equals("""
+foo bar baz
+    ^^^^^^^^"""));
+  });
+
+  test("works for a point span", () {
+    expect(file.location(4).pointSpan().highlight(), equals("""
+foo bar baz
+    ^"""));
+  });
+
+  test("works for a point span at the end of a line", () {
+    expect(file.location(11).pointSpan().highlight(), equals("""
+foo bar baz
+           ^"""));
+  });
+
+  test("works for a point span at the end of the file", () {
+    expect(file.location(38).pointSpan().highlight(), equals("""
+zip zap zop
+           ^"""));
+  });
+
+  test("works for a point span at the end of the file with no trailing newline",
+      () {
+    file = new SourceFile("zip zap zop");
+    expect(file.location(11).pointSpan().highlight(), equals("""
+zip zap zop
+           ^"""));
+  });
+
+  test("works for a point span in an empty file", () {
+    expect(new SourceFile("").location(0).pointSpan().highlight(),
+        equals("""
+
+^"""));
+  });
+
+  test("works for a single-line file without a newline", () {
+    expect(new SourceFile("foo bar").span(0, 7).highlight(),
+        equals("""
+foo bar
+^^^^^^^"""));
+  });
+
+  test("supports lines of preceding context", () {
+    var span = new SourceSpanWithContext(
+        new SourceLocation(5, line: 3, column: 5, sourceUrl: "foo.dart"),
+        new SourceLocation(12, line: 3, column: 12, sourceUrl: "foo.dart"),
+        "foo bar",
+        "previous\nlines\n-----foo bar-----\nfollowing line\n");
+
+    expect(span.highlight(color: colors.YELLOW), equals("""
+previous
+lines
+-----${colors.YELLOW}foo bar${colors.NONE}-----
+     ${colors.YELLOW}^^^^^^^${colors.NONE}"""));
+  });
+
+  group("colors", () {
+    test("doesn't colorize if color is false", () {
+      expect(file.span(4, 7).highlight(color: false), equals("""
+foo bar baz
+    ^^^"""));
+    });
+
+    test("colorizes if color is true", () {
+      expect(file.span(4, 7).highlight(color: true), equals("""
+foo ${colors.RED}bar${colors.NONE} baz
+    ${colors.RED}^^^${colors.NONE}"""));
+    });
+
+    test("uses the given color if it's passed", () {
+      expect(file.span(4, 7).highlight(color: colors.YELLOW), equals("""
+foo ${colors.YELLOW}bar${colors.NONE} baz
+    ${colors.YELLOW}^^^${colors.NONE}"""));
+    });
+  });
+}
diff --git a/test/span_test.dart b/test/span_test.dart
index 113848a..f980f30 100644
--- a/test/span_test.dart
+++ b/test/span_test.dart
@@ -234,51 +234,19 @@
 ${colors.YELLOW}foo bar${colors.NONE}
 ${colors.YELLOW}^^^^^^^${colors.NONE}"""));
     });
-  });
 
-  group("message() with context", () {
-    var spanWithContext;
-    setUp(() {
-      spanWithContext = new SourceSpanWithContext(
+    test("with context, underlines the right column", () {
+      var spanWithContext = new SourceSpanWithContext(
           new SourceLocation(5, sourceUrl: "foo.dart"),
           new SourceLocation(12, sourceUrl: "foo.dart"),
           "foo bar",
           "-----foo bar-----");
-    });
 
-    test("underlines under the right column", () {
       expect(spanWithContext.message("oh no", color: colors.YELLOW), equals("""
 line 1, column 6 of foo.dart: oh no
 -----${colors.YELLOW}foo bar${colors.NONE}-----
      ${colors.YELLOW}^^^^^^^${colors.NONE}"""));
     });
-
-    test("underlines correctly when text appears twice", () {
-      var span = new SourceSpanWithContext(
-          new SourceLocation(9, column: 9, sourceUrl: "foo.dart"),
-          new SourceLocation(12, column: 12, sourceUrl: "foo.dart"),
-          "foo",
-          "-----foo foo-----");
-      expect(span.message("oh no", color: colors.YELLOW), equals("""
-line 1, column 10 of foo.dart: oh no
------foo ${colors.YELLOW}foo${colors.NONE}-----
-         ${colors.YELLOW}^^^${colors.NONE}"""));
-  });
-
-    test("supports lines of preceeding context", () {
-      var span = new SourceSpanWithContext(
-          new SourceLocation(5, line: 3, column: 5, sourceUrl: "foo.dart"),
-          new SourceLocation(12, line: 3, column: 12, sourceUrl: "foo.dart"),
-          "foo bar",
-          "previous\nlines\n-----foo bar-----\nfollowing line\n");
-
-      expect(span.message("oh no", color: colors.YELLOW), equals("""
-line 4, column 6 of foo.dart: oh no
-previous
-lines
------${colors.YELLOW}foo bar${colors.NONE}-----
-     ${colors.YELLOW}^^^^^^^${colors.NONE}"""));
-    });
   });
 
   group("compareTo()", () {