From b35ae209974e23f85d9db78c248521115591444b Mon Sep 17 00:00:00 2001 From: Joel Chen Date: Thu, 16 Jul 2020 09:47:54 -0700 Subject: [PATCH] new tag-renderer base on ES6 strings --- .../xarc-render-context/src/RenderContext.ts | 2 +- .../xarc-render-context/src/TokenModule.ts | 16 +- .../xarc-render-context/src/load-handler.ts | 5 +- .../test/spec/render-context.spec.ts | 2 +- .../test/spec/token-module.spec.ts | 47 +- .../xarc-tag-renderer/config/test/setup.js | 30 -- packages/xarc-tag-renderer/package.json | 17 +- packages/xarc-tag-renderer/src/index.ts | 7 + .../xarc-tag-renderer/src/render-execute.ts | 44 +- .../xarc-tag-renderer/src/render-processor.ts | 157 ++++--- packages/xarc-tag-renderer/src/symbols.ts | 1 + .../xarc-tag-renderer/src/tag-renderer.ts | 423 +++--------------- .../xarc-tag-renderer/src/tag-template.ts | 198 ++++++++ packages/xarc-tag-renderer/src/utils.ts | 4 - .../test/data/template1.html | 13 - .../xarc-tag-renderer/test/data/template1.ts | 64 +++ .../test/data/template2.html | 18 - .../xarc-tag-renderer/test/data/template2.ts | 32 ++ .../xarc-tag-renderer/test/data/template9.ts | 39 ++ .../test/fixtures/token-handler.js | 2 +- .../test/spec/renderer.spec.ts | 0 .../test/spec/simple-renderer.spec.ts | 346 -------------- .../test/spec/tag-renderer.spec.ts | 178 ++++++++ .../test/spec/tag-template.spec.ts | 77 ++++ 24 files changed, 844 insertions(+), 878 deletions(-) delete mode 100644 packages/xarc-tag-renderer/config/test/setup.js create mode 100644 packages/xarc-tag-renderer/src/symbols.ts create mode 100644 packages/xarc-tag-renderer/src/tag-template.ts delete mode 100644 packages/xarc-tag-renderer/src/utils.ts delete mode 100644 packages/xarc-tag-renderer/test/data/template1.html create mode 100644 packages/xarc-tag-renderer/test/data/template1.ts delete mode 100644 packages/xarc-tag-renderer/test/data/template2.html create mode 100644 packages/xarc-tag-renderer/test/data/template2.ts create mode 100644 packages/xarc-tag-renderer/test/data/template9.ts delete mode 100644 packages/xarc-tag-renderer/test/spec/renderer.spec.ts delete mode 100644 packages/xarc-tag-renderer/test/spec/simple-renderer.spec.ts create mode 100644 packages/xarc-tag-renderer/test/spec/tag-renderer.spec.ts create mode 100644 packages/xarc-tag-renderer/test/spec/tag-template.spec.ts diff --git a/packages/xarc-render-context/src/RenderContext.ts b/packages/xarc-render-context/src/RenderContext.ts index c38648bc5..5f1c6ba5f 100644 --- a/packages/xarc-render-context/src/RenderContext.ts +++ b/packages/xarc-render-context/src/RenderContext.ts @@ -113,7 +113,7 @@ export class RenderContext { */ intercept({ responseHandler }) { this._intercepted = { responseHandler }; - throw new Error("electrode-react-webapp: render-context - user intercepted"); + throw new Error("@xarc/render-context: user intercepted response"); } fullStop() { diff --git a/packages/xarc-render-context/src/TokenModule.ts b/packages/xarc-render-context/src/TokenModule.ts index 420467f7a..591c5e0e0 100644 --- a/packages/xarc-render-context/src/TokenModule.ts +++ b/packages/xarc-render-context/src/TokenModule.ts @@ -16,6 +16,7 @@ export class TokenModule { wantsNext: any; props: any; _modCall: any; + _tokenMod: any; constructor(id, pos, props, templateDir) { this.id = id; @@ -43,10 +44,14 @@ export class TokenModule { this[TEMPLATE_DIR] = this.props[TEMPLATE_DIR] || templateDir || process.cwd(); } + set tokenMod(tm) { + this._tokenMod = tm; + } + // if token is a module, then load it - load(options = {}) { + load(options: any = {}) { if (!this.isModule || this.custom !== undefined) return; - let tokenMod = viewTokenModules[this.id]; + let tokenMod = this._tokenMod || viewTokenModules[this.id]; if (tokenMod === undefined) { if (this._modCall) { @@ -56,12 +61,13 @@ export class TokenModule { } viewTokenModules[this.id] = tokenMod; } + if (this._modCall) { // call setup function to get an instance const params = [options, this].concat(this._modCall[1] || []); assert( tokenMod[this._modCall[0]], - `electrode-react-webapp: _call of token ${this.id} - '${this._modCall[0]}' not found` + `@xarc/render-context: _call of token ${this.id} - '${this._modCall[0]}' not found` ); this.custom = tokenMod[this._modCall[0]](...params); } else { @@ -69,7 +75,9 @@ export class TokenModule { } /* if token doesn't provide any code (null) then there's no handler to set for it */ - if (this.custom === null) return; + if (this.custom === null) { + return; + } assert( this.custom && this.custom.process, diff --git a/packages/xarc-render-context/src/load-handler.ts b/packages/xarc-render-context/src/load-handler.ts index b16d0bd1d..7c37c2290 100644 --- a/packages/xarc-render-context/src/load-handler.ts +++ b/packages/xarc-render-context/src/load-handler.ts @@ -15,7 +15,10 @@ const failLoadTokenModule = (msg: string, err: Error) => { }; const notFoundLoadTokenModule = (msg: string, err: Error) => { - console.error(`error: @xarc/render-context can't find token process module ${msg}`, err); + console.error( + `error: @xarc/render-context can't find token process module ${msg}`, + err + ); return () => ({ process: () => `\n@xarc/render-context: token process module ${msg} not found\n` }); diff --git a/packages/xarc-render-context/test/spec/render-context.spec.ts b/packages/xarc-render-context/test/spec/render-context.spec.ts index 7c7295d50..1d9508d5e 100644 --- a/packages/xarc-render-context/test/spec/render-context.spec.ts +++ b/packages/xarc-render-context/test/spec/render-context.spec.ts @@ -100,7 +100,7 @@ describe("token handler in render context", function () { responseHandler: resp => resp }); } catch (e) { - expect(e.message).to.equal("electrode-react-webapp: render-context - user intercepted"); + expect(e.message).to.equal("@xarc/render-context: user intercepted response"); } }); diff --git a/packages/xarc-render-context/test/spec/token-module.spec.ts b/packages/xarc-render-context/test/spec/token-module.spec.ts index eb98ab384..7535119d7 100644 --- a/packages/xarc-render-context/test/spec/token-module.spec.ts +++ b/packages/xarc-render-context/test/spec/token-module.spec.ts @@ -44,14 +44,39 @@ describe("TokenModule ", function () { expect(tk[TEMPLATE_DIR]).to.equal(process.cwd()); //deep.equal({}); }); + + it("should allow explicitly setting the token module", () => { + const tk = new TokenModule("#testSetModule", 0, null, null); + tk.tokenMod = () => null; + expect(tk.custom).equal(undefined); + tk.load(); + expect(tk.custom).equal(null); + }); + + it("should handle token module that's null (no handler)", function () { + const tk = new TokenModule("require(./token-module-null.ts)", 0, {}, templateDir); + tk.load(); + expect(tk.custom).equal(null); + }); }); describe("require from modPath", function () { const tk = new TokenModule("require(./token-module-01.ts)", 0, {}, templateDir); + + it("should load as modPath if token.id contains string 'require'", function () { + tk.load({}); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); + }); + it("should load as modPath if token.id contains string 'require'", function () { tk.load({}); expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); }); + + it("should have same result when called with load()", function () { + tk.load(); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); + }); }); describe("_call in options ", function () { @@ -106,29 +131,7 @@ describe("_call in options ", function () { expect(tk[TEMPLATE_DIR]).to.equal(process.cwd()); //deep.equal({}); tk.load(null); }); -}); -describe("require from modPath", function () { - const tk = new TokenModule("require(./token-module-01.ts)", 0, {}, templateDir); - - it("should load as modPath if token.id contains string 'require'", function () { - tk.load({}); - expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); - }); - - it("should have same result when called with load()", function () { - tk.load(); - expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); - }); -}); - -it("should handle token module that's null (no handler)", function () { - const tk = new TokenModule("require(./token-module-null.ts)", 0, {}, templateDir); - tk.load(); - expect(tk.custom).equal(null); -}); - -describe("_call in options ", function () { it("should load custom _modCall", function () { const tk = new TokenModule("#./custom-call.ts", 0, { _call: "setup" }, templateDir); expect(tk.props).to.deep.equal({ _call: "setup" }); diff --git a/packages/xarc-tag-renderer/config/test/setup.js b/packages/xarc-tag-renderer/config/test/setup.js deleted file mode 100644 index 2b0979685..000000000 --- a/packages/xarc-tag-renderer/config/test/setup.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; - -function tryRequire(path) { - try { - return require(path); - } catch { - return undefined; - } -} - -// Chai setup. -const chai = tryRequire("chai"); -if (!chai) { - console.log(` -mocha setup: chai is not found. Not setting it up for mocha. - To setup chai for your mocha test, run 'clap mocha'.`); -} else { - const sinonChai = tryRequire("sinon-chai"); - - if (!sinonChai) { - console.log(` -mocha setup: sinon-chai is not found. Not setting it up for mocha. - To setup sinon-chai for your mocha test, run 'clap mocha'.`); - } else { - chai.use(sinonChai); - } - - // Exports - global.expect = chai.expect; -} diff --git a/packages/xarc-tag-renderer/package.json b/packages/xarc-tag-renderer/package.json index b164d9551..515961d15 100644 --- a/packages/xarc-tag-renderer/package.json +++ b/packages/xarc-tag-renderer/package.json @@ -11,6 +11,9 @@ "keywords": [], "author": "Electrode ", "license": "Apache-2.0", + "dependencies": { + "@xarc/render-context": "^1.0.0" + }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/chai": "^4.2.11", @@ -20,7 +23,7 @@ "@types/sinon-chai": "^3.2.4", "@typescript-eslint/eslint-plugin": "^2.21.0", "@typescript-eslint/parser": "^2.21.0", - "@xarc/module-dev": "^2.1.1", + "@xarc/module-dev": "^2.1.2", "babel-eslint": "^10.1.0", "chai": "^4.2.0", "eslint": "^6.8.0", @@ -35,12 +38,10 @@ "ts-node": "^8.6.2", "typedoc": "^0.17.4", "typescript": "^3.8.3", - "xstdout": "^0.1.1", - "@xarc/render-context": "^1.0.0" + "xstdout": "^0.1.1" }, "mocha": { "require": [ - "@babel/register", "ts-node/register", "source-map-support/register", "@xarc/module-dev/config/test/setup.js" @@ -51,9 +52,6 @@ "dist", "src" ], - "dependencies": { - "ts-node": "^8.10.2" - }, "nyc": { "extends": [ "@istanbuljs/nyc-config-typescript" @@ -81,8 +79,9 @@ "cache": false }, "fyn": { - "devDependencies": { + "dependencies": { "@xarc/render-context": "../xarc-render-context" - } + }, + "devDependencies": {} } } diff --git a/packages/xarc-tag-renderer/src/index.ts b/packages/xarc-tag-renderer/src/index.ts index 717dc42f7..2006e376e 100644 --- a/packages/xarc-tag-renderer/src/index.ts +++ b/packages/xarc-tag-renderer/src/index.ts @@ -1 +1,8 @@ export { TagRenderer } from "./tag-renderer"; +export { + TagTemplate, + createTemplateTags, + RegisterTokenIds, + Token, + TokenInvoke +} from "./tag-template"; diff --git a/packages/xarc-tag-renderer/src/render-execute.ts b/packages/xarc-tag-renderer/src/render-execute.ts index 79915dcff..3102cf297 100644 --- a/packages/xarc-tag-renderer/src/render-execute.ts +++ b/packages/xarc-tag-renderer/src/render-execute.ts @@ -1,17 +1,24 @@ -/* eslint-disable complexity */ +/* eslint-disable complexity, max-statements */ import { TOKEN_HANDLER } from "@xarc/render-context"; -const executeSteps = { +export const executeSteps = { STEP_HANDLER: 0, STEP_STR_TOKEN: 1, STEP_NO_HANDLER: 2, - STEP_LITERAL_HANDLER: 3 + STEP_LITERAL_HANDLER: 3, + STEP_FUNC_HANDLER: 4 }; -const { STEP_HANDLER, STEP_STR_TOKEN, STEP_NO_HANDLER, STEP_LITERAL_HANDLER } = executeSteps; +const { + STEP_HANDLER, + STEP_STR_TOKEN, + STEP_NO_HANDLER, + STEP_LITERAL_HANDLER, + STEP_FUNC_HANDLER +} = executeSteps; -function renderNext(err: Error, xt) { +export function renderNext(err: Error, xt) { const { renderSteps, context } = xt; if (err) { context.handleError(err); @@ -35,34 +42,43 @@ function renderNext(err: Error, xt) { const tk = step.tk; const withId = step.insertTokenId; switch (step.code) { + case STEP_FUNC_HANDLER: + return context.handleTokenResult("", tk.func(context), e => { + return renderNext(e, xt); + }); case STEP_HANDLER: - if (withId) insertTokenId(tk); + if (withId) { + insertTokenId(tk); + } return context.handleTokenResult(tk.id, tk[TOKEN_HANDLER](context, tk), e => { - if (withId) insertTokenIdEnd(tk); + if (withId) { + insertTokenIdEnd(tk); + } return renderNext(e, xt); }); case STEP_STR_TOKEN: context.output.add(tk.str); break; case STEP_NO_HANDLER: - context.output.add(``); + context.output.add(`\n`); break; case STEP_LITERAL_HANDLER: - if (withId) insertTokenId(tk); + if (withId) { + insertTokenId(tk); + } context.output.add(step.data); - if (withId) insertTokenIdEnd(tk); + if (withId) { + insertTokenIdEnd(tk); + } break; } return renderNext(null, xt); } } -function executeRenderSteps(renderSteps, context) { +export function executeRenderSteps(renderSteps, context) { return new Promise(resolve => { const xt = { stepIndex: 0, renderSteps, context, resolve }; return renderNext(null, xt); }); } - -const RenderExecute = { executeRenderSteps, renderNext, executeSteps }; -export default RenderExecute; diff --git a/packages/xarc-tag-renderer/src/render-processor.ts b/packages/xarc-tag-renderer/src/render-processor.ts index 7ec6a4eb6..b1135b687 100644 --- a/packages/xarc-tag-renderer/src/render-processor.ts +++ b/packages/xarc-tag-renderer/src/render-processor.ts @@ -1,72 +1,127 @@ -import renderExecute from "./render-execute"; +/* eslint-disable max-statements */ + +import { executeRenderSteps, executeSteps } from "./render-execute"; +import { TAG_TYPE } from "./symbols"; +import { TokenModule } from "@xarc/render-context"; const { STEP_HANDLER, STEP_STR_TOKEN, STEP_NO_HANDLER, - STEP_LITERAL_HANDLER -} = renderExecute.executeSteps; + STEP_LITERAL_HANDLER, + STEP_FUNC_HANDLER +} = executeSteps; export class RenderProcessor { renderSteps: any; + _options: any; + _insertTokenIds: boolean; + constructor(options) { - const insertTokenIds = Boolean(options.insertTokenIds); - // the last handler wins if it contains a token - const tokenHandlers = options.tokenHandlers.reverse(); - const makeNullRemovedStep = (tk, cause) => { - return { - tk, - insertTokenId: false, - code: STEP_LITERAL_HANDLER, - data: `\n` - }; + this._options = options; + this._insertTokenIds = Boolean(options.insertTokenIds); + this.renderSteps = this.makeSteps(options.htmlTokens); + } + + makeNullRemovedStep(tk, cause) { + return { + tk, + insertTokenId: false, + code: STEP_LITERAL_HANDLER, + data: `\n` }; - 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 { tk, code: STEP_NO_HANDLER }; - } - const tkFunc = handler.tokens[tk.id]; + } + + makeHandlerStep(tk) { + const options = this._options; + const insertTokenIds = this._insertTokenIds; + + // look for first handler that has a token function for tk.id + const tkFunc = options.asyncTemplate.lookupTokenHandler(tk); + // no handler has function for token + if (!tkFunc) { if (tkFunc === null) { - if (insertTokenIds) return makeNullRemovedStep(tk, "handler set to null"); - return null; - } - if (typeof tkFunc !== "function") { - // not a function, just add it to output - return { - tk, - code: STEP_LITERAL_HANDLER, - insertTokenId: insertTokenIds && !tk.props._noInsertId, - data: tkFunc - }; - } - tk.setHandler(tkFunc); - return { tk, code: STEP_HANDLER, insertTokenId: insertTokenIds && !tk.props._noInsertId }; - }; - 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 not pointing to a module, so lookup from token handlers - if (!tk.isModule) return makeHandlerStep(tk); - if (tk.custom === null) { - if (insertTokenIds) return makeNullRemovedStep(tk, "process return null"); + if (insertTokenIds) { + return this.makeNullRemovedStep(tk, "handler set to null"); + } return null; } + + const msg = `@xarc/tag-renderer: no handler found for token id ${tk.id}`; + console.error(msg); // eslint-disable-line + return { tk, code: STEP_NO_HANDLER }; + } + + if (typeof tkFunc !== "function") { + // not a function, just add it to output return { tk, - code: STEP_HANDLER, - insertTokenId: options.insertTokenIds && !tk.props._noInsertId + code: STEP_LITERAL_HANDLER, + insertTokenId: insertTokenIds && !tk.props._noInsertId, + data: tkFunc + }; + } + + tk.setHandler(tkFunc); + + return { tk, code: STEP_HANDLER, insertTokenId: insertTokenIds && !tk.props._noInsertId }; + } + + makeStep(tk: any) { + const options = this._options; + const insertTokenIds = this._insertTokenIds; + if (tk[TAG_TYPE] === "function") { + return { + tk, + code: STEP_FUNC_HANDLER }; + } + + if (tk[TAG_TYPE] === "register-token-ids") { + tk({ asyncTemplate: options.asyncTemplate }); + return null; + } + + if (tk[TAG_TYPE] === "template") { + return this.makeSteps(tk); + } + + // token is a literal string, just add it to output + if (tk.hasOwnProperty("str")) { + return { tk, code: STEP_STR_TOKEN }; + } + + // token is not pointing to a module, so lookup from token handlers + if (!tk.isModule) { + return this.makeHandlerStep(tk); + } + + if (tk.custom === null) { + if (insertTokenIds) { + return this.makeNullRemovedStep(tk, "process return null"); + } + return null; + } + return { + tk, + code: STEP_HANDLER, + insertTokenId: options.insertTokenIds && !tk.props._noInsertId }; - this.renderSteps = options.htmlTokens.map(makeStep).filter(x => x); } + + makeSteps(tokens) { + let steps = []; + for (const htk of tokens) { + const step = this.makeStep(htk); + if (step) { + steps = steps.concat(step); + } + } + + return steps; + } + render(context) { - return renderExecute.executeRenderSteps(this.renderSteps, context); + return executeRenderSteps(this.renderSteps, context); } } diff --git a/packages/xarc-tag-renderer/src/symbols.ts b/packages/xarc-tag-renderer/src/symbols.ts new file mode 100644 index 000000000..cadc30547 --- /dev/null +++ b/packages/xarc-tag-renderer/src/symbols.ts @@ -0,0 +1 @@ +export const TAG_TYPE = Symbol("tag-type"); diff --git a/packages/xarc-tag-renderer/src/tag-renderer.ts b/packages/xarc-tag-renderer/src/tag-renderer.ts index 12ad0dd2e..9b057beaa 100644 --- a/packages/xarc-tag-renderer/src/tag-renderer.ts +++ b/packages/xarc-tag-renderer/src/tag-renderer.ts @@ -1,46 +1,15 @@ /* eslint-disable max-params, max-statements, no-constant-condition, no-magic-numbers */ -import * as assert from "assert"; -import * as Fs from "fs"; -import { - TOKEN_HANDLER, - TEMPLATE_DIR, - TokenModule, - RenderContext, - loadTokenModuleHandler -} from "@xarc/render-context"; +import { TEMPLATE_DIR, RenderContext } from "@xarc/render-context"; -import { resolvePath } from "./utils"; -import stringArray from "string-array"; import * as _ from "lodash"; -import * as Path from "path"; -import * as Promise from "bluebird"; import { RenderProcessor } from "./render-processor"; -import { makeDefer, each } from "xaa"; - -const tokenTags = { - "") - }, - "/*--%{": { - // for tokens in script and style - open: "\\/\\*--[ \n]*%{", - close: new RegExp("}--\\*/") - } -}; - -const tokenOpenTagRegex = new RegExp( - Object.keys(tokenTags) - .map(x => `(${tokenTags[x].open})`) - .join("|") -); +import { TagTemplate } from "./tag-template"; /** * TagRenderer * - * A simple HTML renderer from string token based template + * A simple renderer for template from ES6 template literals * */ export class TagRenderer { @@ -48,18 +17,33 @@ export class TagRenderer { * Yes, I know, everything any - I just want this to compile for now. */ _options: any; - _tokenHandlers: any; - _handlersMap: any; + _tokenHandlers: any[]; _handlerContext: any; + _handlersMap: {}; + _tokenIdLookupMap: {}; _renderer: any; - _tokens: any; - private _beforeRenders: any; - private _afterRenders: any; - - constructor(options: any) { + _tokens: any[]; + _templateDir: string; + _template: TagTemplate; + + constructor(options: { + template: TagTemplate; + tokenHandlers?: Function | Function[]; + routeOptions?: any; + insertTokenIds?: boolean; + }) { this._options = options; - this._tokenHandlers = [].concat(this._options.tokenHandlers).filter(x => x); + this._template = options.template; + this[TEMPLATE_DIR] = this._template._templateDir; + this._tokens = this._template._templateTags; + + this._tokenHandlers = [] + .concat(this._options.tokenHandlers, this._template._tokenHandlers) + .filter(x => x) + .map(handler => ({ handler })); this._handlersMap = {}; + this._tokenIdLookupMap = {}; + // the same context that gets passed to each token handler's setup function this._handlerContext = _.merge( { @@ -70,14 +54,14 @@ export class TagRenderer { }, options ); - this._initializeTemplate(options.htmlFile); } initializeRenderer(reset = !this._renderer) { - if (reset || !this._renderer) { + if (reset) { this._initializeTokenHandlers(this._tokenHandlers); - this._applyTokenLoad(); + this._applyTokenLoad(this._options); this._renderer = new RenderProcessor({ + asyncTemplate: this, insertTokenIds: this._options.insertTokenIds, htmlTokens: this._tokens, tokenHandlers: this._tokenHandlers @@ -85,349 +69,62 @@ export class TagRenderer { } } - get tokens() { - return this._tokens; - } - - get handlersMap() { - return this._handlersMap; + lookupTokenHandler(tk) { + return this._tokenIdLookupMap[tk.id]; } async render(options) { - const defer = makeDefer(); - const context = new RenderContext(options, this); + let context; try { - await each(this._beforeRenders, (r: any) => r.beforeRender(context)); - await defer.promise; - const result = this._renderer.render(context); - await each(this._afterRenders, (r: any) => r.afterRender(context)); + context = new RenderContext(options, this); + const result = await this._renderer.render(context); context.result = context.isVoidStop ? context.voidResult : result; return context; } catch (err) { context.result = err; + context.error = err; return context; } } - _findTokenIndex( - id = "", - str: string | RegExp = "", - index = 0, - instance = 0, - msg = "AsyncTemplate._findTokenIndex" - ) { - let found; - - if (id) { - found = this.findTokensById(id, instance + 1); - } else if (str) { - found = this.findTokensByStr(str, instance + 1); - } else if (!Number.isInteger(index)) { - throw new Error(`${msg}: invalid id, str, and index`); - } else if (index < 0 || index >= this._tokens.length) { - throw new Error(`${msg}: index ${index} is out of range.`); - } else { - return index; + registerTokenIds(name: string, uniqSym: symbol, handler: Function) { + if (this._handlersMap.hasOwnProperty(uniqSym)) { + return; } - - if (found.length === 0) return false; - - return found[instance].index; + this._handlersMap[uniqSym] = handler; + // remove same handler that's been registered so it goes to the end of the array + this._tokenHandlers = this._tokenHandlers.filter(h => h.handler !== handler); + this._tokenHandlers.push({ name, handler }); + this._initializeTokenHandlers(this._tokenHandlers); } - // - // add tokens at first|last position of the tokens, - // or add tokens before|after token at {id}[instance] or {index} - // ^^^ {insert} - // - Note that item indexes will change after add - // - // returns: - // - number of tokens removed - // - false if nothing was removed - // throws: - // - if id and index are invalid - // - if {insert} is invalid - // - addTokens({ insert = "after", id, index, str, instance = 0, tokens }) { - const create = tk => { - return new TokenModule( - tk.token, - -1, - typeof tk.props === "string" ? this._parseTokenProps(tk.props) : tk.props, - tk.templateDir - ); - }; - - if (insert === "first") { - this._tokens.unshift(...tokens.map(create)); - return 0; - } - - if (insert === "last") { - const x = this._tokens.length; - this._tokens.push(...tokens.map(create)); - return x; - } - - index = this._findTokenIndex(id, str, index, instance, "AsyncTemplate.addTokens"); - if (index === false) return false; - - if (insert === "before") { - this._tokens.splice(index, 0, ...tokens.map(create)); - return index; - } - - if (insert === "after") { - index++; - this._tokens.splice(index, 0, ...tokens.map(create)); - return index; - } - - throw new Error( - `AsyncTemplate.addTokens: insert "${insert}" is not valid, must be first|before|after|last` - ); - } - - // - // remove {count} tokens before|after token at {id}[instance] or {index} - // ^^^ {remove} - // - if removeSelf is true then the token at {id}[instance] or {index} is included for removal - // returns: - // - array of tokens removed - // throws: - // - if id and index are invalid - // - if {remove} is invalid - // - removeTokens({ remove = "after", removeSelf = true, id, str, index, instance = 0, count = 1 }) { - // assert(count > 0, `AsyncTemplate.removeTokens: count ${count} must be > 0`); - - index = this._findTokenIndex(id, str, index, instance, "AsyncTemplate.removeTokens"); - if (index === false) return false; - - const offset = removeSelf ? 0 : 1; - - if (remove === "before") { - let newIndex = index + 1 - count - offset; - if (newIndex < 0) { - newIndex = 0; - count = index + 1 - offset; - } - return this._tokens.splice(newIndex, count); - } else if (remove === "after") { - return this._tokens.splice(index + offset, count); - } else { - throw new Error(`AsyncTemplate.removeTokens: remove "${remove}" must be before|after`); - } - } - - findTokensById(id, count = Infinity) { - if (!Number.isInteger(count)) count = this._tokens.length; - - const found = []; - - for (let index = 0; index < this._tokens.length && found.length < count; index++) { - const token = this._tokens[index]; - if (token.id === id) { - found.push({ index, token }); - } - } - - return found; - } - - findTokensByStr(matcher, count = Infinity) { - if (!Number.isInteger(count)) count = this._tokens.length; - - const found = []; - - let match; - - if (typeof matcher === "string") { - match = str => str.indexOf(matcher) >= 0; - } else if (matcher && matcher.constructor.name === "RegExp") { - match = str => str.match(matcher); - } else { - throw new Error("AsyncTemplate.findTokensByStr: matcher must be a string or RegExp"); - } - for (let index = 0; index < this._tokens.length && found.length < count; index++) { - const token = this._tokens[index]; - if (token.hasOwnProperty.str && match(token.str)) { - found.push({ index, token }); + _applyTokenLoad(options) { + this._tokens.forEach(tokenModule => { + if (tokenModule.load) { + tokenModule.load(options); } - } - - return found; - } - - /* - * break up the template into a list of literal strings and the tokens between them - * - * - each item is of the form: - * - * { str: "literal string" } - * - * or a Token object - */ - - _parseTemplate(template, filepath) { - const tokens = []; - const templateDir = Path.dirname(filepath); - - let pos = 0; - - const parseFail = msg => { - const lineCount = [].concat(template.substring(0, pos).match(/\n/g)).length + 1; - const lastNLIx = template.lastIndexOf("\n", pos); - const lineCol = pos - lastNLIx; - msg = msg.replace(/\n/g, "\\n"); - const pfx = `electrode-react-webapp: ${filepath}: at line ${lineCount} col ${lineCol}`; - throw new Error(`${pfx} - ${msg}`); - }; - - let subTmpl = template; - while (true) { - const openMatch = subTmpl.match(tokenOpenTagRegex); - if (openMatch) { - pos += openMatch.index; - - if (openMatch.index > 0) { - const str = subTmpl.substring(0, openMatch.index).trim(); - // if there are text between a close tag and an open tag, then consider - // that as plain HTML string - if (str) tokens.push({ str }); - } - - const tokenOpenTag = openMatch[0].replace(/[ \n]/g, ""); - const tokenCloseTag = tokenTags[tokenOpenTag].close; - subTmpl = subTmpl.substring(openMatch.index + openMatch[0].length); - const closeMatch = subTmpl.match(tokenCloseTag); - - if (!closeMatch) { - parseFail(`Can't find token close tag for '${openMatch[0]}'`); - } - - const tokenBody = subTmpl - .substring(0, closeMatch.index) - .trim() - .split("\n") - .map(x => x.trim()) - // remove empty and comment lines that start with "//" - .filter(x => x && !x.startsWith("//")) - .join(" "); - - const consumedCount = closeMatch.index + closeMatch[0].length; - subTmpl = subTmpl.substring(consumedCount); - - const token = tokenBody.split(" ", 1)[0]; - if (!token) { - parseFail(`empty token body`); - } - - const tokenProps = tokenBody.substring(token.length).trim(); - - try { - const props = this._parseTokenProps(tokenProps); - props[TEMPLATE_DIR] = templateDir; - - tokens.push(new TokenModule(token, pos, props, props[TEMPLATE_DIR])); - pos += openMatch[0].length + consumedCount; - } catch (e) { - parseFail(`'${tokenBody}' has malformed prop: ${e.message};`); - } - } else { - const str = subTmpl.trim(); - if (str) tokens.push({ str }); - break; - } - } - - return tokens; + }); } - _parseTokenProps(str) { - // check if it's JSON object by looking for "{" - if (str[0] === "{") { - return JSON.parse(str); - } - - const props = {}; - - while (str) { - const m1 = str.match(/([\w]+)=(.)/); - assert(m1 && m1[1], "name must be name=Val"); - const name = m1[1]; - - if (m1[2] === `[`) { - // treat as name=[str1, str2] - str = str.substring(m1[0].length - 1); - const r = stringArray.parse(str, true); - props[name] = r.array; - str = r.remain.trim(); - } else if (m1[2] === `'` || m1[2] === `"` || m1[2] === "`") { - str = str.substring(m1[0].length); - const m2 = str.match(new RegExp(`([^${m1[2]}]+)${m1[2]}`)); - assert(m2, `mismatch quote ${m1[2]}`); - props[name] = m2[1]; - str = str.substring(m2[0].length).trim(); - } else if (m1[2] === " ") { - // empty - props[name] = ""; - str = str.substring(m1[0].length).trim(); - } else { - str = str.substring(m1[0].length - 1); - const m2 = str.match(/([^ ]*)/); // matching name=Prop - props[name] = JSON.parse(m2[1]); - str = str.substring(m2[0].length).trim(); + _initializeTokenHandlers(handlers) { + const tokenIds = handlers.map((h, ix) => { + if (h.loaded) { + return h.loaded.tokens; } - } - return props; - } - - _initializeTemplate(filename) { - const filepath = resolvePath(filename); - const html = Fs.readFileSync(filepath).toString(); - this._tokens = this._parseTemplate(html, filepath); - } + const tokens = h.handler(this._handlerContext, this); - _loadTokenHandler(path) { - const mod = loadTokenModuleHandler(path); - return mod(this._handlerContext); - } + h.loaded = tokens.tokens && typeof tokens.tokens === "object" ? { ...tokens } : { tokens }; - _applyTokenLoad() { - this._tokens.forEach(x => { - if (x.load) { - x.load(this._options); + if (!h.loaded.name) { + h.loaded.name = h.name || `unnamed-token-id-handler-${ix}`; } - }); - } - _initializeTokenHandlers(filenames) { - this._tokenHandlers = filenames.map(fname => { - let handler; - if (typeof fname === "string") { - handler = this._loadTokenHandler(fname); - } else { - handler = fname; - assert(handler.name, "electrode-react-webapp AsyncTemplate token handler missing name"); - } - if (!handler.name) { - handler = { - name: fname, - tokens: handler - }; - } - assert(handler.tokens, "electrode-react-webapp AsyncTemplate token handler missing tokens"); - assert( - !this._handlersMap.hasOwnProperty(handler.name), - `electrode-react-webapp AsyncTemplate token handlers map already contains ${handler.name}` - ); - this._handlersMap[handler.name] = handler; - return handler; + return h.loaded.tokens; }); - this._beforeRenders = this._tokenHandlers.filter(x => x.beforeRender); - this._afterRenders = this._tokenHandlers.filter(x => x.afterRender); + // combine all token IDs into a single object for lookup. + // the last registered handler wins + this._tokenIdLookupMap = Object.assign({}, ...tokenIds); } } diff --git a/packages/xarc-tag-renderer/src/tag-template.ts b/packages/xarc-tag-renderer/src/tag-template.ts new file mode 100644 index 000000000..842c0cc37 --- /dev/null +++ b/packages/xarc-tag-renderer/src/tag-template.ts @@ -0,0 +1,198 @@ +/* eslint-disable max-params */ + +import { TAG_TYPE } from "./symbols"; +import { TokenModule } from "@xarc/render-context"; + +/** + * Create a simple tag template from ES6 template literal strings + * + * Usage: + * + * ```js + * import { TagTemplate, RegisterTokenIds, createTemplateTags, Token, TokenInvoke } from "@xarc/tag-renderer"; + * import { myTokenHandler } from "./my-token-handler"; + * import { myHtmlRenderHandler } from "./my-html-render-handler"; + * import { myTokenIdRegister } from "./my-token-id-handler" + * + * const subTemplate = createTemplateTags`${(context: any) => { + * const { query } = context.options.request; + * if (query.userName) { + * return `hello there ${query.userName}`; + * } + * }}`; + * + * const templateTags = createTemplateTags` + * ${RegisterTokenIds(myTokenIdRegister)} + * ${Token("INITIALIZE", {})} + * + * ${TokenInvoke(myTokenHandler, {})} + * ${subTemplate} + * + * + * ${TokenInvoke(myHtmlRenderHandler, {})} + * + * ` + * + * export const template = TagTemplate({ + * templateTags, templateDir: __dirname + * }) + * ``` + * + * @param literals - string array + * @param args - template literal tag arguments + * + * @return tag template + */ +export const createTemplateTags = (literals: TemplateStringsArray, ...args: any[]) => { + const combined = []; + + for (let i = 0; i < args.length; i++) { + const str = literals[i].trim(); + if (str) { + combined.push({ str }); + } + combined.push(args[i]); + } + + combined.push({ str: literals[args.length] }); + + combined[TAG_TYPE] = "template"; + + return combined; +}; + +/** + * Create a tag to invoke a token by its ID + * + * @param id - id of token + * @param props - props + * + * @returns tag that's a token + */ +export const Token = (id: string | Function, props = {}) => { + const tm = new TokenModule(id, -1, props, process.cwd()); + + tm[TAG_TYPE] = "token"; + return tm; +}; + +/** + * Create a tag to invoke a token module as a function + * + * @param handler - handler function of the token + * @param props - props + * + * @returns tag that's a token module invoker + */ +export const TokenInvoke = (handler: Function, props = {}) => { + const tm = new TokenModule("#tokenInvoke", -1, props, process.cwd()); + tm.tokenMod = handler; + tm[TAG_TYPE] = "token"; + return tm; +}; + +export const RegisterTokenIds = (handler: Function, name?: string) => { + const uniqSym = Symbol("register-token-${name}"); + const register = context => { + context.asyncTemplate.registerTokenIds(name, uniqSym, handler); + }; + register[TAG_TYPE] = "register-token-ids"; + return register; +}; + +export class TagTemplate { + _templateTags: any[]; + _templateDir: string; + _tokenHandlers: Function[]; + + constructor(options: { + templateTags: any[]; + templateDir?: string; + tokenHandlers?: Function | Function[]; + }) { + this._templateTags = options.templateTags.map((tag, ix) => { + if (tag.hasOwnProperty(TAG_TYPE)) { + tag.pos = ix; + } else if (typeof tag === "function") { + return { [TAG_TYPE]: "function", pos: ix, func: tag }; + } + return tag; + }); + this._templateDir = options.templateDir; + this._tokenHandlers = [].concat(options.tokenHandlers); + } + + _findTokenIndex( + id = "", + str: string | RegExp = "", + index = 0, + instance = 0, + msg = "TagRenderer._findTokenIndex" + ) { + let found; + + if (id) { + found = this.findTokensById(id, instance + 1); + } else if (str) { + found = this.findTokensByStr(str, instance + 1); + } else if (!Number.isInteger(index)) { + throw new Error(`${msg}: invalid id, str, and index`); + } else if (index < 0 || index >= this._templateTags.length) { + throw new Error(`${msg}: index ${index} is out of range.`); + } else { + return index; + } + + if (found.length === 0) return false; + + return found[instance].index; + } + + findTokensById(id, count = Infinity) { + if (!Number.isInteger(count)) count = this._templateTags.length; + + const found = []; + + for (let index = 0; index < this._templateTags.length && found.length < count; index++) { + const token = this._templateTags[index]; + if (token.id === id) { + found.push({ index, token }); + } + } + + return found; + } + + /** + * Find a literal token in the template by matching its str + * + * @param matcher - a string or a RegExp to match token's str + * @param count + */ + findTokensByStr(matcher, count = Infinity) { + if (!Number.isInteger(count)) { + count = this._templateTags.length; + } + + const found = []; + + let match; + + if (typeof matcher === "string") { + match = str => str.indexOf(matcher) >= 0; + } else if (matcher && matcher.constructor.name === "RegExp") { + match = str => str.match(matcher); + } else { + throw new Error("TagRenderer.findTokensByStr: matcher must be a string or RegExp"); + } + + for (let index = 0; index < this._templateTags.length && found.length < count; index++) { + const token = this._templateTags[index]; + if (token.hasOwnProperty("str") && match(token.str)) { + found.push({ index, token }); + } + } + + return found; + } +} diff --git a/packages/xarc-tag-renderer/src/utils.ts b/packages/xarc-tag-renderer/src/utils.ts deleted file mode 100644 index 1a0429fd3..000000000 --- a/packages/xarc-tag-renderer/src/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Path from "path"; - -export const resolvePath = filename => - (Path.isAbsolute(filename) && filename) || Path.resolve(filename); diff --git a/packages/xarc-tag-renderer/test/data/template1.html b/packages/xarc-tag-renderer/test/data/template1.html deleted file mode 100644 index 15523529f..000000000 --- a/packages/xarc-tag-renderer/test/data/template1.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/packages/xarc-tag-renderer/test/data/template1.ts b/packages/xarc-tag-renderer/test/data/template1.ts new file mode 100644 index 000000000..ad822450d --- /dev/null +++ b/packages/xarc-tag-renderer/test/data/template1.ts @@ -0,0 +1,64 @@ +import { TagTemplate, createTemplateTags, Token, TokenInvoke, RegisterTokenIds } from "../../src"; + +import * as custom1 from "../fixtures/custom-1"; +import * as tokenHandler from "../fixtures/token-handler"; + +const subTags2 = createTemplateTags`${RegisterTokenIds(tokenHandler)} + ${RegisterTokenIds(() => { + return { + name: "sub-blah-blah-2", + tokens: { + X2: "x2" + } + }; + })} +
sub template tags 2${Token("X2")}
`; + +const subTags = createTemplateTags`${RegisterTokenIds(tokenHandler)} + ${RegisterTokenIds(() => { + return { + name: "sub-blah-blah", + tokens: { + X: "x1" + } + }; + })} +
sub template tags${Token("X")}${subTags2}
`; + +const templateTags = createTemplateTags` + + ${RegisterTokenIds(tokenHandler)} + ${RegisterTokenIds(() => { + return { + name: "blah-blah", + tokens: { + ABC: "ABC", + "ssr-content": "SSR\n", + "webapp-header-bundles": () => "header\n" + } + }; + })} + ${RegisterTokenIds(tokenHandler)} + ${Token("ssr-content")} + ${Token("webapp-header-bundles")} + ${Token("webapp-body-bundles")} + ${Token("PAGE_TITLE")} + ${Token("prefetch-bundles")} + ${TokenInvoke(custom1)} + ${context => { + return `hello world from function: ${Object.keys(context)}\n`; + }} + + ${subTags} + ${Token("meta-tags")} + ${Token("ABC")} + +`; + +export const template = new TagTemplate({ + templateTags, + templateDir: __dirname, + tokenHandlers: [tokenHandler] +}); diff --git a/packages/xarc-tag-renderer/test/data/template2.html b/packages/xarc-tag-renderer/test/data/template2.html deleted file mode 100644 index 5de95e820..000000000 --- a/packages/xarc-tag-renderer/test/data/template2.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/xarc-tag-renderer/test/data/template2.ts b/packages/xarc-tag-renderer/test/data/template2.ts new file mode 100644 index 000000000..c6a8df970 --- /dev/null +++ b/packages/xarc-tag-renderer/test/data/template2.ts @@ -0,0 +1,32 @@ +import { TagTemplate, createTemplateTags, Token, TokenInvoke, RegisterTokenIds } from "../../src"; + +const nullTokenProcess = () => { + return null; +}; + +const templateTags = createTemplateTags` + + ${Token("ssr-content")} + ${Token("webapp-header-bundles")} + ${Token("webapp-body-bundles")} + ${Token("prefetch-bundles")} + ${RegisterTokenIds(() => { + return { + NULL_ID: null + }; + })} + ${Token("NULL_ID")} +
test null id
+ + ${Token("webapp-body-bundles")} + ${Token("meta-tags")} + ${TokenInvoke(nullTokenProcess)} +
test
+ + +${Token("page-title")} +`; + +export const template = new TagTemplate({ templateTags, templateDir: __dirname }); diff --git a/packages/xarc-tag-renderer/test/data/template9.ts b/packages/xarc-tag-renderer/test/data/template9.ts new file mode 100644 index 000000000..7a0072ee2 --- /dev/null +++ b/packages/xarc-tag-renderer/test/data/template9.ts @@ -0,0 +1,39 @@ +import { TagTemplate, createTemplateTags, Token } from "../../src"; + +export const templateTags = createTemplateTags/*html*/ ` + +${Token("INITIALIZE")} + + + + + ${Token("META_TAGS")} + ${Token("PAGE_TITLE")} + ${Token("CRITICAL_CSS")} + ${Token("APP_CONFIG_DATA")} + ${Token("WEBAPP_HEADER_BUNDLES")} + ${Token("WEBAPP_DLL_BUNDLES")} + +
+ ${Token("SSR_CONTENT")} +
+ ${Token("AFTER_SSR_CONTENT")} + ${Token("PREFETCH_BUNDLES")} + ${Token("WEBAPP_BODY_BUNDLES")} + ${Token("WEBAPP_START_SCRIPT")} + + + ${Token("BODY_CLOSED")} + +${Token("HTML_CLOSED")} +${Token("X")}${Token("X")}${Token("X")}${Token("X")}${Token("X")}${Token("X")}`; + +export const template = new TagTemplate({ + templateTags, + templateDir: __dirname, + tokenHandlers: [] +}); diff --git a/packages/xarc-tag-renderer/test/fixtures/token-handler.js b/packages/xarc-tag-renderer/test/fixtures/token-handler.js index 464b94a25..a742eae7e 100644 --- a/packages/xarc-tag-renderer/test/fixtures/token-handler.js +++ b/packages/xarc-tag-renderer/test/fixtures/token-handler.js @@ -40,7 +40,7 @@ module.exports = () => { }, PAGE_TITLE: () => { - return "user-handler-title"; + return "user-handler-title\n"; }, TEST_DYNAMIC_2: () => { diff --git a/packages/xarc-tag-renderer/test/spec/renderer.spec.ts b/packages/xarc-tag-renderer/test/spec/renderer.spec.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/xarc-tag-renderer/test/spec/simple-renderer.spec.ts b/packages/xarc-tag-renderer/test/spec/simple-renderer.spec.ts deleted file mode 100644 index 7241330fc..000000000 --- a/packages/xarc-tag-renderer/test/spec/simple-renderer.spec.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { - RenderContext, - TokenModule, - loadTokenModuleHandler, - TOKEN_HANDLER, - TEMPLATE_DIR -} from "@xarc/render-context"; -import { expect } from "chai"; -import { SimpleRenderer } from "../../src/simple-renderer"; -import * as Path from "path"; -import * as Fs from "fs"; -import * as _ from "lodash"; -import * as xstdout from "xstdout"; - -describe("simple renderer", function () { - it("requires htmlFile in the constructor", function () { - const renderer = new SimpleRenderer({ - htmlFile: "./test/data/template1.html", - tokenHandlers: "./test/fixtures/token-handler" - }); - renderer.initializeRenderer(true); - - expect(renderer._tokens[0].str).to.equal("\n\n"); - expect(renderer._tokens[1].id).to.equal("ssr-content"); - expect(renderer._tokens[2].isModule).to.be.false; - }); - it("it locates tokens", function () { - const renderer = new SimpleRenderer({ - htmlFile: "./test/data/template2.html", - tokenHandlers: "./test/fixtures/token-handler" - }); - renderer.initializeRenderer(true); - - expect(renderer._tokens[0].str).to.equal("\n\n"); - expect(renderer._tokens[1].id).to.equal("ssr-content"); - expect(renderer._tokens[2].isModule).to.be.false; - }); -}); - -describe("_findTokenIndex", function () { - it("should validate and return index", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(simpleRenderer._findTokenIndex(null, null, 0)).to.equal(0); - expect(simpleRenderer._findTokenIndex(null, null, 1)).to.equal(1); - expect(simpleRenderer._findTokenIndex(null, null, 2)).to.equal(2); - expect(simpleRenderer._findTokenIndex(null, null, 3)).to.equal(3); - }); - - it("should find token by id and return its index", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(simpleRenderer._findTokenIndex("webapp-body-bundles")).to.equal(3); - }); - - it("should return false if token by id is not found", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(simpleRenderer._findTokenIndex("foo-bar")).to.equal(false); - }); - - it("should find token by str and return its index", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(simpleRenderer._findTokenIndex(null, `console.log("test")`)).to.equal(5); - }); - - it("should return false if token by str is not found", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(simpleRenderer._findTokenIndex(null, `foo-bar-test-blah-blah`)).to.equal(false); - expect(simpleRenderer._findTokenIndex(null, /foo-bar-test-blah-blah/)).to.equal(false); - }); - - it("should throw if id, str, and index are invalid", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(() => simpleRenderer._findTokenIndex()).to.throw(`invalid id, str, and index`); - }); - - it("should throw if index is out of range", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(() => simpleRenderer._findTokenIndex(null, null, -1)).to.throw( - `index -1 is out of range` - ); - expect(() => - simpleRenderer._findTokenIndex(null, null, simpleRenderer.tokens.length + 100) - ).to.throw(` is out of range`); - }); -}); - -describe("findTokenByStr", function () { - it("should find tokens by str and return result", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - const x = simpleRenderer.findTokensByStr(`html`, simpleRenderer.tokens.length); - expect(x.length).to.equal(2); - const x2 = simpleRenderer.findTokensByStr(/html/, 1); - expect(x2.length).to.equal(1); - const x3 = simpleRenderer.findTokensByStr(/html/, 0); - expect(x3.length).to.equal(0); - }); - - it("should return false if token by str is not found", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(simpleRenderer.findTokensByStr(`foo-bar-test-blah-blah`)).to.deep.equal([]); - expect(simpleRenderer.findTokensByStr(/foo-bar-test-blah-blah/, null)).to.deep.equal([]); - }); - - it("should throw if matcher is invalid", () => { - const htmlFile = Path.join(__dirname, "../data/template2.html"); - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - - expect(() => simpleRenderer.findTokensByStr(null)).to.throw( - "matcher must be a string or RegExp" - ); - }); -}); -describe("intialzieRenderer: ", function () { - it("should parse template multi line tokens with props", () => { - const htmlFile = Path.join(__dirname, "../data/template3.html"); - const silentIntercept = true; - const intercept = xstdout.intercept(silentIntercept); - - const simpleRenderer = new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }); - simpleRenderer.initializeRenderer(); - intercept.restore(); - - const expected = [ - { - str: "\n\n" - }, - { - id: "ssr-content", - isModule: false, - pos: 17, - props: { - attr: ["1", "2", "3"], - args: ["a", "b", "c"], - empty: "", - foo: "bar a [b] c", - hello: "world", - test: true - }, - custom: undefined, - wantsNext: undefined - }, - { - id: "prefetch-bundles", - isModule: false, - pos: 148, - props: {}, - custom: undefined, - wantsNext: undefined - }, - { - str: `" - }, - { - id: "meta-tags", - isModule: false, - pos: 264, - props: {}, - custom: undefined, - wantsNext: undefined - }, - - { - str: "\n\n" - }, - { - id: "page-title", - isModule: false, - pos: 301, - props: {}, - custom: undefined, - wantsNext: undefined - }, - { - custom: undefined, - id: "json-prop", - isModule: false, - pos: 326, - props: { - foo: "bar", - test: [1, 2, 3] - }, - wantsNext: undefined - }, - { - custom: undefined, - id: "space-tags", - isModule: false, - pos: 396, - props: {}, - wantsNext: undefined - }, - { - custom: undefined, - id: "new-line-tags", - isModule: false, - pos: 421, - props: {}, - wantsNext: undefined - }, - { - custom: undefined, - id: "space-newline-tag", - isModule: false, - pos: 456, - props: { - attr1: "hello", - attr2: "world", - attr3: "foo" - }, - wantsNext: undefined - }, - { - _modCall: ["setup"], - custom: { - name: "custom-call" - }, - id: `require("../fixtures/custom-call")`, - isModule: true, - modPath: "../fixtures/custom-call", - pos: 536, - props: { - _call: "setup" - }, - wantsNext: false - } - ]; - expect(typeof _.last(simpleRenderer.tokens).custom.process).to.equal("function"); - delete _.last(simpleRenderer.tokens).custom.process; - expect(simpleRenderer.tokens).to.deep.equal(expected); - }); - - it("should throw for token with invalid props", () => { - const htmlFile = Path.join(__dirname, "../data/template4.html"); - expect( - () => - new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }) - ).to.throw( - `at line 9 col 3 - 'prefetch-bundles bad-prop' has malformed prop: name must be name=Val;` - ); - }); - it("should throw for token empty body", () => { - const htmlFile = Path.join(__dirname, "../data/template7.html"); - expect( - () => - new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }) - ).to.throw(`at line 3 col 5 - empty token body`); - }); - it("should throw for token empty body", () => { - const htmlFile = Path.join(__dirname, "../data/template7.html"); - expect( - () => - new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }) - ).to.throw(`at line 3 col 5 - empty token body`); - }); - - it("should throw for token missing close tag", () => { - const htmlFile = Path.join(__dirname, "../data/template8.html"); - expect( - () => - new SimpleRenderer({ - htmlFile, - tokenHandlers: "./test/fixtures/token-handler" - }) - ).to.throw(`at line 3 col 5 - Can't find token close tag for ' +x1 +
sub template tags 2 +x2 +
+ +ABC` + ); + expect(context.result).contains( + ` +user-handler-title +` + ); + expect(context.result).contains("hello world from function: user,options"); + }); + + it("should catch and return error as result", async () => { + const renderer = new TagRenderer({ + template: template1 + }); + + renderer.registerTokenIds("test", Symbol("test"), () => { + return { + "webapp-body-bundles": () => { + throw new Error("test"); + } + }; + }); + renderer.initializeRenderer(); + + const context = await renderer.render({}); + expect(context.result).to.be.an("Error"); + expect(context.result.message).equal("test"); + }); + + it("should invoke context handleError if a token returns error", async () => { + const renderer = new TagRenderer({ + template: new TagTemplate({ + templateTags: createTemplateTags`${() => { + return Promise.reject(new Error("test blah")); + }}` + }) + }); + + renderer.initializeRenderer(); + renderer.render({}); + const context = await renderer.render({}); + expect(context.result).to.be.an("Error"); + expect(context.result.message).equal("test blah"); + }); + + it("should invoke RegisterTokenIds tag once only", () => { + const renderer = new TagRenderer({ + template: template1 + }); + + const handler = () => ({}); + const sym1 = Symbol("test-1"); + const sym2 = Symbol("test-2"); + renderer.registerTokenIds("test-1", sym1, handler); + renderer.registerTokenIds("test-2", sym2, () => ({})); + renderer.registerTokenIds("test-1", sym1, handler); + // test-1 should not have moved to the end of the registered array + expect(renderer._tokenHandlers[1].name).equal("test-1"); + expect(renderer._tokenHandlers[1].handler).equal(handler); + expect(renderer._tokenHandlers[2].name).equal("test-2"); + }); + + it("should allow context to void progress and return a replacement result", async () => { + const renderer = new TagRenderer({ + template: template1 + }); + + renderer.registerTokenIds("test", Symbol("test"), () => { + return { + "webapp-body-bundles": context => { + context.voidStop("oops - stop"); + } + }; + }); + renderer.initializeRenderer(); + + const context = await renderer.render({}); + expect(context.result).equal("oops - stop"); + }); + + it("should handle token invoke handler return null", async () => { + const renderer = new TagRenderer({ + template: template2 + }); + renderer.initializeRenderer(); + const context = await renderer.render({}); + console.log(context.result); + expect(context.result).contains(` +
test
`); + }); + + it("should handle token invoke handler return null with comments", async () => { + const renderer = new TagRenderer({ + template: template2, + insertTokenIds: true + }); + renderer.initializeRenderer(); + const context = await renderer.render({}); + expect(context.result).contains(``); + }); + + it("should handle token ID being null", async () => { + const renderer = new TagRenderer({ + template: template2 + }); + renderer.initializeRenderer(); + const context = await renderer.render({}); + expect(context.result).contains(` +
test null id
`); + }); + + it("should handle token ID being null with comments", async () => { + const renderer = new TagRenderer({ + template: template2, + insertTokenIds: true + }); + renderer.initializeRenderer(); + const context = await renderer.render({}); + expect(context.result).contains(``); + }); + + describe("initializeRenderer", function () { + it("should not reset already initialized renderer", () => { + const renderer = new TagRenderer({ + template: template1 + }); + + renderer.initializeRenderer(); + const save = renderer._renderer; + renderer.initializeRenderer(); + expect(save).equal(renderer._renderer); + }); + }); +}); diff --git a/packages/xarc-tag-renderer/test/spec/tag-template.spec.ts b/packages/xarc-tag-renderer/test/spec/tag-template.spec.ts new file mode 100644 index 000000000..ab1f5e61b --- /dev/null +++ b/packages/xarc-tag-renderer/test/spec/tag-template.spec.ts @@ -0,0 +1,77 @@ +import { expect } from "chai"; +import { template as template1 } from "../data/template1"; +import { template as template2 } from "../data/template2"; +import { describe, it } from "mocha"; + +describe("tag template", function () { + it("should create a TagTemplate from ES6 template literal strings", () => { + expect(template1._templateTags[0].str).to.equal("\n"); + const ssrToken = template1.findTokensById(`ssr-content`); + expect(ssrToken).to.exist; + }); + + describe("_findTokenIndex", function () { + it("should validate and return index", () => { + expect(template2._findTokenIndex(null, null, 0)).to.equal(0); + expect(template2._findTokenIndex(null, null, 1)).to.equal(1); + expect(template2._findTokenIndex(null, null, 2)).to.equal(2); + expect(template2._findTokenIndex(null, null, 3)).to.equal(3); + }); + + it("should find token by id and return its index", () => { + expect(template2._findTokenIndex("webapp-body-bundles")).to.equal(3); + }); + + it("should return false if token by id is not found", () => { + expect(template2._findTokenIndex("foo-bar")).to.equal(false); + }); + + it("should find token by str and return its index", () => { + const ix = template2._findTokenIndex(null, `console.log("test")`); + expect(ix).to.be.above(0); + }); + + it("should return false if token by str is not found", () => { + expect(template2._findTokenIndex(null, `foo-bar-test-blah-blah`)).to.equal(false); + expect(template2._findTokenIndex(null, /foo-bar-test-blah-blah/)).to.equal(false); + }); + + it("should throw if id, str, and index are invalid", () => { + /* @ts-ignore */ + expect(() => template2._findTokenIndex(null, null, "")).to.throw( + `invalid id, str, and index` + ); + }); + + it("should return first token by default", () => { + expect(template2._findTokenIndex()).equal(0); + }); + + it("should throw if index is out of range", () => { + expect(() => template2._findTokenIndex(null, null, -1)).to.throw(`index -1 is out of range`); + expect(() => + template2._findTokenIndex(null, null, template2._templateTags.length + 100) + ).to.throw(` is out of range`); + }); + }); + + describe("findTokenByStr", function () { + it("should find tokens by str and return result", () => { + const x = template2.findTokensByStr(`html`, template2._templateTags.length); + expect(x.length).to.equal(2); + const x2 = template2.findTokensByStr(/html/, 1); + expect(x2.length).to.equal(1); + const x3 = template2.findTokensByStr(/html/, 0); + expect(x3.length).to.equal(0); + }); + + it("should return false if token by str is not found", () => { + expect(template2.findTokensByStr(`foo-bar-test-blah-blah`)).to.deep.equal([]); + expect(template2.findTokensByStr(/foo-bar-test-blah-blah/, null)).to.deep.equal([]); + }); + + it("should throw if matcher is invalid", () => { + expect(() => template2.findTokensByStr(null)).to.throw("matcher must be a string or RegExp"); + }); + }); +});