├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── make.js ├── package.json ├── parser ├── Mini.g4 ├── parser.in.js └── parser.js ├── src ├── ErrorListener.js ├── Expression.js ├── Nodes.js ├── createNodeTree.js ├── evaluate.js ├── parse.js └── walk.js ├── test ├── TestContext.js ├── eval.test.js ├── index.html ├── index.js ├── parse.test.js ├── syntax-highlighting.test.js └── testHelpers.js └── vendor └── antlr4.js /.gitignore: -------------------------------------------------------------------------------- 1 | .bin/*.jar 2 | dist 3 | node_modules 4 | .test 5 | tmp 6 | coverage 7 | .nyc_output -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | node_js: 6 | - "8" 7 | before_script: 8 | - 'npm install' 9 | script: 10 | - npm run test 11 | - npm run cover 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mini 2 | 3 | A minimal, functional language focused on data analysis and visualization and available in [Stencila](https://stenci.la) documents. 4 | 5 | [![NPM](http://img.shields.io/npm/v/stencila-mini.svg?style=flat)](https://www.npmjs.com/package/stencila-mini) 6 | [![Build status](https://travis-ci.org/stencila/mini.svg?branch=master)](https://travis-ci.org/stencila/mini) 7 | [![Code coverage](https://codecov.io/gh/stencila/mini/branch/master/graph/badge.svg)](https://codecov.io/gh/stencila/mini) 8 | 9 | ## Documentation 10 | 11 | See the documentation at https://github.com/stencila/stencila/tree/master/docs/languages/mini. 12 | 13 | ## Development 14 | 15 | 1. Clone the repo 16 | 17 | ```bash 18 | git clone https://github.com/stencila/mini.git 19 | ``` 20 | 21 | 2. Install a Java Runtime or Java Development Kit (`JDK`) if you don't have one already. 22 | 23 | 3. Download [ANTLR](http://www.antlr.org/download/antlr-4.6-complete.jar) into the local `.bin/` folder: 24 | 25 | ```bash 26 | mkdir -p .bin 27 | curl -o .bin/antlr-4.6-complete.jar http://www.antlr.org/download/antlr-4.6-complete.jar 28 | ``` 29 | 30 | 4. Install dependencies 31 | 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | 5. Test 37 | 38 | ```bash 39 | npm test 40 | ``` 41 | 42 | or use `node make test:browser -w` and open `test/index.html` in your browser. 43 | 44 | 6. Build 45 | 46 | ```bash 47 | node make 48 | ``` 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Expression from './src/Expression' 2 | import parse from './src/parse' 3 | import walk from './src/walk' 4 | import evaluate from './src/evaluate' 5 | 6 | export { Expression, parse, walk, evaluate } 7 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | const b = require('substance-bundler') 2 | const path = require('path') 3 | const fs = require('fs') 4 | 5 | const DIST = 'dist/' 6 | const TMP = 'tmp/' 7 | 8 | b.task('clean', () => { 9 | b.rm(TMP) 10 | b.rm(DIST) 11 | b.rm('coverage') 12 | }) 13 | .describe('Cleans up temporary and build folders.') 14 | 15 | // ATM you we need to checkout the whole project and build a vendor bundle 16 | b.task('antlr4', _bundleANTLR4) 17 | .describe('Prebundle antlr4.js') 18 | 19 | b.task('parser', _generateParser) 20 | .describe('Generates the parser.\nRequires a clone of https://github/antlr4/antlr4.git as sibling folder') 21 | 22 | b.task('lib', ['parser'], _buildLib) 23 | .describe('Builds the library into dist folder.') 24 | 25 | b.task('test:browser', ['lib'], () => { 26 | _buildTestsBrowser() 27 | }) 28 | .describe('Builds the test-suite to be run from test/index.html.') 29 | 30 | b.task('default', ['clean', 'lib']) 31 | 32 | // HELPERS 33 | 34 | function _bundleANTLR4 () { 35 | b.browserify('../antlr4/runtime/JavaScript/src/antlr4/index', { 36 | dest: './vendor/antlr4.js', 37 | exports: ['default'], 38 | debug: false 39 | }) 40 | } 41 | 42 | function _generateParser () { 43 | // we can not build the parser without ANTLR4 44 | // still we don't fail so that travis is working (generated parser is checked in) 45 | if (!fs.existsSync('./.bin/antlr-4.6-complete.jar')) { 46 | console.error('You need to download the antlr4 runtime.') 47 | return 48 | } 49 | b.custom('Generating parser', { 50 | src: './parser/Mini.g4', 51 | dest: './tmp/MiniParser.js', 52 | execute: () => { 53 | const isWin = /^win/.test(process.platform) 54 | let cmd = 'java -jar ./.bin/antlr-4.6-complete.jar -Dlanguage=JavaScript -no-visitor' 55 | // WORKAROUND: antrl4 behaves differently under windows, i.e. it does not generate into folder 'parser' 56 | // thus we need to tell explicitely to do so 57 | if (isWin) cmd += ' -o tmp/parser' 58 | else cmd += ' -o tmp' 59 | cmd += ' parser/Mini.g4' 60 | let exec = require('child_process').exec 61 | return new Promise(function (resolve, reject) { 62 | exec(cmd, (err) => { 63 | if (err) { 64 | reject(new Error(err)) 65 | } else { 66 | resolve() 67 | } 68 | }) 69 | }) 70 | } 71 | }) 72 | // NOTE: current versions commonjs and alias plugins 73 | // are incompatible 74 | // so we first bundle everything to es6 without the alias 75 | // amd in a second step replacing the alias 76 | b.js('./parser/parser.in.js', { 77 | output: [{ 78 | file: './tmp/parser.js', 79 | format: 'es', 80 | sourcemap: false 81 | }], 82 | external: ['antlr4/index'], 83 | commonjs: { 84 | include: ['tmp/parser/**'] 85 | }, 86 | cleanup: true 87 | }) 88 | b.js('./tmp/parser.js', { 89 | output: [{ 90 | file: './parser/parser.js', 91 | format: 'es', 92 | sourcemap: false 93 | }], 94 | alias: { 95 | 'antlr4/index': path.join(__dirname, '/vendor/antlr4.js') 96 | } 97 | }) 98 | } 99 | 100 | // NOTE: this is needed when working on versions from github 101 | function _buildDeps () { 102 | const subsDist = path.join(__dirname, 'node_modules/substance/dist') 103 | if (!fs.existsSync(path.join(subsDist, 'substance.js')) || 104 | !fs.existsSync(path.join(subsDist, 'substance.cjs.js'))) { 105 | b.make('substance') 106 | } 107 | } 108 | 109 | function _buildLib () { 110 | _buildDeps() 111 | 112 | b.js('index.js', { 113 | output: [{ 114 | file: DIST + 'stencila-mini.cjs.js', 115 | format: 'cjs' 116 | }, { 117 | file: DIST + 'stencila-mini.js', 118 | format: 'umd', 119 | name: 'stencilaMini', 120 | globals: { 121 | 'substance': 'substance' 122 | } 123 | }], 124 | external: ['substance'] 125 | }) 126 | } 127 | 128 | function _buildTestsBrowser () { 129 | b.js('./test/index.js', { 130 | output: [{ 131 | file: TMP + 'tests.js', 132 | format: 'umd', 133 | name: 'tests', 134 | globals: { 135 | 'substance': 'window.substance', 136 | 'stencila-mini': 'window.stencilaMini', 137 | 'substance-test': 'window.substanceTest' 138 | } 139 | }], 140 | external: ['substance', 'stencila-mini', 'substance-test'] 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stencila-mini", 3 | "version": "0.15.3", 4 | "description": "", 5 | "main": "dist/stencila-mini.cjs.js", 6 | "jsnext:main": "index.js", 7 | "scripts": { 8 | "prepack": "npm install && node make", 9 | "lint": "standard src/*.js test/*.js", 10 | "start": "node make -w", 11 | "pretest": "npm run lint", 12 | "test": "node make && node --require esm test | tap-spec", 13 | "cover": "nyc --require esm --reporter=lcov --reporter=text node test" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "browserify": "^14.0.0", 19 | "esm": "^3.0.37", 20 | "nyc": "^11.8.0", 21 | "standard": "^11.0.1", 22 | "substance": "1.0.0-preview.65", 23 | "substance-bundler": "0.25.0", 24 | "substance-test": "0.11.0", 25 | "tap-spec": "^4.1.1" 26 | }, 27 | "files": [ 28 | "dist", 29 | "src", 30 | "parser", 31 | "index.js", 32 | "README.md" 33 | ], 34 | "nyc": { 35 | "include": [ 36 | "src/*.js" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /parser/Mini.g4: -------------------------------------------------------------------------------- 1 | grammar Mini; 2 | 3 | mini: mainExpr EOF { $ctx.type = 'evaluation' } 4 | | ID EQ mainExpr EOF { $ctx.type = 'definition'} 5 | ; 6 | 7 | mainExpr: 8 | expr { $ctx.type = 'simple' } 9 | ; 10 | 11 | expr: 12 | expr '.' ID { 13 | $ctx.type = 'select_id' 14 | } 15 | | expr '[' expr ']' { 16 | $ctx.type = 'select_expr' 17 | } 18 | | op=('!'|'+'|'-') expr { switch ($ctx.op.text) { 19 | case '!': $ctx.type = 'not'; break 20 | case '+': $ctx.type = 'positive'; break 21 | case '-': $ctx.type = 'negative'; break 22 | }} 23 | | expr '^' expr { 24 | $ctx.type = 'pow' 25 | } 26 | | expr op=('*'|'/'|'%') expr { switch ($ctx.op.text) { 27 | case '*': $ctx.type = 'multiply'; break 28 | case '/': $ctx.type = 'divide'; break 29 | case '%': $ctx.type = 'remainder'; break 30 | }} 31 | | expr op=('+'|'-') expr { 32 | $ctx.type = ($ctx.op.text === '+') ? 'add' : 'subtract' 33 | } 34 | | expr op=('<'|'<='|'>'|'>=') expr { switch ($ctx.op.text) { 35 | case '<': $ctx.type = 'less'; break 36 | case '<=': $ctx.type = 'less_or_equal'; break 37 | case '>': $ctx.type = 'greater'; break 38 | case '>=': $ctx.type = 'greater_or_equal'; break 39 | }} 40 | | expr op=('=='|'!=') expr { 41 | $ctx.type = ($ctx.op.text === '==') ? 'equal' : 'not_equal' 42 | } 43 | | expr '&&' expr { $ctx.type = 'and' } 44 | | expr '||' expr { $ctx.type = 'or' } 45 | | expr '|' function_call { $ctx.type = 'pipe' } 46 | | BOOLEAN { $ctx.type = 'boolean' } 47 | | number { $ctx.type = 'number' } 48 | | ID {this._input.LA(1) !== MiniParser.EQ}? { $ctx.type = 'var' } 49 | | function_call { $ctx.type = '_call' } 50 | | '(' expr ')' { $ctx.type = 'group' } 51 | | array { $ctx.type = 'array' } 52 | | object { $ctx.type = 'object' } 53 | | STRING { $ctx.type = 'string' } 54 | ; 55 | 56 | function_call: 57 | name=ID '()' { $ctx.type = 'call' } 58 | | name=ID '(' args=call_arguments ')' { $ctx.type = 'call' } 59 | ; 60 | 61 | number: INT { $ctx.type = 'int' } 62 | | FLOAT { $ctx.type = 'float' } 63 | ; 64 | 65 | seq: items+=expr (',' items+=expr)*; 66 | 67 | id_seq: items+= ID (',' items+=ID)*; 68 | 69 | call_arguments: 70 | | args=positional_arguments 71 | | namedArgs=named_arguments 72 | | args=positional_arguments ',' namedArgs=named_arguments 73 | ; 74 | 75 | positional_arguments: expr (',' expr?)*; 76 | 77 | named_arguments: named_argument (',' named_argument?)*; 78 | 79 | named_argument: ID EQ expr { $ctx.type = 'named-argument' }; 80 | 81 | array: '[' ( seq )? ']' { $ctx.type = 'array' } 82 | ; 83 | object: '{' ( keys+=ID ':' vals+=expr (',' keys+=ID ':' vals+=expr)* )? '}' { $ctx.type = 'object' } 84 | ; 85 | 86 | BOOLEAN: 'true'|'false'; 87 | ID : [a-zA-Z_@][a-zA-Z_@0-9]*; 88 | INT : [0-9]+ ; 89 | FLOAT: [0-9]+'.'[0-9]+; 90 | STRING : '"' ( '\\"' | . )*? '"' 91 | | ['] ( '\\'['] | . )*? [']; 92 | EQ : '='; 93 | WS : [ \r\t\n]+ -> skip ; 94 | -------------------------------------------------------------------------------- /parser/parser.in.js: -------------------------------------------------------------------------------- 1 | import antlr4 from 'antlr4/index' 2 | import _MiniLexer from '../tmp/parser/MiniLexer' 3 | import _MiniParser from '../tmp/parser/MiniParser' 4 | import _MiniListener from '../tmp/parser/MiniListener' 5 | 6 | const { InputStream, CommonTokenStream } = antlr4 7 | const treeWalker = antlr4.tree.ParseTreeWalker.DEFAULT 8 | const MiniLexer = _MiniLexer.MiniLexer 9 | const MiniParser = _MiniParser.MiniParser 10 | const MiniListener = _MiniListener.MiniListener 11 | 12 | export { 13 | InputStream, CommonTokenStream, treeWalker, 14 | MiniLexer, MiniParser, MiniListener 15 | } 16 | -------------------------------------------------------------------------------- /src/ErrorListener.js: -------------------------------------------------------------------------------- 1 | export default 2 | class ErrorListener { 3 | constructor () { 4 | this.syntaxErrors = [] 5 | } 6 | 7 | syntaxError (recognizer, offendingSymbol, line, column, msg, error) { 8 | let row = line - 1 9 | let start, token 10 | if (offendingSymbol) { 11 | start = offendingSymbol.start 12 | token = offendingSymbol.text 13 | } 14 | this.syntaxErrors.push({ 15 | type: 'syntax-error', 16 | row, 17 | column, 18 | start, 19 | token, 20 | msg, 21 | error 22 | }) 23 | } 24 | 25 | reportAttemptingFullContext () { 26 | console.error('Attempting Full Context: ', arguments) 27 | } 28 | 29 | reportAmbiguity () { 30 | console.error('Ambiguity:', arguments) 31 | } 32 | 33 | reportContextSensitivity () { 34 | console.error('ContextSensitivity:', arguments) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Expression.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'substance' 2 | import createNodeTree from './createNodeTree' 3 | 4 | export default class Expression extends EventEmitter { 5 | constructor (source, root, syntaxError, nodes, tokens) { 6 | super() 7 | 8 | this.source = source 9 | this.root = root 10 | 11 | this.syntaxError = syntaxError 12 | this.nodes = nodes 13 | this.tokens = tokens 14 | } 15 | 16 | get name () { 17 | if (this.isDefinition()) { 18 | return this.root.name 19 | } 20 | } 21 | 22 | isDefinition () { 23 | return (this.root && this.root.type === 'definition') 24 | } 25 | 26 | async evaluate (context) { 27 | return this.root.evaluate(context) 28 | } 29 | } 30 | 31 | Expression.create = function (source, ast, syntaxError) { 32 | let { root, state } = createNodeTree(ast) 33 | return new Expression(source, root, syntaxError, state.nodes, state.tokens) 34 | } 35 | -------------------------------------------------------------------------------- /src/Nodes.js: -------------------------------------------------------------------------------- 1 | import {isNumber} from 'substance' 2 | 3 | // TODO: we need to think about this more 4 | // we allow to pack primitive values by the calling environment 5 | // e.g. to add type information or apply some encoding 6 | // With nested types e.g. single values of an array or object 7 | // the packing should not be done, instead the packer would need to do something 8 | // on the higher level 9 | const UNPACKED = true 10 | 11 | class ExprNode { 12 | constructor (id, start, end) { 13 | this.id = id 14 | if (!isNumber(start) || !isNumber(end)) { 15 | throw new Error("'start' and 'end' are mandatory") 16 | } 17 | this.start = start 18 | this.end = end 19 | } 20 | 21 | async evaluate (context, unpacked) { 22 | throw new Error('This is abstract') 23 | } 24 | } 25 | 26 | export class Definition extends ExprNode { 27 | constructor (id, start, end, name, rhs) { 28 | super(id, start, end) 29 | this.name = name 30 | this.rhs = rhs 31 | rhs.parent = this 32 | } 33 | 34 | get type () { return 'definition' } 35 | 36 | async evaluate (context, unpacked) { 37 | return this.rhs.evaluate(context, unpacked) 38 | } 39 | } 40 | 41 | export class ArrayNode extends ExprNode { 42 | constructor (id, start, end, items) { 43 | super(id, start, end) 44 | this.items = items 45 | items.forEach(item => { 46 | item.parent = this 47 | }) 48 | } 49 | 50 | get type () { return 'array' } 51 | 52 | async evaluate (context, unpacked) { 53 | let vals = await Promise.all(this.items.map(i => i.evaluate(context, UNPACKED))) 54 | if (unpacked) { 55 | return vals 56 | } else { 57 | return context.pack(vals, this.type) 58 | } 59 | } 60 | } 61 | 62 | export class ObjectNode extends ExprNode { 63 | constructor (id, start, end, entries) { 64 | super(id, start, end) 65 | this.entries = entries 66 | entries.forEach(entry => { 67 | entry.parent = this 68 | }) 69 | } 70 | 71 | get type () { return 'object' } 72 | 73 | async evaluate (context, unpacked) { 74 | let vals = await Promise.all(this.entries.map(e => e.val.evaluate(context, UNPACKED))) 75 | let obj = {} 76 | for (let i = 0; i < this.entries.length; i++) { 77 | let entry = this.entries[i] 78 | obj[entry.key] = vals[i] 79 | } 80 | if (unpacked) { 81 | return obj 82 | } else { 83 | return context.pack(obj) 84 | } 85 | } 86 | } 87 | 88 | class ConstantNode extends ExprNode { 89 | constructor (id, start, end, val) { 90 | super(id, start, end) 91 | this.val = val 92 | } 93 | 94 | async evaluate (context, unpacked) { 95 | if (unpacked) { 96 | return this.val 97 | } else { 98 | return context.pack(this.val, this.type) 99 | } 100 | } 101 | } 102 | 103 | /* 104 | A constant value. 105 | */ 106 | export class NumberNode extends ConstantNode { 107 | get type () { return 'number' } 108 | } 109 | 110 | export class BooleanNode extends ConstantNode { 111 | get type () { return 'boolean' } 112 | } 113 | 114 | export class StringNode extends ConstantNode { 115 | get type () { return 'string' } 116 | } 117 | 118 | export class Var extends ExprNode { 119 | constructor (id, start, end, name) { 120 | super(id, start, end) 121 | this.name = name 122 | } 123 | 124 | get type () { return 'var' } 125 | 126 | async evaluate (context, unpacked) { 127 | let val = await context.resolve(this.name) 128 | if (unpacked) { 129 | return context.unpack(val) 130 | } else { 131 | return val 132 | } 133 | } 134 | } 135 | 136 | export class EmptyArgument extends ExprNode { 137 | get type () { return 'empty-arg' } 138 | 139 | async evaluate () {} 140 | } 141 | 142 | export class FunctionCall extends ExprNode { 143 | constructor (id, start, end, name, args = [], namedArgs = [], modifiers = []) { 144 | super(id, start, end) 145 | this.name = name 146 | this.args = args 147 | this.modifiers = modifiers 148 | this.namedArgs = namedArgs 149 | args.forEach((arg) => { 150 | arg.parent = this 151 | }) 152 | namedArgs.forEach((arg) => { 153 | arg.parent = this 154 | }) 155 | } 156 | 157 | get type () { return 'call' } 158 | 159 | async evaluate (context, unpacked) { 160 | // TODO: what do we expect here? do we want to 161 | // pack the args, or leave this to context.callFunction() 162 | let args = await Promise.all(this.args.map(a => a.evaluate(context))) 163 | let namedArgs = await Promise.all(this.namedArgs.map(a => { 164 | return { 165 | name: a.name, 166 | value: a.evaluate(context) 167 | } 168 | })) 169 | let result = await context.callFunction(this.name, args, namedArgs) 170 | if (unpacked) { 171 | return context.unpack(result) 172 | } else { 173 | return result 174 | } 175 | } 176 | } 177 | 178 | export class NamedArgument extends ExprNode { 179 | constructor (id, start, end, name, rhs) { 180 | super(id, start, end) 181 | this.name = name 182 | this.rhs = rhs 183 | rhs.parent = this 184 | } 185 | 186 | get type () { return 'named-argument' } 187 | 188 | async evaluate (context, unpacked) { 189 | return this.rhs.evaluate(context, unpacked) 190 | } 191 | } 192 | 193 | export class PipeOp extends ExprNode { 194 | constructor (id, start, end, left, right) { 195 | super(id, start, end) 196 | this.left = left 197 | this.right = right 198 | this.left.parent = this 199 | this.right.parent = this 200 | } 201 | 202 | get type () { return 'pipe' } 203 | 204 | async evaluate (context, unpacked) { 205 | // first call the left one 206 | let pipeArg = await this.left.evaluate(context) 207 | 208 | // HACK: adding an additional argument to the RHS 209 | const right = this.right 210 | const args = [{ 211 | name: '_pipe', 212 | async evaluate (context) { 213 | return pipeArg 214 | } 215 | }].concat(right.args) 216 | let rightProxy = new FunctionCall(right.id, right.start, right.end, 217 | right.name, args, right.namedArgs, right.modifiers) 218 | 219 | return rightProxy.evaluate(context, unpacked) 220 | } 221 | } 222 | 223 | export class ErrorNode extends ExprNode { 224 | constructor (id, start, end, exception) { 225 | super(id, start, end) 226 | 227 | this.exception = exception 228 | } 229 | 230 | get type () { return 'error' } 231 | 232 | async evaluate () {} 233 | } 234 | -------------------------------------------------------------------------------- /src/createNodeTree.js: -------------------------------------------------------------------------------- 1 | import {isString, isNumber} from 'substance' 2 | import { 3 | Definition, 4 | NumberNode, StringNode, ArrayNode, ObjectNode, BooleanNode, Var, 5 | FunctionCall, NamedArgument, 6 | PipeOp, 7 | ErrorNode, 8 | EmptyArgument 9 | } from './Nodes' 10 | 11 | /** 12 | * Maps the ANTLR4 AST to a our custom tree model. 13 | */ 14 | export default function createNodeTree (ast) { 15 | let state = { 16 | // generating ids by counting created nodes 17 | nodeId: 0, 18 | nodes: [], 19 | // extra list to all variables, cells, ranges 20 | // to be able to compute dependencies 21 | inputs: [], 22 | // tokens for code highlighting 23 | tokens: [] 24 | } 25 | let root = createFromAST(state, ast) 26 | return { root, state } 27 | } 28 | 29 | function createFromAST (state, ast) { 30 | let node 31 | let [start, end] = _getStartStop(ast) 32 | switch (ast.type) { 33 | case 'evaluation': 34 | return createFromAST(state, ast.children[0]) 35 | case 'definition': { 36 | let lhs = ast.children[0] 37 | state.tokens.push(new Token('output-name', lhs.symbol)) 38 | node = new Definition(state.nodeId++, start, end, lhs.getText(), createFromAST(state, ast.children[2])) 39 | break 40 | } 41 | case 'simple': 42 | return createFromAST(state, ast.children[0]) 43 | // Member selection operator `.` 44 | case 'select_id': { 45 | const value = createFromAST(state, ast.children[0]) 46 | // Create a new string node from the member ID 47 | const id = new StringNode(state.nodeId++, start, end, ast.children[2].getText()) 48 | state.nodes.push(id) 49 | node = new FunctionCall(state.nodeId++, start, end, 'select', [value, id]) 50 | break 51 | } 52 | // Member selection operator `[]` 53 | case 'select_expr': { 54 | const args = exprSequence(state, [ast.children[0], ast.children[2]]) 55 | node = new FunctionCall(state.nodeId++, start, end, 'select', args) 56 | break 57 | } 58 | // Unary operators 59 | case 'not': 60 | case 'positive': 61 | case 'negative': { 62 | const name = ast.type 63 | const args = exprSequence(state, [ast.children[1]]) 64 | node = new FunctionCall(state.nodeId++, start, end, name, args) 65 | break 66 | } 67 | // Binary operators (in order of precedence) 68 | case 'pow': 69 | case 'multiply': 70 | case 'divide': 71 | case 'remainder': 72 | case 'add': 73 | case 'subtract': 74 | case 'less': 75 | case 'less_or_equal': 76 | case 'greater': 77 | case 'greater_or_equal': 78 | case 'equal': 79 | case 'not_equal': 80 | case 'and': 81 | case 'or': { 82 | const name = ast.type 83 | const args = exprSequence(state, [ast.children[0], ast.children[2]]) 84 | node = new FunctionCall(state.nodeId++, start, end, name, args) 85 | break 86 | } 87 | // Pipe operator 88 | case 'pipe': { 89 | node = new PipeOp(state.nodeId++, start, end, 90 | createFromAST(state, ast.children[0]), 91 | createFromAST(state, ast.children[2]) 92 | ) 93 | break 94 | } 95 | case 'int': 96 | case 'float': 97 | case 'number': { 98 | if (ast.children.length !== 1) { 99 | node = new ErrorNode(state.nodeId++, start, end, 'Invalid number.') 100 | } else { 101 | let token = ast.children[0].children[0] 102 | state.tokens.push(new Token('number-literal', token.symbol)) 103 | node = new NumberNode(state.nodeId++, start, end, Number(token.getText())) 104 | } 105 | break 106 | } 107 | case 'boolean': { 108 | let token = ast.children[0] 109 | state.tokens.push(new Token('boolean-literal', token.symbol)) 110 | let bool = (token.getText() === 'true') 111 | node = new BooleanNode(state.nodeId++, start, end, bool) 112 | break 113 | } 114 | case 'string': { 115 | let ctx = ast.children[0] 116 | state.tokens.push(new Token('string-literal', ctx.symbol)) 117 | let str = ctx.getText().slice(1, -1) 118 | node = new StringNode(state.nodeId++, start, end, str) 119 | break 120 | } 121 | case 'array': { 122 | const ctx = ast.children[0] 123 | const seq = ctx.children[1] 124 | let vals = [] 125 | if (seq && seq.items) { 126 | vals = exprSequence(state, seq.items) 127 | } 128 | node = new ArrayNode(state.nodeId++, start, end, vals) 129 | break 130 | } 131 | case 'object': { 132 | const ctx = ast.children[0] 133 | const keys = ctx.keys 134 | const vals = ctx.vals 135 | const entries = [] 136 | for (let i = 0; i < keys.length; i++) { 137 | state.tokens.push(new Token('key', keys[i])) 138 | if (keys[i] && vals[i]) { 139 | let key = keys[i].text 140 | let val = createFromAST(state, vals[i]) 141 | entries.push({ key, val }) 142 | } 143 | } 144 | node = new ObjectNode(state.nodeId++, start, end, entries) 145 | break 146 | } 147 | case 'var': { 148 | node = new Var(state.nodeId++, start, end, ast.getText()) 149 | state.tokens.push(new Token('input-variable-name', { 150 | start: ast.start.start, 151 | stop: ast.stop.stop 152 | })) 153 | state.inputs.push(node) 154 | break 155 | } 156 | case 'group': { 157 | // No need to create an extra node for a group expression '(..)' 158 | return createFromAST(state, ast.children[1]) 159 | } 160 | case '_call': 161 | // HACK: sometimes we need to unwrap 162 | ast = ast.children[0] // eslint-disable-line no-fallthrough 163 | case 'call': { 164 | // ATTENTION we need to be robust regarding partial expressions 165 | let name = ast.name ? ast.name.text : '' 166 | let args, namedArgs 167 | let argsCtx = ast.args 168 | if (argsCtx) { 169 | args = argSequence(state, argsCtx.args) 170 | namedArgs = argSequence(state, argsCtx.namedArgs) 171 | } 172 | if (ast.name) { 173 | state.tokens.push( 174 | new Token('function-name', ast.name) 175 | ) 176 | } 177 | node = new FunctionCall(state.nodeId++, start, end, name, args, namedArgs) 178 | break 179 | } 180 | case 'named-argument': { 181 | let name = ast.children[0] 182 | state.tokens.push(new Token('key', name.symbol)) 183 | node = new NamedArgument(state.nodeId++, start, end, name.toString(), 184 | createFromAST(state, ast.children[2]) 185 | ) 186 | break 187 | } 188 | default: { 189 | if (ast.exception) { 190 | // console.log('Creating ErrorNode with exception', ast) 191 | node = new ErrorNode(state.nodeId++, start, end, ast.exception) 192 | } else { 193 | // console.log('Creating ErrorNode', ast) 194 | node = new ErrorNode(state.nodeId++, start, end, 'Parser error.') 195 | } 196 | } 197 | } 198 | state.nodes.push(node) 199 | return node 200 | } 201 | 202 | class Token { 203 | constructor (type, symbol) { 204 | /* istanbul ignore next */ 205 | const start = symbol.start 206 | const stop = symbol.stop 207 | if (!isString(type)) { 208 | throw new Error('Illegal argument: "type" must be a string') 209 | } 210 | /* istanbul ignore next */ 211 | if (!isNumber(start)) { 212 | throw new Error('Illegal argument: "start" must be a number') 213 | } 214 | /* istanbul ignore next */ 215 | if (!isNumber(stop)) { 216 | throw new Error('Illegal argument: "stop" must be a number') 217 | } 218 | this.type = type 219 | this.start = start 220 | // ATTENTION: seems that symbol.end is inclusive 221 | this.end = stop + 1 222 | this.text = symbol.text 223 | } 224 | } 225 | 226 | function exprSequence (state, items) { 227 | if (!items) return [] 228 | return items.map(c => createFromAST(state, c)) 229 | } 230 | 231 | function argSequence (state, args) { 232 | if (!args) return [] 233 | // HACK: we need to allow empty arguments to support incomplete expressions such as in `sum(x,,y)` 234 | // Still we want to treat it as an error 235 | let result = [] 236 | args = args.children 237 | let last 238 | for (let i = 0; i < args.length; i++) { 239 | let n = args[i] 240 | if (n.getText() === ',') { 241 | if (!last) { 242 | let [start, end] = _getStartStop(n) 243 | result.push(new EmptyArgument(state.nodeId++, start, end)) 244 | } 245 | last = false 246 | } else { 247 | result.push(createFromAST(state, n)) 248 | last = n 249 | } 250 | } 251 | if (!last) { 252 | last = args[args.length - 1] 253 | if (last.getText() === ',') { 254 | let [start, end] = _getStartStop(last) 255 | result.push(new EmptyArgument(state.nodeId++, start, end)) 256 | } 257 | } 258 | return result 259 | } 260 | 261 | function _getStartStop (n) { 262 | if (n.start) { 263 | if (n.stop) { 264 | return [n.start.start, n.stop.stop + 1] 265 | } else { 266 | return [n.start.start, n.start.stop + 1] 267 | } 268 | } else if (n.symbol) { 269 | return [n.symbol.start, n.symbol.stop + 1] 270 | } else { 271 | return [undefined, undefined] 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/evaluate.js: -------------------------------------------------------------------------------- 1 | import parse from './parse' 2 | 3 | export default async function evaluate (str, context) { 4 | let expr = parse(str) 5 | if (expr.syntaxError) { 6 | return expr.syntaxError 7 | } 8 | return expr.evaluate(context) 9 | } 10 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | import { MiniLexer, MiniParser, InputStream, CommonTokenStream } from '../parser/parser' 2 | import ErrorListener from './ErrorListener' 3 | import Expression from './Expression' 4 | 5 | export default function parse (source = '', options = {}) { 6 | const errorListener = new ErrorListener() 7 | const lexer = new MiniLexer(new InputStream(source)) 8 | const parser = new MiniParser(new CommonTokenStream(lexer)) 9 | parser.buildParseTrees = true 10 | if (!options.debug) { 11 | lexer.removeErrorListeners() 12 | parser.removeErrorListeners() 13 | } 14 | lexer.addErrorListener(errorListener) 15 | parser.addErrorListener(errorListener) 16 | // NOTE: 'mini' is the start rule as defined in the grammar file 17 | let ast = parser.mini() 18 | let syntaxError = errorListener.syntaxErrors[0] 19 | if (syntaxError) { 20 | _enhanceSyntaxError(parser, syntaxError) 21 | } 22 | return Expression.create(source, ast, syntaxError) 23 | } 24 | 25 | function _enhanceSyntaxError(parser, syntaxError) { // eslint-disable-line 26 | // TODO: we want to create a human readable message 27 | } 28 | -------------------------------------------------------------------------------- /src/walk.js: -------------------------------------------------------------------------------- 1 | export default function walk (expr, fn) { 2 | // pre-fix dfs walk 3 | let stack = [] 4 | if (expr && expr.root) stack.push(expr.root) 5 | // ATTENTION: to get the correct order of children 6 | // we must push children in reverse order, as we are using 7 | // a stack 8 | while (stack.length) { 9 | const next = stack.pop() 10 | // visit before descending 11 | fn(next) 12 | switch (next.type) { 13 | case 'definition': 14 | stack.push(next.rhs) 15 | break 16 | case 'function': 17 | for (let i = next.args.length - 1; i >= 0; i--) { 18 | stack.push(next.args[i]) 19 | } 20 | break 21 | case 'call': 22 | for (let i = next.namedArgs.length - 1; i >= 0; i--) { 23 | stack.push(next.namedArgs[i]) 24 | } 25 | for (let i = next.args.length - 1; i >= 0; i--) { 26 | stack.push(next.args[i]) 27 | } 28 | break 29 | case 'named-argument': 30 | stack.push(next.rhs) 31 | break 32 | case 'pipe': 33 | stack.push(next.right) 34 | stack.push(next.left) 35 | break 36 | case 'array': 37 | for (let i = next.items.length - 1; i >= 0; i--) { 38 | stack.push(next.items[i]) 39 | } 40 | break 41 | case 'object': 42 | for (let i = next.entries.length - 1; i >= 0; i--) { 43 | stack.push(next.entries[i].val) 44 | } 45 | break 46 | default: 47 | // 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/TestContext.js: -------------------------------------------------------------------------------- 1 | import { evaluate } from '../index' 2 | 3 | // An execution used for testing. Exposes the same API as `MiniContext` in the stencila/stencila repo 4 | export default class TestContext { 5 | constructor () { 6 | this._funs = { 7 | // Operator functions in order of precedence; equal precedence if on same line. 8 | select, 9 | not, 10 | positive, 11 | negative, 12 | pow, 13 | multiply, 14 | divide, 15 | remainder, 16 | add, 17 | subtract, 18 | less, 19 | 'less_or_equal': lessOrEqual, 20 | greater, 21 | 'greater_or_equal': greaterOrEqual, 22 | equal, 23 | 'not_equal': notEqual, 24 | and, 25 | or 26 | } 27 | this._vals = {} 28 | } 29 | 30 | registerFunction (name, fn) { 31 | this._funs[name] = fn 32 | } 33 | 34 | callFunction (name, args, namedArgs) { 35 | let fun = this._funs[name] 36 | if (!fun) throw new Error(`Function "${name}" does not exist`) 37 | let named = {} 38 | namedArgs.forEach(a => { 39 | named[a.name] = a.value 40 | }) 41 | return fun(...args, named) 42 | } 43 | 44 | setValue (name, val) { 45 | this._vals[name] = val 46 | } 47 | 48 | resolve (name) { 49 | return this._vals[name] 50 | } 51 | 52 | evaluate (str) { 53 | return evaluate(str, this) 54 | } 55 | 56 | pack (val) { 57 | return val 58 | } 59 | 60 | unpack (val) { 61 | return val 62 | } 63 | } 64 | 65 | // Operator functions in order of precedence 66 | // 67 | // For operator precendece in other languages see 68 | // http://en.cppreference.com/w/cpp/language/operator_precedence 69 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence 70 | 71 | function select (value, member) { 72 | return value[member] 73 | } 74 | 75 | function not (a) { 76 | return !a 77 | } 78 | 79 | function positive (a) { 80 | return +1 * a 81 | } 82 | 83 | function negative (a) { 84 | return -1 * a 85 | } 86 | 87 | function pow (a, b) { 88 | return Math.pow(a, b) 89 | } 90 | 91 | function multiply (a, b) { 92 | return a * b 93 | } 94 | 95 | function divide (a, b) { 96 | return a / b 97 | } 98 | 99 | function remainder (a, b) { 100 | return a % b 101 | } 102 | 103 | function add (a, b) { 104 | return a + b 105 | } 106 | 107 | function subtract (a, b) { 108 | return a - b 109 | } 110 | 111 | function less (a, b) { 112 | return a < b 113 | } 114 | 115 | function lessOrEqual (a, b) { 116 | return a <= b 117 | } 118 | 119 | function greater (a, b) { 120 | return a > b 121 | } 122 | 123 | function greaterOrEqual (a, b) { 124 | return a >= b 125 | } 126 | 127 | function equal (a, b) { 128 | return a === b 129 | } 130 | 131 | function notEqual (a, b) { 132 | return a !== b 133 | } 134 | 135 | function and (a, b) { 136 | return a && b 137 | } 138 | 139 | function or (a, b) { 140 | return a || b 141 | } 142 | -------------------------------------------------------------------------------- /test/eval.test.js: -------------------------------------------------------------------------------- 1 | import { testAsync } from './testHelpers' 2 | import TestContext from './TestContext' 3 | 4 | const MESSAGE_CORRECT_VALUE = 'Value should be correct' 5 | 6 | testAsync('Eval: Number', async t => { 7 | let res 8 | const context = new TestContext() 9 | res = await context.evaluate('1') 10 | t.equal(res, 1, MESSAGE_CORRECT_VALUE) 11 | t.end() 12 | }) 13 | 14 | testAsync('Eval: Boolean', async t => { 15 | let res 16 | const context = new TestContext() 17 | res = await context.evaluate('true') 18 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 19 | res = await context.evaluate('false') 20 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 21 | t.end() 22 | }) 23 | 24 | testAsync('Eval: String', async t => { 25 | let res 26 | const context = new TestContext() 27 | res = await context.evaluate('"foo"') 28 | t.equal(res, 'foo', MESSAGE_CORRECT_VALUE) 29 | t.end() 30 | }) 31 | 32 | testAsync('Eval: Array', async t => { 33 | let res 34 | const context = new TestContext() 35 | res = await context.evaluate('[1,2,3]') 36 | t.deepEqual(res, [1, 2, 3], MESSAGE_CORRECT_VALUE) 37 | t.end() 38 | }) 39 | 40 | testAsync('Eval: Object', async t => { 41 | let res 42 | const context = new TestContext() 43 | res = await context.evaluate('{ a: 1, b: 2 }') 44 | t.deepEqual(res, { a: 1, b: 2 }, MESSAGE_CORRECT_VALUE) 45 | t.end() 46 | }) 47 | 48 | testAsync('Eval: Select', async t => { 49 | let res 50 | const context = new TestContext() 51 | context.setValue('x', {a: 1, b: 2, c: 3}) 52 | context.setValue('y', [4, 5, 6]) 53 | res = await context.evaluate('x.a') 54 | t.equal(res, 1, MESSAGE_CORRECT_VALUE) 55 | res = await context.evaluate('x.b') 56 | t.equal(res, 2, MESSAGE_CORRECT_VALUE) 57 | res = await context.evaluate('x["c"]') 58 | t.equal(res, 3, MESSAGE_CORRECT_VALUE) 59 | res = await context.evaluate('y[0]') 60 | t.equal(res, 4, MESSAGE_CORRECT_VALUE) 61 | res = await context.evaluate('y[2-1]') 62 | t.equal(res, 5, MESSAGE_CORRECT_VALUE) 63 | t.end() 64 | }) 65 | 66 | testAsync('Eval: Less than', async t => { 67 | let res 68 | const context = new TestContext() 69 | res = await context.evaluate('1<2') 70 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 71 | res = await context.evaluate('2<1') 72 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 73 | t.end() 74 | }) 75 | 76 | testAsync('Eval: Greater than', async t => { 77 | let res 78 | const context = new TestContext() 79 | res = await context.evaluate('2>1') 80 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 81 | res = await context.evaluate('1>2') 82 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 83 | t.end() 84 | }) 85 | 86 | testAsync('Eval: Equal to', async t => { 87 | let res 88 | const context = new TestContext() 89 | res = await context.evaluate('1==1') 90 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 91 | res = await context.evaluate('1==2') 92 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 93 | t.end() 94 | }) 95 | 96 | testAsync('Eval: Less than or equal to', async t => { 97 | let res 98 | const context = new TestContext() 99 | res = await context.evaluate('1<=2') 100 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 101 | res = await context.evaluate('2<=2') 102 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 103 | res = await context.evaluate('2<=1') 104 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 105 | t.end() 106 | }) 107 | 108 | testAsync('Eval: Greater than or equal to', async t => { 109 | let res 110 | const context = new TestContext() 111 | res = await context.evaluate('2>=1') 112 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 113 | res = await context.evaluate('2>=2') 114 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 115 | res = await context.evaluate('1>=2') 116 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 117 | t.end() 118 | }) 119 | 120 | testAsync('Eval: And', async t => { 121 | let res 122 | const context = new TestContext() 123 | res = await context.evaluate('true && true') 124 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 125 | res = await context.evaluate('true && false') 126 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 127 | res = await context.evaluate('1<2 && 3<4') 128 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 129 | res = await context.evaluate('1>2 && 3<4') 130 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 131 | t.end() 132 | }) 133 | 134 | testAsync('Eval: Or', async t => { 135 | let res 136 | const context = new TestContext() 137 | res = await context.evaluate('true || false') 138 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 139 | res = await context.evaluate('false || false') 140 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 141 | res = await context.evaluate('1<2 || 3>4') 142 | t.equal(res, true, MESSAGE_CORRECT_VALUE) 143 | res = await context.evaluate('1>2 || 3>4') 144 | t.equal(res, false, MESSAGE_CORRECT_VALUE) 145 | t.end() 146 | }) 147 | 148 | testAsync('Eval: Plus', async t => { 149 | let res 150 | const context = new TestContext() 151 | res = await context.evaluate('1+2') 152 | t.deepEqual(res, 3, MESSAGE_CORRECT_VALUE) 153 | t.end() 154 | }) 155 | 156 | testAsync('Eval: Times', async t => { 157 | let res 158 | const context = new TestContext() 159 | res = await context.evaluate('2*3') 160 | t.deepEqual(res, 6, MESSAGE_CORRECT_VALUE) 161 | t.end() 162 | }) 163 | 164 | testAsync('Eval: Minus', async t => { 165 | let res 166 | const context = new TestContext() 167 | res = await context.evaluate('5-3') 168 | t.deepEqual(res, 2, MESSAGE_CORRECT_VALUE) 169 | t.end() 170 | }) 171 | 172 | testAsync('Eval: Division', async t => { 173 | let res 174 | const context = new TestContext() 175 | res = await context.evaluate('6/3') 176 | t.deepEqual(res, 2, MESSAGE_CORRECT_VALUE) 177 | t.end() 178 | }) 179 | 180 | testAsync('Eval: Power', async t => { 181 | let res 182 | const context = new TestContext() 183 | res = await context.evaluate('2^3') 184 | t.deepEqual(res, 8, MESSAGE_CORRECT_VALUE) 185 | t.end() 186 | }) 187 | 188 | testAsync('Eval: Groups', async t => { 189 | let res 190 | const context = new TestContext() 191 | res = await context.evaluate('(1+2)*(3+4)') 192 | t.deepEqual(res, 21, MESSAGE_CORRECT_VALUE) 193 | t.end() 194 | }) 195 | 196 | testAsync('Eval: Var', async t => { 197 | let res 198 | const context = new TestContext() 199 | context.setValue('x', 4) 200 | res = await context.evaluate('x') 201 | t.equal(res, 4, MESSAGE_CORRECT_VALUE) 202 | t.end() 203 | }) 204 | 205 | testAsync('Eval: Pipe', async t => { 206 | let res 207 | const context = new TestContext() 208 | context.setValue('x', 4) 209 | context.registerFunction('foo', function (x) { return x + 5 }) 210 | res = await context.evaluate('1 | add(x)') 211 | t.equal(res, 5, MESSAGE_CORRECT_VALUE) 212 | res = await context.evaluate('x | add(2) | divide(2)') 213 | t.equal(res, 3, MESSAGE_CORRECT_VALUE) 214 | res = await context.evaluate('x | foo()') 215 | t.equal(res, 9, MESSAGE_CORRECT_VALUE) 216 | t.end() 217 | }) 218 | 219 | testAsync('Eval: 2*-x', async t => { 220 | let res 221 | const context = new TestContext() 222 | context.setValue('x', 4) 223 | res = await context.evaluate('2*-x') 224 | t.deepEqual(res, -8, MESSAGE_CORRECT_VALUE) 225 | t.end() 226 | }) 227 | 228 | testAsync('Eval: 1+x+2', async t => { 229 | let res 230 | const context = new TestContext() 231 | context.setValue('x', 4) 232 | context.setValue('data', [[10]]) 233 | res = await context.evaluate('1+x+2') 234 | t.equal(res, 7, MESSAGE_CORRECT_VALUE) 235 | t.end() 236 | }) 237 | 238 | testAsync('Eval: 6/2*8', async t => { 239 | let res 240 | const context = new TestContext() 241 | res = await context.evaluate('6/2*8') 242 | t.deepEqual(res, 24, MESSAGE_CORRECT_VALUE) 243 | t.end() 244 | }) 245 | 246 | testAsync('Eval: 2*2==4 && 3+1<=4', async t => { 247 | let res 248 | const context = new TestContext() 249 | res = await context.evaluate('2*2==4 && 3+1<=4') 250 | t.deepEqual(res, true, MESSAGE_CORRECT_VALUE) 251 | t.end() 252 | }) 253 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Substance Mini 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import './parse.test.js' 2 | import './eval.test.js' 3 | import './syntax-highlighting.test.js' 4 | -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | import { module } from 'substance-test' 2 | import { parse, walk } from '../index' 3 | 4 | const test = module('Parse') 5 | 6 | const MESSAGE_CORRECT_AST = 'AST should have correct structure' 7 | 8 | test('Empty Expression', (t) => { 9 | const expr = parse('') 10 | const expectedTypes = ['error'] 11 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 12 | t.end() 13 | }) 14 | 15 | test('Number', (t) => { 16 | const expr = parse('1') 17 | const expectedTypes = ['number'] 18 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 19 | t.end() 20 | }) 21 | 22 | test('Variable', (t) => { 23 | const expr = parse('x') 24 | const expectedTypes = ['var'] 25 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 26 | t.end() 27 | }) 28 | 29 | test('Boolean', (t) => { 30 | let expr = parse('true') 31 | let expectedTypes = ['boolean'] 32 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 33 | expr = parse('false') 34 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 35 | t.end() 36 | }) 37 | 38 | test('String', (t) => { 39 | const expr = parse('"foo"') 40 | const expectedTypes = ['string'] 41 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 42 | t.end() 43 | }) 44 | 45 | test('Array', (t) => { 46 | const expr = parse('[1,x,true]') 47 | const expectedTypes = ['array', 'number', 'var', 'boolean'] 48 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 49 | t.end() 50 | }) 51 | 52 | test('Object', (t) => { 53 | const expr = parse('{foo: 1, bar: x, baz: true}') 54 | const expectedTypes = ['object', 'number', 'var', 'boolean'] 55 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 56 | t.end() 57 | }) 58 | 59 | test('Group', (t) => { 60 | const expr = parse('(1+2)*3') 61 | const expectedTypes = ['call:multiply', 'call:add', 'number', 'number', 'number'] 62 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 63 | t.end() 64 | }) 65 | 66 | test('Select using id', (t) => { 67 | _equal(t, getNodeTypes(parse('a.b')), ['call:select', 'var', 'string'], MESSAGE_CORRECT_AST) 68 | _equal(t, getNodeTypes(parse('{a:1}.a')), ['call:select', 'object', 'number', 'string'], MESSAGE_CORRECT_AST) 69 | t.end() 70 | }) 71 | 72 | test('Select using expression', (t) => { 73 | _equal(t, getNodeTypes(parse('a[1]')), ['call:select', 'var', 'number'], MESSAGE_CORRECT_AST) 74 | _equal(t, getNodeTypes(parse('[0,1][0]')), ['call:select', 'array', 'number', 'number', 'number'], MESSAGE_CORRECT_AST) 75 | _equal(t, getNodeTypes(parse('b[i+1]')), ['call:select', 'var', 'call:add', 'var', 'number'], MESSAGE_CORRECT_AST) 76 | _equal(t, getNodeTypes(parse('table[["col1","col2"]]')), ['call:select', 'var', 'array', 'string', 'string'], MESSAGE_CORRECT_AST) 77 | // this should not be a 'select' 78 | _equal(t, getNodeTypes(parse('sum([1,2,3])')), ['call:sum', 'array', 'number', 'number', 'number'], MESSAGE_CORRECT_AST) 79 | t.end() 80 | }) 81 | 82 | test('Not', (t) => { 83 | _equal(t, getNodeTypes(parse('!true')), ['call:not', 'boolean'], MESSAGE_CORRECT_AST) 84 | t.end() 85 | }) 86 | 87 | test('Positive', (t) => { 88 | _equal(t, getNodeTypes(parse('+1')), ['call:positive', 'number'], MESSAGE_CORRECT_AST) 89 | t.end() 90 | }) 91 | 92 | test('Negative', (t) => { 93 | _equal(t, getNodeTypes(parse('-1')), ['call:negative', 'number'], MESSAGE_CORRECT_AST) 94 | t.end() 95 | }) 96 | 97 | test('Power', (t) => { 98 | const expr = parse('2^3') 99 | const expectedTypes = ['call:pow', 'number', 'number'] 100 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 101 | t.end() 102 | }) 103 | 104 | test('Multiply', (t) => { 105 | const expr = parse('2*3') 106 | const expectedTypes = ['call:multiply', 'number', 'number'] 107 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 108 | t.end() 109 | }) 110 | 111 | test('Divide', (t) => { 112 | const expr = parse('6/3') 113 | const expectedTypes = ['call:divide', 'number', 'number'] 114 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 115 | t.end() 116 | }) 117 | 118 | test('Add', (t) => { 119 | const expr = parse('1+2') 120 | const expectedTypes = ['call:add', 'number', 'number'] 121 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 122 | t.end() 123 | }) 124 | 125 | test('Subtract', (t) => { 126 | const expr = parse('5-3') 127 | const expectedTypes = ['call:subtract', 'number', 'number'] 128 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 129 | t.end() 130 | }) 131 | 132 | test('Less', (t) => { 133 | const expr = parse('1 < 2') 134 | const expectedTypes = ['call:less', 'number', 'number'] 135 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 136 | t.end() 137 | }) 138 | 139 | test('Less or equal', (t) => { 140 | const expr = parse('1 <= 2') 141 | const expectedTypes = ['call:less_or_equal', 'number', 'number'] 142 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 143 | t.end() 144 | }) 145 | 146 | test('Greater', (t) => { 147 | const expr = parse('1 > 2') 148 | const expectedTypes = ['call:greater', 'number', 'number'] 149 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 150 | t.end() 151 | }) 152 | 153 | test('Greater or equal', (t) => { 154 | const expr = parse('1 >= 2') 155 | const expectedTypes = ['call:greater_or_equal', 'number', 'number'] 156 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 157 | t.end() 158 | }) 159 | 160 | test('Equal', (t) => { 161 | const expr = parse('"foo" == "bar"') 162 | const expectedTypes = ['call:equal', 'string', 'string'] 163 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 164 | t.end() 165 | }) 166 | 167 | test('Not equal', (t) => { 168 | const expr = parse('"foo" != "bar"') 169 | const expectedTypes = ['call:not_equal', 'string', 'string'] 170 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 171 | t.end() 172 | }) 173 | 174 | test('And', (t) => { 175 | _equal(t, getNodeTypes(parse('true && false')), ['call:and', 'boolean', 'boolean'], MESSAGE_CORRECT_AST) 176 | _equal(t, getNodeTypes(parse('a==1 && b>=2')), ['call:and', 'call:equal', 'var', 'number', 'call:greater_or_equal', 'var', 'number'], MESSAGE_CORRECT_AST) 177 | t.end() 178 | }) 179 | 180 | test('Or', (t) => { 181 | _equal(t, getNodeTypes(parse('true || false')), ['call:or', 'boolean', 'boolean'], MESSAGE_CORRECT_AST) 182 | _equal(t, getNodeTypes(parse('a==1 || b>=2')), ['call:or', 'call:equal', 'var', 'number', 'call:greater_or_equal', 'var', 'number'], MESSAGE_CORRECT_AST) 183 | t.end() 184 | }) 185 | 186 | test('1+x+2', (t) => { 187 | const expr = parse('1+x+2') 188 | const expectedTypes = ['call:add', 'call:add', 'number', 'var', 'number'] 189 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 190 | t.end() 191 | }) 192 | 193 | test('Definition', (t) => { 194 | const expr = parse('x = 42') 195 | const expectedTypes = ['definition', 'number'] 196 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 197 | t.end() 198 | }) 199 | 200 | test('Function call without arguments', (t) => { 201 | const expr = parse('foo()') 202 | const expectedTypes = ['call:foo'] 203 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 204 | t.end() 205 | }) 206 | 207 | test('Function call with one argument', (t) => { 208 | const expr = parse('foo(x)') 209 | const expectedTypes = ['call:foo', 'var'] 210 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 211 | t.end() 212 | }) 213 | 214 | test('Function call with mixed arguments', (t) => { 215 | const expr = parse('sum(1,x,2)') 216 | const expectedTypes = ['call:sum', 'number', 'var', 'number'] 217 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 218 | t.end() 219 | }) 220 | 221 | test('Function call with one named argument', (t) => { 222 | const expr = parse('foo(x=2)') 223 | const expectedTypes = ['call:foo', 'named-argument', 'number'] 224 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 225 | t.end() 226 | }) 227 | 228 | test('Function call with one positional and one named argument', (t) => { 229 | const expr = parse('foo(1, x=2)') 230 | const expectedTypes = ['call:foo', 'number', 'named-argument', 'number'] 231 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 232 | t.end() 233 | }) 234 | 235 | test('Function call with multiple positional and multiple named arguments', (t) => { 236 | const expr = parse('foo(1, x, true, x=2, y=x, z=false)') 237 | const expectedTypes = ['call:foo', 'number', 'var', 'boolean', 'named-argument', 'number', 'named-argument', 'var', 'named-argument', 'boolean'] 238 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 239 | t.end() 240 | }) 241 | 242 | test('Piping a function call into another', (t) => { 243 | const expr = parse('foo() | bar()') 244 | const expectedTypes = ['pipe', 'call:foo', 'call:bar'] 245 | _equal(t, getNodeTypes(expr), expectedTypes, MESSAGE_CORRECT_AST) 246 | t.end() 247 | }) 248 | 249 | test('1+', (t) => { 250 | const expr = parse('1+') 251 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 252 | t.end() 253 | }) 254 | 255 | test('foo(x,y', (t) => { 256 | const expr = parse('foo(x,y') 257 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 258 | t.end() 259 | }) 260 | 261 | test('x 7', (t) => { 262 | const expr = parse('x 7') 263 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 264 | t.end() 265 | }) 266 | 267 | test('Named arguments before positional arguments', (t) => { 268 | const expr = parse('foo(x=7, 7)') 269 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 270 | t.end() 271 | }) 272 | 273 | test('a: 1, b:2}', (t) => { 274 | const expr = parse('a: 1, b:2}') 275 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 276 | t.end() 277 | }) 278 | 279 | test(", '2'}", (t) => { 280 | const expr = parse(", '2'}") 281 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 282 | t.end() 283 | }) 284 | 285 | test('sum([,5])', (t) => { 286 | const expr = parse('sum([,5])') 287 | t.notNil(expr.syntaxError, 'There should be a syntaxError.') 288 | t.end() 289 | }) 290 | 291 | function _equal (t, arr1, arr2, msg) { 292 | return t.equal(String(arr1), String(arr2), msg) 293 | } 294 | 295 | function getNodeTypes (expr) { 296 | let types = [] 297 | walk(expr, (node) => { 298 | let name 299 | if (node.type === 'call') name = `call:${node.name}` 300 | else name = node.type 301 | types.push(name) 302 | }) 303 | return types 304 | } 305 | -------------------------------------------------------------------------------- /test/syntax-highlighting.test.js: -------------------------------------------------------------------------------- 1 | import { module } from 'substance-test' 2 | import { parse } from '../index' 3 | 4 | const test = module('Syntax Highlighting') 5 | 6 | const MESSAGE_CORRECT_TOKENS = 'Generated tokens should be correct.' 7 | 8 | test('Number', (t) => { 9 | const expr = parse('1') 10 | const expectedTokens = [{ 11 | type: 'number-literal', 12 | start: 0, 13 | end: 1 14 | }] 15 | _checkTokens(t, expr.tokens, expectedTokens, MESSAGE_CORRECT_TOKENS) 16 | t.end() 17 | }) 18 | 19 | test('Numbers', (t) => { 20 | const expr = parse('1+2') 21 | const expectedTokens = [{ 22 | type: 'number-literal', 23 | start: 0, 24 | end: 1 25 | }, { 26 | type: 'number-literal', 27 | start: 2, 28 | end: 3 29 | }] 30 | _checkTokens(t, expr.tokens, expectedTokens, MESSAGE_CORRECT_TOKENS) 31 | t.end() 32 | }) 33 | 34 | test('String', (t) => { 35 | const expr = parse("'foo'") 36 | const expectedTokens = [{ 37 | type: 'string-literal', 38 | start: 0, 39 | end: 5 40 | }] 41 | _checkTokens(t, expr.tokens, expectedTokens, MESSAGE_CORRECT_TOKENS) 42 | t.end() 43 | }) 44 | 45 | test('Boolean', (t) => { 46 | const expr = parse('true') 47 | const expectedTokens = [{ 48 | type: 'boolean-literal', 49 | start: 0, 50 | end: 4 51 | }] 52 | _checkTokens(t, expr.tokens, expectedTokens, MESSAGE_CORRECT_TOKENS) 53 | t.end() 54 | }) 55 | 56 | test('Definition', (t) => { 57 | const expr = parse('x=1') 58 | const expectedTokens = [{ 59 | type: 'output-name', 60 | start: 0, 61 | end: 1 62 | }, { 63 | type: 'number-literal', 64 | start: 2, 65 | end: 3 66 | }] 67 | _checkTokens(t, expr.tokens, expectedTokens, MESSAGE_CORRECT_TOKENS) 68 | t.end() 69 | }) 70 | 71 | test('Object', (t) => { 72 | const expr = parse("{a: true, b: 'foo', c: 1}") 73 | const expectedTokens = [{ 74 | type: 'key', 75 | start: 1, 76 | end: 2 77 | }, { 78 | type: 'boolean-literal', 79 | start: 4, 80 | end: 8 81 | }, { 82 | type: 'key', 83 | start: 10, 84 | end: 11 85 | }, { 86 | type: 'string-literal', 87 | start: 13, 88 | end: 18 89 | }, { 90 | type: 'key', 91 | start: 20, 92 | end: 21 93 | }, { 94 | type: 'number-literal', 95 | start: 23, 96 | end: 24 97 | }] 98 | _checkTokens(t, expr.tokens, expectedTokens, MESSAGE_CORRECT_TOKENS) 99 | t.end() 100 | }) 101 | 102 | function _checkTokens (t, act, exp, msg) { 103 | // only check for some fields 104 | const keys = ['type', 'start', 'end'] 105 | let _act = act.map((t) => { 106 | let _t = {} 107 | keys.forEach((k) => { 108 | _t[k] = t[k] 109 | }) 110 | return _t 111 | }) 112 | return t.equal(JSON.stringify(exp, null, 2), JSON.stringify(_act, null, 2), msg) 113 | } 114 | -------------------------------------------------------------------------------- /test/testHelpers.js: -------------------------------------------------------------------------------- 1 | import { test } from 'substance-test' 2 | import { DefaultDOMElement, platform } from 'substance' 3 | 4 | export function testAsync (name, func) { 5 | test(name, async assert => { 6 | let success = false 7 | try { 8 | await func(assert) 9 | success = true 10 | } finally { 11 | if (!success) { 12 | assert.fail('Test failed with an uncaught exception.') 13 | assert.end() 14 | } 15 | } 16 | }) 17 | } 18 | 19 | export function wait (ms) { 20 | return () => { 21 | return new Promise((resolve) => { 22 | setTimeout(() => { 23 | resolve() 24 | }, ms) 25 | }) 26 | } 27 | } 28 | 29 | export function getMountPoint (t) { 30 | let mountPoint 31 | if (platform.inBrowser) { 32 | mountPoint = t.sandbox 33 | } else { 34 | let htmlDoc = DefaultDOMElement.createDocument('html') 35 | mountPoint = htmlDoc.createElement('div') 36 | htmlDoc.appendChild(mountPoint) 37 | } 38 | return mountPoint 39 | } 40 | --------------------------------------------------------------------------------