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

Plugin system #142

Merged
merged 8 commits into from
Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = tab
indent_size = 2
tab_width = 2

[{package.json,}]
indent_style = tab
indent_size = 2
tab_width = 2

[*.json]
indent_style = tab
indent_size = 2
tab_width = 2
8 changes: 8 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
"exceptions": ["*"]
}
}],
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"linebreak-style": 0,
"arrow-spacing": 1,
"comma-spacing": 1,
"keyword-spacing": 1
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,67 @@ jsep.addIdentifierChar("@");
jsep.removeIdentifierChar('@');
```

### Plugins
JSEP supports defining custom hooks for extending or modifying the expression parsing.
All hooks are bound to the jsep instance and called with a single argument and return void.
The 'this' context provide access to the internal parsing methods of jsep
to allow reuse as needed. Some hook types will pass an object that allows reading/writing
the `node` property as needed.

#### Hook 'this' context
```typescript
export interface HookScope {
index: number;
expr: string;
char: string; // current character of the expression
code: number; // current character code of the expression
gobbleSpaces: () => void;
gobbleExpressions: (number?) => Eexpression[];
gobbleExpression: () => Expression;
gobbleBinaryOp: () => PossibleExpression;
gobbleBinaryExpression: () => PossibleExpression;
gobbleToken: () => PossibleExpression;
gobbleNumericLiteral: () => PossibleExpression;
gobbleStringLiteral: () => PossibleExpression;
gobbleIdentifier: () => PossibleExpression;
gobbleArguments: (number) => PossibleExpression;
gobbleGroup: () => Expression;
gobbleArray: () => PossibleExpression;
throwError: (string) => void;
}
```

#### Hook Types
* `before-all`: called just before starting all expression parsing.
* `after-all`: called after parsing all. Read/Write `arg.node` as required.
* `gobble-expression`: called just before attempting to parse an expression. Set `arg.node` as required.
* `after-expression`: called just after parsing an expression. Read/Write `arg.node` as required.
* `gobble-token`: called just before attempting to parse a token. Set `arg.node` as required.
* `after-token`: called just after parsing a token. Read/Write `arg.node` as required.
* `gobble-spaces`: called when gobbling spaces.

#### How to add plugins:
```javascript
import { Jsep } from 'jsep';
import 'jsep/plugins/ternary';
```

#### Optional Plugins:
* `ternary`: Built-in by default, adds support for ternary `a ? b : c` expressions

#### Writing Your Own Plugin:
Refer to the `jsep/plugins` folder for examples. In general, the file should look something like:
```javascript
import { Jsep } from '../../jsep.js';
Jsep.hooksAdd(/* refer to [Hook Types] */, function myPlugin(env) {
if (this.char === '#') {
env.node = {
/* add a node type */
};
}
});
```

### 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 @@ -46,6 +46,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/**/*.js test/*.js"
"lint": "npx eslint src/**/*.js test/*.js test/plugins/**/*.js"
}
}
53 changes: 53 additions & 0 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export default class Hooks {
/**
* @callback HookCallback
* @this {*|Jsep} this
* @param {Jsep} env
* @returns: void
*/
/**
* Adds the given callback to the list of callbacks for the given hook.
*
* The callback will be invoked when the hook it is registered for is run.
*
* One callback function can be registered to multiple hooks and the same hook multiple times.
*
* @param {string|object} name The name of the hook, or an object of callbacks keyed by name
* @param {HookCallback|boolean} callback The callback function which is given environment variables.
* @param {?boolean} [first=false] Will add the hook to the top of the list (defaults to the bottom)
* @public
*/
add(name, callback, first) {
if (typeof arguments[0] != 'string') {
// Multiple hook callbacks, keyed by name
for (let name in arguments[0]) {
this.add(name, arguments[0][name], arguments[1]);
}
}
else {
(Array.isArray(name) ? name : [name]).forEach(function (name) {
this[name] = this[name] || [];

if (callback) {
this[name][first ? 'unshift' : 'push'](callback);
}
}, this);
}
}

/**
* Runs a hook invoking all registered callbacks with the given environment variables.
*
* Callbacks will be invoked synchronously and in the order in which they were registered.
*
* @param {string} name The name of the hook.
* @param {Object<string, any>} env The environment variables of the hook passed to all callbacks registered.
* @public
*/
run(name, env) {
this[name] = this[name] || [];
this[name].forEach(function (callback) {
callback.call(env && env.context ? env.context : env, env);
});
}
}
6 changes: 6 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Add default plugins:
import jsep from './jsep.js';
import './plugins/ternary/ternary.js';

export * from './jsep.js';
export default jsep;
85 changes: 39 additions & 46 deletions src/jsep.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// JavaScript Expression Parser (JSEP) <%= version %>
// JSEP may be freely distributed under the MIT License
// https://ericsmekens.github.io/jsep/
import Hooks from './hooks.js';

export class Jsep {
/**
* @returns {string}
Expand Down Expand Up @@ -237,6 +239,21 @@ export class Jsep {
throw error;
}

/**
* Run a given hook
* @param {string} name
* @param {jsep.Expression|false} [node]
* @returns {?jsep.Expression}
*/
runHook(name, node) {
if (Jsep.hooks[name]) {
const env = { context: this, node };
Jsep.hooks.run(name, env);
return env.node;
}
return node;
}

/**
* Push `index` up to the next non-space character
*/
Expand All @@ -249,25 +266,25 @@ export class Jsep {
|| ch === Jsep.CR_CODE) {
ch = this.expr.charCodeAt(++this.index);
}
this.runHook('gobble-spaces');
}

/**
* Top-level method to parse all expressions and returns compound or single node
* @returns {jsep.Expression}
*/
parse() {
this.runHook('before-all');
const nodes = this.gobbleExpressions();

// If there's only one expression just try returning the expression
if (nodes.length === 1) {
return nodes[0];
}
else {
return {
const node = nodes.length === 1
? nodes[0]
: {
type: Jsep.COMPOUND,
body: nodes
};
}
return this.runHook('after-all', node);
}

/**
Expand Down Expand Up @@ -305,48 +322,15 @@ export class Jsep {
return nodes;
}

//
/**
* The main parsing function. Much of this code is dedicated to ternary expressions
* The main parsing function.
* @returns {?jsep.Expression}
*/
gobbleExpression() {
const test = this.gobbleBinaryExpression();

const node = this.runHook('gobble-expression') || this.gobbleBinaryExpression();
this.gobbleSpaces();

if (this.code === Jsep.QUMARK_CODE) {
// Ternary expression: test ? consequent : alternate
this.index++;
const consequent = this.gobbleExpression();

if (!consequent) {
this.throwError('Expected expression');
}

this.gobbleSpaces();

if (this.code === Jsep.COLON_CODE) {
this.index++;
const alternate = this.gobbleExpression();

if (!alternate) {
this.throwError('Expected expression');
}
return {
type: Jsep.CONDITIONAL_EXP,
test,
consequent,
alternate
};
}
else {
this.throwError('Expected :');
}
}
else {
return test;
}
return this.runHook('after-expression', node);
}

/**
Expand Down Expand Up @@ -468,6 +452,11 @@ export class Jsep {
let ch, to_check, tc_len, node;

this.gobbleSpaces();
node = this.runHook('gobble-token');
if (node) {
return this.runHook('after-token', node);
}

ch = this.code;

if (Jsep.isDecimalDigit(ch) || ch === Jsep.PERIOD_CODE) {
Expand Down Expand Up @@ -495,12 +484,12 @@ export class Jsep {
(this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))
)) {
this.index += tc_len;
return {
return this.runHook('after-token', {
type: Jsep.UNARY_EXP,
operator: to_check,
argument: this.gobbleToken(),
prefix: true
};
});
}

to_check = to_check.substr(0, --tc_len);
Expand All @@ -515,7 +504,7 @@ export class Jsep {
}

if (!node) {
LeaVerou marked this conversation as resolved.
Show resolved Hide resolved
return false;
return this.runHook('after-token', false);
}

this.gobbleSpaces();
Expand Down Expand Up @@ -565,7 +554,7 @@ export class Jsep {
ch = this.code;
}

return node;
return this.runHook('after-token', node);
}

/**
Expand Down Expand Up @@ -829,7 +818,11 @@ export class Jsep {
}

// Static fields:
const hooks = new Hooks();
Object.assign(Jsep, {
hooks,
hooksAdd: hooks.add.bind(hooks),

// Node Types
// ----------
// This is the full set of types that any JSEP node can be.
Expand Down
34 changes: 34 additions & 0 deletions src/plugins/ternary/ternary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Jsep} from '../../jsep.js';

// Ternary expression: test ? consequent : alternate
Jsep.hooksAdd('after-expression', function gobbleTernary(env) {
if (this.code === Jsep.QUMARK_CODE) {
this.index++;
const test = env.node;
const consequent = this.gobbleExpression();

if (!consequent) {
this.throwError('Expected expression');
}

this.gobbleSpaces();

if (this.code === Jsep.COLON_CODE) {
this.index++;
const alternate = this.gobbleExpression();

if (!alternate) {
this.throwError('Expected expression');
}
env.node = {
type: 'ConditionalExpression',
test,
consequent,
alternate,
};
}
else {
this.throwError('Expected :');
}
}
});
Loading