Repository: jneen/parsimmon
Pull request #245 (2018-05-09 00:32:27)

Fixes issue #191
- [x] Implementation
- [x] Tests
- [x] Documentation


Patch:
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..46e7288
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,4 @@
+package.json
+package-lock.json
+node_modules/
+build/
diff --git a/API.md b/API.md
index f4bfd3b..5b8ea81 100644
--- a/API.md
+++ b/API.md
@@ -40,6 +40,11 @@
   * [Parsimmon.digits](#parsimmondigits)
   * [Parsimmon.whitespace](#parsimmonwhitespace)
   * [Parsimmon.optWhitespace](#parsimmonoptwhitespace)
+  * [Parsimmon.cr](#parsimmoncr)
+  * [Parsimmon.lf](#parsimmonlf)
+  * [Parsimmon.crlf](#parsimmoncrlf)
+  * [Parsimmon.newline](#parsimmonnewline)
+  * [Parsimmon.end](#parsimmonend)
   * [Parsimmon.any](#parsimmonany)
   * [Parsimmon.all](#parsimmonall)
   * [Parsimmon.eof](#parsimmoneof)
@@ -145,7 +150,7 @@ Lang.Value.tryParse('(list 1 2 foo (list nice 3 56 989 asdasdas))');
 
 ## Parsimmon(fn)
 
-**NOTE:** You probably will never need to use this function. Most parsing can be accomplished using `Parsimmon.regexp` and combination with `Parsimmon.seq` and `Parsimmon.alt`.
+**NOTE:** You probably will _never_ need to use this function. Most parsing can be accomplished using `Parsimmon.regexp` and combination with `Parsimmon.seq` and `Parsimmon.alt`.
 
 You can add a primitive parser (similar to the included ones) by using `Parsimmon(fn)`. This is an example of how to create a parser that matches any character except the one provided:
 
@@ -413,6 +418,44 @@ Equivalent to `Parsimmon.regexp(/\s+/)`.
 
 Equivalent to `Parsimmon.regexp(/\s*/)`.
 
+## Parsimmon.cr
+
+Equivalent to `Parsimmon.string("\r")`.
+
+This parser checks for the "carriage return" character, which is used as the line terminator for classic Mac OS 9 text files.
+
+## Parsimmon.lf
+
+Equivalent to `Parsimmon.string("\n")`.
+
+This parser checks for the "line feed" character, which is used as the line terminator for Linux and macOS text files.
+
+## Parsimmon.crlf
+
+Equivalent to `Parsimmon.string("\r\n")`.
+
+This parser checks for the "carriage return" character followed by the "line feed" character, which is used as the line terminator for Windows text files and HTTP headers.
+
+## Parsimmon.newline
+
+Equivalent to:
+
+```js
+Parsimmon.alt(
+  Parsimmon.crlf,
+  Parsimmon.lf,
+  Parsimmon.cr
+).desc("newline")
+```
+
+This flexible parser will match any kind of text file line ending.
+
+## Parsimmon.end
+
+Equivalent to `Parsimmon.alt(Parsimmon.newline, Parsimmon.eof)`.
+
+This is the most general purpose "end of line" parser. It allows the "end of file" in addition to all three text file line endings from `Parsimmon.newline`. This is important because text files frequently do not have line terminators at the end ("trailing newline").
+
 ## Parsimmon.any
 
 A parser that consumes and yields the next character of the input.
diff --git a/package.json b/package.json
index 3418e89..d24c838 100644
--- a/package.json
+++ b/package.json
@@ -2,21 +2,11 @@
   "name": "parsimmon",
   "version": "1.7.3",
   "description": "A monadic LL(infinity) parser combinator library",
-  "keywords": [
-    "parsing",
-    "parse",
-    "parsers",
-    "parser combinators"
-  ],
+  "keywords": ["parsing", "parse", "parsers", "parser combinators"],
   "author": "Jeanine Adkisson <jneen at jneen dot net>",
-  "contributors": [
-    "Brian Mock <brian@mockbrian.com>"
-  ],
+  "contributors": ["Brian Mock <brian@mockbrian.com>"],
   "repository": "https://github.com/jneen/parsimmon",
-  "files": [
-    "src",
-    "build"
-  ],
+  "files": ["src", "build"],
   "main": "src/parsimmon.js",
   "browser": "build/parsimmon.umd.min.js",
   "devDependencies": {
diff --git a/src/parsimmon.js b/src/parsimmon.js
index a809762..dba5957 100644
--- a/src/parsimmon.js
+++ b/src/parsimmon.js
@@ -1011,15 +1011,23 @@ var letter = regexp(/[a-z]/i).desc("a letter");
 var letters = regexp(/[a-z]*/i).desc("optional letters");
 var optWhitespace = regexp(/\s*/).desc("optional whitespace");
 var whitespace = regexp(/\s+/).desc("whitespace");
+var cr = string("\r");
+var lf = string("\n");
+var crlf = string("\r\n");
+var newline = alt(crlf, lf, cr).desc("newline");
+var end = alt(newline, eof);
 
 Parsimmon.all = all;
 Parsimmon.alt = alt;
 Parsimmon.any = any;
+Parsimmon.cr = cr;
 Parsimmon.createLanguage = createLanguage;
+Parsimmon.crlf = crlf;
 Parsimmon.custom = custom;
 Parsimmon.digit = digit;
 Parsimmon.digits = digits;
 Parsimmon.empty = empty;
+Parsimmon.end = end;
 Parsimmon.eof = eof;
 Parsimmon.fail = fail;
 Parsimmon.formatError = formatError;
@@ -1028,9 +1036,11 @@ Parsimmon.isParser = isParser;
 Parsimmon.lazy = lazy;
 Parsimmon.letter = letter;
 Parsimmon.letters = letters;
+Parsimmon.lf = lf;
 Parsimmon.lookahead = lookahead;
 Parsimmon.makeFailure = makeFailure;
 Parsimmon.makeSuccess = makeSuccess;
+Parsimmon.newline = newline;
 Parsimmon.noneOf = noneOf;
 Parsimmon.notFollowedBy = notFollowedBy;
 Parsimmon.of = succeed;


Test patch:
diff --git a/test/core/misc.test.js b/test/core/misc.test.js
new file mode 100644
index 0000000..e745152
--- /dev/null
+++ b/test/core/misc.test.js
@@ -0,0 +1,145 @@
+"use strict";
+
+function ezTest(options) {
+  var name = options.name;
+  var parser = options.parser;
+  var good = options.good;
+  var bad = options.bad;
+  describe(name, function() {
+    good.forEach(function(c) {
+      it('should parse "' + c + '"', function() {
+        assert.strictEqual(parser.tryParse(c), c);
+      });
+    });
+    bad.forEach(function(c) {
+      it('should not parse "' + c + '"', function() {
+        assert.strictEqual(parser.parse(c).status, false);
+      });
+    });
+  });
+}
+
+var whitespace = [
+  " ",
+  "\f",
+  "\n",
+  "\r",
+  "\t",
+  "\v",
+  "\u00a0",
+  "\u1680",
+  "\u2000",
+  "\u2001",
+  "\u2002",
+  "\u2003",
+  "\u2004",
+  "\u2005",
+  "\u2006",
+  "\u2007",
+  "\u2008",
+  "\u2009",
+  "\u200a",
+  "\u2028",
+  "\u2029",
+  "\u202f",
+  "\u205f",
+  "\u3000",
+  "\ufeff"
+];
+
+var whitespaces = whitespace.concat([
+  "\f\f",
+  "   ",
+  "\r\n",
+  "\v\v\t",
+  " \t \t "
+]);
+
+describe("(misc)", function() {
+  describe("Parsimmon.end", function() {
+    var newlineSequences = ["\n", "\r", "\r\n"];
+
+    it("should parse a newline", function() {
+      newlineSequences.forEach(function(c) {
+        assert.strictEqual(Parsimmon.end.tryParse(c), c);
+      });
+      assert.strictEqual(Parsimmon.end.tryParse(""), null);
+    });
+
+    it("should not parse the letter A", function() {
+      assert.deepStrictEqual(Parsimmon.end.parse("A"), {
+        status: false,
+        index: { offset: 0, line: 1, column: 1 },
+        expected: ["EOF", "newline"]
+      });
+    });
+  });
+
+  ezTest({
+    name: "Parsimmon.cr",
+    parser: Parsimmon.cr,
+    good: ["\r"],
+    bad: ["\n", "\r\n", ""]
+  });
+
+  ezTest({
+    name: "Parsimmon.lf",
+    parser: Parsimmon.lf,
+    good: ["\n"],
+    bad: ["\r", "\r\n", ""]
+  });
+
+  ezTest({
+    name: "Parsimmon.crlf",
+    parser: Parsimmon.crlf,
+    good: ["\r\n"],
+    bad: ["\r", "\n", ""]
+  });
+
+  describe("Parsimmon.digit", function() {
+    it("should parse exactly one 0-9 character", function() {
+      assert.strictEqual(Parsimmon.digit.tryParse("4"), "4");
+    });
+  });
+  ezTest({
+    name: "Parsimmon.digit",
+    parser: Parsimmon.digit,
+    good: "0 1 2 3 4 5 6 7 8 9".split(" "),
+    bad: ["a", "", "-"]
+  });
+
+  ezTest({
+    name: "Parsimmon.digits",
+    parser: Parsimmon.digits,
+    good: ["", "007", "420", "69", "1337", "666"],
+    bad: ["a", "-", "1 2 3 4", "-4", "0xcafe"]
+  });
+
+  ezTest({
+    name: "Parsimmon.letter",
+    parser: Parsimmon.letter,
+    good: "a b Z c d e A G h z".split(" "),
+    bad: ["9", ".", "-", ""]
+  });
+
+  ezTest({
+    name: "Parsimmon.letters",
+    parser: Parsimmon.letters,
+    good: [""].concat("aAa bbbB Zzzz cc d e A G h z".split(" ")),
+    bad: ["9", ".", "-"]
+  });
+
+  ezTest({
+    name: "Parsimmon.optWhitespace",
+    parser: Parsimmon.optWhitespace,
+    good: [""].concat(whitespace),
+    bad: ["a", "-", "1 2 3 4", "-4", "0xcafe"]
+  });
+
+  ezTest({
+    name: "Parsimmon.whitespace",
+    parser: Parsimmon.whitespace,
+    good: whitespaces,
+    bad: ["a", "", "-", "1 2 3 4", "-4", "0xcafe"]
+  });
+});