Mirror: The magical sticky regex-based parser generator 馃
1<div align="center"> 2 <img alt="reghex" width="250" src="docs/reghex-logo.png" /> 3 <br /> 4 <br /> 5 <strong> 6 The magical sticky regex-based parser generator 7 </strong> 8 <br /> 9 <br /> 10 <br /> 11</div> 12 13Leveraging the power of sticky regexes and JS code generation, `reghex` allows 14you to code parsers quickly, by surrounding regular expressions with a regex-like 15[DSL](https://en.wikipedia.org/wiki/Domain-specific_language). 16 17With `reghex` you can generate a parser from a tagged template literal, which is 18quick to prototype and generates reasonably compact and performant code. 19 20_This project is still in its early stages and is experimental. Its API may still 21change and some issues may need to be ironed out._ 22 23## Quick Start 24 25##### 1. Install with yarn or npm 26 27```sh 28yarn add reghex 29# or 30npm install --save reghex 31``` 32 33##### 2. Add the plugin to your Babel configuration _(optional)_ 34 35In your `.babelrc`, `babel.config.js`, or `package.json:babel` add: 36 37```json 38{ 39 "plugins": ["reghex/babel"] 40} 41``` 42 43Alternatively, you can set up [`babel-plugin-macros`](https://github.com/kentcdodds/babel-plugin-macros) and 44import `reghex` from `"reghex/macro"` instead. 45 46This step is **optional**. `reghex` can also generate its optimised JS code during runtime only! 47 48##### 3. Have fun writing parsers! 49 50```js 51import { match, parse } from 'reghex'; 52 53const name = match('name')` 54 ${/\w+/} 55`; 56 57parse(name)('hello'); 58// [ "hello", .tag = "name" ] 59``` 60 61## Concepts 62 63The fundamental concept of `reghex` are regexes, specifically 64[sticky regexes](https://www.loganfranken.com/blog/831/es6-everyday-sticky-regex-matches/)! 65These are regular expressions that don't search a target string, but instead match at the 66specific position they're at. The flag for sticky regexes is `y` and hence 67they can be created using `/phrase/y` or `new RegExp('phrase', 'y')`. 68 69**Sticky Regexes** are the perfect foundation for a parsing framework in JavaScript! 70Because they only match at a single position they can be used to match patterns 71continuously, as a parser would. Like global regexes, we can then manipulate where 72they should be matched by setting `regex.lastIndex = index;` and after matching 73read back their updated `regex.lastIndex`. 74 75> **Note:** Sticky Regexes aren't natively 76> [supported in any versions of Internet Explorer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky#Browser_compatibility). `reghex` works around this by imitating its behaviour, which may decrease performance on IE11. 77 78This primitive allows us to build up a parser from regexes that you pass when 79authoring a parser function, also called a "matcher" in `reghex`. When `reghex` compiles 80to parser code, this code is just a sequence and combination of sticky regexes that 81are executed in order! 82 83```js 84let input = 'phrases should be parsed...'; 85let lastIndex = 0; 86 87const regex = /phrase/y; 88function matcher() { 89 let match; 90 // Before matching we set the current index on the RegExp 91 regex.lastIndex = lastIndex; 92 // Then we match and store the result 93 if ((match = regex.exec(input))) { 94 // If the RegExp matches successfully, we update our lastIndex 95 lastIndex = regex.lastIndex; 96 } 97} 98``` 99 100This mechanism is used in all matcher functions that `reghex` generates. 101Internally `reghex` keeps track of the input string and the current index on 102that string, and the matcher functions execute regexes against this state. 103 104## Authoring Guide 105 106You can write "matchers" by importing the `match` import from `reghex` and 107using it to write a matcher expression. 108 109```js 110import { match } from 'reghex'; 111 112const name = match('name')` 113 ${/\w+/} 114`; 115``` 116 117As can be seen above, the `match` function, is called with a "node name" and 118is then called as a tagged template. This template is our **parsing definition**. 119 120`reghex` functions only with its Babel plugin, which will detect `match('name')` 121and replace the entire tag with a parsing function, which may then look like 122the following in your transpiled code: 123 124```js 125import { _pattern /* ... */ } from 'reghex'; 126 127var _name_expression = _pattern(/\w+/); 128var name = function name() { 129 /* ... */ 130}; 131``` 132 133We've now successfully created a matcher, which matches a single regex, which 134is a pattern of one or more letters. We can execute this matcher by calling 135it with the curried `parse` utility: 136 137```js 138import { parse } from 'reghex'; 139 140const result = parse(name)('Tim'); 141 142console.log(result); // [ "Tim", .tag = "name" ] 143console.log(result.tag); // "name" 144``` 145 146If the string (Here: "Tim") was parsed successfully by the matcher, it will 147return an array that contains the result of the regex. The array is special 148in that it will also have a `tag` property set to the matcher's name, here 149`"name"`, which we determined when we defined the matcher as `match('name')`. 150 151```js 152import { parse } from 'reghex'; 153parse(name)('42'); // undefined 154``` 155 156Similarly, if the matcher does not parse an input string successfully, it will 157return `undefined` instead. 158 159### Nested matchers 160 161This on its own is nice, but a parser must be able to traverse a string and 162turn it into an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree). 163To introduce nesting to `reghex` matchers, we can refer to one matcher in another! 164Let's extend our original example; 165 166```js 167import { match } from 'reghex'; 168 169const name = match('name')` 170 ${/\w+/} 171`; 172 173const hello = match('hello')` 174 ${/hello /} ${name} 175`; 176``` 177 178The new `hello` matcher is set to match `/hello /` and then attempts to match 179the `name` matcher afterwards. If either of these matchers fail, it will return 180`undefined` as well and roll back its changes. Using this matcher will give us 181**nested abstract output**. 182 183We can also see in this example that _outside_ of the regex interpolations, 184whitespace and newlines don't matter. 185 186```js 187import { parse } from 'reghex'; 188 189parse(hello)('hello tim'); 190/* 191 [ 192 "hello", 193 ["tim", .tag = "name"], 194 .tag = "hello" 195 ] 196*/ 197``` 198 199Furthermore, interpolations don't have to just be RegHex matchers. They can 200also be functions returning matchers or completely custom matching functions. 201This is useful when your DSL becomes _self-referential_, i.e. when one matchers 202start referencing each other forming a loop. To fix this we can create a 203function that returns our root matcher: 204 205```js 206import { match } from 'reghex'; 207 208const value = match('value')` 209 (${/\w+/} | ${() => root})+ 210`; 211 212const root = match('root')` 213 ${/root/}+ ${value} 214`; 215``` 216 217### Regex-like DSL 218 219We've seen in the previous examples that matchers are authored using tagged 220template literals, where interpolations can either be filled using regexes, 221`${/pattern/}`, or with other matchers `${name}`. 222 223The tagged template syntax supports more ways to match these interpolations, 224using a regex-like Domain Specific Language. Unlike in regexes, whitespace 225and newlines don't matter, which makes it easier to format and read matchers. 226 227We can create **sequences** of matchers by adding multiple expressions in 228a row. A matcher using `${/1/} ${/2/}` will attempt to match `1` and then `2` 229in the parsed string. This is just one feature of the regex-like DSL. The 230available operators are the following: 231 232| Operator | Example | Description | 233| -------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 234| `?` | `${/1/}?` | An **optional** may be used to make an interpolation optional. This means that the interpolation may or may not match. | 235| `*` | `${/1/}*` | A **star** can be used to match an arbitrary amount of interpolation or none at all. This means that the interpolation may repeat itself or may not be matched at all. | 236| `+` | `${/1/}+` | A **plus** is used like `*` and must match one or more times. When the matcher doesn't match, that's considered a failing case, since the match isn't optional. | 237| `\|` | `${/1/} \| ${/2/}` | An **alternation** can be used to match either one thing or another, falling back when the first interpolation fails. | 238| `()` | `(${/1/} ${/2/})+` | A **group** can be used to apply one of the other operators to an entire group of interpolations. | 239| `(?: )` | `(?: ${/1/})` | A **non-capturing group** is like a regular group, but the interpolations matched inside it don't appear in the parser's output. | 240| `(?= )` | `(?= ${/1/})` | A **positive lookahead** checks whether interpolations match, and if so continues the matcher without changing the input. If it matches, it's essentially ignored. | 241| `(?! )` | `(?! ${/1/})` | A **negative lookahead** checks whether interpolations _don't_ match, and if so continues the matcher without changing the input. If the interpolations do match the matcher is aborted. | 242 243We can combine and compose these operators to create more complex matchers. 244For instance, we can extend the original example to only allow a specific set 245of names by using the `|` operator: 246 247```js 248const name = match('name')` 249 ${/tim/} | ${/tom/} | ${/tam/} 250`; 251 252parse(name)('tim'); // [ "tim", .tag = "name" ] 253parse(name)('tom'); // [ "tom", .tag = "name" ] 254parse(name)('patrick'); // undefined 255``` 256 257The above will now only match specific name strings. When one pattern in this 258chain of **alternations** does not match, it will try the next one. 259 260We can also use **groups** to add more matchers around the alternations themselves, 261by surrounding the alternations with `(` and `)` 262 263```js 264const name = match('name')` 265 (${/tim/} | ${/tom/}) ${/!/} 266`; 267 268parse(name)('tim!'); // [ "tim", "!", .tag = "name" ] 269parse(name)('tom!'); // [ "tom", "!", .tag = "name" ] 270parse(name)('tim'); // undefined 271``` 272 273Maybe we're also not that interested in the `"!"` showing up in the output node. 274If we want to get rid of it, we can use a **non-capturing group** to hide it, 275while still requiring it. 276 277```js 278const name = match('name')` 279 (${/tim/} | ${/tom/}) (?: ${/!/}) 280`; 281 282parse(name)('tim!'); // [ "tim", .tag = "name" ] 283parse(name)('tim'); // undefined 284``` 285 286Lastly, like with regexes, `?`, `*`, and `+` may be used as "quantifiers". The first two 287may also be optional and _not_ match their patterns without the matcher failing. 288The `+` operator is used to match an interpolation _one or more_ times, while the 289`*` operators may match _zero or more_ times. Let's use this to allow the `"!"` 290to repeat. 291 292```js 293const name = match('name')` 294 (${/tim/} | ${/tom/})+ (?: ${/!/})* 295`; 296 297parse(name)('tim!'); // [ "tim", .tag = "name" ] 298parse(name)('tim!!!!'); // [ "tim", .tag = "name" ] 299parse(name)('tim'); // [ "tim", .tag = "name" ] 300parse(name)('timtim'); // [ "tim", tim", .tag = "name" ] 301``` 302 303As we can see from the above, like in regexes, quantifiers can be combined with groups, 304non-capturing groups, or other groups. 305 306### Transforming as we match 307 308In the previous sections, we've seen that the **nodes** that `reghex` outputs are arrays containing 309match strings or other nodes and have a special `tag` property with the node's type. 310We can **change this output** while we're parsing by passing a function to our matcher definition. 311 312```js 313const name = match('name', (x) => x[0])` 314 (${/tim/} | ${/tom/}) ${/!/} 315`; 316 317parse(name)('tim'); // "tim" 318``` 319 320In the above example, we're passing a small function, `x => x[0]` to the matcher as a 321second argument. This will change the matcher's output, which causes the parser to 322now return a new output for this matcher. 323 324We can use this function creatively by outputting full AST nodes, maybe even like the 325ones that resemble Babel's output: 326 327```js 328const identifier = match('identifier', (x) => ({ 329 type: 'Identifier', 330 name: x[0], 331}))` 332 ${/[\w_][\w\d_]+/} 333`; 334 335parse(name)('var_name'); // { type: "Identifier", name: "var_name" } 336``` 337 338We've now entirely changed the output of the parser for this matcher. Given that each 339matcher can change its output, we're free to change the parser's output entirely. 340By **returning a falsy value** in this matcher, we can also change the matcher to not have 341matched, which would cause other matchers to treat it like a mismatch! 342 343```js 344import { match, parse } from 'reghex'; 345 346const name = match('name')((x) => { 347 return x[0] !== 'tim' ? x : undefined; 348})` 349 ${/\w+/} 350`; 351 352const hello = match('hello')` 353 ${/hello /} ${name} 354`; 355 356parse(name)('tom'); // ["hello", ["tom", .tag = "name"], .tag = "hello"] 357parse(name)('tim'); // undefined 358``` 359 360Lastly, if we need to create these special array nodes ourselves, we can use `reghex`'s 361`tag` export for this purpose. 362 363```js 364import { tag } from 'reghex'; 365 366tag(['test'], 'node_name'); 367// ["test", .tag = "node_name"] 368``` 369 370**That's it! May the RegExp be ever in your favor.**