├── .flowconfig ├── .gitignore ├── README.md ├── package.json └── parse.js /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [options] 6 | module.system=node 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /index.js 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graham 2 | 3 | A Javascript port of the parser combinator library from Graham Hutton's [Programming in Haskell](http://www.cs.nott.ac.uk/~gmh/book.html). 4 | 5 | ## Usage 6 | 7 | ```sh 8 | $ npm install --save graham 9 | ``` 10 | 11 | ```typescript 12 | var p = require("graham"); 13 | 14 | var myLittleParser = p.either( 15 | p.seq(p.string("left "), (_) => p.seq(p.num, 16 | (n) => p.ret({ "left": n }))), 17 | p.seq(p.string("right "), (_) => p.seq(p.num, 18 | (n) => p.ret({ "right": n }))) 19 | ); 20 | 21 | p.parse(myLittleParser, "left 123"); 22 | // == [{"left": 123}, ""] 23 | 24 | p.parse(myLittleParser, "right 456"); 25 | // == [{"right": 456}, ""] 26 | 27 | p.parse(myLittleParser, "left lol"); 28 | // == null 29 | 30 | p.parse(myLittleParser, "right 123lol"); 31 | // == [{"right": 123}, "lol"] 32 | ``` 33 | 34 | ## License 35 | 36 | Copyright 2014 Bodil Stokke 37 | 38 | This program is free software: you can redistribute it and/or modify 39 | it under the terms of the GNU Lesser General Public License as 40 | published by the Free Software Foundation, either version 3 of the 41 | License, or (at your option) any later version. 42 | 43 | This program is distributed in the hope that it will be useful, but 44 | WITHOUT ANY WARRANTY; without even the implied warranty of 45 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 46 | Lesser General Public License for more details. 47 | 48 | You should have received a copy of the GNU Lesser General Public 49 | License along with this program. If not, see 50 | . 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graham", 3 | "version": "1.2.1", 4 | "description": "Parser combinators for JS.", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublish": "babel -o index.js parse.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/bodil/graham" 12 | }, 13 | "keywords": [ 14 | "parser" 15 | ], 16 | "author": "Bodil Stokke", 17 | "license": "LGPL-3.0+", 18 | "bugs": { 19 | "url": "https://github.com/bodil/graham/issues" 20 | }, 21 | "homepage": "https://github.com/bodil/graham", 22 | "devDependencies": { 23 | "babel": "^5.8.21" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | /* @flow -*- mode: flow -*- */ 2 | 3 | type ParseResult = ?[A, string]; 4 | type Parser = (input: string) => ParseResult; 5 | 6 | export function parse(parser: Parser, input: string): ParseResult { 7 | return parser(input); 8 | } 9 | 10 | export function run(genFunc) { 11 | return function(input) { 12 | const gen = genFunc(); 13 | const runP = function runP(input, val) { 14 | const next = gen.next(val); 15 | if (next.done) { 16 | return [next.value, input]; 17 | } else { 18 | const out = parse(next.value, input); 19 | if (out == null) { 20 | return null; 21 | } else { 22 | const [result, nextInput] = out; 23 | return runP(nextInput, result); 24 | } 25 | } 26 | } 27 | return runP(input); 28 | }; 29 | } 30 | 31 | export function seq(p: Parser, f: (a: A) => Parser): Parser { 32 | return (input) => { 33 | const out = parse(p, input); 34 | return out != null ? parse(f(out[0]), out[1]) : null; 35 | }; 36 | } 37 | 38 | export function either(a: Parser | Array>, b?: Parser): Parser { 39 | if (a instanceof Array) { 40 | const [head, ...tail] = a; 41 | return (tail.length) ? either(head, either(tail)) : head; 42 | } else { 43 | return (input) => { 44 | return parse(a, input) || parse(b, input); 45 | }; 46 | } 47 | } 48 | 49 | export function ret(value: A): Parser { 50 | return (input) => [value, input]; 51 | } 52 | 53 | export function fail(input: string): ParseResult { 54 | return null; 55 | } 56 | 57 | export function item(input: string): ParseResult { 58 | return input.length > 0 ? [input[0], input.slice(1)] : null; 59 | } 60 | 61 | export function sat(p: (c: string) => boolean): Parser { 62 | return seq(item, (v) => p(v) ? ret(v) : fail); 63 | } 64 | 65 | export const isDigit = (c) => /^\d$/.test(c); 66 | export const isSpace = (c) => /^\s$/.test(c); 67 | export const isAlphanum = (c) => /^\w$/.test(c); 68 | export const isLetter = (c) => /^[a-zA-Z]$/.test(c); 69 | export const isUpper = (c) => isLetter(c) && c == c.toUpperCase(); 70 | export const isLower = (c) => isLetter(c) && c == c.toLowerCase(); 71 | 72 | export const digit = sat(isDigit); 73 | export const space = sat(isSpace); 74 | export const alphanum = sat(isAlphanum); 75 | export const letter = sat(isLetter); 76 | export const upper = sat(isUpper); 77 | export const lower = sat(isLower); 78 | 79 | export function char(c: string): Parser { 80 | return sat((i) => i == c); 81 | } 82 | 83 | export function string(s: string): Parser { 84 | if (s.length > 0) { 85 | return seq(char(s[0]), 86 | (_) => seq(string(s.slice(1)), 87 | (_) => ret(s))); 88 | } else { 89 | return ret(""); 90 | } 91 | } 92 | 93 | export function manyA(p: Parser): Parser> { 94 | return either(many1A(p), ret([])); 95 | } 96 | 97 | export function many1A(p: Parser): Parser> { 98 | return seq(p, 99 | (v) => seq(manyA(p), 100 | (vs) => ret([v].concat(vs)))); 101 | } 102 | 103 | export function many(p: Parser): Parser { 104 | return either(many1(p), ret("")); 105 | } 106 | 107 | export function many1(p: Parser): Parser { 108 | return seq(p, 109 | (v) => seq(many(p), 110 | (vs) => ret(v + vs))); 111 | } 112 | 113 | export function str(ps: Array>): Parser { 114 | return ps.length > 0 ? seq(ps[0], (v) => seq(str(ps.slice(1)), (vs) => ret(v + vs))) 115 | : ret(""); 116 | } 117 | 118 | export const spaces = many(space); 119 | export const spaces1 = many1(space); 120 | 121 | export const num: Parser = seq( 122 | str([ 123 | either(char("-"), ret("")), 124 | many(digit), 125 | either(str([ 126 | char("."), 127 | many1(digit) 128 | ]), ret("")) 129 | ]), (s) => { 130 | const n = parseFloat(s); 131 | return isNaN(n) ? fail : ret(n); 132 | }); 133 | 134 | export const point = run(function*() { 135 | const x = yield num; 136 | yield char(","); 137 | const y = yield num; 138 | return {x, y}; 139 | }); 140 | 141 | export function makeParser(p: Parser): ((s: string) => A) { 142 | return (s) => { 143 | const parsed = parse(p, s); 144 | if (parsed == null) { 145 | throw new Error(`Syntax error in descriptor: "${s}"`); 146 | } else { 147 | if (parsed[1] === "") { 148 | return parsed[0]; 149 | } else { 150 | throw new Error(`In descriptor "${s}": expected EOF, saw "${parsed[1]}"`); 151 | } 152 | } 153 | }; 154 | } 155 | --------------------------------------------------------------------------------