Skip to content

Commit

Permalink
extract and consolidate rendering steps execution (#897)
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip committed Aug 6, 2018
1 parent 0bd5a19 commit 1e6b771
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 90 deletions.
64 changes: 64 additions & 0 deletions packages/electrode-react-webapp/lib/render-execute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use strict";

const Promise = require("bluebird");

const { TOKEN_HANDLER } = require("./symbols");

const executeSteps = {
STEP_CALLBACK: 0,
STEP_MAYBE_ASYNC: 1,
STEP_STR_TOKEN: 2,
STEP_NO_HANDLER: 3,
STEP_LITERAL_HANDLER: 4
};

const {
STEP_CALLBACK,
STEP_MAYBE_ASYNC,
STEP_STR_TOKEN,
STEP_NO_HANDLER,
STEP_LITERAL_HANDLER
} = executeSteps;

function renderNext(err, xt) {
const { renderSteps, context } = xt;
if (err) {
// debugger; // eslint-disable-line
context.handleError(err);
}

if (context.isFullStop || context.isVoidStop || xt.stepIndex >= renderSteps.length) {
xt.next = undefined;
return xt.resolve(context.output.close());
} else {
// TODO: support soft stop
const step = renderSteps[xt.stepIndex++];
const tk = step.tk;
switch (step.code) {
case STEP_CALLBACK:
return tk[TOKEN_HANDLER](context, xt.next);
case STEP_MAYBE_ASYNC:
return context.handleTokenResult(tk.id, tk[TOKEN_HANDLER](context), xt.next);
case STEP_STR_TOKEN:
context.output.add(tk.str);
break;
case STEP_NO_HANDLER:
context.output.add(`<!-- unhandled token ${tk.id} -->`);
break;
case STEP_LITERAL_HANDLER:
context.output.add(step.data);
break;
}
return xt.next();
}
}

function executeRenderSteps(renderSteps, context) {
return new Promise(resolve => {
const xt = { stepIndex: 0, renderSteps, context, resolve };
xt.next = err => renderNext(err, xt);
xt.next();
});
}

module.exports = { executeRenderSteps, renderNext, executeSteps };
128 changes: 50 additions & 78 deletions packages/electrode-react-webapp/lib/renderer.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,76 @@
"use strict";

/* eslint-disable max-statements */
const { executeSteps, executeRenderSteps } = require("./render-execute");

const Promise = require("bluebird");
const {
STEP_CALLBACK,
STEP_MAYBE_ASYNC,
STEP_STR_TOKEN,
STEP_NO_HANDLER,
STEP_LITERAL_HANDLER
} = executeSteps;

class Renderer {
constructor(options) {
// the last handler wins if it contains a token
const tokenHandlers = options.tokenHandlers.reverse();

const makeStep = tk => {
// token is a literal string, just add it to output
if (tk.str !== undefined) {
return xt => {
xt.context.output.add(tk.str);
this._next(null, xt);
};
}

// token is not pointing to a module, so use it as an id to lookup from token handlers
if (!tk.isModule) {
// look for first handler that has a token function for tk.id
const handler = tokenHandlers.find(h => h.tokens.hasOwnProperty(tk.id));
const makeHandlerStep = tk => {
// look for first handler that has a token function for tk.id
const handler = tokenHandlers.find(h => h.tokens.hasOwnProperty(tk.id));

// no handler has function for token
if (!handler) {
const msg = `electrode-react-webapp: no handler found for token id ${tk.id}`;
console.error(msg); // eslint-disable-line
return xt => {
xt.context.output.add(`<!-- unhandled token ${tk.id} -->`);
this._next(null, xt);
};
}
// no handler has function for token
if (!handler) {
const msg = `electrode-react-webapp: no handler found for token id ${tk.id}`;
console.error(msg); // eslint-disable-line
return { tk, code: STEP_NO_HANDLER };
}

const tkFunc = handler.tokens[tk.id];
const tkFunc = handler.tokens[tk.id];

if (tkFunc === null) {
return null;
}
if (tkFunc === null) {
return null;
}

if (typeof tkFunc !== "function") {
// not a function, just add it to output
if (typeof tkFunc !== "function") {
return xt => {
xt.context.output.add(tkFunc);
this._next(null, xt);
};
}

// token function takes more than one argument, so pass in a callback for async
if (tkFunc.length > 1) {
return xt => tkFunc.call(tk, xt.context, err => this._next(err, xt));
}

// token function is sync or returns Promise, so pass its return value
// to context.handleTokenResult
return xt =>
xt.context.handleTokenResult(tk.id, tkFunc.call(tk, xt.context), err =>
this._next(err, xt)
);
return { tk, code: STEP_LITERAL_HANDLER, data: tkFunc };
}

// token is a module and its process function wants a next callback
if (tk.wantsNext === true) {
return xt => tk.process(xt.context, err => this._next(err, xt));
tk.setHandler(tkFunc);

const code =
tkFunc.length > 1 // token function takes more than one argument, so it takes callback
? STEP_CALLBACK
: // token function is sync or returns Promise
STEP_MAYBE_ASYNC;

return { tk, code };
};

const makeStep = tk => {
// token is a literal string, just add it to output
if (tk.hasOwnProperty("str")) {
return { tk, code: STEP_STR_TOKEN };
}

// token is a module and its process function is sync so pass its
// return value to context.handleTokenResult
return xt =>
xt.context.handleTokenResult(tk.id, tk.process(xt.context), err => this._next(err, xt));
// token is not pointing to a module, so lookup from token handlers
if (!tk.isModule) return makeHandlerStep(tk);

const code =
tk.wantsNext === true // module's process function wants a next callback
? STEP_CALLBACK
: // module's process function is sync or returns Promise
STEP_MAYBE_ASYNC;

return { tk, code };
};

this.renderSteps = options.htmlTokens
.map(tk => ({ tk, exec: makeStep(tk) }))
.filter(x => x.exec);
this.renderSteps = options.htmlTokens.map(makeStep).filter(x => x);
}

render(context) {
return new Promise((resolve, reject) =>
this._next(null, { context, _tokenIndex: 0, resolve, reject })
);
}

_next(err, xt) {
if (err) {
// debugger; // eslint-disable-line
xt.context.handleError(err);
}

if (
xt.context.isFullStop ||
xt.context.isVoidStop ||
xt._tokenIndex >= this.renderSteps.length
) {
return xt.resolve(xt.context.output.close());
} else {
// TODO: support soft stop
const step = this.renderSteps[xt._tokenIndex++];
return step.exec(xt);
}
return executeRenderSteps(this.renderSteps, context);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/electrode-react-webapp/lib/symbols.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

module.exports = {
TEMPLATE_DIR: Symbol("template dir")
TEMPLATE_DIR: Symbol("template dir"),
TOKEN_HANDLER: Symbol("token handler")
};
10 changes: 5 additions & 5 deletions packages/electrode-react-webapp/lib/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

const assert = require("assert");
const loadHandler = require("./load-handler");
const { TEMPLATE_DIR } = require("./symbols");
const { TEMPLATE_DIR, TOKEN_HANDLER } = require("./symbols");

const viewTokenModules = {};

Expand All @@ -19,6 +19,7 @@ class Token {
if (this.props._call) {
this._modCall = [].concat(this.props._call);
}
this[TOKEN_HANDLER] = null;
}

// if token is a module, then load it
Expand Down Expand Up @@ -53,12 +54,11 @@ class Token {
// if process function takes more than one params, then it should take a
// next callback so it can do async work, and call next after that's done.
this.wantsNext = this.custom.process.length > 1;
this.setHandler(this.custom.process);
}

process(context, next) {
assert(this.isModule, "Only token module can process");
assert(this.custom, "Custom token is not loaded yet");
return this.custom.process(context, next);
setHandler(func) {
this[TOKEN_HANDLER] = func;
}
}

Expand Down
15 changes: 9 additions & 6 deletions packages/electrode-react-webapp/test/spec/token.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const Token = require("../../lib/token");
const expect = require("chai").expect;
const xstdout = require("xstdout");
const { TOKEN_HANDLER } = require("../../lib/symbols");

describe("token", function() {
it("should create token as internal", () => {
Expand All @@ -19,22 +20,22 @@ describe("token", function() {
expect(tk.id).to.equal("#./test/fixtures/custom-call");
expect(tk.isModule).to.equal(true);
tk.load();
expect(tk.process()).to.equal("_call");
expect(tk[TOKEN_HANDLER]()).to.equal("_call");
});

it("should create token as custom and call setup only once for each token", () => {
const tk1 = new Token("#./test/fixtures/custom-count");
expect(tk1.id).to.equal("#./test/fixtures/custom-count");
expect(tk1.isModule).to.equal(true);
tk1.load();
expect(tk1.process()).to.equal("1");
expect(tk1[TOKEN_HANDLER]()).to.equal("1");
tk1.load(); // test re-entry
expect(tk1.process()).to.equal("1");
expect(tk1[TOKEN_HANDLER]()).to.equal("1");
const tk2 = new Token("#./test/fixtures/custom-count");
expect(tk2.id).to.equal("#./test/fixtures/custom-count");
expect(tk2.isModule).to.equal(true);
tk2.load();
expect(tk2.process()).to.equal("2");
expect(tk2[TOKEN_HANDLER]()).to.equal("2");
});

it("should handle custom module not found", () => {
Expand All @@ -44,7 +45,9 @@ describe("token", function() {
const intercept = xstdout.intercept(true);
tk.load();
intercept.restore();
expect(tk.process()).to.equal("\ntoken process module ./test/fixtures/not-found not found\n");
expect(tk[TOKEN_HANDLER]()).to.equal(
"\ntoken process module ./test/fixtures/not-found not found\n"
);
});

it("should handle custom module load failure", () => {
Expand All @@ -54,7 +57,7 @@ describe("token", function() {
const intercept = xstdout.intercept(true);
tk.load();
intercept.restore();
expect(tk.process()).to.equal(
expect(tk[TOKEN_HANDLER]()).to.equal(
"\ntoken process module ./test/fixtures/custom-fail failed to load\n"
);
});
Expand Down

0 comments on commit 1e6b771

Please sign in to comment.