index.js

'use strict';

// TODO : ESM remove the following lines...
const TemplateOpts = require('./lib/template-options');
const Cachier = require('./lib/cachier');
const Sandbox = require('./lib/sandbox');
// TODO : ESM uncomment the following lines...
// TODO : import * as TemplateOpts from './lib/template-options.mjs';
// TODO : import * as Cachier from './lib/cachier.mjs';
// TODO : import * as Sandbox from './lib/sandbox.mjs';

/**
 * Micro rendering template engine
 * @module templeo
 * @example
 * // Hapi.js example:
 * const Hapi = require('hapi');
 * const Vision = require('vision');
 * const HtmlFrmt = require('js-beautify').html;
 * const JsFrmt = require('js-beautify').js;
 * const Engine = require('templeo');
 * const econf = {
 *   partialsURL: 'https://example.com', // partial reads from a server?
 *   contextURL: 'https://example.com', // context read from a server?
 *   partialsPath: 'views/partials', // file path to the partials
 *   defaultExtension: 'html' // can be HTML, JSON, etc.
 * };
 * const cachier = new CachierFiles(econf, HtmlFrmt, JsFrmt);
 * const htmlEngine = new Engine(cachier);
 * // use the following instead if compiled templates don't need to be stored in files
 * // const htmlEngine = new Engine(econf, HtmlFrmt, JsFrmt);
 * const server = Hapi.Server({});
 * await server.register(Vision);
 * server.views({
 *  compileMode: 'async',
 *  relativeTo: '.',
 *  path: 'views',
 *  partialsPath: econf.partialsPath,
 *  defaultExtension: econf.defaultExtension,
 *  layoutPath: 'views/layout',
 *  layout: true,
 *  helpersPath: 'views/helpers',
 *  engines: {
 *    html: htmlEngine,
 *    json: new JsonEngine()
 *  }
 * });
 * // optionally set a partial function that can be accessed in the routes for
 * // instances where partials need to be generated, but not rendered to clients
 * server.app.htmlPartial = htmlEngine.genPartialFunc();
 * await server.start();
 * // it's a good practice to clear files after the server shuts down
 * server.events.on('stop', async () => {
 *  await htmlEngine.clearCache();
 * });
 */
class Engine {
// TODO : ESM use... export class Engine {

  /**
   * Creates a template literal engine
   * @param {TemplateOpts} [opts] The {@link TemplateOpts} to use
   * @param {Function} [readFormatter] The `function(string, readFormatOptions)` that will return a formatted string for __reading__
   * data using the `options.readFormatOptions` from {@link TemplateOpts} as the formatting options. Typically reads are for __HTML__
   * _minification_ and/or _beautifying_.
   * @param {Function} [writeFormatter] The `function(string, writeFormatOptions)` that will return a formatted string for __writting__
   * data using the `options.writeFormatOptions` from {@link TemplateOpts} as the formatting options. Typically reads are for __JS__
   * _minification_ and/or _beautifying_.
   * @param {Object} [log] The log for handling logging output
   * @param {Function} [log.debug] A function that will accept __debug__ level logging messages (i.e. `debug('some message to log')`)
   * @param {Function} [log.info] A function that will accept __info__ level logging messages (i.e. `info('some message to log')`)
   * @param {Function} [log.warn] A function that will accept __warning__ level logging messages (i.e. `warn('some message to log')`)
   * @param {Function} [log.error] A function that will accept __error__ level logging messages (i.e. `error('some message to log')`)
   */
  constructor(opts, readFormatter, writeFormatter, log) {
    const ns = internal(this);
    ns.at.cache = opts instanceof Cachier ? opts : new Cachier(opts, readFormatter, writeFormatter, log);
  }

  /**
   * Creates a new {@link Engine} from a {@link Cachier} instance
   * @param {Cachier} cachier The {@link Cachier} to use for persistence management
   * @returns {Engine} The generated {@link Engine}
   */
  static create(cachier) {
    if (!(cachier instanceof Cachier)) throw new Error(`cachier must be an instance of ${Cachier.name}, not ${cachier ? cachier.constructor.name : cachier}`);
    return new Engine(cachier);
  }

  /**
   * Compiles a template and returns a function that renders the template results using the passed `context` object
   * @param {(String | Boolean)} [content] The raw template content, `true` to read from cache before compilation.
   * Omit to load the template content from cache when the returned rendering function is called.
   * @param {Object} [opts] The options sent for compilation (omit to use the options set on the {@link Engine})
   * @param {URLSearchParams} [params] Any URL search parmeters that will be passed when capturing the primary `template` and/or
   * `context` when needed. Parameters can be excluded from the invocation by replacing `params` with a `callback` (e.g.
   * `compile(content, opts, callback)`).
   * @param {Function} [legacyCallback] Optional _callback style_ support __for LEGACY-ONLY APIs__:  
   * `compile(content, opts, (error, (ctx, opts, cb) => cb(error, results)) => {})` or omit to run via
   * `await compile(content, opts)`. __Omission will return the normal stand-alone renderer that can be serialized/deserialized.
   * When a _legacy callback function_ is specified, serialization/deserialization of the rendering function will not be possible!__
   * In _legacy_ mode {@link Engine.legacyRenderOptions} will be used during any rendering call that does not pass rendering options
   * or passes rendering options that does not contain any properties.
   * @returns {Function} The rendering `async function` that returns a template result string based upon the provided context.
   * The following arguments apply:
   * 1. _{Object}_ `context` The context JSON that can be used as data during rendering
   * 1. _{TemplateOpts}_ `[renderOptions]` The rendering options that will superceed any __compile-time__ options
   * 1. _{Function}_ `[readFormatter]` The function that will format read partials during include discovery (if any). The
   * formatting function takes 1 or 2 arguments with the first being the content that will be formatted and the second being
   * the `options.readFormatOptions`. The returned result should be a valid string.
   * 1. _{Function}_ `[writeFormatter]` The function that will format written sources during include discovery (if any). The
   * formatting function takes 1 or 2 arguments with the first being the content that will be formatted and the second being
   * the `options.writeFormatOptions`. The returned result should be a valid string.
   * 1. _{Object}_ `[sharedStore]` An object used for _in-memory_ storage space that can be shared between rendering functions.
   * This ensures that updated data within a renderer execution will be retained between rendering calls from the same renderer
   * or different renderers that are passed the same _shared store_.
   */
  async compile(content, opts, params, legacyCallback) { // ensures partials are included in the compilation
    const ns = internal(this);
    opts = opts || {};
    var fn, error;
    legacyCallback = typeof params === 'function' ? params : legacyCallback;
    params = params instanceof URLSearchParams ? params : null;
    if (legacyCallback) {
      if (ns.at.cache.log.info) {
        ns.at.cache.log.info('Compiling template w/callback style conventions');
      }
      try {
        fn = await compile(ns, content, params, ns.at.cache.options, opts, null, ns.at.cache);
      } catch (err) {
        error = err;
      }
      // legacy callback-style rendering :(
      legacyCallback(error, async (ctx, opts, cb) => {
        try {
          // opts.constructor.isPrototypeOf(ns.at.cache.options.constructor)
          if (!opts || !Object.getOwnPropertyNames(opts).length) {
            opts = ns.at.legacyRenderOptions;
          } else if (!(opts instanceof TemplateOpts)) opts = new ns.at.cache.options.constructor(opts);
          cb(null, await fn(ctx, opts));
        } catch (err) {
          cb(err);
        }
      });
    } else fn = compile(ns, content, params, ns.at.cache.options, opts, null, ns.at.cache);
    return fn;
  }

  /**
   * @returns {TemplateOpts} The __LEGACY-ONLY API__ {@link TemplateOpts} to use when no rendering options
   * are passed (or are empty) into the rendering function __and a callback function__ is specified when
   * calling {@link Engine.compile}
   * See {@link Engine.compile} for more details.
   */
  get legacyRenderOptions() {
    const ns = internal(this);
    return ns.at.legacyRenderOptions;
  }

  /**
   * The __LEGACY-ONLY API__ {@link TemplateOpts} to use when no rendering options
   * are passed (or are empty) into the rendering function __and a callback function__ is specified when
   * calling {@link Engine.compile}
   * @param {*} opts The options to set
   */
  set legacyRenderOptions(opts) {
    const ns = internal(this);
    ns.at.legacyRenderOptions = opts instanceof TemplateOpts ? opts : new ns.at.cache.options.constructor(opts);
  }

  /**
   * Retrieves a template, partial or context that resides __in-memory__.
   * @async
   * @param {String} name The name that uniquely identifies the template, partial or context
   * @param {URLSearchParams} [params] Any parameters designated during {@link Engine.registerPartial}
   * @param {String} [extension=options.defaultExtension] Optional override for a file extension designation
   * for the template, partial or context designated during {@link Engine.registerPartial}
   * @returns {Object} A copy of the generated data from {@link Engine.registerPartial}
   */
  getRegistered(name, params, extension) {
    const ns = internal(this);
    return ns.at.cache.getRegistered(name, params, extension);
  }

  /**
   * Unregisters a template, partial or context from cache
   * @async
   * @param {String} name The name that uniquely identifies the template, partial or context
   */
  unregister(name) {
    const ns = internal(this);
    return ns.at.cache.unregister(name);
  }

  /**
   * Registers and __caches__ the template, one or more partial templates and/or context JSON.
   * @async
   * @param {Object[]} [data] The template, partials and/or context to register.
   * @param {String} partials[].name The name that uniquely identifies the template, partial or context
   * @param {String} [partials[].content] The raw content that will be registered. Omit when `read === true` to read content from cache.
   * @param {URLSearchParams} [partials[].params] The `URLSearchParams` that will be passed during the content `read`
   * (__ignored when `content` is specified__).
   * @param {String} [partials[].extension] Optional override for a file extension designated for a template, partial or context.
   * @param {Boolean} [read] When `true`, an attempt will be made to also {@link Cachier.read} the template, partials and context that
   * __do not have__ a `content` property set.
   * @param {Boolean} [write] When `true`, an attempt will be made to also {@link Cachier.write} the template, partials and context that
   * __have__ a `content` property set.
   * @returns {Object} An object that contains the registration results:
   * 
   * - `data` The object that contains the template, partial fragments and/or context that have been registered
   *   - `name` The name that uniquely identifies the template, partial or context
   *   - `content` The raw content of the template, partial or context
   *   - `extension` The template file extension designation
   *   - `params` The URLSearchParams passed during the __initial__ content read
   *   - `fromRead` A flag that indicates that the data was set from a read operation
   *   - `overrideFromFileRead` A flag that indicates if the passed partial content was overridden by content from a file read
   * - `dirs` Present __only__ when file system back-end is used. Contains the directories/sub-directories that were created
   */
  register(data, read, write) {
    const ns = internal(this);
    return ns.at.cache.register(data, read, write);
  }

  /**
   * Registers and stores a partial template __in-memory__. Use {@link Engine.register} to `write`/_persist_ partials to cache ({@link Cachier})
   * @async
   * @param {String} name The template name that uniquely identifies the template content
   * @param {(String | URLSearchParams)} contentOrParams Either the partial template content __string__ to register _or_ the
   * `URLSearchParams` that will be passed during the content `read`
   * @param {String} [extension=options.defaultExtension] Optional override for a file extension designation for the partial
   * @returns {String} The partial content
   */
  registerPartial(name, contentOrParams, extension) {
    const ns = internal(this);
    return ns.at.cache.registerPartial(name, contentOrParams, extension);
  }

  /**
   * @returns {Function} A reference safe `async` function to {@link Engine.renderPartial} that can be safely passed into other functions
   */
  renderPartialGenerate() {
    const ns = internal(this);
    return async (name, content) => ns.this.renderPartial(name, content);
  }

  /**
   * On-Demand compilation of a registered templates
   * @param {String} name The name of the registered tempalte
   * @param {Object} [context={}] The object that contains contextual data used by the template
   * @returns {String} The rendered template
   */
  async renderPartial(name, context, renderOptions) {
    const ns = internal(this);
    const func = await ns.at.cache.compile(name);
    return func(context, renderOptions);
  }

  /**
   * Registers a _directive_ function that can be used within template
   * [interpolations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Expression_interpolation)
   * @param {Function} func A __named__ `function` that has no external scope dependencies/closures other than those exposed
   * via templates during rendering
   */
  registerHelper(func) {
    const ns = internal(this);
    return ns.at.cache.registerHelper(func);
  }

  /**
   * Clears the underlying cache
   * @async
   * @param {Boolean} [all=false] `true` to clear __ALL unassociated cache instances__ when possible as well as any partials
   * that have been registered
   */
  clearCache(all = false) {
    const ns = internal(this);
    return ns.at.cache.clear(all);
  }

  /**
   * @returns {TemplateOpts} The engine options
   */
  get options() {
    const ns = internal(this);
    return ns.at.cache.options;
  }
}

// TODO : ESM remove the following lines...
module.exports = Engine;

/**
 * Compiles a templated segment and returns a redering function (__assumes partials are already transpiled- see {@link compile} for partial support__)
 * @private
 * @param {Object} ns The namespace of the template engine
 * @param {(String | Boolean)} [content] The raw template content, `true` to read from cache before compilation.
 * Omit to load the template content from cache when the returned rendering function is called.
 * @param {URLSearchParams} [params] Any URL search parmeters that will be passed when capturing the primary `template` and/or `context` when needed
 * @param {TemplateOpts} [options] The options that overrides the default engine options
 * @param {Object} [ropts] The object definition to be used in the template
 * @param {String} [ropts.filename] When the template name is omitted, an attempt will be made to extract a name from the `filename` using `options.filename`
 * regular expression
 * @param {String} [tname] Name to be given to the template (omit to use the one from `options.filename` or an auto generated name)
 * @param {Cachier} [cache] The {@link Cachier} instance that will handle the {@link Cachier.write} of the compiled template code. Defaults to in-memory
 * cache.
 * @returns {Function} The rendering function described in {@link Engine.compile}
 */
async function compile(ns, content, params, options, ropts, tname, cache) {
  const opts = options instanceof TemplateOpts ? options : new TemplateOpts(options);
  if (!ropts) ropts = opts; // use definitions from the options when none are supplied
  cache = cache instanceof Cachier ? cache : new Cachier(opts);
  const parts = ropts && ropts.filename && ropts.filename.match && ropts.filename.match(opts.filename);
  const tnm = tname || (parts && parts[2]) || (ropts && ropts.defaultTemplateName)
    || opts.defaultTemplateName || `template_${Sandbox.guid(null, false)}`;
  try {
    return await cache.compile(tnm, content, params, parts && parts[3]); // await in order to catch errors
  } catch (err) {
    if (ns.at.cache.log.error) {
      err.stack += `\nCAUSE: Unable to compile template "${tnm}":\n${content}`;
      ns.at.cache.log.error(err);
    }
    throw err;
  }
}

// private mapping substitute until the following is adopted: https://github.com/tc39/proposal-class-fields#private-fields
let map = new WeakMap();
let internal = function(object) {
  if (!map.has(object)) {
    if (object.module && map.has(object.module)) object = object.module;
    else map.set(object, {});
  }
  return {
    at: map.get(object),
    this: object
  };
};

1.0.0 (2019-12-16)

Full Changelog