lib/template-options.js

'use strict';

/**
 * `templeo` options module
 * @module templeo/options
 */
/**
 * Template compilation options
 * @typedef {Object} module:templeo/options.Options
 * @property {Boolean} [useCommonJs=true] When true, __CommonJS Module__ semantics will be used. When false, __ECMAScript Module__ semantics
 * will be used.
 * @property {String} [contextURL=''] A base URL used as prefix for context `read`/`write` operations.
 * @property {String} [partialsURL=''] A base URL used as prefix for partial template HTTP/S `read`/`write` operations.
 * @property {RegExp} [bypassUrlRegExp=/^https?:\/?\/?[^:\/\s]+/i] An expression that will tested against context, partial and template names
 * to determine if the template name will be prefixed with the `contextURL`/`partialsURL` during `read`/`write` operations. Any matches will
 * __not__ be prefixed.
 * @property {String} [defaultExtension='html'] The default extension name to append to template names when performing `read`/`write`
 * operations and an explicit extension is not already provided for a given template name
 * @property {String} [defaultContextExtension='json'] The default extension name to append to the context name when performing `read`/`write`
 * operations and an explicit extension is not already provided for a given context name
 * @property {String} [defaultContextName='context'] The name assigned to the context when executing a rendering funtion without passing a
 * context _object_. The name will be used to `read` the context during compilation.
 * @property {String} [defaultTemplateName='template'] The name assigned to the primary template content passed into {@link Engine.compile},
 * similar to the name assignment when calling {@link Engine.registerPartial}. Set to a _falsy_ value to generate the name.
 * Since the primary template needs to be present prior to compiling,
 * __the option can only be used during compilation and is ignored during rendering.__
 * @property {String} [includesParametersName='params'] The name that will be added to each partial template scope that will contain any
 * parameters passed into the `include` where the partial was added. For example, `` ${ await include`somePartName${ { param: 123 } }` } ``
 * would cause `${ params.param1 }` to equal `123` within the `somePartName` partial, but __not__ in it's parent where the `include` was added.
 * @property {RegExp} [defaultPartialContent=' '] The value to use for a partial when a partial returns a non-string value.
 * @property {String} [renderTimePolicy=read] The policy applied to partial template DB `read`/`write` operations when encountering
 * `include` directives that do not have template content present in cache storage during rendering. When using any other policy
 * __except "none"__, `include` directives defined in a template that reference partials that have not been registered before compiling will
 * result in a _render-time_ read of the referenced content. The following policies are valid (see any extending option implmentations for
 * any additional policies):
 * - `read` Normal reads will occur for missing partial content encountered via __include__ directives during rendering.
 * - `read-write` Same as __read__, but will also __write__ compiled rendering functions when __include__ directives are encountered during rendering.
 * - `none` __Only__ partial templates that are registered before compiling a template will be available to _include_ directives during rendering.
 * Any _include_ directives found that reference partials that are not found will cause render-time errors to be thrown. 
 * @property {Object} [readFetchRequestOptions] The JSON options that will be used when making `read` requests to capture partial content.
 * Depending upon the `Cachier` being used on the `Engine`, the options will be passed into `window.fetch`, `https.request`, some other `read`
 * operation or simply ignored.
 * @property {Object} [writeFetchRequestOptions] The JSON options that will be used when making `write` requests for generated rendering functions.
 * Depending upon the `Cachier` being used on the `Engine`, the options will be passed into `window.fetch`, `https.request` some other `write`
 * operation or simply ignored.
 * @property {Object} [readFormatOptions] The formatting options passed into an optional _formatter function_ specified during cache construction
 * or passed into the _rendering function_ that will format read template content. For example, when a template is read and the desired code can
 * be formatted using the _formatter function_ (e.g. `js-beautify`, etc.). The template content will be passed as the first argument and the
 * `readFormatOptions` will be passed as the second argument. The returned string will be used as the content for the template.
 * @property {Object} [writeFormatOptions] The formatting options passed into an optional _format function_ specified during cache construction
 * or passed into the _rendering function_ that will format the compiled template code that will be written. For example, when a generated template
 * renderer is generated either during compilation or during `include` discovery, the desired code can be formatted using `js-beautify`, `uglify-js`
 * or any other module that supports the provided arguments. The first argument will be the generated rendering function (as a string). and the
 * `writeFormatOptions` will be passed as the second argument. The returned string will be used as the formatted source code that will be written.
 * @property {String} [encoding='utf8'] The text encoding used by the templates during reads
 * [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) value or a partial is _included_ that cannot be resolved. Wwhen partial
 * content is empty or cannot be found errors may be thrown when the template engine is used within an external template plugin.
 * @property {String} [varName='it'] The variable name of passed context objects that will be accessible by reference within template expressions
 * (e.g. `${ it.somePassedVal }`). __Compile-time only option.__
 * @property {RegExp} [filename=/^(.*[\\\/]|^)([^\.]*)(.*)$/] Expression that will be used to capture template names from passed definition
 * __pseudo__ filenames when a template name hasn't been passed
 * 
 * __ NOTE: Should contain 3 capture groups:__
 * 1. Full __pseudo__ path excluding final __pseudo__ filename w/o extension
 * 1. __Pseudo__ file name w/o extension
 * 1. Extension with preceding `.`
 * @property {Boolean} [cacheRawTemplates=true] If set to `false`, raw template content will not be cached (thus will perform a _read_ on every use-
 * __NOT FOR PRODUCTION USE!__)
 * @property {Boolean} [useSourceURL=true] Whether or not to add a
 * [`//# sourceURL](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Debug_eval_sources) to generated rendering functions. Some option
 * types may have a different default value.
 * @property {Boolean} [debugger=false] When `true`, a [`debugger`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger)
 * statement will be inserted into each compiled template rendering function at the __end__ of execution (compile-time options only)
 */

const OPTIONS = Object.freeze({
  defaults: Object.freeze({
    useCommonJs: true,
    contextURL: '',
    templateURL: '',
    partialsURL: '',
    bypassUrlRegExp: /^https?:\/?\/?[^:\/\s]+/i,
    defaultExtension: 'html',
    defaultContextExtension: 'json',
    defaultContextName: 'context',
    defaultTemplateName: 'template',
    includesParametersName: 'params',
    defaultPartialContent: ' ',
    renderTimePolicy: 'read',
    readFetchRequestOptions: null,
    writeFetchRequestOptions: null,
    readFormatOptions: null,
    writeFormatOptions: null,
    encoding: 'utf-8',
    varName: 'it',
    filename: /^(.*[\\\/]|^)([^\.]*)(.*)$/,
    cacheRawTemplates: true, // use with caution: when false, loads template file(s) on every request!!!
    useSourceURL: true,
    debugger: false
  }),
  valids: Object.freeze({
    renderTimePolicy: Object.freeze([
      'read',
      'read-write',
      'none'
    ])
  })
});

/**
 * Template compilation options. See {@link module:templeo/options.Options} for a full listing of options.
 * @see module:templeo/options.Options
 */
class TemplateOpts {
// TODO : ESM use... export class TemplateOpts {

  /**
   * Template compilation options
   * @see module:templeo/options.Options
   * @param {module:templeo/options.Options} [opts] The template compilation options
   */
  constructor(opts) {
    this.build(opts, this.constructor.defaultOptions, this);
  }

  /**
   * Builds the {@link Options} by iterating over the `dflt` (default options) object properties and either adding them to the `to`
   * object when the `opts` object does not contain the property value (or it's invalid for that option) or adds the value from `opts`
   * when the property is present and contains a valid value for the option being iterated. The final `to` object/properties will be
   * frozen using `Object.freeze` and `Object.defineProperty`.
   * @see module:templeo/options.Options
   * @param {module:templeo/options.Options} opts The template compilation options
   * @param {Object} optd An object that generatd from {@link TemplateOpts.defaultOptionMerge} that contains the default options
   * @param {Object} optd.defaults The object that contains option properties with default property values as the default values
   * @param {Object} [optd.valids] An object that contains option names as properties. Each property value should contain an `Array`
   * of _primitive_ values that represent valid values for a given option.
   * @param {Object} to The object where the options will be set- typically a {@link TemplateOpts} instance
   * @param {Object} [valids] An object that contains option names as properties. Each property should contain an `Array` of primitive
   * values that represent valid values for a given option.
   * @returns {Object} The `to` object
   */
  build(opts, optd, to) {
    var val, isRx, noOpt;
    for (let key in optd.defaults) {
      isRx = optd.defaults[key] && optd.defaults[key] instanceof RegExp;
      if (optd.defaults[key] && !isRx && typeof optd.defaults[key] === 'object') {
        val = {};
        for (let key2 in optd.defaults[key]) {
          noOpt = !opts || !opts[key] || typeof opts[key][key2] === 'undefined'
          val[key2] = optd.deriveOption ? optd.deriveOption(noOpt, opts, optd, key, key2) :
            noOpt ? optd.defaults[key][key2] : opts[key][key2];
        }
        Object.freeze(val);
      } else {
        noOpt = !opts || typeof opts[key] === 'undefined';
        val = optd.deriveOption ? optd.deriveOption(noOpt, opts, optd, key) :
          noOpt ? optd.defaults[key] : opts[key];
      }
      if (optd.valids && optd.valids[key] && !optd.valids[key].includes(val)) {
        throw new Error(`"${to.constructor.name}" for option "${key}" is set to "${val}", but it must be one of: ${optd.valids[key].join()}`);
      }
      Object.defineProperty(to, key, { enumerable: true, value: val });
      //if (isRx) to[key].toJSON = function toJSON() {
      //  return { class: RegExp.name, source: this.source, flags: this.flags }; // ensure regular expressions are retained during serialization (i.e. JSON.stringify) 
      //};
    }
    return to;
  }

  /**
   * Merges option objects into another object
   * @protected
   * @param {Object} options The option object to merge options __from__
   * @param {Object} to The object where that will be merged __into__
   */
  static defaultOptionMerge(options, to) {
    if (!to.hasOwnProperty('deriveOption') && typeof options.deriveOption === 'function') to.deriveOption = options.deriveOption;
    if (!to.hasOwnProperty('defaults')) to.defaults = {};
    if (!to.hasOwnProperty('valids')) to.valids = {};
    for (let key in options.defaults) {
      if (!to.defaults.hasOwnProperty(key)) {
        to.defaults[key] = options.defaults[key];
      }
      if (options.valids && options.valids.hasOwnProperty(key)) {
        if (to.valids.hasOwnProperty(key)) {
          to.valids[key] = Object.freeze([...options.valids[key], ...to.valids[key]]);
        } else to.valids[key] = options.valids[key];
      }
    }
    if (options === OPTIONS) { // merge complete, freeze merged options
      Object.freeze(to.defaults);
      Object.freeze(to.valids);
      Object.freeze(to);
    } else TemplateOpts.defaultOptionMerge(OPTIONS, to);
  }

  /**
   * @see module:templeo/options.Options
   * @returns {Object} An immutable object that describes how {@link module:templeo/options.Options} will be built
   * @returns {Object} `defaults` The object that contains option properties with default property values as the default values
   * @returns {Object} `valids` An object that contains option names as properties. Each property value should contain an `Array`
   * of _primitive_ values that represent valid values for a given option.
   */
  static get defaultOptions() {
    return OPTIONS;
  }
}

// TODO : ESM remove the following line...
module.exports = TemplateOpts;

1.0.0 (2019-12-16)

Full Changelog