├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .verb.md ├── LICENSE ├── README.md ├── docs ├── compiling.md ├── core-concepts.md ├── crash-course.md ├── getting-started.md ├── options.md ├── overview.md ├── parsing.md └── plugins.md ├── examples ├── dot.js ├── errors.js ├── guide-examples.js ├── parser.js └── tiny-globs.js ├── gulpfile.js ├── index.js ├── lib ├── compiler.js ├── error.js ├── parser.js ├── position.js └── source-maps.js ├── package.json ├── support └── src │ └── content │ ├── compiling.md │ ├── core-concepts.md │ ├── crash-course.md │ ├── getting-started.md │ ├── guides │ └── creating-your-first-parser.md │ ├── options.md │ ├── overview.md │ ├── parsing.md │ └── plugins.md ├── test ├── compile.js ├── compiler.js ├── nodes.js ├── parse.js ├── parser.js ├── position.js ├── snapdragon.capture.js ├── snapdragon.compile.js ├── snapdragon.options.js ├── snapdragon.parse.js └── snapdragon.regex.js └── verbfile.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [{**/{actual,fixtures,expected,templates}/**,*.md}] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "browser": false, 8 | "es6": true, 9 | "node": true, 10 | "mocha": true 11 | }, 12 | 13 | "parserOptions":{ 14 | "ecmaVersion": 9, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "modules": true, 18 | "experimentalObjectRestSpread": true 19 | } 20 | }, 21 | 22 | "globals": { 23 | "document": false, 24 | "navigator": false, 25 | "window": false 26 | }, 27 | 28 | "rules": { 29 | "accessor-pairs": 2, 30 | "arrow-spacing": [2, { "before": true, "after": true }], 31 | "block-spacing": [2, "always"], 32 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 33 | "comma-dangle": [2, "never"], 34 | "comma-spacing": [2, { "before": false, "after": true }], 35 | "comma-style": [2, "last"], 36 | "constructor-super": 2, 37 | "curly": [2, "multi-line"], 38 | "dot-location": [2, "property"], 39 | "eol-last": 2, 40 | "eqeqeq": [2, "allow-null"], 41 | "generator-star-spacing": [2, { "before": true, "after": true }], 42 | "handle-callback-err": [2, "^(err|error)$" ], 43 | "indent": [2, 2, { "SwitchCase": 1 }], 44 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 45 | "keyword-spacing": [2, { "before": true, "after": true }], 46 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 47 | "new-parens": 2, 48 | "no-array-constructor": 2, 49 | "no-caller": 2, 50 | "no-class-assign": 2, 51 | "no-cond-assign": 2, 52 | "no-const-assign": 2, 53 | "no-control-regex": 2, 54 | "no-debugger": 2, 55 | "no-delete-var": 2, 56 | "no-dupe-args": 2, 57 | "no-dupe-class-members": 2, 58 | "no-dupe-keys": 2, 59 | "no-duplicate-case": 2, 60 | "no-empty-character-class": 2, 61 | "no-eval": 2, 62 | "no-ex-assign": 2, 63 | "no-extend-native": 2, 64 | "no-extra-bind": 2, 65 | "no-extra-boolean-cast": 2, 66 | "no-extra-parens": [2, "functions"], 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 2, 69 | "no-func-assign": 2, 70 | "no-implied-eval": 2, 71 | "no-inner-declarations": [2, "functions"], 72 | "no-invalid-regexp": 2, 73 | "no-irregular-whitespace": 2, 74 | "no-iterator": 2, 75 | "no-label-var": 2, 76 | "no-labels": 2, 77 | "no-lone-blocks": 2, 78 | "no-mixed-spaces-and-tabs": 2, 79 | "no-multi-spaces": 2, 80 | "no-multi-str": 2, 81 | "no-multiple-empty-lines": [2, { "max": 1 }], 82 | "no-native-reassign": 0, 83 | "no-negated-in-lhs": 2, 84 | "no-new": 2, 85 | "no-new-func": 2, 86 | "no-new-object": 2, 87 | "no-new-require": 2, 88 | "no-new-wrappers": 2, 89 | "no-obj-calls": 2, 90 | "no-octal": 2, 91 | "no-octal-escape": 2, 92 | "no-proto": 0, 93 | "no-redeclare": 2, 94 | "no-regex-spaces": 2, 95 | "no-return-assign": 2, 96 | "no-self-compare": 2, 97 | "no-sequences": 2, 98 | "no-shadow-restricted-names": 2, 99 | "no-spaced-func": 2, 100 | "no-sparse-arrays": 2, 101 | "no-this-before-super": 2, 102 | "no-throw-literal": 2, 103 | "no-trailing-spaces": 0, 104 | "no-undef": 2, 105 | "no-undef-init": 2, 106 | "no-unexpected-multiline": 2, 107 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 108 | "no-unreachable": 2, 109 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 110 | "no-useless-call": 0, 111 | "no-with": 2, 112 | "one-var": [0, { "initialized": "never" }], 113 | "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], 114 | "padded-blocks": [0, "never"], 115 | "quotes": [2, "single", "avoid-escape"], 116 | "radix": 2, 117 | "semi": [2, "always"], 118 | "semi-spacing": [2, { "before": false, "after": true }], 119 | "space-before-blocks": [2, "always"], 120 | "space-before-function-paren": [2, "never"], 121 | "space-in-parens": [2, "never"], 122 | "space-infix-ops": 2, 123 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 124 | "spaced-comment": [0, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 125 | "use-isnan": 2, 126 | "valid-typeof": 2, 127 | "wrap-iife": [2, "any"], 128 | "yoda": [2, "never"] 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.* text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | .idea 4 | .vscode 5 | *.sublime-* 6 | 7 | # test related, or directories generated by tests 8 | test/actual 9 | actual 10 | coverage 11 | .nyc* 12 | 13 | # npm 14 | node_modules 15 | npm-debug.log 16 | 17 | # yarn 18 | yarn.lock 19 | yarn-error.log 20 | 21 | # misc 22 | _gh_pages 23 | _draft 24 | _drafts 25 | bower_components 26 | vendor 27 | temp 28 | tmp 29 | TODO.md 30 | package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - linux 4 | - osx 5 | language: node_js 6 | node_js: 7 | - node 8 | - '14' 9 | - '12' 10 | - '10' 11 | - '9' 12 | - '8' 13 | - '7' 14 | - '6' 15 | - '5' 16 | - '4' 17 | - '0.12' 18 | - '0.10' 19 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | Created by [jonschlinkert]({%= author.url %}) and [doowb](https://github.com/doowb). 2 | 3 | **Features** 4 | 5 | - Bootstrap your own parser, get sourcemap support for free 6 | - All parsing and compiling is handled by simple, reusable middleware functions 7 | - Inspired by the parsers in [pug][] and [css][]. 8 | 9 | ## Quickstart example 10 | 11 | All of the examples in this document assume the following two lines of setup code exist first: 12 | 13 | ```js 14 | var Snapdragon = require('{%= name %}'); 15 | var snapdragon = new Snapdragon(); 16 | ``` 17 | 18 | **Parse a string** 19 | 20 | ```js 21 | var ast = snapdragon.parser 22 | // parser handlers (essentially middleware) 23 | // used for parsing substrings to create tokens 24 | .set('foo', function () {}) 25 | .set('bar', function () {}) 26 | .parse('some string', options); 27 | ``` 28 | 29 | **Compile an AST returned from `.parse()`** 30 | 31 | ```js 32 | var result = snapdragon.compiler 33 | // compiler handlers (essentially middleware), 34 | // called on a node when the `node.type` matches 35 | // the name of the handler 36 | .set('foo', function () {}) 37 | .set('bar', function () {}) 38 | // pass the `ast` from the parse method 39 | .compile(ast) 40 | 41 | // the compiled string 42 | console.log(result.output); 43 | ``` 44 | 45 | See the [examples](./examples/). 46 | 47 | ## Parsing 48 | 49 | **Parser handlers** 50 | 51 | Parser handlers are middleware functions responsible for matching substrings to create tokens: 52 | 53 | **Example handler** 54 | 55 | ```js 56 | var ast = snapdragon.parser 57 | .set('dot', function() { 58 | var pos = this.position(); 59 | var m = this.match(/^\./); 60 | if (!m) return; 61 | return pos({ 62 | // the "type" will be used by the compiler later on, 63 | // we'll go over this in the compiler docs 64 | type: 'dot', 65 | // "val" is the string captured by ".match", 66 | // in this case that would be '.' 67 | val: m[0] 68 | }); 69 | }) 70 | .parse('.'[, options]) 71 | ``` 72 | 73 | _As a side node, it's not scrictly required to set the `type` on the token, since the parser will add it to the token if it's undefined, based on the name of the handler. But it's good practice since tokens aren't always returned._ 74 | 75 | **Example token** 76 | 77 | And the resulting tokens look something like this: 78 | 79 | ```js 80 | { 81 | type: 'dot', 82 | val: '.' 83 | } 84 | ``` 85 | 86 | **Position** 87 | 88 | Next, `pos()` is called on the token as it's returned, which patches the token with the `position` of the string that was captured: 89 | 90 | ```js 91 | { type: 'dot', 92 | val: '.', 93 | position: 94 | { start: { lineno: 1, column: 1 }, 95 | end: { lineno: 1, column: 2 } }} 96 | ``` 97 | 98 | **Life as an AST node** 99 | 100 | When the token is returned, the parser pushes it onto the `nodes` array of the "previous" node (since we're in a tree, the "previous" node might be literally the last node that was created, or it might be the "parent" node inside a nested context, like when parsing brackets or something with an open or close), at which point the token begins its life as an AST node. 101 | 102 | 103 | **Wrapping up** 104 | 105 | In the parser calls all handlers and cannot find a match for a substring, an error is thrown. 106 | 107 | Assuming the parser finished parsing the entire string, an AST is returned. 108 | 109 | 110 | ## Compiling 111 | 112 | The compiler's job is to take the AST created by the [parser](#parsing) and convert it to a new string. It does this by iterating over each node on the AST and calling a function on the node based on its `type`. 113 | 114 | This function is called a "handler". 115 | 116 | **Compiler handlers** 117 | 118 | Handlers are _named_ middleware functions that are called on a node when `node.type` matches the name of a registered handler. 119 | 120 | ```js 121 | var result = snapdragon.compiler 122 | .set('dot', function (node) { 123 | console.log(node.val) 124 | //=> '.' 125 | return this.emit(node.val); 126 | }) 127 | ``` 128 | 129 | If `node.type` does not match a registered handler, an error is thrown. 130 | 131 | 132 | **Source maps** 133 | 134 | If you want source map support, make sure to emit the entire node as the second argument as well (this allows the compiler to get the `node.position`). 135 | 136 | ```js 137 | var res = snapdragon.compiler 138 | .set('dot', function (node) { 139 | return this.emit(node.val, node); 140 | }) 141 | ``` 142 | 143 | ## All together 144 | 145 | This is a very basic example, but it shows how to parse a dot, then compile it as an escaped dot. 146 | 147 | ```js 148 | var Snapdragon = require('..'); 149 | var snapdragon = new Snapdragon(); 150 | 151 | var ast = snapdragon.parser 152 | .set('dot', function () { 153 | var pos = this.position(); 154 | var m = this.match(/^\./); 155 | if (!m) return; 156 | return pos({ 157 | type: 'dot', 158 | val: m[0] 159 | }) 160 | }) 161 | .parse('.') 162 | 163 | var result = snapdragon.compiler 164 | .set('dot', function (node) { 165 | return this.emit('\\' + node.val); 166 | }) 167 | .compile(ast) 168 | 169 | console.log(result.output); 170 | //=> '\.' 171 | ``` 172 | 173 | ## API 174 | 175 | ### Parse 176 | {%= apidocs("lib/parser.js") %} 177 | 178 | ### Compile 179 | {%= apidocs("lib/compiler.js") %} 180 | 181 | 182 | ## Snapdragon in the wild 183 | {%= verb.related.description %} 184 | {%= related(verb.related.implementations) %} 185 | 186 | ## History 187 | 188 | ### v0.9.0 189 | 190 | **Breaking changes!** 191 | 192 | In an attempt to make snapdragon lighter, more versatile, and more pluggable, some major changes were made in this release. 193 | 194 | - `parser.capture` was externalized to [snapdragon-capture][] 195 | - `parser.capturePair` was externalized to [snapdragon-capture-set][] 196 | - Nodes are now an instance of [snapdragon-node][] 197 | 198 | ### v0.5.0 199 | 200 | **Breaking changes!** 201 | 202 | Substantial breaking changes were made in v0.5.0! Most of these changes are part of a larger refactor that will be finished in 0.6.0, including the introduction of a `Lexer` class. 203 | 204 | - Renderer was renamed to `Compiler` 205 | - the `.render` method was renamed to `.compile` 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018, Jon Schlinkert. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snapdragon [![NPM version](https://img.shields.io/npm/v/snapdragon.svg?style=flat)](https://www.npmjs.com/package/snapdragon) [![NPM monthly downloads](https://img.shields.io/npm/dm/snapdragon.svg?style=flat)](https://npmjs.org/package/snapdragon) [![NPM total downloads](https://img.shields.io/npm/dt/snapdragon.svg?style=flat)](https://npmjs.org/package/snapdragon) [![Linux Build Status](https://img.shields.io/travis/here-be/snapdragon.svg?style=flat&label=Travis)](https://travis-ci.org/here-be/snapdragon) 2 | 3 | > Easy-to-use plugin system for creating powerful, fast and versatile parsers and compilers, with built-in source-map support. 4 | 5 | Please consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support. 6 | 7 | ## Table of Contents 8 | 9 |
10 | Details 11 | 12 | - [Install](#install) 13 | - [Quickstart example](#quickstart-example) 14 | - [Parsing](#parsing) 15 | - [Compiling](#compiling) 16 | - [All together](#all-together) 17 | - [API](#api) 18 | * [Parse](#parse) 19 | * [Compile](#compile) 20 | - [Snapdragon in the wild](#snapdragon-in-the-wild) 21 | - [History](#history) 22 | * [v0.9.0](#v090) 23 | * [v0.5.0](#v050) 24 | - [About](#about) 25 | 26 |
27 | 28 | ## Install 29 | 30 | Install with [npm](https://www.npmjs.com/): 31 | 32 | ```sh 33 | $ npm install --save snapdragon 34 | ``` 35 | 36 | Created by [jonschlinkert](https://github.com/jonschlinkert) and [doowb](https://github.com/doowb). 37 | 38 | **Features** 39 | 40 | * Bootstrap your own parser, get sourcemap support for free 41 | * All parsing and compiling is handled by simple, reusable middleware functions 42 | * Inspired by the parsers in [pug](https://pugjs.org/) and [css](https://github.com/reworkcss/css). 43 | 44 | ## Quickstart example 45 | 46 | All of the examples in this document assume the following two lines of setup code exist first: 47 | 48 | ```js 49 | var Snapdragon = require('snapdragon'); 50 | var snapdragon = new Snapdragon(); 51 | ``` 52 | 53 | **Parse a string** 54 | 55 | ```js 56 | var ast = snapdragon.parser 57 | // parser handlers (essentially middleware) 58 | // used for parsing substrings to create tokens 59 | .set('foo', function () {}) 60 | .set('bar', function () {}) 61 | .parse('some string', options); 62 | ``` 63 | 64 | **Compile an AST returned from `.parse()`** 65 | 66 | ```js 67 | var result = snapdragon.compiler 68 | // compiler handlers (essentially middleware), 69 | // called on a node when the `node.type` matches 70 | // the name of the handler 71 | .set('foo', function () {}) 72 | .set('bar', function () {}) 73 | // pass the `ast` from the parse method 74 | .compile(ast) 75 | 76 | // the compiled string 77 | console.log(result.output); 78 | ``` 79 | 80 | See the [examples](./examples/). 81 | 82 | ## Parsing 83 | 84 | **Parser handlers** 85 | 86 | Parser handlers are middleware functions responsible for matching substrings to create tokens: 87 | 88 | **Example handler** 89 | 90 | ```js 91 | var ast = snapdragon.parser 92 | .set('dot', function() { 93 | var pos = this.position(); 94 | var m = this.match(/^\./); 95 | if (!m) return; 96 | return pos({ 97 | // the "type" will be used by the compiler later on, 98 | // we'll go over this in the compiler docs 99 | type: 'dot', 100 | // "val" is the string captured by ".match", 101 | // in this case that would be '.' 102 | val: m[0] 103 | }); 104 | }) 105 | .parse('.'[, options]) 106 | ``` 107 | 108 | _As a side node, it's not scrictly required to set the `type` on the token, since the parser will add it to the token if it's undefined, based on the name of the handler. But it's good practice since tokens aren't always returned._ 109 | 110 | **Example token** 111 | 112 | And the resulting tokens look something like this: 113 | 114 | ```js 115 | { 116 | type: 'dot', 117 | val: '.' 118 | } 119 | ``` 120 | 121 | **Position** 122 | 123 | Next, `pos()` is called on the token as it's returned, which patches the token with the `position` of the string that was captured: 124 | 125 | ```js 126 | { type: 'dot', 127 | val: '.', 128 | position: 129 | { start: { lineno: 1, column: 1 }, 130 | end: { lineno: 1, column: 2 } }} 131 | ``` 132 | 133 | **Life as an AST node** 134 | 135 | When the token is returned, the parser pushes it onto the `nodes` array of the "previous" node (since we're in a tree, the "previous" node might be literally the last node that was created, or it might be the "parent" node inside a nested context, like when parsing brackets or something with an open or close), at which point the token begins its life as an AST node. 136 | 137 | **Wrapping up** 138 | 139 | In the parser calls all handlers and cannot find a match for a substring, an error is thrown. 140 | 141 | Assuming the parser finished parsing the entire string, an AST is returned. 142 | 143 | ## Compiling 144 | 145 | The compiler's job is to take the AST created by the [parser](#parsing) and convert it to a new string. It does this by iterating over each node on the AST and calling a function on the node based on its `type`. 146 | 147 | This function is called a "handler". 148 | 149 | **Compiler handlers** 150 | 151 | Handlers are _named_ middleware functions that are called on a node when `node.type` matches the name of a registered handler. 152 | 153 | ```js 154 | var result = snapdragon.compiler 155 | .set('dot', function (node) { 156 | console.log(node.val) 157 | //=> '.' 158 | return this.emit(node.val); 159 | }) 160 | ``` 161 | 162 | If `node.type` does not match a registered handler, an error is thrown. 163 | 164 | **Source maps** 165 | 166 | If you want source map support, make sure to emit the entire node as the second argument as well (this allows the compiler to get the `node.position`). 167 | 168 | ```js 169 | var res = snapdragon.compiler 170 | .set('dot', function (node) { 171 | return this.emit(node.val, node); 172 | }) 173 | ``` 174 | 175 | ## All together 176 | 177 | This is a very basic example, but it shows how to parse a dot, then compile it as an escaped dot. 178 | 179 | ```js 180 | var Snapdragon = require('..'); 181 | var snapdragon = new Snapdragon(); 182 | 183 | var ast = snapdragon.parser 184 | .set('dot', function () { 185 | var pos = this.position(); 186 | var m = this.match(/^\./); 187 | if (!m) return; 188 | return pos({ 189 | type: 'dot', 190 | val: m[0] 191 | }) 192 | }) 193 | .parse('.') 194 | 195 | var result = snapdragon.compiler 196 | .set('dot', function (node) { 197 | return this.emit('\\' + node.val); 198 | }) 199 | .compile(ast) 200 | 201 | console.log(result.output); 202 | //=> '\.' 203 | ``` 204 | 205 | ## API 206 | 207 | ### [Parser](lib/parser.js#L27) 208 | 209 | Create a new `Parser` with the given `input` and `options`. 210 | 211 | **Params** 212 | 213 | * `input` **{String}** 214 | * `options` **{Object}** 215 | 216 | **Example** 217 | 218 | ```js 219 | var Snapdragon = require('snapdragon'); 220 | var Parser = Snapdragon.Parser; 221 | var parser = new Parser(); 222 | ``` 223 | 224 | ### [.error](lib/parser.js#L97) 225 | 226 | Throw a formatted error message with details including the cursor position. 227 | 228 | **Params** 229 | 230 | * `msg` **{String}**: Message to use in the Error. 231 | * `node` **{Object}** 232 | * `returns` **{undefined}** 233 | 234 | **Example** 235 | 236 | ```js 237 | parser.set('foo', function(node) { 238 | if (node.val !== 'foo') { 239 | throw this.error('expected node.val to be "foo"', node); 240 | } 241 | }); 242 | ``` 243 | 244 | ### [.define](lib/parser.js#L115) 245 | 246 | Define a non-enumberable property on the `Parser` instance. This is useful in plugins, for exposing methods inside handlers. 247 | 248 | **Params** 249 | 250 | * `key` **{String}**: propery name 251 | * `val` **{any}**: property value 252 | * `returns` **{Object}**: Returns the Parser instance for chaining. 253 | 254 | **Example** 255 | 256 | ```js 257 | parser.define('foo', 'bar'); 258 | ``` 259 | 260 | ### [.node](lib/parser.js#L133) 261 | 262 | Create a new [Node](#node) with the given `val` and `type`. 263 | 264 | **Params** 265 | 266 | * `val` **{Object}** 267 | * `type` **{String}** 268 | * `returns` **{Object}**: returns the [Node](#node) instance. 269 | 270 | **Example** 271 | 272 | ```js 273 | parser.node('/', 'slash'); 274 | ``` 275 | 276 | ### [.position](lib/parser.js#L155) 277 | 278 | Mark position and patch `node.position`. 279 | 280 | * `returns` **{Function}**: Returns a function that takes a `node` 281 | 282 | **Example** 283 | 284 | ```js 285 | parser.set('foo', function(node) { 286 | var pos = this.position(); 287 | var match = this.match(/foo/); 288 | if (match) { 289 | // call `pos` with the node 290 | return pos(this.node(match[0])); 291 | } 292 | }); 293 | ``` 294 | 295 | ### [.set](lib/parser.js#L187) 296 | 297 | Add parser `type` with the given visitor `fn`. 298 | 299 | **Params** 300 | 301 | * `type` **{String}** 302 | * `fn` **{Function}** 303 | 304 | **Example** 305 | 306 | ```js 307 | parser.set('all', function() { 308 | var match = this.match(/^./); 309 | if (match) { 310 | return this.node(match[0]); 311 | } 312 | }); 313 | ``` 314 | 315 | ### [.get](lib/parser.js#L206) 316 | 317 | Get parser `type`. 318 | 319 | **Params** 320 | 321 | * `type` **{String}** 322 | 323 | **Example** 324 | 325 | ```js 326 | var fn = parser.get('slash'); 327 | ``` 328 | 329 | ### [.push](lib/parser.js#L229) 330 | 331 | Push a node onto the stack for the given `type`. 332 | 333 | **Params** 334 | 335 | * `type` **{String}** 336 | * `returns` **{Object}** `token` 337 | 338 | **Example** 339 | 340 | ```js 341 | parser.set('all', function() { 342 | var match = this.match(/^./); 343 | if (match) { 344 | var node = this.node(match[0]); 345 | this.push(node); 346 | return node; 347 | } 348 | }); 349 | ``` 350 | 351 | ### [.pop](lib/parser.js#L261) 352 | 353 | Pop a token off of the stack of the given `type`. 354 | 355 | **Params** 356 | 357 | * `type` **{String}** 358 | * `returns` **{Object}**: Returns a token 359 | 360 | **Example** 361 | 362 | ```js 363 | parser.set('close', function() { 364 | var match = this.match(/^\}/); 365 | if (match) { 366 | var node = this.node({ 367 | type: 'close', 368 | val: match[0] 369 | }); 370 | 371 | this.pop(node.type); 372 | return node; 373 | } 374 | }); 375 | ``` 376 | 377 | ### [.isInside](lib/parser.js#L294) 378 | 379 | Return true if inside a "set" of the given `type`. Sets are created manually by adding a type to `parser.sets`. A node is "inside" a set when an `*.open` node for the given `type` was previously pushed onto the set. The type is removed from the set by popping it off when the `*.close` node for the given type is reached. 380 | 381 | **Params** 382 | 383 | * `type` **{String}** 384 | * `returns` **{Boolean}** 385 | 386 | **Example** 387 | 388 | ```js 389 | parser.set('close', function() { 390 | var pos = this.position(); 391 | var m = this.match(/^\}/); 392 | if (!m) return; 393 | if (!this.isInside('bracket')) { 394 | throw new Error('missing opening bracket'); 395 | } 396 | }); 397 | ``` 398 | 399 | ### [.isType](lib/parser.js#L324) 400 | 401 | Return true if `node` is the given `type`. 402 | 403 | **Params** 404 | 405 | * `node` **{Object}** 406 | * `type` **{String}** 407 | * `returns` **{Boolean}** 408 | 409 | **Example** 410 | 411 | ```js 412 | parser.isType(node, 'brace'); 413 | ``` 414 | 415 | ### [.prev](lib/parser.js#L340) 416 | 417 | Get the previous AST node from the `parser.stack` (when inside a nested context) or `parser.nodes`. 418 | 419 | * `returns` **{Object}** 420 | 421 | **Example** 422 | 423 | ```js 424 | var prev = this.prev(); 425 | ``` 426 | 427 | ### [.prev](lib/parser.js#L394) 428 | 429 | Match `regex`, return captures, and update the cursor position by `match[0]` length. 430 | 431 | **Params** 432 | 433 | * `regex` **{RegExp}** 434 | * `returns` **{Object}** 435 | 436 | **Example** 437 | 438 | ```js 439 | // make sure to use the starting regex boundary: "^" 440 | var match = this.match(/^\./); 441 | ``` 442 | 443 | **Params** 444 | 445 | * `input` **{String}** 446 | * `returns` **{Object}**: Returns an AST with `ast.nodes` 447 | 448 | **Example** 449 | 450 | ```js 451 | var ast = parser.parse('foo/bar'); 452 | ``` 453 | 454 | ### [Compiler](lib/compiler.js#L24) 455 | 456 | Create a new `Compiler` with the given `options`. 457 | 458 | **Params** 459 | 460 | * `options` **{Object}** 461 | * `state` **{Object}**: Optionally pass a "state" object to use inside visitor functions. 462 | 463 | **Example** 464 | 465 | ```js 466 | var Snapdragon = require('snapdragon'); 467 | var Compiler = Snapdragon.Compiler; 468 | var compiler = new Compiler(); 469 | ``` 470 | 471 | ### [.error](lib/compiler.js#L67) 472 | 473 | Throw a formatted error message with details including the cursor position. 474 | 475 | **Params** 476 | 477 | * `msg` **{String}**: Message to use in the Error. 478 | * `node` **{Object}** 479 | * `returns` **{undefined}** 480 | 481 | **Example** 482 | 483 | ```js 484 | compiler.set('foo', function(node) { 485 | if (node.val !== 'foo') { 486 | throw this.error('expected node.val to be "foo"', node); 487 | } 488 | }); 489 | ``` 490 | 491 | ### [.emit](lib/compiler.js#L86) 492 | 493 | Concat the given string to `compiler.output`. 494 | 495 | **Params** 496 | 497 | * `string` **{String}** 498 | * `node` **{Object}**: Optionally pass the node to use for position if source maps are enabled. 499 | * `returns` **{String}**: returns the string 500 | 501 | **Example** 502 | 503 | ```js 504 | compiler.set('foo', function(node) { 505 | this.emit(node.val, node); 506 | }); 507 | ``` 508 | 509 | ### [.noop](lib/compiler.js#L104) 510 | 511 | Emit an empty string to effectively "skip" the string for the given `node`, but still emit the position and node type. 512 | 513 | **Params** 514 | 515 | * **{Object}**: node 516 | 517 | **Example** 518 | 519 | ```js 520 | // example: do nothing for beginning-of-string 521 | snapdragon.compiler.set('bos', compiler.noop); 522 | ``` 523 | 524 | ### [.define](lib/compiler.js#L124) 525 | 526 | Define a non-enumberable property on the `Compiler` instance. This is useful in plugins, for exposing methods inside handlers. 527 | 528 | **Params** 529 | 530 | * `key` **{String}**: propery name 531 | * `val` **{any}**: property value 532 | * `returns` **{Object}**: Returns the Compiler instance for chaining. 533 | 534 | **Example** 535 | 536 | ```js 537 | compiler.define('customMethod', function() { 538 | // do stuff 539 | }); 540 | ``` 541 | 542 | ### [.set](lib/compiler.js#L152) 543 | 544 | Add a compiler `fn` for the given `type`. Compilers are called when the `.compile` method encounters a node of the given type to generate the output string. 545 | 546 | **Params** 547 | 548 | * `type` **{String}** 549 | * `fn` **{Function}** 550 | 551 | **Example** 552 | 553 | ```js 554 | compiler 555 | .set('comma', function(node) { 556 | this.emit(','); 557 | }) 558 | .set('dot', function(node) { 559 | this.emit('.'); 560 | }) 561 | .set('slash', function(node) { 562 | this.emit('/'); 563 | }); 564 | ``` 565 | 566 | ### [.get](lib/compiler.js#L168) 567 | 568 | Get the compiler of the given `type`. 569 | 570 | **Params** 571 | 572 | * `type` **{String}** 573 | 574 | **Example** 575 | 576 | ```js 577 | var fn = compiler.get('slash'); 578 | ``` 579 | 580 | ### [.visit](lib/compiler.js#L188) 581 | 582 | Visit `node` using the registered compiler function associated with the `node.type`. 583 | 584 | **Params** 585 | 586 | * `node` **{Object}** 587 | * `returns` **{Object}**: returns the node 588 | 589 | **Example** 590 | 591 | ```js 592 | compiler 593 | .set('i', function(node) { 594 | this.visit(node); 595 | }) 596 | ``` 597 | 598 | ### [.mapVisit](lib/compiler.js#L226) 599 | 600 | Iterate over `node.nodes`, calling [visit](#visit) on each node. 601 | 602 | **Params** 603 | 604 | * `node` **{Object}** 605 | * `returns` **{Object}**: returns the node 606 | 607 | **Example** 608 | 609 | ```js 610 | compiler 611 | .set('i', function(node) { 612 | utils.mapVisit(node); 613 | }) 614 | ``` 615 | 616 | ### [.compile](lib/compiler.js#L250) 617 | 618 | Compile the given `AST` and return a string. Iterates over `ast.nodes` with [mapVisit](#mapVisit). 619 | 620 | **Params** 621 | 622 | * `ast` **{Object}** 623 | * `options` **{Object}**: Compiler options 624 | * `returns` **{Object}**: returns the node 625 | 626 | **Example** 627 | 628 | ```js 629 | var ast = parser.parse('foo'); 630 | var str = compiler.compile(ast); 631 | ``` 632 | 633 | ## Snapdragon in the wild 634 | 635 | A few of the libraries that use snapdragon: 636 | 637 | * [braces](https://www.npmjs.com/package/braces): Bash-like brace expansion, implemented in JavaScript. Safer than other brace expansion libs, with complete support… [more](https://github.com/micromatch/braces) | [homepage](https://github.com/micromatch/braces "Bash-like brace expansion, implemented in JavaScript. Safer than other brace expansion libs, with complete support for the Bash 4.3 braces specification, without sacrificing speed.") 638 | * [breakdance](https://www.npmjs.com/package/breakdance): Breakdance is a node.js library for converting HTML to markdown. Highly pluggable, flexible and easy… [more](http://breakdance.io) | [homepage](http://breakdance.io "Breakdance is a node.js library for converting HTML to markdown. Highly pluggable, flexible and easy to use. It's time for your markup to get down.") 639 | * [expand-brackets](https://www.npmjs.com/package/expand-brackets): Expand POSIX bracket expressions (character classes) in glob patterns. | [homepage](https://github.com/jonschlinkert/expand-brackets "Expand POSIX bracket expressions (character classes) in glob patterns.") 640 | * [extglob](https://www.npmjs.com/package/extglob): Extended glob support for JavaScript. Adds (almost) the expressive power of regular expressions to glob… [more](https://github.com/micromatch/extglob) | [homepage](https://github.com/micromatch/extglob "Extended glob support for JavaScript. Adds (almost) the expressive power of regular expressions to glob patterns.") 641 | * [micromatch](https://www.npmjs.com/package/micromatch): Glob matching for javascript/node.js. A drop-in replacement and faster alternative to minimatch and multimatch. | [homepage](https://github.com/micromatch/micromatch "Glob matching for javascript/node.js. A drop-in replacement and faster alternative to minimatch and multimatch.") 642 | * [nanomatch](https://www.npmjs.com/package/nanomatch): Fast, minimal glob matcher for node.js. Similar to micromatch, minimatch and multimatch, but complete Bash… [more](https://github.com/micromatch/nanomatch) | [homepage](https://github.com/micromatch/nanomatch "Fast, minimal glob matcher for node.js. Similar to micromatch, minimatch and multimatch, but complete Bash 4.3 wildcard support only (no support for exglobs, posix brackets or braces)") 643 | 644 | ## History 645 | 646 | ### v0.9.0 647 | 648 | **Breaking changes!** 649 | 650 | In an attempt to make snapdragon lighter, more versatile, and more pluggable, some major changes were made in this release. 651 | 652 | * `parser.capture` was externalized to [snapdragon-capture](https://github.com/jonschlinkert/snapdragon-capture) 653 | * `parser.capturePair` was externalized to [snapdragon-capture-set](https://github.com/jonschlinkert/snapdragon-capture-set) 654 | * Nodes are now an instance of [snapdragon-node](https://github.com/jonschlinkert/snapdragon-node) 655 | 656 | ### v0.5.0 657 | 658 | **Breaking changes!** 659 | 660 | Substantial breaking changes were made in v0.5.0! Most of these changes are part of a larger refactor that will be finished in 0.6.0, including the introduction of a `Lexer` class. 661 | 662 | * Renderer was renamed to `Compiler` 663 | * the `.render` method was renamed to `.compile` 664 | 665 | ## About 666 | 667 |
668 | Contributing 669 | 670 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 671 | 672 |
673 | 674 |
675 | Running Tests 676 | 677 | Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command: 678 | 679 | ```sh 680 | $ npm install && npm test 681 | ``` 682 | 683 |
684 | 685 |
686 | Building docs 687 | 688 | _(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_ 689 | 690 | To generate the readme, run the following command: 691 | 692 | ```sh 693 | $ npm install -g verbose/verb#dev verb-generate-readme && verb 694 | ``` 695 | 696 |
697 | 698 | ### Related projects 699 | 700 | A few of the libraries that use snapdragon: 701 | 702 | * [snapdragon-capture-set](https://www.npmjs.com/package/snapdragon-capture-set): Plugin that adds a `.captureSet()` method to snapdragon, for matching and capturing substrings that have… [more](https://github.com/jonschlinkert/snapdragon-capture-set) | [homepage](https://github.com/jonschlinkert/snapdragon-capture-set "Plugin that adds a `.captureSet()` method to snapdragon, for matching and capturing substrings that have an `open` and `close`, like braces, brackets, etc") 703 | * [snapdragon-capture](https://www.npmjs.com/package/snapdragon-capture): Snapdragon plugin that adds a capture method to the parser instance. | [homepage](https://github.com/jonschlinkert/snapdragon-capture "Snapdragon plugin that adds a capture method to the parser instance.") 704 | * [snapdragon-node](https://www.npmjs.com/package/snapdragon-node): Snapdragon utility for creating a new AST node in custom code, such as plugins. | [homepage](https://github.com/jonschlinkert/snapdragon-node "Snapdragon utility for creating a new AST node in custom code, such as plugins.") 705 | * [snapdragon-util](https://www.npmjs.com/package/snapdragon-util): Utilities for the snapdragon parser/compiler. | [homepage](https://github.com/here-be/snapdragon-util "Utilities for the snapdragon parser/compiler.") 706 | 707 | ### Contributors 708 | 709 | | **Commits** | **Contributor** | 710 | | --- | --- | 711 | | 156 | [jonschlinkert](https://github.com/jonschlinkert) | 712 | | 3 | [doowb](https://github.com/doowb) | 713 | | 2 | [danez](https://github.com/danez) | 714 | | 1 | [EdwardBetts](https://github.com/EdwardBetts) | 715 | 716 | ### Author 717 | 718 | **Jon Schlinkert** 719 | 720 | * [LinkedIn Profile](https://linkedin.com/in/jonschlinkert) 721 | * [GitHub Profile](https://github.com/jonschlinkert) 722 | * [Twitter Profile](https://twitter.com/jonschlinkert) 723 | 724 | ### License 725 | 726 | Copyright © 2018, [Jon Schlinkert](https://github.com/jonschlinkert). 727 | Released under the [MIT License](LICENSE). 728 | 729 | *** 730 | 731 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.6.0, on March 20, 2018._ -------------------------------------------------------------------------------- /docs/compiling.md: -------------------------------------------------------------------------------- 1 | # Compiling with snapdragon 2 | 3 |
4 | Pre-requisites 5 | If you're not quite sure how an AST works, don't sweat it. Not every programmer needs to interact with an AST, and the first experience with one is daunting for everyone. 6 | 7 | To get the most from this documentation, we suggest you head over to the [begin/parsers-compilers](https://github.com/begin/parsers-compilers) project to brush up. Within a few minutes you'll know everything you need to proceed! 8 |
9 | 10 | -------------------------------------------------------------------------------- /docs/core-concepts.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Core concepts 4 | 5 | - [Lexer](#parser) 6 | * Token Stream 7 | * Token 8 | * Scope 9 | - [Parser](#parser) 10 | * [Node](#node) 11 | * Stack 12 | * [AST](#ast) 13 | - [Compiler](#compiler) 14 | * State 15 | - [Renderer](#renderer) 16 | * Contexts 17 | * Context 18 | 19 | ## Lexer 20 | 21 | - [ ] Token 22 | - [ ] Tokens 23 | - [ ] Scope 24 | 25 | ## Parser 26 | 27 | ### AST 28 | 29 | TODO 30 | 31 | ### Node 32 | 33 | #### Properties 34 | 35 | Officially supported properties 36 | 37 | - `type` 38 | - `val` 39 | - `nodes` 40 | 41 | **Related** 42 | 43 | - The [snapdragon-position][] plugin adds support for `node.position`, which patches the `node` with the start and end position of a captured value. 44 | - The [snapdragon-scope][] plugin adds support for `node.scope`, which patches the `node` with lexical scope of the node. 45 | 46 | ## Compiler 47 | 48 | TODO 49 | 50 | ## Renderer 51 | 52 | TODO 53 | 54 | [verb][] 55 | -------------------------------------------------------------------------------- /docs/crash-course.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | ## Crash course 4 | 5 | ### Parser 6 | 7 | The parser's job is create an AST from a string. It does this by looping over registered parser-middleware to create nodes from captured substrings. 8 | 9 | **Parsing** 10 | 11 | When a middleware returns a node, the parser updates the string position and starts over again with the first middleware. 12 | 13 | **Parser middleware** 14 | 15 | Each parser-middleware is responsible for matching and capturing a specific "type" of substring, and optionally returning a `node` with information about what was captured. 16 | 17 | **Node** 18 | 19 | A `node` is an object that is used for storing information about a captured substring, or to mark a significant point or delimiter in the AST or string. 20 | 21 | The only required property is `node.type`. 22 | 23 | Every node has a `node.type` that 24 | 25 | semantically describes a substring that was captured by a middleware - or some other purpose of the node, along with any other information that might be useful later during parsing or compiling. 26 | 27 | of a specific `node.type` that semantically describes the capturing substrings 28 | . Matching is typically performed using a regular expression, but any means can be used. 29 | 30 | Upon capturing a substring, the parser-middleware 31 | 32 | - capturing and/or further processing relevant part(s) of the captured substring 33 | - returning a node with information that semantically describes the substring that was captured, along with 34 | 35 | When a parser returns a node, that indicates 36 | 37 | by calling each user-defined middleware (referred to as "parsers") until one returns a node. 38 | Each parser middleware 39 | middleware 40 | a string and calling user-defined "parsers" 41 | 42 | **AST** 43 | 44 | which is an object with "nodes", where each "node" is an object with a `type` 45 | 46 | **Nodes** 47 | 48 | A `node` is an object that is used for storing and describing information about a captured substring. 49 | 50 | Every node in the AST has a `type` property, and either: 51 | 52 | - `val`: a captured substring 53 | - `nodes`: an array of child nodes 54 | 55 | When the substring is delimited - by, for example, braces, brackets, parentheses, etc - the `node` will 56 | 57 | In fact, the AST itself is a `node` with type `root`, and a `nodes` array, which contains all of other nodes on the AST. 58 | 59 | **Example** 60 | 61 | The absolute simplest AST for a single-character string might look something like this: 62 | 63 | ```js 64 | var ast = { 65 | type: 'root', 66 | nodes: [ 67 | { 68 | type: 'text', 69 | val: 'a' 70 | } 71 | ] 72 | }; 73 | ``` 74 | 75 | Nodes may have any additional properties, but they must have 76 | 77 | Parsers and compilers have a one-to-one relationship. 78 | 79 | The parser uses middleware for 80 | 81 | Some of the node "types" on our final AST will also roughly end up reflecting the goals we described in our high level strategy. Our strategy gives us a starting point, but it's good to be flexible. In reality our parser might end up doing something completely different than what we expected in the beginning. 82 | 83 | ### Compiler 84 | 85 | The compiler's job is to render a string. It does this by iterating over an AST, and using the information contained in each node to determine what to render. 86 | 87 | **A compiler for every parser** 88 | 89 | Parsers and compilers have a one-to-one relationship. 90 | 91 | The parser uses middleware for 92 | 93 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Getting started 4 | 5 | [What is snapdragon, and who created it?](overview.html) 6 | 7 | - Installing snapdragon 8 | - Basic usage 9 | - Next steps 10 | 11 | ## Installing snapdragon 12 | 13 | ## Usage documentation 14 | 15 | **Learn how to use snapdragon** 16 | 17 | The following documentation tells you how to download and start using snapdragon. If you're intestested in creating snapdragon plugins, or you want to understand more about how snapdragon works you can find links to [developer documentation](#developer-documentation) below. 18 | 19 | is API-focused 20 | how to the API methods that are 21 | 22 | ## Developer documentation 23 | 24 | **Learn how to create plugins or hack on snapdragon** 25 | 26 | In the developer documentation, you will learn how Snapdragon works "under the hood" and how to create plugins. If you're more interested in test driving snapdragon, 27 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | WIP (draft) 4 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Overview 4 | 5 | Thanks for visiting the snapdragon documentation! Please [let us know](../../issues) if you find any typos, outdated or incorrect information. Pull requests welcome. 6 | 7 | ## What is snapdragon? 8 | 9 | At its heart, snapdragon does two things: 10 | 11 | - Parsing: the [snapdragon parser](parsing.md) takes a string and converts it to an AST 12 | - Compiling: the [snapdragon compiler](compiling.md) takes the AST from the snapdragon parser and converts it to another string. 13 | 14 | **Plugins** 15 | 16 | ## What can snapdragon do? 17 | 18 | You can use snapdragon to parse and convert a string into something entirely different, or use it to create "formatters" for beautifying code or plain text. 19 | 20 | **In the wild** 21 | 22 | Here's how some real projects are using snapdragon: 23 | 24 | * [breakdance][]: uses snapdragon to convert HTML to markdown using an AST from [cheerio][]: 25 | * [micromatch][]: uses snapdragon to create regex from glob patterns 26 | * [extglob][]: uses snapdragon to create regex from glob patterns 27 | * [braces][]: uses snapdragon to create regex for bash-like brace-expansion 28 | * [expand-reflinks][]: uses snapdragon to parse and re-write markdown [reference links](http://spec.commonmark.org/0.25/#link-reference-definitions) 29 | 30 | ## About 31 | 32 | Snapdragon was created by, [Jon Schlinkert](https://github.com/jonschlinkert), author of [assemble][], [generate][], [update][], [micromatch][], [remarkable][] and many other node.js projects. 33 | 34 | If you'd like to learn more about me or my projects, or you want to get in touch, please feel free to: 35 | 36 | - follow me on [github]() for notifications and updates about my github projects 37 | - follow me on [twitter]() 38 | - connect with me on [linkedin](https://www.linkedin.com/in/jonschlinkert) 39 | -------------------------------------------------------------------------------- /docs/parsing.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Parsing with snapdragon 4 | 5 |
6 | Pre-requisites 7 | If you're not quite sure how an AST works, don't sweat it. Not every programmer needs to interact with an AST, and the first experience with one is daunting for everyone. 8 | 9 | To get the most from this documentation, we suggest you head over to the [begin/parsers-compilers](https://github.com/begin/parsers-compilers) project to brush up. Within a few minutes you'll know everything you need to proceed! 10 |
11 | 12 |
13 | Table of contents 14 | - Usage 15 | - Developer 16 | * Parser 17 | * Parsers 18 | * Custom parsers 19 |
20 | 21 | ## API 22 | 23 | ## Parser 24 | 25 | The snapdragon [Parser]() class contains all of the functionality and methods that are used for creating an AST from a string. 26 | 27 | To understand what `Parser` does, 28 | 29 | The snapdragon parser takes a string and creates an by 30 | 31 | 1. looping over the string 32 | 1. invoking registered [parsers](#parsers) to create new AST nodes. 33 | 34 | The following documentation describes this in more detail. 35 | 36 | checking to see if any registered [parsers](#parsers) match the sub-string at the current position, and: 37 | * if a parser matches, it is called, possibly resuling in a new AST node (this is up to the parser function) 38 | * if _no matches are found_, an error is throw notifying you that the s 39 | 40 | ## Parsers 41 | 42 | Snapdragon parsers are functions that are registered by name, and are invoked by the `.parse` method as it loops over the given string. 43 | 44 | **How parsers work** 45 | 46 | A very basic parser function might look something like this: 47 | 48 | ```js 49 | function() { 50 | var parsed = this.parsed; 51 | var pos = this.position(); 52 | var m = this.match(regex); 53 | if (!m || !m[0]) return; 54 | 55 | var prev = this.prev(); 56 | var node = pos({ 57 | type: type, 58 | val: m[0] 59 | }); 60 | 61 | define(node, 'match', m); 62 | define(node, 'inside', this.stack.length > 0); 63 | define(node, 'parent', prev); 64 | define(node, 'parsed', parsed); 65 | define(node, 'rest', this.input); 66 | prev.nodes.push(node); 67 | } 68 | ``` 69 | 70 | TODO 71 | 72 | ## Custom parsers 73 | 74 | TODO 75 | 76 | ## Plugins 77 | 78 | TODO 79 | 80 | ```js 81 | parser.use(function() {}); 82 | ``` 83 | 84 | ```js 85 | snapdragon.parser.use(function() {}); 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Snapdragon plugins 4 | 5 | ```js 6 | var snapdragon = new Snapdgragon(); 7 | // register plugins 8 | snapdragon.use(function() {}); 9 | 10 | // register parser plugins 11 | snapdragon.parser.use(function() {}); 12 | 13 | // register compiler plugins 14 | snapdragon.compiler.use(function() {}); 15 | 16 | // parse 17 | var ast = snapdragon.parse('foo/bar'); 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/dot.js: -------------------------------------------------------------------------------- 1 | var Snapdragon = require('..'); 2 | var snapdragon = new Snapdragon(); 3 | 4 | var ast = snapdragon.parser 5 | .set('dot', function () { 6 | var pos = this.position(); 7 | var m = this.match(/^\./); 8 | if (!m) return; 9 | return pos({ 10 | // define the `type` of compiler to use 11 | // setting this value is optional, since the 12 | // parser will add it based on the name used 13 | // when registering the handler, but it's 14 | // good practice since tokens aren't always 15 | // returned 16 | type: 'dot', 17 | val: m[0] 18 | }) 19 | }) 20 | .parse('.') 21 | 22 | var result = snapdragon.compiler 23 | .set('dot', function (node) { 24 | return this.emit('\\' + node.val); 25 | }) 26 | .compile(ast) 27 | 28 | console.log(result.output); 29 | //=> '\.' 30 | -------------------------------------------------------------------------------- /examples/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Parser = require('../lib/parser'); 4 | 5 | var parser = new Parser() 6 | .set('at', function() { 7 | var pos = this.position(); 8 | var match = this.match(/^@/); 9 | if (match) { 10 | return pos({val: match[0]}); 11 | } 12 | }) 13 | .set('slash', function() { 14 | var pos = this.position(); 15 | var match = this.match(/^\//); 16 | if (match) { 17 | return pos({val: match[0]}); 18 | } 19 | }) 20 | .set('text', function() { 21 | var pos = this.position(); 22 | var match = this.match(/^\w+/); 23 | if (match) { 24 | return pos({val: match[0]}); 25 | } 26 | }) 27 | 28 | var ast = parser.parse('git@github.com:foo/bar.git'); 29 | console.log(ast); 30 | -------------------------------------------------------------------------------- /examples/guide-examples.js: -------------------------------------------------------------------------------- 1 | var Snapdragon = require('..'); 2 | var snapdragon = new Snapdragon(); 3 | 4 | /** 5 | * 6 | */ 7 | 8 | var ast = snapdragon.parse('foo/*.js'); 9 | console.log(ast); 10 | -------------------------------------------------------------------------------- /examples/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Parser = require('../lib/parser'); 4 | 5 | var parser = new Parser() 6 | .set('at', function() { 7 | var pos = this.position(); 8 | var match = this.match(/^@/); 9 | if (match) { 10 | return pos({val: match[0]}); 11 | } 12 | }) 13 | .set('slash', function() { 14 | var pos = this.position(); 15 | var match = this.match(/^\//); 16 | if (match) { 17 | return pos({val: match[0]}); 18 | } 19 | }) 20 | .set('text', function() { 21 | var pos = this.position(); 22 | var match = this.match(/^\w+/); 23 | if (match) { 24 | return pos({val: match[0]}); 25 | } 26 | }) 27 | .set('dot', function() { 28 | var pos = this.position(); 29 | var match = this.match(/^\./); 30 | if (match) { 31 | return pos({val: match[0]}); 32 | } 33 | }) 34 | .set('colon', function() { 35 | var pos = this.position(); 36 | var match = this.match(/^:/); 37 | if (match) { 38 | return pos({val: match[0]}); 39 | } 40 | }) 41 | 42 | var ast = parser.parse('git@github.com:foo/bar.git'); 43 | console.log(ast); 44 | -------------------------------------------------------------------------------- /examples/tiny-globs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Snapdragon = require('..'); 4 | var Snapdragon = new Snapdragon(); 5 | 6 | /** 7 | * 1 8 | */ 9 | 10 | 11 | // var parser = new Parser(); 12 | // console.log(parser.parse('foo/*.js')); 13 | 14 | 15 | /** 16 | * 2 17 | */ 18 | 19 | 20 | snapdragon.parser 21 | .set('text', function() { 22 | var pos = this.position(); 23 | var m = this.match(/^\w+/); 24 | if (m) { 25 | return pos(this.node(m[0])); 26 | } 27 | }) 28 | .set('slash', function() { 29 | var pos = this.position(); 30 | var m = this.match(/^\//); 31 | if (m) { 32 | return pos(this.node(m[0])); 33 | } 34 | }) 35 | .set('star', function() { 36 | var pos = this.position(); 37 | var m = this.match(/^\*/); 38 | if (m) { 39 | return pos(this.node(m[0])); 40 | } 41 | }) 42 | .set('dot', function() { 43 | var pos = this.position(); 44 | var m = this.match(/^\./); 45 | if (m) { 46 | return pos(this.node(m[0])); 47 | } 48 | }); 49 | 50 | console.log(parser.parse('foo/*.js')); 51 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var istanbul = require('gulp-istanbul'); 5 | var eslint = require('gulp-eslint'); 6 | var mocha = require('gulp-mocha'); 7 | var unused = require('gulp-unused'); 8 | 9 | gulp.task('coverage', function() { 10 | return gulp.src(['lib/*.js', 'index.js']) 11 | .pipe(istanbul({includeUntested: true})) 12 | .pipe(istanbul.hookRequire()); 13 | }); 14 | 15 | gulp.task('mocha', ['coverage'], function() { 16 | return gulp.src(['test/*.js']) 17 | .pipe(mocha()) 18 | .pipe(istanbul.writeReports()); 19 | }); 20 | 21 | gulp.task('eslint', function() { 22 | return gulp.src(['*.js', 'lib/*.js', 'test/*.js']) 23 | .pipe(eslint()) 24 | .pipe(eslint.format()); 25 | }); 26 | 27 | gulp.task('unused', function() { 28 | return gulp.src(['index.js', 'lib/*.js']) 29 | .pipe(unused({keys: Object.keys(require('./lib/utils.js'))})); 30 | }); 31 | 32 | gulp.task('default', ['coverage', 'eslint', 'mocha']); 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var define = require('define-property'); 4 | var extend = require('extend-shallow'); 5 | var Compiler = require('./lib/compiler'); 6 | var Parser = require('./lib/parser'); 7 | 8 | /** 9 | * Create a new instance of `Snapdragon` with the given `options`. 10 | * 11 | * ```js 12 | * var Snapdragon = require('snapdragon'); 13 | * var snapdragon = new Snapdragon(); 14 | * ``` 15 | * @param {Object} `options` 16 | * @api public 17 | */ 18 | 19 | function Snapdragon(options) { 20 | if (typeof options === 'string') { 21 | var protoa = Object.create(Snapdragon.prototype); 22 | Snapdragon.call(protoa); 23 | return protoa.render.apply(protoa, arguments); 24 | } 25 | 26 | if (!(this instanceof Snapdragon)) { 27 | var protob = Object.create(Snapdragon.prototype); 28 | Snapdragon.call(protob); 29 | return protob; 30 | } 31 | 32 | this.define('cache', {}); 33 | this.options = extend({source: 'string'}, options); 34 | this.isSnapdragon = true; 35 | this.plugins = { 36 | fns: [], 37 | preprocess: [], 38 | visitors: {}, 39 | before: {}, 40 | after: {} 41 | }; 42 | } 43 | 44 | /** 45 | * Register a plugin `fn`. 46 | * 47 | * ```js 48 | * var snapdragon = new Snapdgragon([options]); 49 | * snapdragon.use(function() { 50 | * console.log(this); //<= snapdragon instance 51 | * console.log(this.parser); //<= parser instance 52 | * console.log(this.compiler); //<= compiler instance 53 | * }); 54 | * ``` 55 | * @param {Object} `fn` 56 | * @api public 57 | */ 58 | 59 | Snapdragon.prototype.use = function(fn) { 60 | fn.call(this, this); 61 | return this; 62 | }; 63 | 64 | /** 65 | * Define a non-enumerable property or method on the Snapdragon instance. 66 | * Useful in plugins for adding convenience methods that can be used in 67 | * nodes. 68 | * 69 | * ```js 70 | * snapdraong.define('isTypeFoo', function(node) { 71 | * return node.type === 'foo'; 72 | * }); 73 | * 74 | * // inside a handler 75 | * snapdragon.set('razzle-dazzle', function(node) { 76 | * if (this.isTypeFoo(node.parent)) { 77 | * // do stuff 78 | * } 79 | * }); 80 | * ``` 81 | * @param {String} `name` Name of the property or method being defined 82 | * @param {any} `val` Property value 83 | * @return {Object} Returns the instance for chaining. 84 | * @api public 85 | */ 86 | 87 | Snapdragon.prototype.define = function(key, val) { 88 | define(this, key, val); 89 | return this; 90 | }; 91 | 92 | /** 93 | * Parse the given `str` and return an AST. 94 | * 95 | * ```js 96 | * var snapdragon = new Snapdgragon([options]); 97 | * var ast = snapdragon.parse('foo/bar'); 98 | * console.log(ast); 99 | * ``` 100 | * @param {String} `str` 101 | * @param {Object} `options` Set `options.sourcemap` to true to enable source maps. 102 | * @return {Object} Returns an AST. 103 | * @api public 104 | */ 105 | 106 | Snapdragon.prototype.parse = function(str, options) { 107 | var opts = extend({}, this.options, options); 108 | var ast = this.parser.parse(str, opts); 109 | // add non-enumerable parser reference to AST 110 | define(ast, 'parser', this.parser); 111 | return ast; 112 | }; 113 | 114 | /** 115 | * Compile an `ast` returned from `snapdragon.parse()` 116 | * 117 | * ```js 118 | * // compile 119 | * var res = snapdragon.compile(ast); 120 | * // get the compiled output string 121 | * console.log(res.output); 122 | * ``` 123 | * @param {Object} `ast` 124 | * @param {Object} `options` 125 | * @return {Object} Returns an object with an `output` property with the rendered string. 126 | * @api public 127 | */ 128 | 129 | Snapdragon.prototype.compile = function(ast, options) { 130 | var opts = extend({}, this.options, options); 131 | return this.compiler.compile(ast, opts); 132 | }; 133 | 134 | /** 135 | * Renders the given string or AST by calling `snapdragon.parse()` (if it's a string) 136 | * then `snapdragon.compile()`, and returns the output string. 137 | * 138 | * ```js 139 | * // setup parsers and compilers, then call render 140 | * var str = snapdragon.render([string_or_ast]); 141 | * console.log(str); 142 | * ``` 143 | * @param {Object} `ast` 144 | * @param {Object} `options` 145 | * @return {Object} Returns an object with an `output` property with the rendered string. 146 | * @api public 147 | */ 148 | 149 | Snapdragon.prototype.render = function(ast, options) { 150 | if (typeof ast === 'string') { 151 | ast = this.parse(ast, options); 152 | } 153 | return this.compile(ast, options).output; 154 | }; 155 | 156 | /** 157 | * Get or set a `Snapdragon.Compiler` instance. 158 | * @api public 159 | */ 160 | 161 | Object.defineProperty(Snapdragon.prototype, 'compiler', { 162 | configurable: true, 163 | set: function(val) { 164 | this.cache.compiler = val; 165 | }, 166 | get: function() { 167 | if (!this.cache.compiler) { 168 | this.cache.compiler = new Compiler(this.options); 169 | } 170 | return this.cache.compiler; 171 | } 172 | }); 173 | 174 | /** 175 | * Get or set a `Snapdragon.Parser` instance. 176 | * @api public 177 | */ 178 | 179 | Object.defineProperty(Snapdragon.prototype, 'parser', { 180 | configurable: true, 181 | set: function(val) { 182 | this.cache.parser = val; 183 | }, 184 | get: function() { 185 | if (!this.cache.parser) { 186 | this.cache.parser = new Parser(this.options); 187 | } 188 | return this.cache.parser; 189 | } 190 | }); 191 | 192 | /** 193 | * Get the compilers from a `Snapdragon.Compiler` instance. 194 | * @api public 195 | */ 196 | 197 | Object.defineProperty(Snapdragon.prototype, 'compilers', { 198 | get: function() { 199 | return this.compiler.compilers; 200 | } 201 | }); 202 | 203 | /** 204 | * Get the parsers from a `Snapdragon.Parser` instance. 205 | * @api public 206 | */ 207 | 208 | Object.defineProperty(Snapdragon.prototype, 'parsers', { 209 | get: function() { 210 | return this.parser.parsers; 211 | } 212 | }); 213 | 214 | /** 215 | * Get the regex cache from a `Snapdragon.Parser` instance. 216 | * @api public 217 | */ 218 | 219 | Object.defineProperty(Snapdragon.prototype, 'regex', { 220 | get: function() { 221 | return this.parser.regex; 222 | } 223 | }); 224 | 225 | /** 226 | * Expose `Parser` and `Compiler` 227 | */ 228 | 229 | Snapdragon.Compiler = Compiler; 230 | Snapdragon.Parser = Parser; 231 | 232 | /** 233 | * Expose `Snapdragon` 234 | */ 235 | 236 | module.exports = Snapdragon; 237 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var use = require('use'); 4 | var util = require('snapdragon-util'); 5 | var Emitter = require('component-emitter'); 6 | var define = require('define-property'); 7 | var extend = require('extend-shallow'); 8 | var error = require('./error'); 9 | 10 | /** 11 | * Create a new `Compiler` with the given `options`. 12 | * 13 | * ```js 14 | * var Snapdragon = require('snapdragon'); 15 | * var Compiler = Snapdragon.Compiler; 16 | * var compiler = new Compiler(); 17 | * ``` 18 | * @param {Object} `options` 19 | * @param {Object} `state` Optionally pass a "state" object to use inside visitor functions. 20 | * @api public 21 | */ 22 | 23 | function Compiler(options, state) { 24 | this.options = extend({source: 'string'}, options); 25 | this.emitter = new Emitter(); 26 | this.on = this.emitter.on.bind(this.emitter); 27 | this.isCompiler = true; 28 | this.state = state || {}; 29 | this.state.inside = this.state.inside || {}; 30 | this.compilers = {}; 31 | this.output = ''; 32 | this.indent = ''; 33 | this.set('eos', function(node) { 34 | return this.emit(node.val, node); 35 | }); 36 | this.set('bos', function(node) { 37 | return this.emit(node.val, node); 38 | }); 39 | use(this); 40 | } 41 | 42 | /** 43 | * Prototype methods 44 | */ 45 | 46 | Compiler.prototype = { 47 | 48 | /** 49 | * Throw a formatted error message with details including the cursor position. 50 | * 51 | * ```js 52 | * compiler.set('foo', function(node) { 53 | * if (node.val !== 'foo') { 54 | * throw this.error('expected node.val to be "foo"', node); 55 | * } 56 | * }); 57 | * ``` 58 | * @name .error 59 | * @param {String} `msg` Message to use in the Error. 60 | * @param {Object} `node` 61 | * @return {undefined} 62 | * @api public 63 | */ 64 | 65 | error: function(/*msg, node*/) { 66 | return error.apply(this, arguments); 67 | }, 68 | 69 | /** 70 | * Concat the given string to `compiler.output`. 71 | * 72 | * ```js 73 | * compiler.set('foo', function(node) { 74 | * this.emit(node.val, node); 75 | * }); 76 | * ``` 77 | * @name .emit 78 | * @param {String} `string` 79 | * @param {Object} `node` Optionally pass the node to use for position if source maps are enabled. 80 | * @return {String} returns the string 81 | * @api public 82 | */ 83 | 84 | emit: function(val, node) { 85 | this.output += val; 86 | return val; 87 | }, 88 | 89 | /** 90 | * Emit an empty string to effectively "skip" the string for the given `node`, 91 | * but still emit the position and node type. 92 | * 93 | * ```js 94 | * // example: do nothing for beginning-of-string 95 | * snapdragon.compiler.set('bos', compiler.noop); 96 | * ``` 97 | * @name .noop 98 | * @param {Object} node 99 | * @api public 100 | */ 101 | 102 | noop: function(node) { 103 | this.emit('', node); 104 | }, 105 | 106 | /** 107 | * Define a non-enumberable property on the `Compiler` instance. This is useful 108 | * in plugins, for exposing methods inside handlers. 109 | * 110 | * ```js 111 | * compiler.define('customMethod', function() { 112 | * // do stuff 113 | * }); 114 | * ``` 115 | * @name .define 116 | * @param {String} `key` propery name 117 | * @param {any} `val` property value 118 | * @return {Object} Returns the Compiler instance for chaining. 119 | * @api public 120 | */ 121 | 122 | define: function(key, val) { 123 | define(this, key, val); 124 | return this; 125 | }, 126 | 127 | /** 128 | * Add a compiler `fn` for the given `type`. Compilers are called 129 | * when the `.compile` method encounters a node of the given type to 130 | * generate the output string. 131 | * 132 | * ```js 133 | * compiler 134 | * .set('comma', function(node) { 135 | * this.emit(','); 136 | * }) 137 | * .set('dot', function(node) { 138 | * this.emit('.'); 139 | * }) 140 | * .set('slash', function(node) { 141 | * this.emit('/'); 142 | * }); 143 | * ``` 144 | * @name .set 145 | * @param {String} `type` 146 | * @param {Function} `fn` 147 | * @api public 148 | */ 149 | 150 | set: function(type, fn) { 151 | this.compilers[type] = fn; 152 | return this; 153 | }, 154 | 155 | /** 156 | * Get the compiler of the given `type`. 157 | * 158 | * ```js 159 | * var fn = compiler.get('slash'); 160 | * ``` 161 | * @name .get 162 | * @param {String} `type` 163 | * @api public 164 | */ 165 | 166 | get: function(type) { 167 | return this.compilers[type]; 168 | }, 169 | 170 | /** 171 | * Visit `node` using the registered compiler function associated with the 172 | * `node.type`. 173 | * 174 | * ```js 175 | * compiler 176 | * .set('i', function(node) { 177 | * this.visit(node); 178 | * }) 179 | * ``` 180 | * @name .visit 181 | * @param {Object} `node` 182 | * @return {Object} returns the node 183 | * @api public 184 | */ 185 | 186 | visit: function(node) { 187 | if (util.isOpen(node)) { 188 | util.addType(this.state, node); 189 | } 190 | 191 | this.emitter.emit('node', node); 192 | 193 | var fn = this.compilers[node.type] || this.compilers.unknown; 194 | if (typeof fn !== 'function') { 195 | throw this.error('compiler "' + node.type + '" is not registered', node); 196 | } 197 | 198 | var val = fn.call(this, node) || node; 199 | if (util.isNode(val)) { 200 | node = val; 201 | } 202 | 203 | if (util.isClose(node)) { 204 | util.removeType(this.state, node); 205 | } 206 | return node; 207 | }, 208 | 209 | /** 210 | * Iterate over `node.nodes`, calling [visit](#visit) on each node. 211 | * 212 | * ```js 213 | * compiler 214 | * .set('i', function(node) { 215 | * utils.mapVisit(node); 216 | * }) 217 | * ``` 218 | * @name .mapVisit 219 | * @param {Object} `node` 220 | * @return {Object} returns the node 221 | * @api public 222 | */ 223 | 224 | mapVisit: function(parent) { 225 | var nodes = parent.nodes || parent.children; 226 | for (var i = 0; i < nodes.length; i++) { 227 | var node = nodes[i]; 228 | if (!node.parent) node.parent = parent; 229 | nodes[i] = this.visit(node) || node; 230 | } 231 | }, 232 | 233 | /** 234 | * Compile the given `AST` and return a string. Iterates over `ast.nodes` 235 | * with [mapVisit](#mapVisit). 236 | * 237 | * ```js 238 | * var ast = parser.parse('foo'); 239 | * var str = compiler.compile(ast); 240 | * ``` 241 | * @name .compile 242 | * @param {Object} `ast` 243 | * @param {Object} `options` Compiler options 244 | * @return {Object} returns the node 245 | * @api public 246 | */ 247 | 248 | compile: function(ast, options) { 249 | var opts = extend({}, this.options, options); 250 | this.ast = ast; 251 | this.output = ''; 252 | 253 | // source map support 254 | if (opts.sourcemap) { 255 | var sourcemaps = require('./source-maps'); 256 | sourcemaps(this); 257 | this.mapVisit(this.ast); 258 | this.applySourceMaps(); 259 | this.map = opts.sourcemap === 'generator' ? this.map : this.map.toJSON(); 260 | } else { 261 | this.mapVisit(this.ast); 262 | } 263 | 264 | return this; 265 | } 266 | }; 267 | 268 | /** 269 | * Expose `Compiler` 270 | */ 271 | 272 | module.exports = Compiler; 273 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var get = require('get-value'); 4 | 5 | module.exports = function(msg, node) { 6 | node = node || {}; 7 | var pos = node.position || {}; 8 | var line = get(node, 'position.end.line') || 1; 9 | var column = get(node, 'position.end.column') || 1; 10 | var source = this.options.source; 11 | 12 | var message = source + ' : ' + msg; 13 | var err = new Error(message); 14 | err.source = source; 15 | err.reason = msg; 16 | err.pos = pos; 17 | 18 | if (this.options.silent) { 19 | this.errors.push(err); 20 | } else { 21 | throw err; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var use = require('use'); 4 | var util = require('snapdragon-util'); 5 | var Cache = require('map-cache'); 6 | var Node = require('snapdragon-node'); 7 | var define = require('define-property'); 8 | var extend = require('extend-shallow'); 9 | var Emitter = require('component-emitter'); 10 | var isObject = require('isobject'); 11 | var Position = require('./position'); 12 | var error = require('./error'); 13 | 14 | /** 15 | * Create a new `Parser` with the given `input` and `options`. 16 | * 17 | * ```js 18 | * var Snapdragon = require('snapdragon'); 19 | * var Parser = Snapdragon.Parser; 20 | * var parser = new Parser(); 21 | * ``` 22 | * @param {String} `input` 23 | * @param {Object} `options` 24 | * @api public 25 | */ 26 | 27 | function Parser(options) { 28 | this.options = extend({source: 'string'}, options); 29 | this.isParser = true; 30 | this.Node = Node; 31 | this.init(this.options); 32 | use(this); 33 | } 34 | 35 | /** 36 | * Prototype methods 37 | */ 38 | 39 | Parser.prototype = Emitter({ 40 | constructor: Parser, 41 | 42 | init: function(options) { 43 | this.orig = ''; 44 | this.input = ''; 45 | this.parsed = ''; 46 | 47 | this.currentType = 'root'; 48 | this.setCount = 0; 49 | this.count = 0; 50 | this.column = 1; 51 | this.line = 1; 52 | 53 | this.regex = new Cache(); 54 | this.errors = this.errors || []; 55 | this.parsers = this.parsers || {}; 56 | this.types = this.types || []; 57 | this.sets = this.sets || {}; 58 | this.fns = this.fns || []; 59 | this.tokens = []; 60 | this.stack = []; 61 | 62 | this.typeStack = []; 63 | this.setStack = []; 64 | 65 | var pos = this.position(); 66 | this.bos = pos(this.node({ 67 | type: 'bos', 68 | val: '' 69 | })); 70 | 71 | this.ast = pos(this.node({ 72 | type: this.options.astType || 'root', 73 | errors: this.errors 74 | })); 75 | 76 | this.ast.pushNode(this.bos); 77 | this.nodes = [this.ast]; 78 | }, 79 | 80 | /** 81 | * Throw a formatted error message with details including the cursor position. 82 | * 83 | * ```js 84 | * parser.set('foo', function(node) { 85 | * if (node.val !== 'foo') { 86 | * throw this.error('expected node.val to be "foo"', node); 87 | * } 88 | * }); 89 | * ``` 90 | * @name .error 91 | * @param {String} `msg` Message to use in the Error. 92 | * @param {Object} `node` 93 | * @return {undefined} 94 | * @api public 95 | */ 96 | 97 | error: function(/*msg, node*/) { 98 | return error.apply(this, arguments); 99 | }, 100 | 101 | /** 102 | * Define a non-enumberable property on the `Parser` instance. This is useful 103 | * in plugins, for exposing methods inside handlers. 104 | * 105 | * ```js 106 | * parser.define('foo', 'bar'); 107 | * ``` 108 | * @name .define 109 | * @param {String} `key` propery name 110 | * @param {any} `val` property value 111 | * @return {Object} Returns the Parser instance for chaining. 112 | * @api public 113 | */ 114 | 115 | define: function(key, val) { 116 | define(this, key, val); 117 | return this; 118 | }, 119 | 120 | /** 121 | * Create a new [Node](#node) with the given `val` and `type`. 122 | * 123 | * ```js 124 | * parser.node('/', 'slash'); 125 | * ``` 126 | * @name .node 127 | * @param {Object} `val` 128 | * @param {String} `type` 129 | * @return {Object} returns the [Node](#node) instance. 130 | * @api public 131 | */ 132 | 133 | node: function(val, type) { 134 | return new this.Node(val, type); 135 | }, 136 | 137 | /** 138 | * Mark position and patch `node.position`. 139 | * 140 | * ```js 141 | * parser.set('foo', function(node) { 142 | * var pos = this.position(); 143 | * var match = this.match(/foo/); 144 | * if (match) { 145 | * // call `pos` with the node 146 | * return pos(this.node(match[0])); 147 | * } 148 | * }); 149 | * ``` 150 | * @name .position 151 | * @return {Function} Returns a function that takes a `node` 152 | * @api public 153 | */ 154 | 155 | position: function() { 156 | var start = { line: this.line, column: this.column }; 157 | var parsed = this.parsed; 158 | var self = this; 159 | 160 | return function(node) { 161 | if (!node.isNode) node = new Node(node); 162 | node.define('position', new Position(start, self)); 163 | node.define('parsed', parsed); 164 | node.define('inside', self.stack.length > 0); 165 | node.define('rest', self.input); 166 | return node; 167 | }; 168 | }, 169 | 170 | /** 171 | * Add parser `type` with the given visitor `fn`. 172 | * 173 | * ```js 174 | * parser.set('all', function() { 175 | * var match = this.match(/^./); 176 | * if (match) { 177 | * return this.node(match[0]); 178 | * } 179 | * }); 180 | * ``` 181 | * @name .set 182 | * @param {String} `type` 183 | * @param {Function} `fn` 184 | * @api public 185 | */ 186 | 187 | set: function(type, fn) { 188 | if (this.types.indexOf(type) === -1) { 189 | this.types.push(type); 190 | } 191 | this.parsers[type] = fn.bind(this); 192 | return this; 193 | }, 194 | 195 | /** 196 | * Get parser `type`. 197 | * 198 | * ```js 199 | * var fn = parser.get('slash'); 200 | * ``` 201 | * @name .get 202 | * @param {String} `type` 203 | * @api public 204 | */ 205 | 206 | get: function(type) { 207 | return this.parsers[type]; 208 | }, 209 | 210 | /** 211 | * Push a node onto the stack for the given `type`. 212 | * 213 | * ```js 214 | * parser.set('all', function() { 215 | * var match = this.match(/^./); 216 | * if (match) { 217 | * var node = this.node(match[0]); 218 | * this.push(node); 219 | * return node; 220 | * } 221 | * }); 222 | * ``` 223 | * @name .push 224 | * @param {String} `type` 225 | * @return {Object} `token` 226 | * @api public 227 | */ 228 | 229 | push: function(type, token) { 230 | this.sets[type] = this.sets[type] || []; 231 | this.count++; 232 | this.stack.push(token); 233 | this.setStack.push(token); 234 | this.typeStack.push(type); 235 | return this.sets[type].push(token); 236 | }, 237 | 238 | /** 239 | * Pop a token off of the stack of the given `type`. 240 | * 241 | * ```js 242 | * parser.set('close', function() { 243 | * var match = this.match(/^\}/); 244 | * if (match) { 245 | * var node = this.node({ 246 | * type: 'close', 247 | * val: match[0] 248 | * }); 249 | * 250 | * this.pop(node.type); 251 | * return node; 252 | * } 253 | * }); 254 | * ``` 255 | * @name .pop 256 | * @param {String} `type` 257 | * @returns {Object} Returns a token 258 | * @api public 259 | */ 260 | 261 | pop: function(type) { 262 | if (this.sets[type]) { 263 | this.count--; 264 | this.stack.pop(); 265 | this.setStack.pop(); 266 | this.typeStack.pop(); 267 | return this.sets[type].pop(); 268 | } 269 | }, 270 | 271 | /** 272 | * Return true if inside a "set" of the given `type`. Sets are created 273 | * manually by adding a type to `parser.sets`. A node is "inside" a set 274 | * when an `*.open` node for the given `type` was previously pushed onto the set. 275 | * The type is removed from the set by popping it off when the `*.close` 276 | * node for the given type is reached. 277 | * 278 | * ```js 279 | * parser.set('close', function() { 280 | * var pos = this.position(); 281 | * var m = this.match(/^\}/); 282 | * if (!m) return; 283 | * if (!this.isInside('bracket')) { 284 | * throw new Error('missing opening bracket'); 285 | * } 286 | * }); 287 | * ``` 288 | * @name .isInside 289 | * @param {String} `type` 290 | * @return {Boolean} 291 | * @api public 292 | */ 293 | 294 | isInside: function(type) { 295 | if (typeof type === 'undefined') { 296 | return this.count > 0; 297 | } 298 | if (!Array.isArray(this.sets[type])) { 299 | return false; 300 | } 301 | return this.sets[type].length > 0; 302 | }, 303 | 304 | isDirectlyInside: function(type) { 305 | if (typeof type === 'undefined') { 306 | return this.count > 0 ? util.last(this.typeStack) : null; 307 | } 308 | return util.last(this.typeStack) === type; 309 | }, 310 | 311 | /** 312 | * Return true if `node` is the given `type`. 313 | * 314 | * ```js 315 | * parser.isType(node, 'brace'); 316 | * ``` 317 | * @name .isType 318 | * @param {Object} `node` 319 | * @param {String} `type` 320 | * @return {Boolean} 321 | * @api public 322 | */ 323 | 324 | isType: function(node, type) { 325 | return node && node.type === type; 326 | }, 327 | 328 | /** 329 | * Get the previous AST node from the `parser.stack` (when inside a nested 330 | * context) or `parser.nodes`. 331 | * 332 | * ```js 333 | * var prev = this.prev(); 334 | * ``` 335 | * @name .prev 336 | * @return {Object} 337 | * @api public 338 | */ 339 | 340 | prev: function(n) { 341 | return this.stack.length > 0 342 | ? util.last(this.stack, n) 343 | : util.last(this.nodes, n); 344 | }, 345 | 346 | /** 347 | * Update line and column based on `str`. 348 | */ 349 | 350 | consume: function(len) { 351 | this.input = this.input.substr(len); 352 | }, 353 | 354 | /** 355 | * Returns the string up to the given `substring`, 356 | * if it exists, and advances the cursor position past the substring. 357 | */ 358 | 359 | advanceTo: function(str, i) { 360 | var idx = this.input.indexOf(str, i); 361 | if (idx !== -1) { 362 | var val = this.input.slice(0, idx); 363 | this.consume(idx + str.length); 364 | return val; 365 | } 366 | }, 367 | 368 | /** 369 | * Update column based on `str`. 370 | */ 371 | 372 | updatePosition: function(str, len) { 373 | var lines = str.match(/\n/g); 374 | if (lines) this.line += lines.length; 375 | var i = str.lastIndexOf('\n'); 376 | this.column = ~i ? len - i : this.column + len; 377 | this.parsed += str; 378 | this.consume(len); 379 | }, 380 | 381 | /** 382 | * Match `regex`, return captures, and update the cursor position by `match[0]` length. 383 | * 384 | * ```js 385 | * // make sure to use the starting regex boundary: "^" 386 | * var match = this.match(/^\./); 387 | * ``` 388 | * @name .prev 389 | * @param {RegExp} `regex` 390 | * @return {Object} 391 | * @api public 392 | */ 393 | 394 | match: function(regex) { 395 | var m = regex.exec(this.input); 396 | if (m) { 397 | this.updatePosition(m[0], m[0].length); 398 | return m; 399 | } 400 | }, 401 | 402 | /** 403 | * Push `node` to `parent.nodes` and assign `node.parent` 404 | */ 405 | 406 | pushNode: function(node, parent) { 407 | if (node && parent) { 408 | if (parent === node) parent = this.ast; 409 | define(node, 'parent', parent); 410 | 411 | if (parent.nodes) parent.nodes.push(node); 412 | if (this.sets.hasOwnProperty(parent.type)) { 413 | this.currentType = parent.type; 414 | } 415 | } 416 | }, 417 | 418 | /** 419 | * Capture end-of-string 420 | */ 421 | 422 | eos: function() { 423 | if (this.input) return; 424 | var pos = this.position(); 425 | var prev = this.prev(); 426 | 427 | while (prev.type !== 'root' && !prev.visited) { 428 | if (this.options.strict === true) { 429 | throw new SyntaxError('invalid syntax:' + prev); 430 | } 431 | 432 | if (!util.hasOpenAndClose(prev)) { 433 | define(prev.parent, 'escaped', true); 434 | define(prev, 'escaped', true); 435 | } 436 | 437 | this.visit(prev, function(node) { 438 | if (!util.hasOpenAndClose(node.parent)) { 439 | define(node.parent, 'escaped', true); 440 | define(node, 'escaped', true); 441 | } 442 | }); 443 | 444 | prev = prev.parent; 445 | } 446 | 447 | var node = pos(this.node(this.append || '', 'eos')); 448 | if (typeof this.options.eos === 'function') { 449 | node = this.options.eos.call(this, node); 450 | } 451 | 452 | if (this.parsers.eos) { 453 | this.parsers.eos.call(this, node); 454 | } 455 | 456 | define(node, 'parent', this.ast); 457 | return node; 458 | }, 459 | 460 | /** 461 | * Run parsers to advance the cursor position 462 | */ 463 | 464 | getNext: function() { 465 | var parsed = this.parsed; 466 | var len = this.types.length; 467 | var idx = -1; 468 | 469 | while (++idx < len) { 470 | var type = this.types[idx]; 471 | var tok = this.parsers[type].call(this); 472 | if (tok === true) { 473 | break; 474 | } 475 | 476 | if (tok) { 477 | tok.type = tok.type || type; 478 | define(tok, 'rest', this.input); 479 | define(tok, 'parsed', parsed); 480 | this.last = tok; 481 | this.tokens.push(tok); 482 | this.emit('node', tok); 483 | return tok; 484 | } 485 | } 486 | }, 487 | 488 | /** 489 | * Run parsers to get the next AST node 490 | */ 491 | 492 | advance: function() { 493 | var input = this.input; 494 | this.pushNode(this.getNext(), this.prev()); 495 | 496 | // if we're here and input wasn't modified, throw an error 497 | if (this.input && input === this.input) { 498 | var chokedOn = this.input.slice(0, 10); 499 | var err = this.error('no parser for: "' + chokedOn, this.last); 500 | if (this.hasListeners('error')) { 501 | this.emit('error', err); 502 | } else { 503 | throw err; 504 | } 505 | } 506 | }, 507 | 508 | /** 509 | * Parse the given string an return an AST object. 510 | * 511 | * ```js 512 | * var ast = parser.parse('foo/bar'); 513 | * ``` 514 | * @param {String} `input` 515 | * @return {Object} Returns an AST with `ast.nodes` 516 | * @api public 517 | */ 518 | 519 | parse: function(input) { 520 | if (typeof input !== 'string') { 521 | throw new TypeError('expected a string'); 522 | } 523 | 524 | this.init(this.options); 525 | this.orig = input; 526 | this.input = input; 527 | 528 | // run parsers 529 | while (this.input) this.advance(); 530 | 531 | // balance unmatched sets, if not disabled 532 | balanceSets(this, this.stack.pop()); 533 | 534 | // create end-of-string node 535 | var eos = this.eos(); 536 | var ast = this.prev(); 537 | if (ast.type === 'root') { 538 | this.pushNode(eos, ast); 539 | } 540 | return this.ast; 541 | }, 542 | 543 | /** 544 | * Visit `node` with the given `fn` 545 | */ 546 | 547 | visit: function(node, fn) { 548 | if (!isObject(node) || node.isNode !== true) { 549 | throw new Error('expected node to be an instance of Node'); 550 | } 551 | if (node.visited) return; 552 | node.define('visited', true); 553 | node = fn(node) || node; 554 | if (node.nodes) { 555 | this.mapVisit(node.nodes, fn, node); 556 | } 557 | return node; 558 | }, 559 | 560 | /** 561 | * Map visit over array of `nodes`. 562 | */ 563 | 564 | mapVisit: function(nodes, fn, parent) { 565 | for (var i = 0; i < nodes.length; i++) { 566 | this.visit(nodes[i], fn); 567 | } 568 | } 569 | }); 570 | 571 | function balanceSets(parser, node) { 572 | if (node && parser.options.strict === true) { 573 | throw parser.error('imbalanced "' + node.type + '": "' + parser.orig + '"'); 574 | } 575 | if (node && node.nodes && node.nodes.length) { 576 | var first = node.nodes[0]; 577 | first.val = '\\' + first.val; 578 | } 579 | } 580 | 581 | /** 582 | * Expose `Parser` 583 | */ 584 | 585 | module.exports = Parser; 586 | -------------------------------------------------------------------------------- /lib/position.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Store the position for a node 5 | */ 6 | 7 | module.exports = function Position(start, parser) { 8 | this.start = start; 9 | this.end = { line: parser.line, column: parser.column }; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/source-maps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var define = require('define-property'); 6 | var sourceMapResolve = require('source-map-resolve'); 7 | var SourceMap = require('source-map'); 8 | 9 | /** 10 | * Expose `mixin()`. 11 | * This code is based on `source-maps-support.js` in reworkcss/css 12 | * https://github.com/reworkcss/css/blob/master/lib/stringify/source-map-support.js 13 | * Copyright (c) 2012 TJ Holowaychuk 14 | */ 15 | 16 | module.exports = mixin; 17 | 18 | /** 19 | * Mixin source map support into `compiler`. 20 | * 21 | * @param {Object} `compiler` 22 | * @api public 23 | */ 24 | 25 | function mixin(compiler) { 26 | define(compiler, '_comment', compiler.comment); 27 | compiler.map = new SourceMap.SourceMapGenerator(); 28 | compiler.position = { line: 1, column: 1 }; 29 | compiler.content = {}; 30 | compiler.files = {}; 31 | 32 | for (var key in exports) { 33 | define(compiler, key, exports[key]); 34 | } 35 | } 36 | 37 | /** 38 | * Update position. 39 | * 40 | * @param {String} str 41 | */ 42 | 43 | exports.updatePosition = function(str) { 44 | var lines = str.match(/\n/g); 45 | if (lines) this.position.line += lines.length; 46 | var i = str.lastIndexOf('\n'); 47 | this.position.column = ~i ? str.length - i : this.position.column + str.length; 48 | }; 49 | 50 | /** 51 | * Emit `str` with `position`. 52 | * 53 | * @param {String} str 54 | * @param {Object} [pos] 55 | * @return {String} 56 | */ 57 | 58 | exports.emit = function(str, node) { 59 | var position = node.position || {}; 60 | var source = position.source; 61 | if (source) { 62 | if (position.filepath) { 63 | source = unixify(position.filepath); 64 | } 65 | 66 | this.map.addMapping({ 67 | source: source, 68 | generated: { 69 | line: this.position.line, 70 | column: Math.max(this.position.column - 1, 0) 71 | }, 72 | original: { 73 | line: position.start.line, 74 | column: position.start.column - 1 75 | } 76 | }); 77 | 78 | if (position.content) { 79 | this.addContent(source, position); 80 | } 81 | if (position.filepath) { 82 | this.addFile(source, position); 83 | } 84 | } 85 | 86 | this.updatePosition(str); 87 | this.output += str; 88 | return str; 89 | }; 90 | 91 | /** 92 | * Adds a file to the source map output if it has not already been added 93 | * @param {String} `file` 94 | * @param {Object} `pos` 95 | */ 96 | 97 | exports.addFile = function(file, position) { 98 | if (typeof position.content !== 'string') return; 99 | if (Object.prototype.hasOwnProperty.call(this.files, file)) return; 100 | this.files[file] = position.content; 101 | }; 102 | 103 | /** 104 | * Adds a content source to the source map output if it has not already been added 105 | * @param {String} `source` 106 | * @param {Object} `position` 107 | */ 108 | 109 | exports.addContent = function(source, position) { 110 | if (typeof position.content !== 'string') return; 111 | if (Object.prototype.hasOwnProperty.call(this.content, source)) return; 112 | this.map.setSourceContent(source, position.content); 113 | }; 114 | 115 | /** 116 | * Applies any original source maps to the output and embeds the source file 117 | * contents in the source map. 118 | */ 119 | 120 | exports.applySourceMaps = function() { 121 | Object.keys(this.files).forEach(function(file) { 122 | var content = this.files[file]; 123 | this.map.setSourceContent(file, content); 124 | 125 | if (this.options.inputSourcemaps === true) { 126 | var originalMap = sourceMapResolve.resolveSync(content, file, fs.readFileSync); 127 | if (originalMap) { 128 | var map = new SourceMap.SourceMapConsumer(originalMap.map); 129 | var relativeTo = originalMap.sourcesRelativeTo; 130 | this.map.applySourceMap(map, file, unixify(path.dirname(relativeTo))); 131 | } 132 | } 133 | }, this); 134 | }; 135 | 136 | /** 137 | * Process comments, drops sourceMap comments. 138 | * @param {Object} node 139 | */ 140 | 141 | exports.comment = function(node) { 142 | if (/^# sourceMappingURL=/.test(node.comment)) { 143 | return this.emit('', node.position); 144 | } 145 | return this._comment(node); 146 | }; 147 | 148 | /** 149 | * Convert backslash in the given string to forward slashes 150 | */ 151 | 152 | function unixify(fp) { 153 | return fp.split(/\\+/).join('/'); 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snapdragon", 3 | "description": "Easy-to-use plugin system for creating powerful, fast and versatile parsers and compilers, with built-in source-map support.", 4 | "version": "0.12.1", 5 | "homepage": "https://github.com/here-be/snapdragon", 6 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)", 7 | "contributors": [ 8 | "Brian Woodward (https://twitter.com/doowb)", 9 | "Daniel Tschinder (https://github.com/danez)", 10 | "Jon Schlinkert (http://twitter.com/jonschlinkert)" 11 | ], 12 | "repository": "here-be/snapdragon", 13 | "bugs": { 14 | "url": "https://github.com/here-be/snapdragon/issues" 15 | }, 16 | "license": "MIT", 17 | "files": [ 18 | "index.js", 19 | "lib" 20 | ], 21 | "main": "index.js", 22 | "engines": { 23 | "node": ">=0.10.0" 24 | }, 25 | "scripts": { 26 | "test": "mocha" 27 | }, 28 | "dependencies": { 29 | "component-emitter": "^1.2.1", 30 | "define-property": "^2.0.2", 31 | "extend-shallow": "^3.0.2", 32 | "get-value": "^2.0.6", 33 | "isobject": "^3.0.0", 34 | "map-cache": "^0.2.2", 35 | "snapdragon-node": "^1.0.6", 36 | "snapdragon-util": "^4.0.0", 37 | "source-map": "^0.5.6", 38 | "source-map-resolve": "^0.6.0", 39 | "use": "^3.1.0" 40 | }, 41 | "devDependencies": { 42 | "mocha": "^3.2.0", 43 | "snapdragon-capture-set": "^1.0.1", 44 | "snapdragon-capture": "^0.2.0", 45 | "gulp": "^3.9.1", 46 | "gulp-istanbul": "^1.1.1", 47 | "gulp-eslint": "^3.0.1", 48 | "gulp-mocha": "^3.0.1", 49 | "gulp-unused": "^0.2.1", 50 | "gulp-format-md": "^0.1.11", 51 | "verb-generate-readme": "^0.6.0" 52 | }, 53 | "keywords": [ 54 | "lexer", 55 | "snapdragon" 56 | ], 57 | "verb": { 58 | "toc": "collapsible", 59 | "layout": "default", 60 | "tasks": [ 61 | "readme" 62 | ], 63 | "plugins": [ 64 | "gulp-format-md" 65 | ], 66 | "related": { 67 | "description": "A few of the libraries that use snapdragon:", 68 | "implementations": [ 69 | "braces", 70 | "breakdance", 71 | "expand-brackets", 72 | "extglob", 73 | "micromatch", 74 | "nanomatch" 75 | ], 76 | "list": [ 77 | "snapdragon-capture", 78 | "snapdragon-capture-set", 79 | "snapdragon-node", 80 | "snapdragon-util" 81 | ] 82 | }, 83 | "reflinks": [ 84 | "css", 85 | "pug", 86 | "snapdragon-capture", 87 | "snapdragon-capture-set", 88 | "snapdragon-node" 89 | ], 90 | "lint": { 91 | "reflinks": true 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /support/src/content/compiling.md: -------------------------------------------------------------------------------- 1 | # Compiling with snapdragon 2 | 3 |
4 | Pre-requisites 5 | If you're not quite sure how an AST works, don't sweat it. Not every programmer needs to interact with an AST, and the first experience with one is daunting for everyone. 6 | 7 | To get the most from this documentation, we suggest you head over to the [begin/parsers-compilers](https://github.com/begin/parsers-compilers) project to brush up. Within a few minutes you'll know everything you need to proceed! 8 |
9 | 10 | -------------------------------------------------------------------------------- /support/src/content/core-concepts.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Core concepts 4 | 5 | - [Lexer](#parser) 6 | * Token Stream 7 | * Token 8 | * Scope 9 | - [Parser](#parser) 10 | * [Node](#node) 11 | * Stack 12 | * [AST](#ast) 13 | - [Compiler](#compiler) 14 | * State 15 | - [Renderer](#renderer) 16 | * Contexts 17 | * Context 18 | 19 | ## Lexer 20 | 21 | - [ ] Token 22 | - [ ] Tokens 23 | - [ ] Scope 24 | 25 | ## Parser 26 | 27 | ### AST 28 | 29 | TODO 30 | 31 | ### Node 32 | 33 | #### Properties 34 | 35 | Officially supported properties 36 | 37 | - `type` 38 | - `val` 39 | - `nodes` 40 | 41 | **Related** 42 | 43 | - The [snapdragon-position][] plugin adds support for `node.position`, which patches the `node` with the start and end position of a captured value. 44 | - The [snapdragon-scope][] plugin adds support for `node.scope`, which patches the `node` with lexical scope of the node. 45 | 46 | 47 | ## Compiler 48 | 49 | TODO 50 | 51 | ## Renderer 52 | 53 | TODO 54 | 55 | 56 | [verb][] 57 | -------------------------------------------------------------------------------- /support/src/content/crash-course.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | ## Crash course 4 | 5 | ### Parser 6 | 7 | The parser's job is create an AST from a string. It does this by looping over registered parser-middleware to create nodes from captured substrings. 8 | 9 | **Parsing** 10 | 11 | When a middleware returns a node, the parser updates the string position and starts over again with the first middleware. 12 | 13 | **Parser middleware** 14 | 15 | Each parser-middleware is responsible for matching and capturing a specific "type" of substring, and optionally returning a `node` with information about what was captured. 16 | 17 | 18 | **Node** 19 | 20 | A `node` is an object that is used for storing information about a captured substring, or to mark a significant point or delimiter in the AST or string. 21 | 22 | The only required property is `node.type`. 23 | 24 | Every node has a `node.type` that 25 | 26 | semantically describes a substring that was captured by a middleware - or some other purpose of the node, along with any other information that might be useful later during parsing or compiling. 27 | 28 | 29 | of a specific `node.type` that semantically describes the capturing substrings 30 | . Matching is typically performed using a regular expression, but any means can be used. 31 | 32 | Upon capturing a substring, the parser-middleware 33 | 34 | - capturing and/or further processing relevant part(s) of the captured substring 35 | - returning a node with information that semantically describes the substring that was captured, along with 36 | 37 | When a parser returns a node, that indicates 38 | 39 | by calling each user-defined middleware (referred to as "parsers") until one returns a node. 40 | Each parser middleware 41 | middleware 42 | a string and calling user-defined "parsers" 43 | 44 | **AST** 45 | 46 | which is an object with "nodes", where each "node" is an object with a `type` 47 | 48 | **Nodes** 49 | 50 | A `node` is an object that is used for storing and describing information about a captured substring. 51 | 52 | Every node in the AST has a `type` property, and either: 53 | 54 | - `val`: a captured substring 55 | - `nodes`: an array of child nodes 56 | 57 | When the substring is delimited - by, for example, braces, brackets, parentheses, etc - the `node` will 58 | 59 | In fact, the AST itself is a `node` with type `root`, and a `nodes` array, which contains all of other nodes on the AST. 60 | 61 | **Example** 62 | 63 | The absolute simplest AST for a single-character string might look something like this: 64 | 65 | ```js 66 | var ast = { 67 | type: 'root', 68 | nodes: [ 69 | { 70 | type: 'text', 71 | val: 'a' 72 | } 73 | ] 74 | }; 75 | ``` 76 | 77 | Nodes may have any additional properties, but they must have 78 | 79 | Parsers and compilers have a one-to-one relationship. 80 | 81 | The parser uses middleware for 82 | 83 | 84 | Some of the node "types" on our final AST will also roughly end up reflecting the goals we described in our high level strategy. Our strategy gives us a starting point, but it's good to be flexible. In reality our parser might end up doing something completely different than what we expected in the beginning. 85 | 86 | ### Compiler 87 | 88 | The compiler's job is to render a string. It does this by iterating over an AST, and using the information contained in each node to determine what to render. 89 | 90 | **A compiler for every parser** 91 | 92 | Parsers and compilers have a one-to-one relationship. 93 | 94 | The parser uses middleware for 95 | 96 | -------------------------------------------------------------------------------- /support/src/content/getting-started.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Getting started 4 | 5 | [What is snapdragon, and who created it?](overview.html) 6 | 7 | ## Table of contents 8 | 9 | - Installing snapdragon 10 | - Basic usage 11 | - Next steps 12 | 13 | 14 | ## Installing snapdragon 15 | 16 | ## Usage documentation 17 | 18 | **Learn how to use snapdragon** 19 | 20 | The following documentation tells you how to download and start using snapdragon. If you're intestested in creating snapdragon plugins, or you want to understand more about how snapdragon works you can find links to [developer documentation](#developer-documentation) below. 21 | 22 | is API-focused 23 | how to the API methods that are 24 | 25 | ## Developer documentation 26 | 27 | **Learn how to create plugins or hack on snapdragon** 28 | 29 | In the developer documentation, you will learn how Snapdragon works "under the hood" and how to create plugins. If you're more interested in test driving snapdragon, 30 | -------------------------------------------------------------------------------- /support/src/content/guides/creating-your-first-parser.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating your first Snapdragon parser 3 | --- 4 | 5 | This guide will show you how to create a basic parser by starting off with the string we want to parse, and gradually adding the code we need based on feedback from Snapdragon. 6 | 7 | Let's go! 8 | 9 | ## Prerequisites 10 | 11 | Before we dive in, let's make sure you have snapdragon installed and setup properly. 12 | 13 | ### Install snapdragon 14 | 15 | You can use either [npm](https://npmjs.com) or [yarn](https://yarnpkg.com/) to install snapdragon: 16 | 17 | **Install with NPM** 18 | 19 | ```sh 20 | $ npm install snapdragon 21 | ``` 22 | 23 | **Install with yarn** 24 | 25 | ```sh 26 | $ yarn add snapdragon 27 | ``` 28 | 29 | ### Setup snapdragon 30 | 31 | Create a file in the current working directory named `parser.js` (or whatever you prefer), and add the following code: 32 | 33 | ```js 34 | // add snapdragon using node's "require()" 35 | var Snapdragon = require('snapdragon'); 36 | 37 | // create an instance of Snapdragon. This is the basis for your very own application. 38 | var snapdragon = new Snapdragon(); 39 | ``` 40 | 41 | With that out of the way, let's get started on our parser! 42 | 43 | ## Parsing strategy 44 | 45 | Feel free to skip this section and jump [straight to the code](#learning-by-doing), or follow along as we discuss our high-level parser strategy and goals. 46 | 47 | ### Defining success 48 | 49 | The purpose of this guide isn't to parse something complicated or super-interesting. It's to show you how the parser works. If we accomplish that, then you're only limited by your imagination! 50 | 51 | **The goal** 52 | 53 | The string we're going to parse is: `foo/*.js` (a basic glob pattern). 54 | 55 | _(sidebar: whilst there are numerous approaches one could take to parsing or tokenizing any string, and there are many other factors that would need to be considered, such as escaping, user-defined options, and so on, we're going to keep this simple for illustrative purposes, thus these things fall outside of the scope of this guide)_ 56 | 57 | It's always good to have a basic parsing strategy before you start. As it relates to glob patterns, our high level strategy might be something like: "I want my parser to be able to differentiate between wildcards (stars in this case), slashes, and non-wildcard strings". 58 | 59 | Our parser will be considered "successful" once it is able to do these things. 60 | 61 | ### Begin with the end in mind 62 | 63 | **The expected result** 64 | 65 | Our final AST will be an object with "nodes", where each "node" is an object with a `type` that semantically describes a substring that was captured by the parser. 66 | 67 | Some of the node "types" on our final AST will also roughly end up reflecting the goals we described in our high level strategy. Our strategy gives us a starting point, but it's good to be flexible. In reality our parser might end up doing something completely different than what we expected in the beginning. 68 | 69 | ## Learning by doing 70 | 71 | Okay, it's time to start writing code. To parse `foo/*.js` we'll need to figure out how to capture each "type" of substring. 72 | 73 | Although we won't be doing any compiling in this guide, it will help to understand the role the compiler plays, so that you can factor that into your decisions with the parser. 74 | 75 | **For every node type, there is a parser and a compiler** 76 | 77 | 78 | 79 | 80 | 81 | The actual approach you use for determining where one substring ends and another begins can be a combination of regex, string position/index, or any other mechanism available to you in javascript. Whatever approach you take, Snapdragon's job is to make it as easy as possible for for you. 82 | 83 | ** 84 | 85 | 86 | 87 | node `type` Snapdragon uses "parsers" are the middleware that to capture substrings. This is what we're going to create next. 88 | 89 | 90 | But instead of thinking about code and what to capture, let's try a different approach and take advantage of snapdragon's error reporting to figure out the next step. 91 | 92 | Update `parser.js` with the following code: 93 | 94 | ```js 95 | var Snapdragon = require('snapdragon'); 96 | var snapdragon = new Snapdragon(); 97 | 98 | /** 99 | * 100 | */ 101 | 102 | var ast = snapdragon.parse('foo/*.js'); 103 | console.log(ast); 104 | ``` 105 | 106 | Then run the following command: 107 | 108 | ```sh 109 | $ node parser.js 110 | ``` 111 | 112 | You should see an error message that looks something like the following: 113 | 114 | ```console 115 | Error: string : no parser for: "foo/*.js 116 | ``` 117 | 118 | There are a few important bits of information in this message: 119 | 120 | - `line:1 column: 1` tells us where in the input string this is happening. It's no surprise that we're getting an error on the very first character of our string. 121 | - `no parser for:` tells us that no "parsers" are registered for the substring that follows in the error message. 122 | 123 | 124 | ### 125 | -------------------------------------------------------------------------------- /support/src/content/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | WIP (draft) 4 | -------------------------------------------------------------------------------- /support/src/content/overview.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Overview 4 | 5 | Thanks for visiting the snapdragon documentation! Please [let us know](../../issues) if you find any typos, outdated or incorrect information. Pull requests welcome. 6 | 7 | ## What is snapdragon? 8 | 9 | At its heart, snapdragon does two things: 10 | 11 | - Parsing: the [snapdragon parser](parsing.md) takes a string and converts it to an AST 12 | - Compiling: the [snapdragon compiler](compiling.md) takes the AST from the snapdragon parser and converts it to another string. 13 | 14 | **Plugins** 15 | 16 | 17 | 18 | ## What can snapdragon do? 19 | 20 | You can use snapdragon to parse and convert a string into something entirely different, or use it to create "formatters" for beautifying code or plain text. 21 | 22 | **In the wild** 23 | 24 | Here's how some real projects are using snapdragon: 25 | 26 | * [breakdance][]: uses snapdragon to convert HTML to markdown using an AST from [cheerio][]: 27 | * [micromatch][]: uses snapdragon to create regex from glob patterns 28 | * [extglob][]: uses snapdragon to create regex from glob patterns 29 | * [braces][]: uses snapdragon to create regex for bash-like brace-expansion 30 | * [expand-reflinks][]: uses snapdragon to parse and re-write markdown [reference links](http://spec.commonmark.org/0.25/#link-reference-definitions) 31 | 32 | ## About 33 | 34 | Snapdragon was created by, [Jon Schlinkert]({%= author.url %}), author of [assemble][], [generate][], [update][], [micromatch][], [remarkable][] and many other node.js projects. 35 | 36 | If you'd like to learn more about me or my projects, or you want to get in touch, please feel free to: 37 | 38 | - follow me on [github]({%= author.twitter %}) for notifications and updates about my github projects 39 | - follow me on [twitter]({%= author.twitter %}) 40 | - connect with me on [linkedin](https://www.linkedin.com/in/jonschlinkert) 41 | -------------------------------------------------------------------------------- /support/src/content/parsing.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Parsing with snapdragon 4 | 5 |
6 | Pre-requisites 7 | If you're not quite sure how an AST works, don't sweat it. Not every programmer needs to interact with an AST, and the first experience with one is daunting for everyone. 8 | 9 | To get the most from this documentation, we suggest you head over to the [begin/parsers-compilers](https://github.com/begin/parsers-compilers) project to brush up. Within a few minutes you'll know everything you need to proceed! 10 |
11 | 12 | 13 |
14 | Table of contents 15 | - Usage 16 | - Developer 17 | * Parser 18 | * Parsers 19 | * Custom parsers 20 |
21 | 22 | ## API 23 | 24 | ## Parser 25 | 26 | The snapdragon [Parser]() class contains all of the functionality and methods that are used for creating an AST from a string. 27 | 28 | To understand what `Parser` does, 29 | 30 | The snapdragon parser takes a string and creates an by 31 | 32 | 1. looping over the string 33 | 1. invoking registered [parsers](#parsers) to create new AST nodes. 34 | 35 | The following documentation describes this in more detail. 36 | 37 | 38 | checking to see if any registered [parsers](#parsers) match the sub-string at the current position, and: 39 | * if a parser matches, it is called, possibly resuling in a new AST node (this is up to the parser function) 40 | * if _no matches are found_, an error is throw notifying you that the s 41 | 42 | 43 | ## Parsers 44 | 45 | Snapdragon parsers are functions that are registered by name, and are invoked by the `.parse` method as it loops over the given string. 46 | 47 | **How parsers work** 48 | 49 | A very basic parser function might look something like this: 50 | 51 | ```js 52 | function() { 53 | var parsed = this.parsed; 54 | var pos = this.position(); 55 | var m = this.match(regex); 56 | if (!m || !m[0]) return; 57 | 58 | var prev = this.prev(); 59 | var node = pos({ 60 | type: type, 61 | val: m[0] 62 | }); 63 | 64 | define(node, 'match', m); 65 | define(node, 'inside', this.stack.length > 0); 66 | define(node, 'parent', prev); 67 | define(node, 'parsed', parsed); 68 | define(node, 'rest', this.input); 69 | prev.nodes.push(node); 70 | } 71 | ``` 72 | 73 | TODO 74 | 75 | 76 | ## Custom parsers 77 | 78 | TODO 79 | 80 | ## Plugins 81 | 82 | TODO 83 | 84 | 85 | ```js 86 | parser.use(function() {}); 87 | ``` 88 | 89 | 90 | ```js 91 | snapdragon.parser.use(function() {}); 92 | ``` 93 | -------------------------------------------------------------------------------- /support/src/content/plugins.md: -------------------------------------------------------------------------------- 1 | WIP (draft) 2 | 3 | # Snapdragon plugins 4 | 5 | ```js 6 | var snapdragon = new Snapdgragon(); 7 | // register plugins 8 | snapdragon.use(function() {}); 9 | 10 | // register parser plugins 11 | snapdragon.parser.use(function() {}); 12 | 13 | // register compiler plugins 14 | snapdragon.compiler.use(function() {}); 15 | 16 | // parse 17 | var ast = snapdragon.parse('foo/bar'); 18 | ``` 19 | -------------------------------------------------------------------------------- /test/compile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Compile = require('../lib/compiler'); 6 | var Parser = require('../lib/parser'); 7 | var compiler; 8 | var parser; 9 | 10 | describe('compiler', function() { 11 | beforeEach(function() { 12 | compiler = new Compile(); 13 | compiler 14 | .set('parens.open', function(node) { 15 | return this.emit('(', node); 16 | }) 17 | .set('parens.close', function(node) { 18 | return this.emit(')', node); 19 | }); 20 | parser = new Parser(); 21 | parser 22 | .set('text', function() { 23 | var pos = this.position(); 24 | var match = this.match(/^\w+/); 25 | if (match) { 26 | return pos(this.node(match[0])); 27 | } 28 | }) 29 | .set('slash', function() { 30 | var pos = this.position(); 31 | var match = this.match(/^\//); 32 | if (match) { 33 | return pos(this.node(match[0])) 34 | } 35 | }) 36 | .set('parens.open', function() { 37 | var pos = this.position(); 38 | var match = this.match(/^\(/); 39 | if (match) { 40 | return pos(this.node(match[0])) 41 | } 42 | }) 43 | .set('parens.close', function() { 44 | var pos = this.position(); 45 | var match = this.match(/^\)/); 46 | if (match) { 47 | return pos(this.node(match[0])) 48 | } 49 | }); 50 | }); 51 | 52 | describe('errors', function(cb) { 53 | it('should throw an error when a compiler is missing', function(cb) { 54 | try { 55 | var ast = parser.parse('a/b/c'); 56 | compiler.compile(ast); 57 | cb(new Error('expected an error')); 58 | } catch (err) { 59 | assert(err); 60 | assert.equal(err.message, 'string : compiler "text" is not registered'); 61 | cb(); 62 | } 63 | }); 64 | }); 65 | 66 | describe('.compile', function() { 67 | beforeEach(function() { 68 | compiler 69 | .set('text', function(node) { 70 | return this.emit(node.val); 71 | }) 72 | .set('slash', function(node) { 73 | return this.emit('-'); 74 | }); 75 | }); 76 | 77 | it('should set the result on `output`', function() { 78 | var ast = parser.parse('a/b/c'); 79 | var res = compiler.compile(ast); 80 | assert.equal(res.output, 'a-b-c'); 81 | }); 82 | 83 | it('should compile close without open', function() { 84 | var ast = parser.parse('a)'); 85 | var res = compiler.compile(ast); 86 | assert.equal(res.output, 'a)'); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Compiler = require('../lib/compiler'); 6 | var compiler; 7 | 8 | describe('compiler', function() { 9 | beforeEach(function() { 10 | compiler = new Compiler(); 11 | }); 12 | 13 | describe('constructor:', function() { 14 | it('should return an instance of Compiler:', function() { 15 | assert(compiler instanceof Compiler); 16 | }); 17 | }); 18 | 19 | // ensures that we catch and document API changes 20 | describe('prototype methods:', function() { 21 | var methods = [ 22 | 'error', 23 | 'set', 24 | 'emit', 25 | 'visit', 26 | 'mapVisit', 27 | 'compile' 28 | ]; 29 | 30 | methods.forEach(function(method) { 31 | it('should expose the `' + method + '` method', function() { 32 | assert.equal(typeof compiler[method], 'function', method); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | var captureSet = require('snapdragon-capture-set'); 7 | var Parser = require('../lib/parser'); 8 | var parser; 9 | var ast; 10 | 11 | describe('parser', function() { 12 | beforeEach(function() { 13 | parser = new Parser(); 14 | parser.use(captureSet()); 15 | parser.captureSet('brace', /^\{/, /^\}/); 16 | 17 | parser.set('text', function() { 18 | var pos = this.position(); 19 | var match = this.match(/^[^{}]/); 20 | if (match) { 21 | return pos(this.node(match[0])); 22 | } 23 | }); 24 | 25 | parser.set('comma', function() { 26 | var pos = this.position(); 27 | var match = this.match(/,/); 28 | if (match) { 29 | return pos(this.node(match[0])); 30 | } 31 | }); 32 | 33 | ast = parser.parse('a{b,{c,d},e}f'); 34 | }); 35 | 36 | describe('.isType', function() { 37 | it('should return true if "node" is the given "type"', function() { 38 | assert(ast.isType('root')); 39 | assert(ast.nodes[0].isType('bos')); 40 | }); 41 | }); 42 | 43 | describe('.hasType', function() { 44 | it('should return true if "node" has the given "type"', function() { 45 | assert(ast.hasType('bos')); 46 | assert(ast.hasType('eos')); 47 | }); 48 | }); 49 | 50 | describe('.first', function() { 51 | it('should get the first node in node.nodes', function() { 52 | assert(ast.first); 53 | assert(ast.first.isType('bos')); 54 | }); 55 | }); 56 | 57 | describe('.last', function() { 58 | it('should get the last node in node.nodes', function() { 59 | assert(ast.last); 60 | assert(ast.last.isType('eos')); 61 | }); 62 | }); 63 | 64 | describe('.next', function() { 65 | it('should get the next node in an array of nodes', function() { 66 | 67 | // console.log(ast) 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | var Parser = require('../lib/parser'); 7 | var parser; 8 | 9 | describe('parser', function() { 10 | beforeEach(function() { 11 | parser = new Parser(); 12 | }); 13 | 14 | describe('errors', function(cb) { 15 | it('should throw an error when invalid args are passed to parse', function(cb) { 16 | var parser = new Parser(); 17 | try { 18 | parser.parse(); 19 | cb(new Error('expected an error')); 20 | } catch (err) { 21 | assert(err); 22 | assert.equal(err.message, 'expected a string'); 23 | cb(); 24 | } 25 | }); 26 | }); 27 | 28 | describe('bos', function() { 29 | it('should set a beginning-of-string node', function() { 30 | var parser = new Parser(); 31 | parser.set('all', function() { 32 | var pos = this.position(); 33 | var m = this.match(/^.*/); 34 | if (!m) return; 35 | return pos({ 36 | type: 'all', 37 | val: m[0] 38 | }); 39 | }); 40 | 41 | var ast = parser.parse('a/b'); 42 | assert.equal(ast.nodes[0].type, 'bos'); 43 | }); 44 | }); 45 | 46 | describe('eos', function() { 47 | it('should set an end-of-string node', function() { 48 | var parser = new Parser(); 49 | parser.set('all', function() { 50 | var pos = this.position(); 51 | var m = this.match(/^.*/); 52 | if (!m) return; 53 | return pos({ 54 | type: 'all', 55 | val: m[0] 56 | }); 57 | }); 58 | 59 | var ast = parser.parse('a/b'); 60 | assert.equal(ast.nodes[ast.nodes.length - 1].type, 'eos'); 61 | }); 62 | }); 63 | 64 | describe('.set():', function() { 65 | it('should register middleware', function() { 66 | parser.set('all', function() { 67 | var pos = this.position(); 68 | var m = this.match(/^.*/); 69 | if (!m) return; 70 | return pos({ 71 | type: 'all', 72 | val: m[0] 73 | }); 74 | }); 75 | 76 | parser.parse('a/b'); 77 | assert(parser.parsers.hasOwnProperty('all')); 78 | }); 79 | 80 | it('should use middleware to parse', function() { 81 | parser.set('all', function() { 82 | var pos = this.position(); 83 | var m = this.match(/^.*/); 84 | if (!m) return; 85 | return pos({ 86 | type: 'all', 87 | val: m[0] 88 | }); 89 | }); 90 | 91 | parser.parse('a/b'); 92 | assert.equal(parser.parsed, 'a/b'); 93 | assert.equal(parser.input, ''); 94 | }); 95 | 96 | it('should create ast node:', function() { 97 | parser.set('all', function() { 98 | var pos = this.position(); 99 | var m = this.match(/^.*/); 100 | if (!m) return; 101 | return pos({ 102 | type: 'all', 103 | val: m[0] 104 | }); 105 | }); 106 | 107 | parser.parse('a/b'); 108 | assert.equal(parser.ast.nodes.length, 3); 109 | }); 110 | 111 | it('should be chainable:', function() { 112 | parser 113 | .set('text', function() { 114 | var pos = this.position(); 115 | var m = this.match(/^\w+/); 116 | if (!m) return; 117 | return pos({ 118 | type: 'text', 119 | val: m[0] 120 | }); 121 | }) 122 | .set('slash', function() { 123 | var pos = this.position(); 124 | var m = this.match(/^\//); 125 | if (!m) return; 126 | return pos({ 127 | type: 'slash', 128 | val: m[0] 129 | }); 130 | }); 131 | 132 | parser.parse('a/b'); 133 | assert.equal(parser.ast.nodes.length, 5); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('ast', function() { 139 | beforeEach(function() { 140 | parser = new Parser(); 141 | parser 142 | .set('text', function() { 143 | var pos = this.position(); 144 | var m = this.match(/^\w+/); 145 | if (!m) return; 146 | return pos({ 147 | type: 'text', 148 | val: m[0] 149 | }); 150 | }) 151 | .set('slash', function() { 152 | var pos = this.position(); 153 | var m = this.match(/^\//); 154 | if (!m) return; 155 | return pos({ 156 | type: 'slash', 157 | val: m[0] 158 | }); 159 | }); 160 | }); 161 | 162 | describe('orig:', function() { 163 | it('should add pattern to orig property', function() { 164 | parser.parse('a/b'); 165 | assert.equal(parser.orig, 'a/b'); 166 | }); 167 | }); 168 | 169 | describe('recursion', function() { 170 | beforeEach(function() { 171 | parser.set('text', function() { 172 | var pos = this.position(); 173 | var m = this.match(/^\w/); 174 | if (!m) return; 175 | return pos({ 176 | val: m[0] 177 | }); 178 | }); 179 | 180 | parser.set('open', function() { 181 | var pos = this.position(); 182 | var m = this.match(/^\{/); 183 | if (!m) return; 184 | return pos({ 185 | val: m[0] 186 | }); 187 | }); 188 | 189 | parser.set('close', function() { 190 | var pos = this.position(); 191 | var m = this.match(/^\}/); 192 | if (!m) return; 193 | return pos({ 194 | val: m[0] 195 | }); 196 | }); 197 | 198 | parser.set('comma', function() { 199 | var pos = this.position(); 200 | var m = this.match(/,/); 201 | if (!m) return; 202 | return pos({ 203 | val: m[0] 204 | }); 205 | }); 206 | }); 207 | 208 | it('should set original string on `orig`', function() { 209 | parser.parse('a{b,{c,d},e}f'); 210 | assert.equal(parser.orig, 'a{b,{c,d},e}f'); 211 | }); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Parser = require('../lib/parser'); 6 | var parser; 7 | 8 | describe('parser', function() { 9 | beforeEach(function() { 10 | parser = new Parser(); 11 | }); 12 | 13 | describe('constructor:', function() { 14 | it('should return an instance of Parser:', function() { 15 | assert(parser instanceof Parser); 16 | }); 17 | }); 18 | 19 | // ensures that we catch and document API changes 20 | describe('prototype methods:', function() { 21 | var methods = [ 22 | 'updatePosition', 23 | 'position', 24 | 'error', 25 | 'set', 26 | 'parse', 27 | 'match', 28 | 'use' 29 | ]; 30 | 31 | methods.forEach(function(method) { 32 | it('should expose the `' + method + '` method', function() { 33 | assert.equal(typeof parser[method], 'function'); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('parsers', function() { 39 | beforeEach(function() { 40 | parser = new Parser(); 41 | }); 42 | 43 | describe('.set():', function() { 44 | it('should register a named middleware', function() { 45 | parser.set('all', function() { 46 | var pos = this.position(); 47 | var m = this.match(/^.*/); 48 | if (!m) return; 49 | return pos({ 50 | type: 'all', 51 | val: m[0] 52 | }); 53 | }); 54 | 55 | assert(typeof parser.parsers.all === 'function'); 56 | }); 57 | 58 | it('should expose named parsers to middleware:', function() { 59 | var count = 0; 60 | 61 | parser.set('word', function() { 62 | var pos = this.position(); 63 | var m = this.match(/^\w/); 64 | if (!m) return; 65 | 66 | return pos({ 67 | type: 'word', 68 | val: m[0] 69 | }); 70 | }); 71 | 72 | parser.set('slash', function() { 73 | var pos = this.position(); 74 | var m = this.match(/^\//); 75 | if (!m) return; 76 | 77 | var word = this.parsers.word(); 78 | var prev = this.prev(); 79 | 80 | var node = pos({ 81 | type: 'slash', 82 | val: m[0] 83 | }); 84 | 85 | if (word && word.type === 'word') { 86 | count++; 87 | } 88 | 89 | prev.nodes.push(node); 90 | prev.nodes.push(word); 91 | }); 92 | 93 | parser.parse('a/b'); 94 | assert.equal(parser.ast.nodes.length, 5); 95 | assert.equal(count, 1); 96 | }); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/position.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Position = require('../lib/position'); 6 | 7 | describe('Position', function() { 8 | it('should export a function', function() { 9 | assert.equal(typeof Position, 'function'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/snapdragon.capture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | var capture = require('snapdragon-capture'); 7 | var snapdragon; 8 | 9 | describe('.capture (plugin usage)', function() { 10 | beforeEach(function() { 11 | snapdragon = new Snapdragon(); 12 | snapdragon.use(capture()); 13 | }); 14 | 15 | describe('errors', function(cb) { 16 | it('should throw an error when invalid args are passed to parse', function(cb) { 17 | try { 18 | snapdragon.parse(); 19 | cb(new Error('expected an error')); 20 | } catch (err) { 21 | assert(err); 22 | assert.equal(err.message, 'expected a string'); 23 | cb(); 24 | } 25 | }); 26 | }); 27 | 28 | describe('.capture():', function() { 29 | it('should register a parser', function() { 30 | snapdragon.capture('all', /^.*/); 31 | snapdragon.parse('a/b'); 32 | assert(snapdragon.parsers.hasOwnProperty('all')); 33 | }); 34 | 35 | it('should use middleware to parse', function() { 36 | snapdragon.capture('all', /^.*/); 37 | snapdragon.parse('a/b'); 38 | assert.equal(snapdragon.parser.parsed, 'a/b'); 39 | assert.equal(snapdragon.parser.input, ''); 40 | }); 41 | 42 | it('should create ast node:', function() { 43 | snapdragon.capture('all', /^.*/); 44 | snapdragon.parse('a/b'); 45 | assert.equal(snapdragon.parser.ast.nodes.length, 3); 46 | }); 47 | 48 | it('should be chainable:', function() { 49 | snapdragon.parser 50 | .capture('text', /^\w+/) 51 | .capture('slash', /^\//); 52 | 53 | snapdragon.parse('a/b'); 54 | assert.equal(snapdragon.parser.ast.nodes.length, 5); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('ast', function() { 60 | beforeEach(function() { 61 | snapdragon = new Snapdragon(); 62 | snapdragon.use(capture()); 63 | snapdragon 64 | .capture('text', /^\w+/) 65 | .capture('slash', /^\//); 66 | }); 67 | 68 | describe('orig:', function() { 69 | it('should add pattern to orig property', function() { 70 | snapdragon.parse('a/b'); 71 | assert.equal(snapdragon.parser.orig, 'a/b'); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/snapdragon.compile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | var snapdragon; 7 | var parser; 8 | 9 | describe('snapdragon.compiler', function() { 10 | beforeEach(function() { 11 | snapdragon = new Snapdragon(); 12 | snapdragon.parser 13 | .set('text', function() { 14 | var pos = this.position(); 15 | var match = this.match(/^\w+/); 16 | if (match) { 17 | return pos(this.node(match[0])); 18 | } 19 | }) 20 | .set('slash', function() { 21 | var pos = this.position(); 22 | var match = this.match(/^\//); 23 | if (match) { 24 | return pos(this.node(match[0])) 25 | } 26 | }); 27 | }); 28 | 29 | describe('errors', function(cb) { 30 | it('should throw an error when a compiler is missing', function(cb) { 31 | try { 32 | var ast = snapdragon.parse('a/b/c'); 33 | snapdragon.compile(ast); 34 | cb(new Error('expected an error')); 35 | } catch (err) { 36 | assert(err); 37 | assert.equal(err.message, 'string : compiler "text" is not registered'); 38 | cb(); 39 | } 40 | }); 41 | }); 42 | 43 | describe('snapdragon.compile', function() { 44 | beforeEach(function() { 45 | snapdragon.compiler 46 | .set('text', function(node) { 47 | return this.emit(node.val); 48 | }) 49 | .set('slash', function(node) { 50 | return this.emit('-'); 51 | }); 52 | }); 53 | 54 | it('should set the result on `output`', function() { 55 | var ast = snapdragon.parse('a/b/c'); 56 | var res = snapdragon.compile(ast); 57 | assert.equal(res.output, 'a-b-c'); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/snapdragon.options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | 7 | describe('.options', function() { 8 | it('should correctly accept and store options in constructor', function() { 9 | var snap = new Snapdragon({ 10 | a: true, 11 | b: null, 12 | c: false, 13 | d: 'd' 14 | }); 15 | 16 | assert.strictEqual(snap.options['a'], true); 17 | assert.strictEqual(snap.options['b'], null); 18 | assert.strictEqual(snap.options['c'], false); 19 | assert.strictEqual(snap.options['d'], 'd'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/snapdragon.parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | var snapdragon; 7 | 8 | describe('parser', function() { 9 | beforeEach(function() { 10 | snapdragon = new Snapdragon(); 11 | }); 12 | 13 | describe('errors', function(cb) { 14 | it('should throw an error when invalid args are passed to parse', function(cb) { 15 | try { 16 | snapdragon.parse(); 17 | cb(new Error('expected an error')); 18 | } catch (err) { 19 | assert(err); 20 | assert.equal(err.message, 'expected a string'); 21 | cb(); 22 | } 23 | }); 24 | }); 25 | 26 | describe('.set():', function() { 27 | it('should register middleware', function() { 28 | snapdragon.parser.set('all', function() { 29 | var pos = this.position(); 30 | var m = this.match(/^.*/); 31 | if (!m) return; 32 | return pos({ 33 | type: 'all', 34 | val: m[0] 35 | }); 36 | }); 37 | 38 | snapdragon.parse('a/b'); 39 | assert(snapdragon.parsers.hasOwnProperty('all')); 40 | }); 41 | 42 | it('should use middleware to parse', function() { 43 | snapdragon.parser.set('all', function() { 44 | var pos = this.position(); 45 | var m = this.match(/^.*/); 46 | return pos({ 47 | type: 'all', 48 | val: m[0] 49 | }); 50 | }); 51 | 52 | snapdragon.parse('a/b'); 53 | assert.equal(snapdragon.parser.parsed, 'a/b'); 54 | assert.equal(snapdragon.parser.input, ''); 55 | }); 56 | 57 | it('should create ast node:', function() { 58 | snapdragon.parser.set('all', function() { 59 | var pos = this.position(); 60 | var m = this.match(/^.*/); 61 | return pos({ 62 | type: 'all', 63 | val: m[0] 64 | }); 65 | }); 66 | 67 | snapdragon.parse('a/b'); 68 | assert.equal(snapdragon.parser.ast.nodes.length, 3); 69 | }); 70 | 71 | it('should be chainable:', function() { 72 | snapdragon.parser 73 | .set('text', function() { 74 | var pos = this.position(); 75 | var m = this.match(/^\w+/); 76 | if (!m) return; 77 | return pos({ 78 | type: 'text', 79 | val: m[0] 80 | }); 81 | }) 82 | .set('slash', function() { 83 | var pos = this.position(); 84 | var m = this.match(/^\//); 85 | if (!m) return; 86 | return pos({ 87 | type: 'slash', 88 | val: m[0] 89 | }); 90 | }); 91 | 92 | snapdragon.parse('a/b'); 93 | assert.equal(snapdragon.parser.ast.nodes.length, 5); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('ast', function() { 99 | beforeEach(function() { 100 | snapdragon = new Snapdragon(); 101 | snapdragon.parser 102 | .set('text', function() { 103 | var pos = this.position(); 104 | var m = this.match(/^\w+/); 105 | if (!m) return; 106 | return pos({ 107 | type: 'text', 108 | val: m[0] 109 | }); 110 | }) 111 | .set('slash', function() { 112 | var pos = this.position(); 113 | var m = this.match(/^\//); 114 | if (!m) return; 115 | return pos({ 116 | type: 'slash', 117 | val: m[0] 118 | }); 119 | }); 120 | }); 121 | 122 | describe('orig:', function() { 123 | it('should add pattern to orig property', function() { 124 | snapdragon.parse('a/b'); 125 | assert.equal(snapdragon.parser.orig, 'a/b'); 126 | }); 127 | }); 128 | 129 | describe('recursion', function() { 130 | beforeEach(function() { 131 | snapdragon.parser.set('text', function() { 132 | var pos = this.position(); 133 | var m = this.match(/^\w/); 134 | if (!m) return; 135 | return pos({ 136 | type: 'text', 137 | val: m[0] 138 | }); 139 | }); 140 | 141 | snapdragon.parser.set('open', function() { 142 | var pos = this.position(); 143 | var m = this.match(/^{/); 144 | if (!m) return; 145 | return pos({ 146 | type: 'open', 147 | val: m[0] 148 | }); 149 | }); 150 | 151 | snapdragon.parser.set('close', function() { 152 | var pos = this.position(); 153 | var m = this.match(/^}/); 154 | if (!m) return; 155 | return pos({ 156 | type: 'close', 157 | val: m[0] 158 | }); 159 | }); 160 | 161 | snapdragon.parser.set('comma', function() { 162 | var pos = this.position(); 163 | var m = this.match(/,/); 164 | if (!m) return; 165 | return pos({ 166 | type: 'comma', 167 | val: m[0] 168 | }); 169 | }); 170 | }); 171 | 172 | it('should set original string on `orig`', function() { 173 | snapdragon.parse('a{b,{c,d},e}f'); 174 | assert.equal(snapdragon.parser.orig, 'a{b,{c,d},e}f'); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/snapdragon.regex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('mocha'); 4 | var assert = require('assert'); 5 | var Snapdragon = require('..'); 6 | var capture = require('snapdragon-capture'); 7 | var snapdragon; 8 | 9 | describe('parser', function() { 10 | beforeEach(function() { 11 | snapdragon = new Snapdragon(); 12 | snapdragon.use(capture()); 13 | }); 14 | 15 | describe('.regex():', function() { 16 | it('should expose a regex cache with regex from registered parsers', function() { 17 | snapdragon.capture('dot', /^\./); 18 | snapdragon.capture('text', /^\w+/); 19 | snapdragon.capture('all', /^.+/); 20 | 21 | assert(snapdragon.regex.__data__.hasOwnProperty('dot')); 22 | assert(snapdragon.regex.__data__.hasOwnProperty('all')); 23 | assert(snapdragon.regex.__data__.hasOwnProperty('text')); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /verbfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(verb) { 4 | verb.use(require('verb-generate-readme')); 5 | verb.preLayout(/\.md$/, function(file, next) { 6 | if (!/(verb|readme)/.test(file.stem)) { 7 | file.layout = null; 8 | } 9 | next(); 10 | }); 11 | 12 | verb.task('docs', function(cb) { 13 | return verb.src('support/src/content/*.md', {cwd: __dirname}) 14 | .pipe(verb.renderFile('md', {layout: null})) 15 | .pipe(verb.dest('docs')) 16 | }); 17 | 18 | verb.task('default', ['docs', 'readme']); 19 | }; 20 | --------------------------------------------------------------------------------