// TODO: allow '-' to trim up until newline. Use [^\S\n\r] instead of \s
// TODO: only include trimLeft polyfill if not in ES6
/* END TYPES */
var promiseImpl = new Function('return this')().Promise;
var asyncFunc = false;
try {
    asyncFunc = new Function('return (async function(){}).constructor')();
}
catch (e) {
    // We shouldn't actually ever have any other errors, but...
    if (!(e instanceof SyntaxError)) {
        throw e;
    }
}
function hasOwnProp(obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
}
function copyProps(toObj, fromObj, notConfig) {
    for (var key in fromObj) {
        if (hasOwnProp(fromObj, key)) {
            if (fromObj[key] != null &&
                typeof fromObj[key] == 'object' &&
                (key === 'storage' || key === 'prefixes') &&
                !notConfig // not called from Cache.load
            ) {
                // plugins or storage
                // Note: this doesn't merge from initial config!
                // Deep clone instead of assigning
                // TODO: run checks on this
                toObj[key] = copyProps(/*toObj[key] ||*/ {}, fromObj[key]);
            }
            else {
                toObj[key] = fromObj[key];
            }
        }
    }
    return toObj;
}
function trimWS(str, env, wsLeft, wsRight) {
    var leftTrim;
    var rightTrim;
    if (typeof env.autoTrim === 'string') {
        leftTrim = rightTrim = env.autoTrim;
        // Don't need to check if env.autoTrim is false
        // Because leftTrim, rightTrim are initialized as falsy
    }
    else if (Array.isArray(env.autoTrim)) {
        // kinda confusing
        // but _}} will trim the left side of the following string
        leftTrim = env.autoTrim[1];
        rightTrim = env.autoTrim[0];
    }
    if (wsLeft || wsLeft === false) {
        leftTrim = wsLeft;
    }
    if (wsRight || wsRight === false) {
        rightTrim = wsRight;
    }
    if (leftTrim === 'slurp' && rightTrim === 'slurp') {
        return str.trim();
    }
    if (leftTrim === '_' || leftTrim === 'slurp') {
        // console.log('trimming left' + leftTrim)
        // full slurp
        // eslint-disable-next-line no-extra-boolean-cast
        if (!!String.prototype.trimLeft) {
            str = str.trimLeft();
        }
        else {
            str = str.replace(/^[\s\uFEFF\xA0]+/, '');
        }
    }
    else if (leftTrim === '-' || leftTrim === 'nl') {
        // console.log('trimming left nl' + leftTrim)
        // nl trim
        str = str.replace(/^(?:\n|\r|\r\n)/, '');
    }
    if (rightTrim === '_' || rightTrim === 'slurp') {
        // console.log('trimming right' + rightTrim)
        // full slurp
        // eslint-disable-next-line no-extra-boolean-cast
        if (!!String.prototype.trimRight) {
            str = str.trimRight();
        }
        else {
            str = str.replace(/[\s\uFEFF\xA0]+$/, '');
        }
    }
    else if (rightTrim === '-' || rightTrim === 'nl') {
        // console.log('trimming right nl' + rightTrim)
        // nl trim
        str = str.replace(/(?:\n|\r|\r\n)$/, ''); // TODO: make sure this gets \r\n
    }
    return str;
}

/* END TYPES */
var Cacher = /** @class */ (function () {
    function Cacher(cache) {
        this.cache = cache;
    }
    Cacher.prototype.define = function (key, val) {
        this.cache[key] = val;
    };
    Cacher.prototype.get = function (key) {
        // string | array.
        // TODO: allow array of keys to look down
        // TODO: create plugin to allow referencing helpers, filters with dot notation
        return this.cache[key];
    };
    Cacher.prototype.remove = function (key) {
        delete this.cache[key];
    };
    Cacher.prototype.reset = function () {
        this.cache = {};
    };
    Cacher.prototype.load = function (cacheObj) {
        // TODO: this will err with deep objects and `storage` or `plugins` keys.
        // Update Feb 26: EDITED so it shouldn't err
        copyProps(this.cache, cacheObj, true);
    };
    return Cacher;
}());

function setPrototypeOf(obj, proto) {
    if (Object.setPrototypeOf) {
        Object.setPrototypeOf(obj, proto);
    }
    else {
        obj.__proto__ = proto;
    }
}
function SqrlErr(message) {
    var err = new Error(message);
    setPrototypeOf(err, SqrlErr.prototype);
    return err;
}
SqrlErr.prototype = Object.create(Error.prototype, {
    name: { value: 'Squirrelly Error', enumerable: false }
});
// TODO: Class transpilation adds a lot to the bundle size
function ParseErr(message, str, indx) {
    var whitespace = str.slice(0, indx).split(/\n/);
    var lineNo = whitespace.length;
    var colNo = whitespace[lineNo - 1].length + 1;
    message +=
        ' at line ' +
            lineNo +
            ' col ' +
            colNo +
            ':\n\n' +
            '  ' +
            str.split(/\n/)[lineNo - 1] +
            '\n' +
            '  ' +
            Array(colNo).join(' ') +
            '^';
    throw SqrlErr(message);
}

/* END TYPES */
var asyncRegExp = /^async +/;
var templateLitReg = /`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g;
var singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;
var doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;
var specialCharsReg = /[.*+\-?^${}()|[\]\\]/g;
function escapeRegExp(string) {
    // From MDN
    return specialCharsReg.test(string)
        ? string.replace(specialCharsReg, '\\$&') // $& means the whole matched string
        : string;
}
function parse(str, env) {
    /* Adding for EJS compatibility */
    if (env.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;
    var envPrefixes = env.prefixes;
    var prefixes = [
        envPrefixes.h,
        envPrefixes.b,
        envPrefixes.i,
        envPrefixes.r,
        envPrefixes.c,
        envPrefixes.e
    ].reduce(function (accumulator, prefix) {
        if (accumulator && prefix) {
            return accumulator + '|' + escapeRegExp(prefix);
        }
        else if (prefix) {
            // accumulator is empty
            return escapeRegExp(prefix);
        }
        else {
            // prefix and accumulator are both empty strings
            return accumulator;
        }
    }, '');
    var parseCloseReg = new RegExp('([|()]|=>)|' + // powerchars
        '(\'|"|`|\\/\\*)|\\s*((\\/)?(-|_)?' + // comments, strings
        escapeRegExp(env.tags[1]) +
        ')', 'g');
    var tagOpenReg = new RegExp('([^]*?)' + escapeRegExp(env.tags[0]) + '(-|_)?\\s*(' + prefixes + ')?\\s*', 'g');
    var startInd = 0;
    var trimNextLeftWs = false;
    function parseTag(tagOpenIndex, currentType) {
        var currentObj = { f: [] };
        var numParens = 0;
        var currentAttribute = 'c'; // default - Valid values: 'c'=content, 'f'=filter, 'fp'=filter params, 'p'=param, 'n'=name
        if (currentType === 'h' || currentType === 'b' || currentType === 'c') {
            currentAttribute = 'n';
        }
        else if (currentType === 'r') {
            currentObj.raw = true;
            currentType = 'i';
        }
        function addAttrValue(indx) {
            var valUnprocessed = str.slice(startInd, indx);
            // console.log(valUnprocessed)
            var val = valUnprocessed.trim();
            if (currentAttribute === 'f') {
                if (val === 'safe') {
                    currentObj.raw = true;
                }
                else {
                    if (env.async && asyncRegExp.test(val)) {
                        val = val.replace(asyncRegExp, '');
                        currentObj.f.push([val, '', true]);
                    }
                    else {
                        currentObj.f.push([val, '']);
                    }
                }
            }
            else if (currentAttribute === 'fp') {
                currentObj.f[currentObj.f.length - 1][1] += val;
            }
            else if (currentAttribute === 'err') {
                if (val) {
                    var found = valUnprocessed.search(/\S/);
                    ParseErr('invalid syntax', str, startInd + found);
                }
            }
            else {
                // if (currentObj[currentAttribute]) { // TODO make sure no errs
                //   currentObj[currentAttribute] += val
                // } else {
                currentObj[currentAttribute] = val;
                // }
            }
            startInd = indx + 1;
        }
        parseCloseReg.lastIndex = startInd;
        var m;
        // tslint:disable-next-line:no-conditional-assignment
        while ((m = parseCloseReg.exec(str)) !== null) {
            var char = m[1];
            var punctuator = m[2];
            var tagClose = m[3];
            var slash = m[4];
            var wsControl = m[5];
            var i = m.index;
            if (char) {
                // Power character
                if (char === '(') {
                    if (numParens === 0) {
                        if (currentAttribute === 'n') {
                            addAttrValue(i);
                            currentAttribute = 'p';
                        }
                        else if (currentAttribute === 'f') {
                            addAttrValue(i);
                            currentAttribute = 'fp';
                        }
                    }
                    numParens++;
                }
                else if (char === ')') {
                    numParens--;
                    if (numParens === 0 && currentAttribute !== 'c') {
                        // Then it's closing a filter, block, or helper
                        addAttrValue(i);
                        currentAttribute = 'err'; // Reset the current attribute
                    }
                }
                else if (numParens === 0 && char === '|') {
                    addAttrValue(i); // this should actually always be whitespace or empty
                    currentAttribute = 'f';
                }
                else if (char === '=>') {
                    addAttrValue(i);
                    startInd += 1; // this is 2 chars
                    currentAttribute = 'res';
                }
            }
            else if (punctuator) {
                if (punctuator === '/*') {
                    var commentCloseInd = str.indexOf('*/', parseCloseReg.lastIndex);
                    if (commentCloseInd === -1) {
                        ParseErr('unclosed comment', str, m.index);
                    }
                    parseCloseReg.lastIndex = commentCloseInd + 2; // since */ is 2 characters, and we're using indexOf rather than a RegExp
                }
                else if (punctuator === "'") {
                    singleQuoteReg.lastIndex = m.index;
                    var singleQuoteMatch = singleQuoteReg.exec(str);
                    if (singleQuoteMatch) {
                        parseCloseReg.lastIndex = singleQuoteReg.lastIndex;
                    }
                    else {
                        ParseErr('unclosed string', str, m.index);
                    }
                }
                else if (punctuator === '"') {
                    doubleQuoteReg.lastIndex = m.index;
                    var doubleQuoteMatch = doubleQuoteReg.exec(str);
                    if (doubleQuoteMatch) {
                        parseCloseReg.lastIndex = doubleQuoteReg.lastIndex;
                    }
                    else {
                        ParseErr('unclosed string', str, m.index);
                    }
                }
                else if (punctuator === '`') {
                    templateLitReg.lastIndex = m.index;
                    var templateLitMatch = templateLitReg.exec(str);
                    if (templateLitMatch) {
                        parseCloseReg.lastIndex = templateLitReg.lastIndex;
                    }
                    else {
                        ParseErr('unclosed string', str, m.index);
                    }
                }
            }
            else if (tagClose) {
                addAttrValue(i);
                startInd = i + m[0].length;
                tagOpenReg.lastIndex = startInd;
                // console.log('tagClose: ' + startInd)
                trimNextLeftWs = wsControl;
                if (slash && currentType === 'h') {
                    currentType = 's';
                } // TODO throw err
                currentObj.t = currentType;
                return currentObj;
            }
        }
        ParseErr('unclosed tag', str, tagOpenIndex);
        return currentObj; // To prevent TypeScript from erroring
    }
    function parseContext(parentObj, firstParse) {
        parentObj.b = []; // assume there will be blocks // TODO: perf optimize this
        parentObj.d = [];
        var lastBlock = false;
        var buffer = [];
        function pushString(strng, shouldTrimRightOfString) {
            if (strng) {
                // if string is truthy it must be of type 'string'
                // TODO: benchmark replace( /(\\|')/g, '\\$1')
                strng = trimWS(strng, env, trimNextLeftWs, // this will only be false on the first str, the next ones will be null or undefined
                shouldTrimRightOfString);
                if (strng) {
                    // replace \ with \\, ' with \'
                    strng = strng.replace(/\\|'/g, '\\$&').replace(/\r\n|\n|\r/g, '\\n');
                    // we're going to convert all CRLF to LF so it doesn't take more than one replace
                    buffer.push(strng);
                }
            }
        }
        // Random TODO: parentObj.b doesn't need to have t: #
        var tagOpenMatch;
        // tslint:disable-next-line:no-conditional-assignment
        while ((tagOpenMatch = tagOpenReg.exec(str)) !== null) {
            var precedingString = tagOpenMatch[1];
            var shouldTrimRightPrecedingString = tagOpenMatch[2];
            var prefix = tagOpenMatch[3] || '';
            var prefixType;
            for (var key in envPrefixes) {
                if (envPrefixes[key] === prefix) {
                    prefixType = key;
                    break;
                }
            }
            pushString(precedingString, shouldTrimRightPrecedingString);
            startInd = tagOpenMatch.index + tagOpenMatch[0].length;
            if (!prefixType) {
                ParseErr('unrecognized tag type: ' + prefix, str, startInd);
            }
            var currentObj = parseTag(tagOpenMatch.index, prefixType);
            // ===== NOW ADD THE OBJECT TO OUR BUFFER =====
            var currentType = currentObj.t;
            if (currentType === 'h') {
                var hName = currentObj.n || '';
                if (env.async && asyncRegExp.test(hName)) {
                    currentObj.a = true;
                    currentObj.n = hName.replace(asyncRegExp, '');
                }
                currentObj = parseContext(currentObj); // currentObj is the parent object
                buffer.push(currentObj);
            }
            else if (currentType === 'c') {
                // tag close
                if (parentObj.n === currentObj.n) {
                    if (lastBlock) {
                        // If there's a previous block
                        lastBlock.d = buffer;
                        parentObj.b.push(lastBlock);
                    }
                    else {
                        parentObj.d = buffer;
                    }
                    // console.log('parentObj: ' + JSON.stringify(parentObj))
                    return parentObj;
                }
                else {
                    ParseErr("Helper start and end don't match", str, tagOpenMatch.index + tagOpenMatch[0].length);
                }
            }
            else if (currentType === 'b') {
                // block
                // TODO: make sure async stuff inside blocks are recognized
                if (lastBlock) {
                    // If there's a previous block
                    lastBlock.d = buffer;
                    parentObj.b.push(lastBlock);
                }
                else {
                    parentObj.d = buffer;
                }
                var blockName = currentObj.n || '';
                if (env.async && asyncRegExp.test(blockName)) {
                    currentObj.a = true;
                    currentObj.n = blockName.replace(asyncRegExp, '');
                }
                lastBlock = currentObj; // Set the 'lastBlock' object to the value of the current block
                buffer = [];
            }
            else if (currentType === 's') {
                var selfClosingHName = currentObj.n || '';
                if (env.async && asyncRegExp.test(selfClosingHName)) {
                    currentObj.a = true;
                    currentObj.n = selfClosingHName.replace(asyncRegExp, '');
                }
                buffer.push(currentObj);
            }
            else {
                buffer.push(currentObj);
            }
            // ===== DONE ADDING OBJECT TO BUFFER =====
        }
        if (firstParse) {
            pushString(str.slice(startInd, str.length), false);
            parentObj.d = buffer;
        }
        else {
            throw SqrlErr('unclosed helper "' + parentObj.n + '"');
            // It should have returned by now
        }
        return parentObj;
    }
    var parseResult = parseContext({ f: [] }, true);
    // console.log(JSON.stringify(parseResult))
    if (env.plugins) {
        for (var i = 0; i < env.plugins.length; i++) {
            var plugin = env.plugins[i];
            if (plugin.processAST) {
                parseResult.d = plugin.processAST(parseResult.d, env);
            }
        }
    }
    return parseResult.d; // Parse the very outside context
}

// import SqrlErr from './err'
/* END TYPES */
function compileToString(str, env) {
    var buffer = parse(str, env);
    var res = "var tR='';" +
        (env.useWith ? 'with(' + env.varName + '||{}){' : '') +
        compileScope(buffer, env) +
        'if(cb){cb(null,tR)} return tR' +
        (env.useWith ? '}' : '');
    if (env.plugins) {
        for (var i = 0; i < env.plugins.length; i++) {
            var plugin = env.plugins[i];
            if (plugin.processFnString) {
                res = plugin.processFnString(res, env);
            }
        }
    }
    return res;
    // TODO: is `return cb()` necessary, or could we just do `cb()`
}
function filter(str, filters) {
    for (var i = 0; i < filters.length; i++) {
        var name = filters[i][0];
        var params = filters[i][1];
        var isFilterAsync = filters[i][2];
        // if (isFilterAsync && !env.async) {
        //   throw SqrlErr("Async filter '" + name + "' in non-async env")
        // }
        // Let the JS compiler do this, compile() will catch it
        str = (isFilterAsync ? 'await ' : '') + "c.l('F','" + name + "')(" + str;
        if (params) {
            str += ',' + params;
        }
        str += ')';
    }
    return str;
}
// TODO: Use type intersections for TemplateObject, etc.
// so I don't have to make properties mandatory
function compileHelper(env, res, descendants, params, isAsync, name) {
    var ret = '{exec:' +
        (isAsync ? 'async ' : '') +
        compileScopeIntoFunction(descendants, res, env) +
        ',params:[' +
        params +
        ']';
    if (name) {
        ret += ",name:'" + name + "'";
    }
    if (isAsync) {
        ret += ',async:true';
    }
    ret += '}';
    return ret;
}
function compileBlocks(blocks, env) {
    var ret = '[';
    for (var i = 0; i < blocks.length; i++) {
        var block = blocks[i];
        ret += compileHelper(env, block.res || '', block.d, block.p || '', block.a, block.n);
        if (i < blocks.length) {
            ret += ',';
        }
    }
    ret += ']';
    return ret;
}
function compileScopeIntoFunction(buff, res, env) {
    return 'function(' + res + "){var tR='';" + compileScope(buff, env) + 'return tR}';
}
function compileScope(buff, env) {
    var i = 0;
    var buffLength = buff.length;
    var returnStr = '';
    for (i; i < buffLength; i++) {
        var currentBlock = buff[i];
        if (typeof currentBlock === 'string') {
            var str = currentBlock;
            // we know string exists
            returnStr += "tR+='" + str + "';";
        }
        else {
            var type = currentBlock.t; // h, s, e, i
            var content = currentBlock.c || '';
            var filters = currentBlock.f;
            var name = currentBlock.n || '';
            var params = currentBlock.p || '';
            var res = currentBlock.res || '';
            var blocks = currentBlock.b;
            var isAsync = !!currentBlock.a; // !! is to booleanize it
            // if (isAsync && !env.async) {
            //   throw SqrlErr("Async block or helper '" + name + "' in non-async env")
            // }
            // Let compiler do this
            if (type === 'i') {
                if (env.defaultFilter) {
                    content = "c.l('F','" + env.defaultFilter + "')(" + content + ')';
                }
                var filtered = filter(content, filters);
                if (!currentBlock.raw && env.autoEscape) {
                    filtered = "c.l('F','e')(" + filtered + ')';
                }
                returnStr += 'tR+=' + filtered + ';';
                // reference
            }
            else if (type === 'h') {
                // helper
                if (env.storage.nativeHelpers.get(name)) {
                    returnStr += env.storage.nativeHelpers.get(name)(currentBlock, env);
                }
                else {
                    var helperReturn = (isAsync ? 'await ' : '') +
                        "c.l('H','" +
                        name +
                        "')(" +
                        compileHelper(env, res, currentBlock.d, params, isAsync);
                    if (blocks) {
                        helperReturn += ',' + compileBlocks(blocks, env);
                    }
                    else {
                        helperReturn += ',[]';
                    }
                    helperReturn += ',c)';
                    returnStr += 'tR+=' + filter(helperReturn, filters) + ';';
                }
            }
            else if (type === 's') {
                // self-closing helper
                returnStr +=
                    'tR+=' +
                        filter((isAsync ? 'await ' : '') + "c.l('H','" + name + "')({params:[" + params + ']},[],c)', filters) +
                        ';';
            }
            else if (type === 'e') {
                // execute
                returnStr += content + '\n';
            }
        }
    }
    return returnStr;
}

function errWithBlocksOrFilters(name, blocks, // false means don't check
filters, native) {
    if (blocks && blocks.length > 0) {
        throw SqrlErr((native ? 'Native' : '') + "Helper '" + name + "' doesn't accept blocks");
    }
    if (filters && filters.length > 0) {
        throw SqrlErr((native ? 'Native' : '') + "Helper '" + name + "' doesn't accept filters");
    }
}
/* ASYNC LOOP FNs */
function asyncArrLoop(arr, index, fn, res, cb) {
    fn(arr[index], index).then(function (val) {
        res += val;
        if (index === arr.length - 1) {
            cb(res);
        }
        else {
            asyncArrLoop(arr, index + 1, fn, res, cb);
        }
    });
}
function asyncObjLoop(obj, keys, index, fn, res, cb) {
    fn(keys[index], obj[keys[index]]).then(function (val) {
        res += val;
        if (index === keys.length - 1) {
            cb(res);
        }
        else {
            asyncObjLoop(obj, keys, index + 1, fn, res, cb);
        }
    });
}
var escMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
};
function replaceChar(s) {
    return escMap[s];
}
function XMLEscape(str) {
    // To deal with XSS. Based on Escape implementations of Mustache.JS and Marko, then customized.
    var newStr = String(str);
    if (/[&<>"']/.test(newStr)) {
        return newStr.replace(/[&<>"']/g, replaceChar);
    }
    else {
        return newStr;
    }
}

/* END TYPES */
var templates = new Cacher({});
/* ASYNC LOOP FNs */
var helpers = new Cacher({
    each: function (content, blocks) {
        var res = '';
        var arr = content.params[0];
        errWithBlocksOrFilters('each', blocks, false);
        if (content.async) {
            return new Promise(function (resolve) {
                asyncArrLoop(arr, 0, content.exec, res, resolve);
            });
        }
        else {
            for (var i = 0; i < arr.length; i++) {
                res += content.exec(arr[i], i);
            }
            return res;
        }
    },
    foreach: function (content, blocks) {
        var obj = content.params[0];
        errWithBlocksOrFilters('foreach', blocks, false);
        if (content.async) {
            return new Promise(function (resolve) {
                asyncObjLoop(obj, Object.keys(obj), 0, content.exec, '', resolve);
            });
        }
        else {
            var res = '';
            for (var key in obj) {
                if (!hasOwnProp(obj, key))
                    continue;
                res += content.exec(key, obj[key]); // todo: check on order
            }
            return res;
        }
    },
    include: function (content, blocks, config) {
        errWithBlocksOrFilters('include', blocks, false);
        var template = config.storage.templates.get(content.params[0]);
        if (!template) {
            throw SqrlErr('Could not fetch template "' + content.params[0] + '"');
        }
        return template(content.params[1], config);
    },
    extends: function (content, blocks, config) {
        var data = content.params[1] || {};
        data.content = content.exec();
        for (var i = 0; i < blocks.length; i++) {
            var currentBlock = blocks[i];
            data[currentBlock.name] = currentBlock.exec();
        }
        var template = config.storage.templates.get(content.params[0]);
        if (!template) {
            throw SqrlErr('Could not fetch template "' + content.params[0] + '"');
        }
        return template(data, config);
    },
    useScope: function (content, blocks) {
        errWithBlocksOrFilters('useScope', blocks, false);
        return content.exec(content.params[0]);
    }
});
var nativeHelpers = new Cacher({
    if: function (buffer, env) {
        errWithBlocksOrFilters('if', false, buffer.f, true);
        var returnStr = 'if(' + buffer.p + '){' + compileScope(buffer.d, env) + '}';
        if (buffer.b) {
            for (var i = 0; i < buffer.b.length; i++) {
                var currentBlock = buffer.b[i];
                if (currentBlock.n === 'else') {
                    returnStr += 'else{' + compileScope(currentBlock.d, env) + '}';
                }
                else if (currentBlock.n === 'elif') {
                    returnStr += 'else if(' + currentBlock.p + '){' + compileScope(currentBlock.d, env) + '}';
                }
            }
        }
        return returnStr;
    },
    try: function (buffer, env) {
        errWithBlocksOrFilters('try', false, buffer.f, true);
        if (!buffer.b || buffer.b.length !== 1 || buffer.b[0].n !== 'catch') {
            throw SqrlErr("native helper 'try' only accepts 1 block, 'catch'");
        }
        var returnStr = 'try{' + compileScope(buffer.d, env) + '}';
        var currentBlock = buffer.b[0];
        returnStr +=
            'catch' +
                (currentBlock.res ? '(' + currentBlock.res + ')' : '') +
                '{' +
                compileScope(currentBlock.d, env) +
                '}';
        return returnStr;
    },
    block: function (buffer, env) {
        errWithBlocksOrFilters('block', buffer.b, buffer.f, true);
        var returnStr = 'if(!' +
            env.varName +
            '[' +
            buffer.p +
            ']){tR+=(' +
            compileScopeIntoFunction(buffer.d, '', env) +
            ')()}else{tR+=' +
            env.varName +
            '[' +
            buffer.p +
            ']}';
        return returnStr;
    }
});
var filters = new Cacher({ e: XMLEscape });

/* END TYPES */
var defaultConfig = {
    varName: 'it',
    autoTrim: [false, 'nl'],
    autoEscape: true,
    defaultFilter: false,
    tags: ['{{', '}}'],
    l: function (container, name) {
        if (container === 'H') {
            var hRet = this.storage.helpers.get(name);
            if (hRet) {
                return hRet;
            }
            else {
                throw SqrlErr("Can't find helper '" + name + "'");
            }
        }
        else if (container === 'F') {
            var fRet = this.storage.filters.get(name);
            if (fRet) {
                return fRet;
            }
            else {
                throw SqrlErr("Can't find filter '" + name + "'");
            }
        }
    },
    async: false,
    storage: {
        helpers: helpers,
        nativeHelpers: nativeHelpers,
        filters: filters,
        templates: templates
    },
    prefixes: {
        h: '@',
        b: '#',
        i: '',
        r: '*',
        c: '/',
        e: '!'
    },
    cache: false,
    plugins: [],
    useWith: false
};
defaultConfig.l.bind(defaultConfig);
function getConfig(override, baseConfig) {
    // TODO: run more tests on this
    var res = {}; // Linked
    copyProps(res, defaultConfig); // Creates deep clone of res, 1 layer deep
    if (baseConfig) {
        copyProps(res, baseConfig);
    }
    if (override) {
        copyProps(res, override);
    }
    res.l.bind(res);
    return res;
}

/* END TYPES */
function compile(str, env) {
    var options = getConfig(env || {});
    var ctor = Function; // constructor
    /* ASYNC HANDLING */
    // The below code is modified from mde/ejs. All credit should go to them.
    if (options.async) {
        // Have to use generated function for this, since in envs without support,
        // it breaks in parsing
        if (asyncFunc) {
            ctor = asyncFunc;
        }
        else {
            throw SqrlErr("This environment doesn't support async/await");
        }
    }
    /* END ASYNC HANDLING */
    try {
        return new ctor(options.varName, 'c', // SqrlConfig
        'cb', // optional callback
        compileToString(str, options)); // eslint-disable-line no-new-func
    }
    catch (e) {
        if (e instanceof SyntaxError) {
            throw SqrlErr('Bad template syntax\n\n' +
                e.message +
                '\n' +
                Array(e.message.length + 1).join('=') +
                '\n' +
                compileToString(str, options));
        }
        else {
            throw e;
        }
    }
}

var fs = require('fs');
var path = require('path');
var _BOM = /^\uFEFF/;
/* END TYPES */
/**
 * Get the path to the included file from the parent file path and the
 * specified path.
 *
 * @param {String}  name       specified path
 * @param {String}  parentfile parent file path
 * @param {Boolean} [isDir=false] whether parent file path is a directory
 * @return {String}
 */
function getWholeFilePath(name, parentfile, isDirectory) {
    var includePath = path.resolve(isDirectory ? parentfile : path.dirname(parentfile), // returns directory the parent file is in
    name // file
    );
    var ext = path.extname(name);
    if (!ext) {
        includePath += '.sqrl';
    }
    return includePath;
}
/**
 * Get the path to the included file by Options
 *
 * @param  {String}  path    specified path
 * @param  {Options} options compilation options
 * @return {String}
 */
function getPath(path, options) {
    var includePath;
    var filePath;
    var views = options.views;
    var match = /^[A-Za-z]+:\\|^\//.exec(path);
    // Abs path
    if (match && match.length) {
        includePath = getWholeFilePath(path.replace(/^\/*/, ''), options.root || '/', true);
    }
    else {
        // Relative paths
        // Look relative to a passed filename first
        if (options.filename) {
            filePath = getWholeFilePath(path, options.filename);
            if (fs.existsSync(filePath)) {
                includePath = filePath;
            }
        }
        // Then look in any views directories
        if (!includePath) {
            if (Array.isArray(views) &&
                views.some(function (v) {
                    filePath = getWholeFilePath(path, v, true);
                    return fs.existsSync(filePath);
                })) {
                includePath = filePath;
            }
        }
        if (!includePath) {
            throw SqrlErr('Could not find the include file "' + path + '"');
        }
    }
    return includePath;
}
function readFile(filePath) {
    return fs
        .readFileSync(filePath)
        .toString()
        .replace(_BOM, ''); // TODO: is replacing BOM's necessary?
}
function loadFile(filePath, options) {
    var config = getConfig(options);
    var template = readFile(filePath);
    try {
        var compiledTemplate = compile(template, config);
        config.storage.templates.define(config.filename, compiledTemplate);
        return compiledTemplate;
    }
    catch (e) {
        throw SqrlErr('Loading file: ' + filePath + ' failed');
    }
}

// express is set like: app.engine('html', require('squirrelly').renderFile)
/* END TYPES */
/**
 * Get the template from a string or a file, either compiled on-the-fly or
 * read from cache (if enabled), and cache the template if needed.
 *
 * If `options.cache` is true, this function reads the file from
 * `options.filename` so it must be set prior to calling this function.
 *
 * @param {Options} options   compilation options
 * @param {String} [template] template source
 * @return {(TemplateFunction|ClientFunction)}
 * Depending on the value of `options.client`, either type might be returned.
 * @static
 */
function handleCache(options) {
    var filename = options.filename;
    if (options.cache) {
        var func = options.storage.templates.get(filename);
        if (func) {
            return func;
        }
        else {
            return loadFile(filename, options);
        }
    }
    return compile(readFile(filename), options);
}
/**
 * Try calling handleCache with the given options and data and call the
 * callback with the result. If an error occurs, call the callback with
 * the error. Used by renderFile().
 *
 * @param {Options} options    compilation options
 * @param {Object} data        template data
 * @param {RenderFileCallback} cb callback
 * @static
 */
function tryHandleCache(options, data, cb) {
    var result;
    if (!cb) {
        // No callback, try returning a promise
        if (typeof promiseImpl === 'function') {
            return new promiseImpl(function (resolve, reject) {
                try {
                    result = handleCache(options)(data, options);
                    resolve(result);
                }
                catch (err) {
                    reject(err);
                }
            });
        }
        else {
            throw SqrlErr("Please provide a callback function, this env doesn't support Promises");
        }
    }
    else {
        try {
            handleCache(options)(data, options, cb);
        }
        catch (err) {
            return cb(err);
        }
    }
}
/**
 * Get the template function.
 *
 * If `options.cache` is `true`, then the template is cached.
 *
 * @param {String}  path    path for the specified file
 * @param {Options} options compilation options
 * @return {(TemplateFunction|ClientFunction)}
 * Depending on the value of `options.client`, either type might be returned
 * @static
 */
function includeFile(path, options) {
    // the below creates a new options object, using the parent filepath of the old options object and the path
    var newFileOptions = getConfig({ filename: getPath(path, options) }, options);
    // TODO: make sure properties are currectly copied over
    return handleCache(newFileOptions);
}
function renderFile(filename, data, cb) {
    data = data || {};
    var Config = getConfig(data);
    // TODO: make sure above doesn't error. We do set filename down below
    if (data.settings) {
        // Pull a few things from known locations
        if (data.settings.views) {
            Config.views = data.settings.views;
        }
        if (data.settings['view cache']) {
            Config.cache = true;
        }
        // Undocumented after Express 2, but still usable, esp. for
        // items that are unsafe to be passed along with data, like `root`
        var viewOpts = data.settings['view options'];
        if (viewOpts) {
            copyProps(Config, viewOpts);
        }
    }
    Config.filename = filename; // Make sure filename is right
    return tryHandleCache(Config, data, cb);
}

/* END TYPES */
function includeFileHelper(content, blocks, config) {
    if (blocks && blocks.length > 0) {
        throw SqrlErr("Helper 'includeFile' doesn't accept blocks");
    }
    return includeFile(content.params[0], config)(content.params[1], config);
}
function extendsFileHelper(content, blocks, config) {
    var data = content.params[1] || {};
    data.content = content.exec();
    for (var i = 0; i < blocks.length; i++) {
        var currentBlock = blocks[i];
        data[currentBlock.name] = currentBlock.exec();
    }
    return includeFile(content.params[0], config)(data, config);
}

/* END TYPES */
function handleCache$1(template, options) {
    var templateFunc;
    if (options.cache && options.name && options.storage.templates.get(options.name)) {
        return options.storage.templates.get(options.name);
    }
    if (typeof template === 'function') {
        templateFunc = template;
    }
    else {
        templateFunc = compile(template, options);
    }
    if (options.cache && options.name) {
        options.storage.templates.define(options.name, templateFunc);
    }
    return templateFunc;
}
function render(template, data, env, cb) {
    var options = getConfig(env || {});
    if (options.async) {
        var result;
        if (!cb) {
            // No callback, try returning a promise
            if (typeof promiseImpl === 'function') {
                return new promiseImpl(function (resolve, reject) {
                    try {
                        result = handleCache$1(template, options)(data, options);
                        resolve(result);
                    }
                    catch (err) {
                        reject(err);
                    }
                });
            }
            else {
                throw SqrlErr("Please provide a callback function, this env doesn't support Promises");
            }
        }
        else {
            try {
                handleCache$1(template, options)(data, options, cb);
            }
            catch (err) {
                return cb(err);
            }
        }
    }
    else {
        return handleCache$1(template, options)(data, options);
    }
}

/* Export file stuff */
/* END TYPES */
helpers.define('includeFile', includeFileHelper);
helpers.define('extendsFile', extendsFileHelper);

export { renderFile as __express, compile, compileScope, compileScopeIntoFunction, compileToString, defaultConfig, filters, getConfig, helpers, loadFile, nativeHelpers, parse, render, renderFile, templates };
//# sourceMappingURL=squirrelly.es.js.map
