Repository: jneen/parsimmon
Pull request #240 (2018-04-06 17:13:49)

Error formatting
This PR addresses #239 

I'm not using any external dependencies to add colors, because it all boils down to having [supports-color](https://www.npmjs.com/package/supports-color) included into the project and I don't feel like forcing this in any way.

I'm willinbg to work more on this to get it to the point where the users can feel a positive change in ttheir experience with the error repoting.

Please leave your feedback! 👐 

Here's an example of how the error messages might look with this PR incliuded:

#### Node

<img width="547" alt="screen shot 2018-04-06 at 11 06 33" src="https://user-images.githubusercontent.com/3983879/38434265-0d4b903e-39ce-11e8-8474-b0bd241a799b.png">

#### Browser(Chrome)

<img width="499" alt="screen shot 2018-04-06 at 11 06 39" src="https://user-images.githubusercontent.com/3983879/38434267-0fbc6e60-39ce-11e8-9bfd-715b4df6a2d0.png">

- [x]  Pad the line numbers with spaces as necessary (and add a test for this)
- [x]   Implement binary formatError as described above

### Buffer error formatting
- [x]  "line numbers" should be in hexadecimal
- [x]  each "line" should be eight bytes
- [x]  each group of four bytes should have two spaces after it so it's easier to
read, making it sort of feel like paragraphs

theqabalist: I'm not entirely sure the best way to format buffers, because they are not line oriented like a lot of practical string parsing is.  So showing the whole buffer is probably really noisy.  Maybe something like byte offsets plus bytes in a similar manner to what is being proposed from surrounding context like:

```
     | 1050 1051 1052 1053 1054 1055
0xFF | 0xFF    X 0xFF 0xFF 0xFF 0xFF
               ^
Expected byte 0x00.
```
wavebeem: are the 1050, 1051, etc the byte offsets? what is the 0xFF on the left in the gutter area?
theqabalist: Yeah, those are byte offsets.  I may have misinterpreted the gutter in the original formatting thing, but I assumed it was the actual value from the position, since the position had an x in it.
theqabalist: I like that too, although I worry about length a bit.  Like some of the messages I have parsed are like 1300 bytes, and displaying the entirety of it seems overwhelming.
wavebeem: Thanks! I see two things left:

- [ ] Pad the line numbers with spaces as necessary (and add a test for this)
- [ ] Implement binary formatError as described above

*padding example*

```
-- Parsing failed ------------------------

   8 | }
   9 | const n = Math.floor(10 * Math.random());
> 10 | for (var x = 0; x < n; x++ {
     |                            ^

Expected one of the following:

foo, bar, baz, quux
```
halfzebra: Thanks for the feedback, I'll look into it asap. :+1: 
halfzebra: Just an update, I'm still working on this 🙂 
wavebeem: @halfzebra are you still working on this? no problem if you still don't have time; i might start looking into it too.
wavebeem: Coming along nicely 😄 
wavebeem: what are the odds i'm going through github and see you just pushed 33 seconds ago?! i hadn't even gotten the email yet 😄 
halfzebra: Finally made the first draft of the buffer parsing error formatter, please let me know if there's anything wrong with the implementation.

I will put some more time into this and hopefully finish it soon 🙂 
wavebeem: I've been looking at how some hex editors work for displaying information in a
more human friendly way and there's a couple things I'd like to see still:

- "line numbers" should be in hexadecimal
- each "line" should be eight bytes
- each group of four bytes should have _two_ spaces after it so it's easier to
  read, making it sort of feel like paragraphs

## before

```
   20 | 00 00 00 00 00 00 00 00 00 00
   30 | 00 00 00 00 00 00 00 00 00 00
   40 | 00 00 00 00 00 00 00 00 00 00
   50 | 00 00 00 00 00 00 00 00 00 00
   60 | 00 00 00 00 00 00 00 00 00 00
>  70 | 00 00 ff 00 00 00 00 00 00 00
      |       ^^
   80 | 00 00 00 00 00 00 00 00 00 00
   90 | 00 00 00 00 00 00 00 00 00 00
  100 | 00 00 00 00 00 00 00 00 00 00
  110 | 00 00 00 00 00 00 00 00 00 00
```

## after

```
   20 | 00 00 00 00  00 00 00 00
   30 | 00 00 00 00  00 00 00 00
   40 | 00 00 00 00  00 00 00 00
   50 | 00 00 00 00  00 00 00 00
   60 | 00 00 00 00  00 00 00 00
>  70 | 00 00 ff 00  00 00 00 00
      |       ^^
   80 | 00 00 00 00  00 00 00 00
   90 | 00 00 00 00  00 00 00 00
  100 | 00 00 00 00  00 00 00 00
  110 | 00 00 00 00  00 00 00 00
```

This way it's easy to visually jump to the specific bytes without having to
count, and also 4-byte groupings are super common anyway (32-bit integers). And
then everything is hex-based and consistent, like in the hex editors I'm looking
at.

Also if you get tired of this I'm always happy to finish it up too, I know I can
be a little picky when it comes to new features.
anko: My favourite hobby is ruining parties by bringing up the full extent and horror of Unicode, so here goes! :raised_hands:

Any column-sensitive formatting could be thrown off by characters that are specified as *rendered at a different width*, since this applies even in monospace:

 - the [zero-width space](https://en.wikipedia.org/wiki/Zero-width_space), and
 - [fullwidth forms in Asian scripts](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms).

I'm unsure whether this list is exhaustive, and I don't know how to find out.

To make matters worse, some of these are rendered differently in different terminals. For example, `'a​b'` (with a zero-width space character between the *a* and *b*) is rendered like this in Firefox's console—

![FF console representation](https://user-images.githubusercontent.com/5231746/41622829-20a58ed2-7411-11e8-8b84-90cdfbaea7d9.png)

—but like this in alacritty (a terminal)—

![alacritty representation](https://user-images.githubusercontent.com/5231746/41622809-0e3f4ce2-7411-11e8-9f3a-f1d9e09613d3.png)

I've encountered terminals that rendered fullwidth characters as single-width too. If some terminal uses a web browser as a renderer, who knows what it'll do. :woman_shrugging: :man_shrugging:
 - - -

So if we want the `^` on the next line to point at the right thing in the output, and we can't rely on the characters being printed with any particular width, the only option I can see is to replace any non-unit-width characters in the column-sensitive part of the output with a ['�' (U+FFFD; the Unicode replacement character)](https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character), and add a warning to the end of the error text that explains this, and at least provides a mapping for the poor user in charge of figuring it out.  Something like this:

    Some Unicode characters were omitted due to display limitations, and appear as '�'.
    These are, in order of appearance:
      - U+200B 'ZERO WIDTH SPACE': ​
      - U+FF21 'FULLWIDTH LATIN CAPITAL LETTER A': Ａ
halfzebra: @wavebeem thanks for the feedback, the byte buffer parsing error requirements are quite straightforward. I think I can get this done over the next weekend. I've never done any work with byte buffers, so it's a trial and error. I'll let you know!

@anko I'm afraid you haven't ruined the party 🙂It's surely an interesting case, but I feel like in this PR I have to focus on fixing the formatting first.

Patch:
diff --git a/src/parsimmon.js b/src/parsimmon.js
index 35bc2f8..df73686 100644
--- a/src/parsimmon.js
+++ b/src/parsimmon.js
@@ -364,38 +364,212 @@ function assertString(x) {
   }
 }
 
+// -*- Error Formatting -*-
+
+var linesBeforeStringError = 2;
+var linesAfterStringError = 3;
+var bytesPerLine = 8;
+var bytesBefore = bytesPerLine * 5;
+var bytesAfter = bytesPerLine * 4;
+var radix = 16;
+var byteLength = 2;
+var defaultLinePrefix = "  ";
+
+function repeat(string, amount) {
+  return new Array(amount + 1).join(string);
+}
+
 function formatExpected(expected) {
   if (expected.length === 1) {
-    return expected[0];
+    return "Expected:\n\n" + expected[0];
   }
-  return "one of " + expected.join(", ");
+  return "Expected one of the following: \n\n" + expected.join(", ");
+}
+
+function leftPad(str, pad, char) {
+  var add = pad - str.length;
+  if (add <= 0) {
+    return str;
+  }
+  return repeat(char, add) + str;
+}
+
+function toChunks(arr, chunkSize) {
+  var length = arr.length;
+  var chunks = [];
+  var chunkIndex = 0;
+
+  if (length <= chunkSize) {
+    return [arr.slice()];
+  }
+
+  for (var i = 0; i < length; i++) {
+    if (!chunks[chunkIndex]) {
+      chunks.push([]);
+    }
+
+    chunks[chunkIndex].push(arr[i]);
+
+    if ((i + 1) % chunkSize === 0) {
+      chunkIndex++;
+    }
+  }
+
+  return chunks;
+}
+
+// Get a range of indexes including `i`-th element and `before` and `after` amount of elements from `arr`.
+function rangeFromIndexAndOffsets(i, before, after, length) {
+  return {
+    // Guard against the negative upper bound for lines included in the output.
+    from: i - before > 0 ? i - before : 0,
+    to: i + after > length ? length : i + after
+  };
+}
+
+function byteRangeToRange(byteRange) {
+  // Exception for inputs smaller than `bytesPerLine`
+  if (byteRange.from === 0 && byteRange.to === 1) {
+    return {
+      from: byteRange.from,
+      to: byteRange.to
+    };
+  }
+
+  return {
+    from: byteRange.from / bytesPerLine,
+    // Round `to`, so we don't get float if the amount of bytes is not divisible by `bytesPerLine`
+    to: Math.floor(byteRange.to / bytesPerLine)
+  };
 }
 
 function formatGot(input, error) {
   var index = error.index;
   var i = index.offset;
+
+  var verticalMarkerLength = 1;
+  var column;
+  var lineWithErrorIndex;
+  var lines;
+  var lineRange;
+
   if (i === input.length) {
-    return ", got the end of the input";
+    return "Got the end of the input";
+  }
+
+  if (isBuffer(input)) {
+    var byteLineWithErrorIndex = i - (i % bytesPerLine);
+    var columnByteIndex = i - byteLineWithErrorIndex;
+    var byteRange = rangeFromIndexAndOffsets(
+      byteLineWithErrorIndex,
+      bytesBefore,
+      bytesAfter + bytesPerLine,
+      input.length
+    );
+    var bytes = input.slice(byteRange.from, byteRange.to);
+    var bytesInChunks = toChunks(bytes.toJSON().data, bytesPerLine);
+
+    var byteLines = map(function(byteRow) {
+      return map(function(byteValue) {
+        // Prefix byte values with a `0`
+        return leftPad(byteValue.toString(radix), byteLength, "0");
+      }, byteRow);
+    }, bytesInChunks);
+
+    lineRange = byteRangeToRange(byteRange);
+    lineWithErrorIndex = byteLineWithErrorIndex / bytesPerLine;
+    column = columnByteIndex * 3;
+
+    // Account for an extra space.
+    if (columnByteIndex > 4) {
+      column += 1;
+    }
+    verticalMarkerLength = byteLength;
+    lines = map(function(byteLine) {
+      return byteLine.length <= 4
+        ? byteLine.join(" ")
+        : byteLine.slice(0, 4).join(" ") + "  " + byteLine.slice(4).join(" ");
+    }, byteLines);
+  } else {
+    var inputLines = input.split(/\r\n|[\n\r\u2028\u2029]/);
+    column = index.column - 1;
+    lineWithErrorIndex = index.line - 1;
+    lineRange = rangeFromIndexAndOffsets(
+      lineWithErrorIndex,
+      linesBeforeStringError,
+      linesAfterStringError,
+      inputLines.length
+    );
+
+    lines = inputLines.slice(lineRange.from, lineRange.to);
   }
+
+  var lineWithErrorCurrentIndex = lineWithErrorIndex - lineRange.from;
+  var lastLineNumberLabelLength = lineRange.to.toString().length;
+
   if (isBuffer(input)) {
-    return " at byte " + index.offset;
+    lastLineNumberLabelLength = (
+      (lineRange.to > 0 ? lineRange.to - 1 : lineRange.to) * 8
+    ).toString(16).length;
+
+    if (lastLineNumberLabelLength < 2) {
+      lastLineNumberLabelLength = 2;
+    }
   }
-  var prefix = i > 0 ? "'..." : "'";
-  var suffix = input.length - i > 12 ? "...'" : "'";
-  return (
-    " at line " +
-    index.line +
-    " column " +
-    index.column +
-    ", got " +
-    prefix +
-    input.slice(i, i + 12) +
-    suffix
+
+  var linesWithLineNumbers = reduce(
+    function(acc, lineSource, index) {
+      var isLineWithError = index === lineWithErrorCurrentIndex;
+      var prefix = isLineWithError ? "> " : defaultLinePrefix;
+      var lineNumber = isBuffer(input)
+        ? ((lineRange.from + index) * 8).toString(16)
+        : (lineRange.from + index + 1).toString();
+      if (isBuffer(input)) {
+        if (lineNumber.length < 2) {
+          lineNumber = leftPad(lineNumber, 2, "0");
+        }
+      }
+      var lineNumberLabel =
+        lineNumber.length < lastLineNumberLabelLength
+          ? leftPad(
+              lineNumber,
+              lastLineNumberLabelLength,
+              isBuffer(input) ? "0" : " "
+            )
+          : lineNumber;
+
+      return [].concat(
+        acc,
+        [prefix + lineNumberLabel + " | " + lineSource],
+        isLineWithError
+          ? [
+              // (isBuffer(input) ? " " : "") +
+              defaultLinePrefix +
+                repeat(" ", lastLineNumberLabelLength) +
+                " | " +
+                leftPad("", column, " ") +
+                repeat("^", verticalMarkerLength)
+            ]
+          : []
+      );
+    },
+    [],
+    lines
   );
+
+  return linesWithLineNumbers.join("\n");
 }
 
 function formatError(input, error) {
-  return "expected " + formatExpected(error.expected) + formatGot(input, error);
+  return [
+    "\n",
+    "-- PARSING FAILED " + repeat("-", 50),
+    "\n\n",
+    formatGot(input, error),
+    "\n\n",
+    formatExpected(error.expected),
+    "\n"
+  ].join("");
 }
 
 function flags(re) {


Test patch:
diff --git a/test/core/formatError.test.js b/test/core/formatError.test.js
index 7288c62..9e43cad 100644
--- a/test/core/formatError.test.js
+++ b/test/core/formatError.test.js
@@ -1,5 +1,13 @@
 "use strict";
 
+function fill(length, filler) {
+  var res = [];
+  for (var i = 0; i < length; i++) {
+    res.push(filler);
+  }
+  return res;
+}
+
 suite("formatError", function() {
   test("end of input", function() {
     var parser = Parsimmon.alt(
@@ -7,7 +15,15 @@ suite("formatError", function() {
       Parsimmon.fail("b"),
       Parsimmon.fail("c")
     );
-    var expectation = "expected one of a, b, c, got the end of the input";
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "Got the end of the input\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "a, b, c\n";
     var input = "";
     var answer = Parsimmon.formatError(input, parser.parse(input));
     assert.deepEqual(answer, expectation);
@@ -23,9 +39,334 @@ suite("formatError", function() {
       )
     );
     var expectation =
-      "expected one of a, b, c at line 1 column 2, got '...x11111111111...'";
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "> 1 | 1x1111111111111111111111111111\n" +
+      "    |  ^\n\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "a, b, c\n";
     var input = "1x1111111111111111111111111111";
     var answer = Parsimmon.formatError(input, parser.parse(input));
     assert.deepEqual(answer, expectation);
   });
+
+  test("milti-line input", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.string("\n")
+        .many()
+        .then(Parsimmon.string("a"))
+    );
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "  1 | \n" +
+      "  2 | \n" +
+      "> 3 | b\n" +
+      "    | ^\n" +
+      "  4 | \n" +
+      "  5 | \n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "'\n', 'a'\n";
+    var input = "\n\nb\n\n\n";
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("multi-line line-number padding", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.string("\n")
+        .many()
+        .then(Parsimmon.string("a"))
+    );
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "   8 | \n" +
+      "   9 | \n" +
+      "> 10 | b\n" +
+      "     | ^\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "'\n', 'a'\n";
+
+    var input = new Array(9).join("\n") + "\nb";
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("multi-line line-number with 3-digits", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.string("\n")
+        .many()
+        .then(Parsimmon.string("b"))
+    );
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n\n" +
+      "  116 | \n" +
+      "  117 | \n" +
+      "> 118 | c\n" +
+      "      | ^\n" +
+      "  119 | \n" +
+      "  120 | \n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "'\n', 'b'\n";
+
+    var input = new Array(117).join("\n") + "\nc" + new Array(10).join("\n");
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+    // console.log(answer);
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("multi-line line-number with 3-digit to 4-digit line numbers", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.string("\n")
+        .many()
+        .then(Parsimmon.string("c"))
+    );
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n\n" +
+      "   998 | \n" +
+      "   999 | \n" +
+      "> 1000 | d\n" +
+      "       | ^\n" +
+      "  1001 | \n" +
+      "  1002 | \n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "'\n', 'c'\n";
+
+    var input = new Array(999).join("\n") + "\nd" + new Array(10).join("\n");
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("byte buffer error at the end of the short input", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.Binary.byte(0x00).many(),
+      Parsimmon.Binary.byte(0x01)
+    );
+
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "> 00 | 00 02\n" +
+      "     |    ^^\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "0x00, 0x01" +
+      "\n";
+
+    var input = Buffer.from([0x0, 0x02]);
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("byte buffer error with a value one character long", function() {
+    var parser = Parsimmon.seq(Parsimmon.Binary.byte(0x1));
+
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "> 00 | 02\n" +
+      "     | ^^\n" +
+      "\n" +
+      "Expected:\n" +
+      "\n" +
+      "0x01" +
+      "\n";
+
+    var input = Buffer.from([0x2]);
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("byte buffer error with multi-line input", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.Binary.byte(0x00).many(),
+      Parsimmon.Binary.byte(0x01)
+    );
+
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "  10 | 00 00 00 00  00 00 00 00\n" +
+      "  18 | 00 00 00 00  00 00 00 00\n" +
+      "  20 | 00 00 00 00  00 00 00 00\n" +
+      "  28 | 00 00 00 00  00 00 00 00\n" +
+      "  30 | 00 00 00 00  00 00 00 00\n" +
+      "> 38 | 00 00 ff 00  00 00 00 00\n" +
+      "     |       ^^\n" +
+      "  40 | 00 00 00 00  00 00 00 00\n" +
+      "  48 | 00 00 00 00  00 00 00 00\n" +
+      "  50 | 00 00 00 00  00 00 00 00\n" +
+      "  58 | 00 00 00 00  00 00 00 00\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "0x00, 0x01" +
+      "\n";
+
+    var input = Buffer.from(
+      [].concat(
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0], // <- Error
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0],
+        [0x0, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0]
+      )
+    );
+
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("byte buffer error at the first line of the multi-line input", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.Binary.byte(0x00).many(),
+      Parsimmon.Binary.byte(0x01)
+    );
+
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "> 00 | 00 00 ff 00  00 00 00 00\n" +
+      "     |       ^^\n" +
+      "  08 | 00 00 00 00\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "0x00, 0x01" +
+      "\n";
+
+    var input = Buffer.from(
+      [].concat([0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0], [0x0, 0x0, 0x0, 0x0])
+    );
+
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("byte buffer error at the end of the multi-line input", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.Binary.byte(0x00).many(),
+      Parsimmon.Binary.byte(0x01)
+    );
+
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "  20 | 00 00 00 00  00 00 00 00\n" +
+      "  28 | 00 00 00 00  00 00 00 00\n" +
+      "  30 | 00 00 00 00  00 00 00 00\n" +
+      "  38 | 00 00 00 00  00 00 00 00\n" +
+      "  40 | 00 00 00 00  00 00 00 00\n" +
+      "> 48 | 00 00 00 00  00 00 00 ff\n" +
+      "     |                       ^^\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "0x00, 0x01" +
+      "\n";
+
+    // 90 bytes
+    var input = Buffer.from(
+      [].concat(
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff] // <- Error
+      )
+    );
+
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
+
+  test("parsing error in a large byte buffer", function() {
+    var parser = Parsimmon.seq(
+      Parsimmon.Binary.byte(0x00).many(),
+      Parsimmon.Binary.byte(0x01)
+    );
+
+    var expectation =
+      "\n" +
+      "-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "  0fd0 | 00 00 00 00  00 00 00 00\n" +
+      "  0fd8 | 00 00 00 00  00 00 00 00\n" +
+      "  0fe0 | 00 00 00 00  00 00 00 00\n" +
+      "  0fe8 | 00 00 00 00  00 00 00 00\n" +
+      "  0ff0 | 00 00 00 00  00 00 00 00\n" +
+      "> 0ff8 | 00 00 00 00  00 00 00 ff\n" +
+      "       |                       ^^\n" +
+      "  1000 | 00 00 00 00  00 00 00 00\n" +
+      "  1008 | 00 00 00 00  00 00 00 00\n" +
+      "  1010 | 00 00 00 00  00 00 00 00\n" +
+      "  1018 | 00 00 00 00  00 00 00 00\n" +
+      "\n" +
+      "Expected one of the following: \n" +
+      "\n" +
+      "0x00, 0x01" +
+      "\n";
+
+    var list = fill(8 * 511, 0x0);
+
+    var input = Buffer.from(
+      [].concat(
+        list,
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff], // <- Error
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00],
+        [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00]
+      )
+    );
+
+    var answer = Parsimmon.formatError(input, parser.parse(input));
+
+    assert.deepEqual(answer, expectation);
+  });
 });
diff --git a/test/core/parse.test.js b/test/core/parse.test.js
index 2d95cac..0132d66 100644
--- a/test/core/parse.test.js
+++ b/test/core/parse.test.js
@@ -29,7 +29,7 @@ suite(".parse", function() {
       var parser = Parsimmon.Binary.byte(0);
       assert.throws(function() {
         parser.tryParse(Buffer.from([0xf]));
-      }, /at byte/);
+      }, /0x00/);
     });
   });
 });
diff --git a/test/core/string.test.js b/test/core/string.test.js
index 6852993..341d976 100644
--- a/test/core/string.test.js
+++ b/test/core/string.test.js
@@ -18,7 +18,14 @@ test("Parsimmon.string", function() {
   });
 
   assert.equal(
-    "expected 'x' at line 1 column 1, got 'y'",
+    "\n-- PARSING FAILED --------------------------------------------------\n" +
+      "\n" +
+      "> 1 | y\n" +
+      "    | ^\n" +
+      "\n" +
+      "Expected:\n" +
+      "\n" +
+      "'x'\n",
     Parsimmon.formatError("y", res)
   );