OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / eta / dist / eta.module.mjs

657 lines · javascript ref:
import * as path from 'node:path';
import * as fs from 'node:fs';

/**
 * Handles storage and accessing of values
 *
 * In this case, we use it to store compiled template functions
 * Indexed by their `name` or `filename`
 */
class Cacher {
  constructor(cache) {
    this.cache = void 0;
    this.cache = cache;
  }
  define(key, val) {
    this.cache[key] = val;
  }
  get(key) {
    return this.cache[key];
  }
  remove(key) {
    delete this.cache[key];
  }
  reset() {
    this.cache = {};
  }
  load(cacheObj) {
    this.cache = {
      ...this.cache,
      ...cacheObj
    };
  }
}

class EtaError extends Error {
  constructor(message) {
    super(message);
    this.name = "Eta Error";
  }
}
class EtaParseError extends EtaError {
  constructor(message) {
    super(message);
    this.name = "EtaParser Error";
  }
}
class EtaRuntimeError extends EtaError {
  constructor(message) {
    super(message);
    this.name = "EtaRuntime Error";
  }
}
class EtaFileResolutionError extends EtaError {
  constructor(message) {
    super(message);
    this.name = "EtaFileResolution Error";
  }
}
class EtaNameResolutionError extends EtaError {
  constructor(message) {
    super(message);
    this.name = "EtaNameResolution Error";
  }
}
/**
 * Throws an EtaError with a nicely formatted error and message showing where in the template the error occurred.
 */
function ParseErr(message, str, indx) {
  const whitespace = str.slice(0, indx).split(/\n/);
  const lineNo = whitespace.length;
  const colNo = whitespace[lineNo - 1].length + 1;
  message += " at line " + lineNo + " col " + colNo + ":\n\n" + "  " + str.split(/\n/)[lineNo - 1] + "\n" + "  " + Array(colNo).join(" ") + "^";
  throw new EtaParseError(message);
}
function RuntimeErr(originalError, str, lineNo, path) {
  // code gratefully taken from https://github.com/mde/ejs and adapted
  const lines = str.split("\n");
  const start = Math.max(lineNo - 3, 0);
  const end = Math.min(lines.length, lineNo + 3);
  const filename = path;
  // Error context
  const context = lines.slice(start, end).map(function (line, i) {
    const curr = i + start + 1;
    return (curr == lineNo ? " >> " : "    ") + curr + "| " + line;
  }).join("\n");
  const header = filename ? filename + ":" + lineNo + "\n" : "line " + lineNo + "\n";
  const err = new EtaRuntimeError(header + context + "\n\n" + originalError.message);
  err.name = originalError.name; // the original name (e.g. ReferenceError) may be useful
  throw err;
}

/* END TYPES */
/* istanbul ignore next */
const AsyncFunction = async function () {}.constructor; // eslint-disable-line @typescript-eslint/no-empty-function
/**
 * Takes a template string and returns a template function that can be called with (data, config)
 *
 * @param str - The template string
 * @param config - A custom configuration object (optional)
 */
function compile(str, options) {
  const config = this.config;
  /* ASYNC HANDLING */
  // code gratefully taken from https://github.com/mde/ejs and adapted
  const ctor = options && options.async ? AsyncFunction : Function;
  /* END ASYNC HANDLING */
  try {
    return new ctor(config.varName, "options", this.compileToString.call(this, str, options)); // eslint-disable-line no-new-func
  } catch (e) {
    if (e instanceof SyntaxError) {
      throw new EtaParseError("Bad template syntax\n\n" + e.message + "\n" + Array(e.message.length + 1).join("=") + "\n" + this.compileToString.call(this, str, options) + "\n");
    } else {
      throw e;
    }
  }
}

/* TYPES */
/* END TYPES */
/**
 * Compiles a template string to a function string. Most often users just use `compile()`, which calls `compileToString` and creates a new function using the result
 */
function compileToString(str, options) {
  const config = this.config;
  const isAsync = options && options.async;
  const compileBody = this.compileBody;
  const buffer = this.parse.call(this, str);
  // note: when the include function passes through options, the only parameter that matters is the filepath parameter
  let res = `${config.functionHeader}
let include = (template, data) => this.render(template, data, options);
let includeAsync = (template, data) => this.renderAsync(template, data, options);

let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction${config.debug ? ', line: 1, templateStr: "' + str.replace(/\\|"/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n") + '"' : ""}};

function layout(path, data) {
  __eta.layout = path;
  __eta.layoutData = data;
}${config.debug ? "try {" : ""}${config.useWith ? "with(" + config.varName + "||{}){" : ""}

${compileBody.call(this, buffer)}
if (__eta.layout) {
  __eta.res = ${isAsync ? "await includeAsync" : "include"} (__eta.layout, {...${config.varName}, body: __eta.res, ...__eta.layoutData});
}
${config.useWith ? "}" : ""}${config.debug ? "} catch (e) { this.RuntimeErr(e, __eta.templateStr, __eta.line, options.filepath) }" : ""}
return __eta.res;
`;
  if (config.plugins) {
    for (let i = 0; i < config.plugins.length; i++) {
      const plugin = config.plugins[i];
      if (plugin.processFnString) {
        res = plugin.processFnString(res, config);
      }
    }
  }
  return res;
}
/**
 * Loops through the AST generated by `parse` and transform each item into JS calls
 *
 * **Example**
 *
 * ```js
 * let templateAST = ['Hi ', { val: 'it.name', t: 'i' }]
 * compileBody.call(Eta, templateAST)
 * // => "__eta.res+='Hi '\n__eta.res+=__eta.e(it.name)\n"
 * ```
 */
function compileBody(buff) {
  const config = this.config;
  let i = 0;
  const buffLength = buff.length;
  let returnStr = "";
  for (i; i < buffLength; i++) {
    const currentBlock = buff[i];
    if (typeof currentBlock === "string") {
      const str = currentBlock;
      // we know string exists
      returnStr += "__eta.res+='" + str + "'\n";
    } else {
      const type = currentBlock.t; // "r", "e", or "i"
      let content = currentBlock.val || "";
      if (config.debug) returnStr += "__eta.line=" + currentBlock.lineNo + "\n";
      if (type === "r") {
        // raw
        if (config.autoFilter) {
          content = "__eta.f(" + content + ")";
        }
        returnStr += "__eta.res+=" + content + "\n";
      } else if (type === "i") {
        // interpolate
        if (config.autoFilter) {
          content = "__eta.f(" + content + ")";
        }
        if (config.autoEscape) {
          content = "__eta.e(" + content + ")";
        }
        returnStr += "__eta.res+=" + content + "\n";
      } else if (type === "e") {
        // execute
        returnStr += content + "\n";
      }
    }
  }
  return returnStr;
}

/**
 * Takes a string within a template and trims it, based on the preceding tag's whitespace control and `config.autoTrim`
 */
function trimWS(str, config, wsLeft, wsRight) {
  let leftTrim;
  let rightTrim;
  if (Array.isArray(config.autoTrim)) {
    // Slightly confusing,
    // but _}} will trim the left side of the following string
    leftTrim = config.autoTrim[1];
    rightTrim = config.autoTrim[0];
  } else {
    leftTrim = rightTrim = config.autoTrim;
  }
  if (wsLeft || wsLeft === false) {
    leftTrim = wsLeft;
  }
  if (wsRight || wsRight === false) {
    rightTrim = wsRight;
  }
  if (!rightTrim && !leftTrim) {
    return str;
  }
  if (leftTrim === "slurp" && rightTrim === "slurp") {
    return str.trim();
  }
  if (leftTrim === "_" || leftTrim === "slurp") {
    // full slurp
    str = str.trimStart();
  } else if (leftTrim === "-" || leftTrim === "nl") {
    // nl trim
    str = str.replace(/^(?:\r\n|\n|\r)/, "");
  }
  if (rightTrim === "_" || rightTrim === "slurp") {
    // full slurp
    str = str.trimEnd();
  } else if (rightTrim === "-" || rightTrim === "nl") {
    // nl trim
    str = str.replace(/(?:\r\n|\n|\r)$/, "");
  }
  return str;
}
/**
 * A map of special HTML characters to their XML-escaped equivalents
 */
const escMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&quot;",
  "'": "&#39;"
};
function replaceChar(s) {
  return escMap[s];
}
/**
 * XML-escapes an input value after converting it to a string
 *
 * @param str - Input value (usually a string)
 * @returns XML-escaped string
 */
function XMLEscape(str) {
  // To deal with XSS. Based on Escape implementations of Mustache.JS and Marko, then customized.
  const newStr = String(str);
  if (/[&<>"']/.test(newStr)) {
    return newStr.replace(/[&<>"']/g, replaceChar);
  } else {
    return newStr;
  }
}

/* END TYPES */
/** Eta's base (global) configuration */
const defaultConfig = {
  autoEscape: true,
  autoFilter: false,
  autoTrim: [false, "nl"],
  cache: false,
  cacheFilepaths: true,
  debug: false,
  escapeFunction: XMLEscape,
  // default filter function (not used unless enables) just stringifies the input
  filterFunction: val => String(val),
  functionHeader: "",
  parse: {
    exec: "",
    interpolate: "=",
    raw: "~"
  },
  plugins: [],
  rmWhitespace: false,
  tags: ["<%", "%>"],
  useWith: false,
  varName: "it",
  defaultExtension: ".eta"
};

/* END TYPES */
const templateLitReg = /`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g;
const singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;
const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;
/** Escape special regular expression characters inside a string */
function escapeRegExp(string) {
  // From MDN
  return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}

function getLineNo(str, index) {
  return str.slice(0, index).split("\n").length;
}
function parse(str) {
  const config = this.config;
  let buffer = [];
  let trimLeftOfNextStr = false;
  let lastIndex = 0;
  const parseOptions = config.parse;
  if (config.plugins) {
    for (let i = 0; i < config.plugins.length; i++) {
      const plugin = config.plugins[i];
      if (plugin.processTemplate) {
        str = plugin.processTemplate(str, config);
      }
    }
  }
  /* Adding for EJS compatibility */
  if (config.rmWhitespace) {
    // Code taken directly from EJS
    // Have to use two separate replaces here as `^` and `$` operators don't
    // work well with `\r` and empty lines don't work well with the `m` flag.
    // Essentially, this replaces the whitespace at the beginning and end of
    // each line and removes multiple newlines.
    str = str.replace(/[\r\n]+/g, "\n").replace(/^\s+|\s+$/gm, "");
  }
  /* End rmWhitespace option */
  templateLitReg.lastIndex = 0;
  singleQuoteReg.lastIndex = 0;
  doubleQuoteReg.lastIndex = 0;
  function pushString(strng, shouldTrimRightOfString) {
    if (strng) {
      // if string is truthy it must be of type 'string'
      strng = trimWS(strng, config, trimLeftOfNextStr,
      // this will only be false on the first str, the next ones will be null or undefined
      shouldTrimRightOfString);
      if (strng) {
        // replace \ with \\, ' with \'
        // we're going to convert all CRLF to LF so it doesn't take more than one replace
        strng = strng.replace(/\\|'/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n");
        buffer.push(strng);
      }
    }
  }
  const prefixes = [parseOptions.exec, parseOptions.interpolate, parseOptions.raw].reduce(function (accumulator, prefix) {
    if (accumulator && prefix) {
      return accumulator + "|" + escapeRegExp(prefix);
    } else if (prefix) {
      // accumulator is falsy
      return escapeRegExp(prefix);
    } else {
      // prefix and accumulator are both falsy
      return accumulator;
    }
  }, "");
  const parseOpenReg = new RegExp(escapeRegExp(config.tags[0]) + "(-|_)?\\s*(" + prefixes + ")?\\s*", "g");
  const parseCloseReg = new RegExp("'|\"|`|\\/\\*|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")", "g");
  let m;
  while (m = parseOpenReg.exec(str)) {
    const precedingString = str.slice(lastIndex, m.index);
    lastIndex = m[0].length + m.index;
    const wsLeft = m[1];
    const prefix = m[2] || ""; // by default either ~, =, or empty
    pushString(precedingString, wsLeft);
    parseCloseReg.lastIndex = lastIndex;
    let closeTag;
    let currentObj = false;
    while (closeTag = parseCloseReg.exec(str)) {
      if (closeTag[1]) {
        const content = str.slice(lastIndex, closeTag.index);
        parseOpenReg.lastIndex = lastIndex = parseCloseReg.lastIndex;
        trimLeftOfNextStr = closeTag[2];
        const currentType = prefix === parseOptions.exec ? "e" : prefix === parseOptions.raw ? "r" : prefix === parseOptions.interpolate ? "i" : "";
        currentObj = {
          t: currentType,
          val: content
        };
        break;
      } else {
        const char = closeTag[0];
        if (char === "/*") {
          const commentCloseInd = str.indexOf("*/", parseCloseReg.lastIndex);
          if (commentCloseInd === -1) {
            ParseErr("unclosed comment", str, closeTag.index);
          }
          parseCloseReg.lastIndex = commentCloseInd;
        } else if (char === "'") {
          singleQuoteReg.lastIndex = closeTag.index;
          const singleQuoteMatch = singleQuoteReg.exec(str);
          if (singleQuoteMatch) {
            parseCloseReg.lastIndex = singleQuoteReg.lastIndex;
          } else {
            ParseErr("unclosed string", str, closeTag.index);
          }
        } else if (char === '"') {
          doubleQuoteReg.lastIndex = closeTag.index;
          const doubleQuoteMatch = doubleQuoteReg.exec(str);
          if (doubleQuoteMatch) {
            parseCloseReg.lastIndex = doubleQuoteReg.lastIndex;
          } else {
            ParseErr("unclosed string", str, closeTag.index);
          }
        } else if (char === "`") {
          templateLitReg.lastIndex = closeTag.index;
          const templateLitMatch = templateLitReg.exec(str);
          if (templateLitMatch) {
            parseCloseReg.lastIndex = templateLitReg.lastIndex;
          } else {
            ParseErr("unclosed string", str, closeTag.index);
          }
        }
      }
    }
    if (currentObj) {
      if (config.debug) {
        currentObj.lineNo = getLineNo(str, m.index);
      }
      buffer.push(currentObj);
    } else {
      ParseErr("unclosed tag", str, m.index);
    }
  }
  pushString(str.slice(lastIndex, str.length), false);
  if (config.plugins) {
    for (let i = 0; i < config.plugins.length; i++) {
      const plugin = config.plugins[i];
      if (plugin.processAST) {
        buffer = plugin.processAST(buffer, config);
      }
    }
  }
  return buffer;
}

/* END TYPES */
function handleCache(template, options) {
  const templateStore = options && options.async ? this.templatesAsync : this.templatesSync;
  if (this.resolvePath && this.readFile && !template.startsWith("@")) {
    const templatePath = options.filepath;
    const cachedTemplate = templateStore.get(templatePath);
    if (this.config.cache && cachedTemplate) {
      return cachedTemplate;
    } else {
      const templateString = this.readFile(templatePath);
      const templateFn = this.compile(templateString, options);
      if (this.config.cache) templateStore.define(templatePath, templateFn);
      return templateFn;
    }
  } else {
    const cachedTemplate = templateStore.get(template);
    if (cachedTemplate) {
      return cachedTemplate;
    } else {
      throw new EtaNameResolutionError("Failed to get template '" + template + "'");
    }
  }
}
function render(template,
// template name or template function
data, meta) {
  let templateFn;
  const options = {
    ...meta,
    async: false
  };
  if (typeof template === "string") {
    if (this.resolvePath && this.readFile && !template.startsWith("@")) {
      options.filepath = this.resolvePath(template, options);
    }
    templateFn = handleCache.call(this, template, options);
  } else {
    templateFn = template;
  }
  const res = templateFn.call(this, data, options);
  return res;
}
function renderAsync(template,
// template name or template function
data, meta) {
  let templateFn;
  const options = {
    ...meta,
    async: true
  };
  if (typeof template === "string") {
    if (this.resolvePath && this.readFile && !template.startsWith("@")) {
      options.filepath = this.resolvePath(template, options);
    }
    templateFn = handleCache.call(this, template, options);
  } else {
    templateFn = template;
  }
  const res = templateFn.call(this, data, options);
  // Return a promise
  return Promise.resolve(res);
}
function renderString(template, data) {
  const templateFn = this.compile(template, {
    async: false
  });
  return render.call(this, templateFn, data);
}
function renderStringAsync(template, data) {
  const templateFn = this.compile(template, {
    async: true
  });
  return renderAsync.call(this, templateFn, data);
}

/* END TYPES */
class Eta$1 {
  constructor(customConfig) {
    this.config = void 0;
    this.RuntimeErr = RuntimeErr;
    this.compile = compile;
    this.compileToString = compileToString;
    this.compileBody = compileBody;
    this.parse = parse;
    this.render = render;
    this.renderAsync = renderAsync;
    this.renderString = renderString;
    this.renderStringAsync = renderStringAsync;
    this.filepathCache = {};
    this.templatesSync = new Cacher({});
    this.templatesAsync = new Cacher({});
    // resolvePath takes a relative path from the "views" directory
    this.resolvePath = null;
    this.readFile = null;
    if (customConfig) {
      this.config = {
        ...defaultConfig,
        ...customConfig
      };
    } else {
      this.config = {
        ...defaultConfig
      };
    }
  }
  // METHODS
  configure(customConfig) {
    this.config = {
      ...this.config,
      ...customConfig
    };
  }
  withConfig(customConfig) {
    return {
      ...this,
      config: {
        ...this.config,
        ...customConfig
      }
    };
  }
  loadTemplate(name, template,
  // template string or template function
  options) {
    if (typeof template === "string") {
      const templates = options && options.async ? this.templatesAsync : this.templatesSync;
      templates.define(name, this.compile(template, options));
    } else {
      let templates = this.templatesSync;
      if (template.constructor.name === "AsyncFunction" || options && options.async) {
        templates = this.templatesAsync;
      }
      templates.define(name, template);
    }
  }
}

/* END TYPES */
function readFile(path) {
  let res = "";
  try {
    res = fs.readFileSync(path, "utf8");
    // eslint-disable-line @typescript-eslint/no-explicit-any
  } catch (err) {
    if ((err == null ? void 0 : err.code) === "ENOENT") {
      throw new EtaFileResolutionError(`Could not find template: ${path}`);
    } else {
      throw err;
    }
  }
  return res;
}
function resolvePath(templatePath, options) {
  let resolvedFilePath = "";
  const views = this.config.views;
  if (!views) {
    throw new EtaFileResolutionError("Views directory is not defined");
  }
  const baseFilePath = options && options.filepath;
  const defaultExtension = this.config.defaultExtension === undefined ? ".eta" : this.config.defaultExtension;
  // how we index cached template paths
  const cacheIndex = JSON.stringify({
    filename: baseFilePath,
    path: templatePath,
    views: this.config.views
  });
  templatePath += path.extname(templatePath) ? "" : defaultExtension;
  // if the file was included from another template
  if (baseFilePath) {
    // check the cache
    if (this.config.cacheFilepaths && this.filepathCache[cacheIndex]) {
      return this.filepathCache[cacheIndex];
    }
    const absolutePathTest = absolutePathRegExp.exec(templatePath);
    if (absolutePathTest && absolutePathTest.length) {
      const formattedPath = templatePath.replace(/^\/*|^\\*/, "");
      resolvedFilePath = path.join(views, formattedPath);
    } else {
      resolvedFilePath = path.join(path.dirname(baseFilePath), templatePath);
    }
  } else {
    resolvedFilePath = path.join(views, templatePath);
  }
  if (dirIsChild(views, resolvedFilePath)) {
    // add resolved path to the cache
    if (baseFilePath && this.config.cacheFilepaths) {
      this.filepathCache[cacheIndex] = resolvedFilePath;
    }
    return resolvedFilePath;
  } else {
    throw new EtaFileResolutionError(`Template '${templatePath}' is not in the views directory`);
  }
}
function dirIsChild(parent, dir) {
  const relative = path.relative(parent, dir);
  return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
}
const absolutePathRegExp = /^\\|^\//;

class Eta extends Eta$1 {
  constructor(...args) {
    super(...args);
    this.readFile = readFile;
    this.resolvePath = resolvePath;
  }
}

export { Eta, EtaError, EtaFileResolutionError, EtaNameResolutionError, EtaParseError, EtaRuntimeError };
//# sourceMappingURL=eta.module.mjs.map