Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Arrow function support + array methods #123

Closed
wants to merge 9 commits into from
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,81 @@ jsep.addIdentifierChar("@");
jsep.removeIdentifierChar('@');
```

### Plugins
jsep supports defining custom hooks for extending or modifying the expression parsing.
All hooks are called with a single argument and return void.
The hook argument provides access to the internal parsing methods of jsep
to allow reuse as needed.

#### Hook Argument

```typescript
import { PossibleExpression } from 'jsep';

export interface HookScope {
index: number;
expr: string;
exprI: string;
exprICode: () => number;
gobbleSpaces: () => void;
gobbleExpression: () => Expression;
gobbleBinaryOp: () => PossibleExpression;
gobbleBinaryExpression: () => PossibleExpression;
gobbleToken: () => PossibleExpression;
gobbleNumericLiteral: () => PossibleExpression;
gobbleStringLiteral: () => PossibleExpression;
gobbleIdentifier: () => PossibleExpression;
gobbleArguments: (number) => PossibleExpression;
gobbleGroup: () => Expression;
gobbleArray: () => PossibleExpression;
throwError: (string) => void;
nodes?: PossibleExpression[];
node?: PossibleExpression;
}
```

#### Hook Types
* `before-all`: called just before starting all expression parsing
* `after-all`: called just before returning from parsing
* `gobble-expression`: called just before attempting to parse an expression
* `after-expression`: called just after parsing an expression
* `gobble-token`: called just before attempting to parse a token
* `after-token`: called just after parsing a token
* `gobble-spaces`: called when gobbling spaces

### How to add Hooks
```javascript
// single:
jsep.hooks.add('after-expression', function(env) {
console.log('got expression', JSON.stringify(env.node, null, 2));
});
// last argument will add to the top of the array, instead of the bottom by default
jsep.hooks.add('after-all', () => console.log('done'), true);

// multi:
const myHooks = {
'before-all': env => console.log(`parsing ${env.expr}`),
'after-all': env => console.log(`found ${env.nodes.length} nodes`);
};
jsep.hooks.add(myHooks);
```

#### How to add plugins:
```javascript
const jsep = require('jsep');
const plugins = require('jsep/plugins');
plugins.forEach(p => p(jsep));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we switch to ESM-style imports here?

```

#### Optional Plugins:
* `arrowFunction`: Adds arrow-function support, `(a) => x`, `x => x`
* `assignment`: Adds support for assignment expressions
* `ignoreComments`: Adds support for ignoring comments: `// comment` and `/* comment */`
* `new`: Adds support for the `new` operator
* `object`: Adds support for object expressions
* `templateLiteral`: Adds support for template literals, `` `this ${value + `${nested}}` ``
* `ternary`: Built-in by default, adds support for ternary `a ? b : c` expressions

### License

jsep is under the MIT license. See LICENSE file.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
"build:watch": "npx rollup -c --watch",
"test": "npx http-server -p 49649 --silent & npx node-qunit-puppeteer http://localhost:49649/test/unit_tests.html",
"docco": "npx docco src/jsep.js --css=src/docco.css --output=annotated_source/",
"lint": "npx eslint src/jsep.js"
"lint": "npx eslint src/jsep.js plugins/**/*.js"
}
}
40 changes: 40 additions & 0 deletions plugins/arrow-function/jsep-arrow-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export default function (jsep) {
if (typeof jsep === 'undefined') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is a good pattern, for the following reasons:

  1. It exports a function that should never be called twice, but it's very easy to accidentally call it multiple times. What if multiple libraries on the same page include jsep, each importing different but overlapping sets of plugins?
  2. It forces each plugin to use boilerplate to check that jsep actually exists
  3. It depends on third party code to call it properly.
  4. The entire plugin code needs to be inside a callback, which is awkward.

Ideally, third-party code should not need to run any code to activate plugins, including them should just work.

One way to do that is if each plugin simply imports jsep using classic ESM imports.
Then, third party code using jsep can just import both:

import jsep from "jsep.js";
import "jsep-plugins/jsep-arrow-function.js";

The main downside of this is that since plugins import jsep, they need to know its path (relative or not). import.meta.url could help here.

return;
}

const EQUAL_CODE = 61; // =
const GTHAN_CODE = 62; // >
const ARROW_EXP = 'ArrowFunctionExpression';
jsep.addBinaryOp('=>', 0);

jsep.hooks.add('gobble-expression', function gobbleArrowExpression(env) {
if (env.exprICode(env.index) === EQUAL_CODE) {
// arrow expression: () => expr
env.index++;
if (env.exprICode(env.index) === GTHAN_CODE) {
env.index++;
env.node = {
type: ARROW_EXP,
params: env.node ? [env.node] : null,
body: env.gobbleExpression(),
};
}
else {
env.throwError('Expected >');
}
}
});

// This is necessary when adding '=' as a binary operator (for assignment)
// Otherwise '>' throws an error for the right-hand side
jsep.hooks.add('after-expression', function fixBinaryArrow(env) {
if (env.node && env.node.operator === '=>') {
env.node = {
type: 'ArrowFunctionExpression',
params: env.node.left ? [env.node.left] : null,
body: env.node.right,
};
}
});
};
28 changes: 28 additions & 0 deletions plugins/assignment/jsep-assignment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default function (jsep) {
if (typeof jsep === 'undefined') {
return;
}

const assignmentOperators = new Set([
'=',
'*=',
'**=',
'/=',
'%=',
'+=',
'-=',
'<<=',
'>>=',
'>>>=',
'&=',
'^=',
'|=',
]);
assignmentOperators.forEach(op => jsep.addBinaryOp(op, 0.9));

jsep.hooks.add('after-expression', function gobbleAssignment(env) {
if (assignmentOperators.has(env.node.operator)) {
env.node.type = 'AssignmentExpression';
}
});
};
36 changes: 36 additions & 0 deletions plugins/ignore-comments/jsep-ignore-comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export default function (jsep) {
if (typeof jsep === 'undefined') {
return;
}

const FSLSH_CODE = 47; // /
const ASTSK_CODE = 42; // *
const LF_CODE = 10;

jsep.hooks.add('gobble-spaces', function gobbleComment(env) {
if (env.exprICode(env.index) === FSLSH_CODE) {
let ch = env.exprICode(env.index + 1);
if (ch === FSLSH_CODE) {
// read to end of line
env.index += 2;
while (ch !== LF_CODE && !isNaN(ch)) {
ch = env.exprICode(++env.index);
}
}
else if (ch === ASTSK_CODE) {
// read to */ or end of input
env.index += 2;
while (!isNaN(ch)) {
ch = env.exprICode(++env.index);
if (ch === ASTSK_CODE) {
ch = env.exprICode(++env.index);
if (ch === FSLSH_CODE) {
env.index += 1;
break;
}
}
}
}
}
});
};
24 changes: 24 additions & 0 deletions plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import arrowFunction from './arrow-function/jsep-arrow-function.js';
import assignment from './assignment/jsep-assignment.js';
import ignoreComments from './ignore-comments/jsep-ignore-comments.js';
import newExpression from './new/jsep-new.js';
import object from './object/jsep-object.js';
import templateLiteral from './template-literal/jsep-template-literal.js';

export {
arrowFunction,
assignment,
ignoreComments,
newExpression,
object,
templateLiteral,
};

export default [
arrowFunction,
assignment,
ignoreComments,
newExpression,
object,
templateLiteral,
];
18 changes: 18 additions & 0 deletions plugins/new/jsep-new.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function (jsep) {
if (typeof jsep === 'undefined') {
return;
}

jsep.addUnaryOp('new');

jsep.hooks.add('after-token', function gobbleNew(env) {
const node = env.node;
if (node && node.operator === 'new') {
if (!node.argument || node.argument.type !== 'CallExpression') {
env.throwError('Expected new function()');
}
env.node = node.argument;
env.node.type = 'NewExpression';
}
});
};
66 changes: 66 additions & 0 deletions plugins/object/jsep-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export default function (jsep) {
if (typeof jsep === 'undefined') {
return;
}

const PERIOD_CODE = 46; // '.'
const OCURLY_CODE = 123; // {
const CCURLY_CODE = 125; // }
const OBJECT_EXPRESSION = 'ObjectExpression';
const PROPERTY = 'Property';
const SPREAD_ELEMENT = 'SpreadElement';
jsep.addBinaryOp(':', 0.5);

const gobbleObject = function (type) {
return function (env) {
if (env.exprICode(env.index) === OCURLY_CODE) {
env.index++;
const args = env.gobbleArguments(CCURLY_CODE);
const properties = args.map((arg) => {
if (arg.type === 'SpreadElement') {
return arg;
}
if (arg.type === 'Identifier') {
return {
type: PROPERTY,
computed: false,
key: arg.name,
shorthand: true,
};
}
if (arg.type === 'BinaryExpression') {
const computed = arg.left.type === 'ArrayExpression';
return {
type: PROPERTY,
computed,
key: computed
? arg.left.elements[0]
: arg.left,
value: arg.right,
shorthand: false,
};
}
env.throwError('Unexpected object property');
});

env.node = {
type,
properties,
};
}
};
};
jsep.hooks.add('gobble-expression', gobbleObject(OBJECT_EXPRESSION));
jsep.hooks.add('after-token', gobbleObject(OBJECT_EXPRESSION));

jsep.hooks.add('gobble-token', function gobbleSpread(env) {
// check for spread operator:
if ([0, 1, 2].every(i => env.exprICode(env.index + i) === PERIOD_CODE)) {
env.index += 3;
env.node = {
type: SPREAD_ELEMENT,
argument: env.gobbleExpression(),
};
}
});
};
Loading