UNPKG

teddy

Version:

🧸 Teddy is the most readable and easy to learn templating language there is!

416 lines • 57.8 kB
(function(root,factory){if(typeof exports==="object"&&typeof module==="object")module.exports=factory();else if(typeof define==="function"&&define.amd)define("teddy",[],factory);else if(typeof exports==="object")exports["teddy"]=factory();else root["teddy"]=factory()})(this,(()=>/******/(()=>{// webpackBootstrap /******/var __webpack_modules__={ /***/"./cheerioPolyfill.js": /*!****************************!*\ !*** ./cheerioPolyfill.js ***! \****************************/ /***/(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__)=>{"use strict";__webpack_require__.r(__webpack_exports__); /* harmony export */__webpack_require__.d(__webpack_exports__,{ /* harmony export */load:()=>/* binding */load /* harmony export */}); // stub out cheerio using native dom methods for frontend so we don't have to bundle cheerio on the frontend function load(html){const doc=parseTeddyDOMFromString(html);// create a DOM // return a querySelector function with function chains // e.g. dom('include') or dom(el) from teddy const $=function(query){// query can be a string, or a dom object // if query is a string, we need to create a dom object from the string: an object with elements in it, e.g. a list of include tag objects if(typeof query==="string"){const els=doc.querySelectorAll(query);return els;// return the object collection } // if query is an object, it's assumed we're trying to perform operations on a single dom node const el=query;return{ // e.g. dom(el).children() from teddy children:function(){return el.childNodes}, // e.g. dom(el).find() from teddy find:function(selector){return el.querySelectorAll(selector)}, // e.g. dom(arg).html() from teddy html:function(){return getTeddyDOMInnerHTML(el)}, // e.g. dom(arg).toString() from teddy toString:function(){return getTeddyDOMOuterHTML(el)}, // e.g. dom(el).attr('teddydeferreddynamicinclude', 'true') from teddy attr:function(attr,val){return el.setAttribute(attr,val)}, // dom(el).removeAttr(attr) from teddy removeAttr:function(attr){return el.removeAttribute(attr)}, // e.g. dom(el).replaceWith(localDom.html()) from teddy replaceWith:function(html){ // can either be a string or an array of elements if(typeof html==="object"){let newHtml="";for(const el of html)if(el.nodeType===window.Node.COMMENT_NODE)newHtml+="\x3c!--"+el.textContent+"--\x3e";else newHtml+=el.outerHTML||el.textContent;html=newHtml}const temp=document.createElement("div");temp.innerHTML=html;el.replaceWith(...temp.childNodes)}, // e.g. dom(el).remove() from teddy remove:function(){return el.remove()}}} // e.g. dom.html() from teddy; $.html=function(){return getTeddyDOMInnerHTML(doc)} // e.g. dom.toString() from teddy; $.toString=function(){return getTeddyDOMOuterHTML(doc)};return $}load.isCheerioPolyfill=true; // DOM parser function like DOMParser's parseFromString but allows Teddy elements to exist in places where they otherwise wouldn't be allowed, like inside of <select> elements function parseTeddyDOMFromString(html){const selfClosingTags=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);const root=document.createElement("body");const dom=[root];const openTags=[];// stack to track open tags const tagAndCommentRegex=/<\/?([a-zA-Z0-9]+)([^>]*)>|<!--([\s\S]*?)-->/g;const attrRegex=/([a-zA-Z0-9-:._]+)(?:=(["'])(.*?)\2|([^>\s]+))?/g;let lastIndex=0;let match; // loop through each match and build a DOM while((match=tagAndCommentRegex.exec(html))!==null){if(!dom[dom.length-1])throw new Error("Error parsing your template. There may be a coding mistake in your HTML. Look for extra closing </tags> and other common mistakes.");const textBeforeMatch=html.slice(lastIndex,match.index); // append text nodes if(textBeforeMatch.trim()){const textNode=document.createTextNode(textBeforeMatch);dom[dom.length-1].appendChild(textNode)}if(match[0].startsWith("\x3c!--")){ // handle comments const commentNode=document.createComment(match[3]);dom[dom.length-1].appendChild(commentNode)}else{ // handle tags const[fullMatch,tagName,attrString]=match;const lowerCaseTagName=tagName.toLowerCase();const isClosingTag=fullMatch.startsWith("</");if(isClosingTag){if(selfClosingTags.has(lowerCaseTagName)){ // convert incorrect closing tag for self-closing tag to self-closing tag const element=document.createElement(tagName);dom[dom.length-1].appendChild(element)}else // check if the closing tag matches the most recent open tag if(openTags.length>0&&openTags[openTags.length-1]===lowerCaseTagName){openTags.pop();dom.pop()}}else{ // create a new element const element=document.createElement(tagName); // set attributes let attrMatch;const attrMap=new Map;while((attrMatch=attrRegex.exec(attrString))!==null){const attrName=attrMatch[1];const attrValue=attrMatch[3]||attrMatch[4]||""; // handle duplicate attributes for special tags if(attrMap.has(attrName)){let count=1;let newAttrName;do{newAttrName=`${attrName}-teddyduplicate${count}`;count++}while(attrMap.has(newAttrName));attrMap.set(newAttrName,attrValue)}else attrMap.set(attrName,attrValue)} // apply attributes to the element for(const[name,value]of attrMap)try{ // replace elements with `src` attributes with `data-teddy-defer-attr-src` so the browser doesn't try to prefetch the asset // this is needed because the value of the `src` attribute could be a {teddyVariable} and that fetch won't resolve switch(lowerCaseTagName){case"img":case"video":case"audio":case"iframe":case"script":if(name==="src")element.setAttribute("data-teddy-defer-attr-src",value);// replace src with data-teddy-defer-attr-src else element.setAttribute(name,value||"");break;case"link":if(name==="href")element.setAttribute("data-teddy-defer-attr-href",value);// replace src with data-teddy-defer-attr-href else element.setAttribute(name,value||"");break;default:element.setAttribute(name,value||"")}}catch(e){console.warn("Error parsing an element attribute. You might have a typo in your HTML. A common cause is two spaces between element attributes.")} // append the new element to the current parent dom[dom.length-1].appendChild(element); // push the new element to the dom if it's not self-closing if(!selfClosingTags.has(lowerCaseTagName)&&!fullMatch.endsWith("/>")){dom.push(element);openTags.push(lowerCaseTagName)}}}lastIndex=tagAndCommentRegex.lastIndex} // append any remaining text after the last match if(lastIndex<html.length){const remainingText=html.slice(lastIndex);if(remainingText.trim()){const textNode=document.createTextNode(remainingText);dom[dom.length-1].appendChild(textNode)}}return root} // custom functions to get inner/outer HTML without escaping various things to prevent teddy from infinitely escaping them const doublyEncodedEntities={"&amp;amp;":"&amp;","&amp;lt;":"&lt;","&amp;gt;":"&gt;","&amp;quot;":"&quot;","&amp;#39;":"&#39;","&amp;#x2F;":"&#x2F;"};const entityEntries=Object.entries(doublyEncodedEntities);function getTeddyDOMInnerHTML(node){ // build html string let html="";for(const child of node.childNodes)if(child.nodeType===window.Node.ELEMENT_NODE){let outerHTML=child.outerHTML;for(const[doublyEncoded,singleEncoded]of entityEntries)outerHTML=outerHTML.replace(new RegExp(doublyEncoded,"g"),singleEncoded);html+=outerHTML}else if(child.nodeType===window.Node.TEXT_NODE){let textContent=child.textContent;for(const[doublyEncoded,singleEncoded]of entityEntries)textContent=textContent.replace(new RegExp(doublyEncoded,"g"),singleEncoded);html+=textContent}else if(child.nodeType===window.Node.COMMENT_NODE){let commentContent=child.textContent;for(const[doublyEncoded,singleEncoded]of entityEntries)commentContent=commentContent.replace(new RegExp(doublyEncoded,"g"),singleEncoded);html+=`\x3c!--${commentContent}--\x3e`}return html}function getTeddyDOMOuterHTML(node){ // start with the outerHTML of the node let outerHTML="";if(node.nodeType===window.Node.ELEMENT_NODE)outerHTML=node.outerHTML;else if(node.nodeType===window.Node.TEXT_NODE)outerHTML=node.textContent;else if(node.nodeType===window.Node.COMMENT_NODE)outerHTML=`\x3c!--${node.textContent}--\x3e`; // replace doubly encoded entities for(const[doublyEncoded,singleEncoded]of entityEntries)outerHTML=outerHTML.replace(new RegExp(doublyEncoded,"g"),singleEncoded);return outerHTML} /***/}, /***/"?2259": /*!**********************!*\ !*** path (ignored) ***! \**********************/ /***/()=>{}, /***/"?c221": /*!********************!*\ !*** fs (ignored) ***! \********************/ /***/()=>{} /******/}; /************************************************************************/ /******/ // The module cache /******/var __webpack_module_cache__={}; /******/ /******/ // The require function /******/function __webpack_require__(moduleId){ /******/ // Check if module is in cache /******/var cachedModule=__webpack_module_cache__[moduleId]; /******/if(cachedModule!==void 0) /******/return cachedModule.exports; /******/ /******/ // Create a new module (and put it into the cache) /******/var module=__webpack_module_cache__[moduleId]={ /******/ // no module.id needed /******/ // no module.loaded needed /******/exports:{} /******/}; /******/ /******/ // Execute the module function /******/__webpack_modules__[moduleId](module,module.exports,__webpack_require__); /******/ /******/ // Return the exports of the module /******/return module.exports; /******/} /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/(()=>{ /******/ // define getter functions for harmony exports /******/__webpack_require__.d=(exports,definition)=>{ /******/for(var key in definition) /******/if(__webpack_require__.o(definition,key)&&!__webpack_require__.o(exports,key)) /******/Object.defineProperty(exports,key,{enumerable:true,get:definition[key]}); /******/ /******/ /******/}; /******/})(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/(()=>{ /******/__webpack_require__.o=(obj,prop)=>Object.prototype.hasOwnProperty.call(obj,prop) /******/})(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/(()=>{ /******/ // define __esModule on exports /******/__webpack_require__.r=exports=>{ /******/if(typeof Symbol!=="undefined"&&Symbol.toStringTag) /******/Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}); /******/ /******/Object.defineProperty(exports,"__esModule",{value:true}); /******/}; /******/})(); /******/ /************************************************************************/var __webpack_exports__={}; // This entry needs to be wrapped in an IIFE because it needs to be in strict mode. (()=>{"use strict"; /*!******************!*\ !*** ./teddy.js ***! \******************/__webpack_require__.r(__webpack_exports__); /* harmony export */__webpack_require__.d(__webpack_exports__,{ /* harmony export */default:()=>__WEBPACK_DEFAULT_EXPORT__ /* harmony export */}); /* harmony import */var fs__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(/*! fs */"?c221"); /* harmony import */var path__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(/*! path */"?2259"); /* harmony import */var cheerio_slim__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(/*! cheerio/slim */"./cheerioPolyfill.js"); // #region globals // node filesystem module // node path module // dom parser const cheerioOptions={xml:{xmlMode:false,lowerCaseAttributeNames:false,decodeEntities:false}};const browser=cheerio_slim__WEBPACK_IMPORTED_MODULE_2__.load.isCheerioPolyfill;// true if we are executing in the browser context const params={};// teddy parameters setDefaultParams();// set params to the defaults let templates={};// loaded templates are stored as object collections, e.g. { "myTemplate.html": "<p>some markup</p>"} const caches={};// a place to store cached portions of templates const templateCaches={};// a place to store cached full templates // #endregion // #region private methods // loads the template from the filesystem function loadTemplate(template){ // ensure template is a string if(typeof template!=="string"){if(params.verbosity>1)console.warn("teddy.loadTemplate attempted to load a template which is not a string.");return""}const name=template;let register=false;if(!templates[template]&&template.indexOf("<")===-1&&fs__WEBPACK_IMPORTED_MODULE_0__&&fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync){ // template is not found, it is not code, and we're in the node.js context register=true; // append extension if not present if(template.slice(-5)!==".html")template+=".html";try{template=fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(template,"utf8")}catch(e){try{template=fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(params.templateRoot+template,"utf8")}catch(e){try{template=fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(params.templateRoot+"/"+template,"utf8")}catch(e){ // do nothing, attempt to render it as code register=false}}}}else if(templates[template]){template=templates[template];register=true}else{ // didn't find it; append extension if not present and check it again if(template.slice(-5)!==".html")template+=".html";if(templates[template]){template=templates[template];register=true}template=removeTeddyComments(template)}if(register){ // register the new template and return the code template=removeTeddyComments(template);templates[name]=template;return template}else // return the template name which is presumed to be code return template.slice(-5)===".html"?template.substring(0,template.length-5):template} // remove teddy {! comments !} and <!--! comments -->; also replace <!--# content --> with <escape>content</escape> function removeTeddyComments(renderedTemplate){let oldTemplate;do{oldTemplate=renderedTemplate;let vars;try{vars=matchByDelimiter(renderedTemplate,"{!","!}")}catch(e){return renderedTemplate;// it will match {! comments {! with comments in them !} !} but if there are unbalanced brackets, just return the original text }for(let i=0;i<vars.length;i++)renderedTemplate=renderedTemplate.replace(`{!${vars[i]}!}`,"");try{vars=matchByDelimiter(renderedTemplate,"\x3c!--!","--\x3e")}catch(e){return renderedTemplate}for(let i=0;i<vars.length;i++)renderedTemplate=renderedTemplate.replace(`\x3c!--!${vars[i]}--\x3e`,"");try{vars=matchByDelimiter(renderedTemplate,"\x3c!--#","--\x3e")}catch(e){return renderedTemplate}for(let i=0;i<vars.length;i++)renderedTemplate=renderedTemplate.replace(`\x3c!--#${vars[i]}--\x3e`,`<escape>${vars[i]}</escape>`)}while(oldTemplate!==renderedTemplate);return renderedTemplate} // find all cache elements and replace them with the rendered contents of their cache, then remove the cache element function replaceCacheElements(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("cache:not([defer])");if(tags.length>0)for(const el of tags){if(browser)el.attribs=getAttribs(el);const name=el.attribs.name;if(name.includes("{"))continue;const key=el.attribs.key||"none";if(key.includes("{"))continue;const cache=caches[name];if(cache&&cache.entries){const keyVal=el.attribs.key?getOrSetObjectByDotNotation(model,key):"none";if(cache.entries[keyVal]){const now=Date.now(); // if max age is not set, then there is no max age and the cache content is still valid // or if last accessed + max age > now then the cache is not stale and the cache is still valid if(!(cache.maxAge&&!cache.maxage)||cache.entries[keyVal].lastAccessed+(cache.maxAge||cache.maxage)>now){const cacheContent=cache.entries[keyVal].markup;cache.entries[keyVal].lastAccessed=now;dom(el).replaceWith(cacheContent)}else{ // if last accessed + max age <= now then the cache is stale and the cache is no longer valid delete caches[name].entries[keyVal];dom(el).attr("defer","true");// create a new cache }}else dom(el).attr("defer","true");// no cache exists for this yet; create after the template renders }else dom(el).attr("defer","true");// no cache exists for this yet; create after the template renders parsedTags++}}while(parsedTags);return dom} // add an id to all <noteddy> or <noparse> tags, then remove their content temporarily until the template is fully parsed function tagNoParseBlocks(dom,model){let parsedTags;do{parsedTags=0;let tags=dom("noteddy:not([id]), noparse:not([id])");if(tags.length>0)for(const el of tags){const id=model._noTeddyBlocks.push(dom(el).html())-1;dom(el).replaceWith(`<noteddy id="${id}"></noteddy>`);parsedTags++}tags=dom("pre:not([id]):not([parse])");if(tags.length>0)for(const el of tags){const id=model._noTeddyBlocks.push(dom(el).toString())-1;dom(el).replaceWith(`<noteddy id="${id}" pre="true"></noteddy>`);parsedTags++}}while(parsedTags);return dom} // parse <include> tags function parseIncludes(dom,model,dynamic){let parsedTags;let passes=0;do{passes++;if(passes>params.maxPasses)throw new Error(`teddy could not finish rendering the template because the max number of passes over the template (${params.maxPasses}) was exceeded; there may be an infinite loop in your template logic.`);parsedTags=0;let tags; // dynamic includes are includes like <include src="{sourcedFromVariable}"></include> if(dynamic)tags=dom("include");// parse all includes else tags=dom("include:not([teddydeferreddynamicinclude])");// parse only includes that aren't dynamic if(tags.length>0)for(const el of tags){ // ensure this isn't the child of a no parse block let foundBody=false;let next=false;let parent=el.parent||el.parentNode;while(!foundBody){let parentName;if(!parent)parentName="body";else parentName=parent.nodeName?.toLowerCase()||parent.name;if(parentName==="noparse"||parentName==="noteddy"){next=true;break}else if(parentName==="body")foundBody=true;else parent=parent.parent||parent.parentNode}if(next)continue; // get attributes if(browser)el.attribs=getAttribs(el);const src=el.attribs.src;if(!src){if(params.verbosity>1)console.warn("teddy encountered an include tag with no src attribute.");continue}if(src.startsWith("{")){dom(el).attr("teddydeferreddynamicinclude","true");// mark it dynamic and then skip it continue}loadTemplate(src);// load the partial into the template list let contents=templates[src]||"";if(typeof templates[src]!=="string"&&params.includeNotFoundBehavior==="display"){contents=`Template "${src}" not found!`;if(params.verbosity>1)console.warn(`teddy encountered an include tag with a src set to a template that could not be found: ${src}`)}const localModel=Object.assign({},model);for(const arg of dom(el).children()){const argName=browser?arg.nodeName?.toLowerCase():arg.name;if(argName==="arg"){if(browser)arg.attribs=getAttribs(arg);const argval=Object.keys(arg.attribs)[0];getOrSetObjectByDotNotation(localModel,argval,dom(arg).html())}}const hasNoteddy=contents.includes("</noteddy>");const hasNoparse=contents.includes("</noparse>");const hasPre=contents.includes("</pre>");const hasIf=contents.includes("</if>");const hasUnless=contents.includes("</unless>");const hasTrue=contents.includes(" true=");const hasFalse=contents.includes(" false=");const hasLoop=contents.includes("</loop>");const hasInline=contents.includes("</inline>");const hasEscape=contents.includes("</escape>")||contents.includes("\x3c!--#");const hasSelected=contents.includes(" selected-value=")||contents.includes(" checked-value=");if(hasEscape)contents=parseEscapes(contents);let localDom;if(hasNoteddy||hasNoparse||hasPre){localDom=(0,cheerio_slim__WEBPACK_IMPORTED_MODULE_2__.load)(contents,cheerioOptions);localDom=tagNoParseBlocks(localDom,localModel);contents=localDom.html()}localDom=(0,cheerio_slim__WEBPACK_IMPORTED_MODULE_2__.load)(parseVars(contents,localModel),cheerioOptions);if(hasIf||hasUnless)localDom=parseConditionals(localDom,localModel);if(hasTrue||hasFalse)localDom=parseOneLineConditionals(localDom,localModel);if(hasLoop)localDom=parseLoops(localDom,localModel);if(hasInline)localDom=parseInlines(localDom,localModel);if(hasSelected)localDom=parseSelectedAttributeValues(localDom,localModel);dom(el).replaceWith(localDom.html());parsedTags++}}while(parsedTags);return dom} // parse <if>, <elseif>, <unless>, <elseunless>, and <else> tags function parseConditionals(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("if, unless");if(tags.length>0)for(const el of tags){ // ensure this isn't the child of a loop or a no parse block let foundBody=false;let next=false;let parent=el.parent||el.parentNode;while(!foundBody){let parentName;if(!parent)parentName="body";else parentName=parent.nodeName?.toLowerCase()||parent.name;if(parentName==="loop"||parentName==="noparse"||parentName==="noteddy"){next=true;break}else if(parentName==="body")foundBody=true;else parent=parent.parent||parent.parentNode}if(next)continue; // get conditions let args=[];if(browser)el.attribs=getAttribs(el);for(let attr in el.attribs){if(attr.includes("-teddyduplicate"))attr=attr.split("-teddyduplicate")[0];// the condition is a duplicate, so remove the `-teddyduplicate1` from `conditionName-teddyduplicate1`, `conditionName-teddyduplicate2`, etc let val=el.attribs[attr];if(val){if(val.startsWith("{"))val=parseVars(val,model);args.push(`${attr}=${val}`)}else args.push(attr)} // check if it's an if tag and not an unless tag let isIf=true;const elName=browser?el.nodeName?.toLowerCase():el.name;if(elName==="unless")isIf=false; // evaluate conditional const condResult=evaluateConditional(args,model);if(isIf&&condResult||!isIf&&!condResult){ // render the true block and discard the elseif, elseunless, and else blocks let nextSibling=el.nextSibling;const removeStack=[];while(nextSibling){const nextSiblingName=browser?nextSibling.nodeName?.toLowerCase():nextSibling.name;switch(nextSiblingName){case"elseif":case"elseunless":case"else":removeStack.push(nextSibling);nextSibling=nextSibling.nextSibling;break;case"if":case"unless":nextSibling=false;break;default:nextSibling=nextSibling.nextSibling}}for(const element of removeStack)dom(element).replaceWith("");dom(el).replaceWith(el.childNodes||el.children);parsedTags++}else{ // true block is false; find the next elseif, elseunless, or else tag to evaluate let nextSibling=el.nextSibling;while(nextSibling){const nextSiblingName=browser?nextSibling.nodeName?.toLowerCase():nextSibling.name;switch(nextSiblingName){case"elseif": // get conditions args=[];if(browser)nextSibling.attribs=getAttribs(nextSibling);for(const attr in nextSibling.attribs){const val=nextSibling.attribs[attr];if(val)args.push(`${attr}=${val}`);else args.push(attr)}if(evaluateConditional(args,model)){ // render the true block and discard the elseif, elseunless, and else blocks const replaceSibling=nextSibling;dom(replaceSibling).replaceWith(replaceSibling.childNodes||replaceSibling.children);nextSibling=el.nextSibling;const removeStack=[];while(nextSibling){const nextSiblingName=browser?nextSibling.nodeName?.toLowerCase():nextSibling.name;switch(nextSiblingName){case"elseif":case"elseunless":case"else":removeStack.push(nextSibling);nextSibling=nextSibling.nextSibling;break;case"if":case"unless":nextSibling=false;break;default:nextSibling=nextSibling.nextSibling}}for(const element of removeStack)dom(element).replaceWith("");nextSibling=false;parsedTags++}else{ // true block is false; find the next elseif, elseunless, or else tag to evaluate const siblingToWipe=nextSibling;nextSibling=nextSibling.nextSibling;dom(siblingToWipe).replaceWith("")}break;case"elseunless": // get conditions args=[];if(browser)nextSibling.attribs=getAttribs(nextSibling);for(const attr in nextSibling.attribs){const val=nextSibling.attribs[attr];if(val)args.push(`${attr}=${val}`);else args.push(attr)}if(!evaluateConditional(args,model)){ // render the true block and discard the elseif, elseunless, and else blocks const replaceSibling=nextSibling;dom(replaceSibling).replaceWith(replaceSibling.childNodes||replaceSibling.children);nextSibling=el.nextSibling;const removeStack=[];while(nextSibling){const nextSiblingName=browser?nextSibling.nodeName?.toLowerCase():nextSibling.name;switch(nextSiblingName){case"elseif":case"elseunless":case"else":removeStack.push(nextSibling);nextSibling=nextSibling.nextSibling;break;case"if":case"unless":nextSibling=false;break;default:nextSibling=nextSibling.nextSibling}}for(const element of removeStack)dom(element).replaceWith("");nextSibling=false;parsedTags++}else{ // true block is false; find the next elseif, elseunless, or else tag to evaluate const siblingToWipe=nextSibling;nextSibling=nextSibling.nextSibling;dom(siblingToWipe).replaceWith("")}break;case"else": // else is always true, so if we've gotten here, then there's nothing to evaluate and we've reached the end of the conditional blocks dom(nextSibling).replaceWith(nextSibling.childNodes||nextSibling.children);nextSibling=false;parsedTags++;break;case"if":case"unless": // if we encounter another fresh if statement or unless statement, then there's nothing left to evaluate and we've reached the end of this conditional's blocks nextSibling=false;break;default: // if we encounter any other element or a text node we assume there could still be more elseif, elseunless, or else tags ahead so we keep going nextSibling=nextSibling.nextSibling}}dom(el).replaceWith("");// remove the original if statement once done with finding its siblings }}}while(parsedTags);return dom} // evaluates a single <if> or <unless> tag function evaluateConditional(conditions,model){const conditionsLength=conditions.length; // loop through conditions and reduce them to booleans for(let i=0;i<conditionsLength;i++){const condition=conditions[i];if(typeof condition==="boolean")continue;// if the condition is already a boolean then we don't need to reduce it to a boolean to evaluate it // reject conditions with invalid formatting if(condition.startsWith("=")||condition.endsWith("=")){if(params.verbosity>1)console.warn('teddy encountered a conditional statement with "=" at the beginning or end of a condition.');return false}if(condition.includes(":")&&!condition.startsWith("not:")){if(params.verbosity>1)console.warn('teddy encountered a conditional statement with a "not:" that isn\'t at the beginning of a condition.');return false} // deal with boolean logic if(condition==="and")if(conditions[i-1]&&evaluateCondition(conditions[i+1],model)){ // if both sides of an and are true, then reduce all 3 condition blocks to true conditions[i-1]=true;conditions[i]=true;conditions[i+1]=true}else{ // if either side of an and is false, then reduce all 3 condition blocks to false conditions[i-1]=false;conditions[i]=false;conditions[i+1]=false}else if(condition==="or")if(conditions[i-1]||evaluateCondition(conditions[i+1],model)) // if either side of an or is true, then reduce all 3 condition blocks to true, as well as all condition blocks that precded this or conditions.fill(true,0,i+2);else{ // if both sides of an or are false, then reduce all 3 condition blocks to false conditions[i-1]=false;conditions[i]=false;conditions[i+1]=false}else if(condition==="xor")if(!!conditions[i-1]===!!evaluateCondition(conditions[i+1],model)){ // if both sides of an xor are equal to each other, then reduce all 3 condition blocks to false conditions[i-1]=false;conditions[i]=false;conditions[i+1]=false}else{ // if the two sides of an xor are not equal to each other, then reduce all 3 condition blocks to true conditions[i-1]=true;conditions[i]=true;conditions[i+1]=true}else conditions[i]=evaluateCondition(condition,model)}return conditions.every((item=>item===true))||false;// if any of the booleans are false, then return false. otherwise return true } // determines whether a single condition in a teddy conditional is true or false function evaluateCondition(condition,model){let not;// stores whether the :not modifier is present if(typeof condition==="string"&&condition.includes("=")){// it's an equality check condition not=!!condition.startsWith("not:");// true if "not:" is present if(not)condition=condition.slice(4);// remove the :not prefix const parts=condition.split("=");// something="Some content" const cond=parts[0];// something delete parts[0];// remove the something= const val=parts.join("");// "Some content" — the path.join method ensures the string gets rebuilt even if it contains another = character const lookup=getOrSetObjectByDotNotation(model,cond); // the == is necessary because teddy does type-insensitive equality checks if(lookup==val)return!not;// eslint-disable-line else return not;// false }else{// it's a presence check not=typeof condition==="string"?!!condition.startsWith("not:"):false;// true if "not:" is present if(not)condition=condition.slice(4);// remove the :not prefix const lookup=getOrSetObjectByDotNotation(model,condition);if(lookup){if(typeof lookup==="object"&&Object.keys(lookup).length===0)return not;// false; empty object or array return!not;// true; var is present }else return not;// false; var is not present }} // render one-line if attributes, e.g. <p if-something="value" true="class='class-applied-if-true'" false="class='class-applied-if-false'">hello</p> function parseOneLineConditionals(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("[true], [false]");if(tags.length>0)for(const el of tags){ // skip parsing this if it uses variables as part of its conditions; it will get caught in the next pass after parseVars runs let defer=false;if(browser)el.attribs=getAttribs(el);for(const attr in el.attribs){const val=el.attribs[attr];if(val.startsWith("{")){defer=true;break}}if(defer){dom(el).attr("teddydeferredonelineconditional","true");continue} // ensure this isn't the child of a loop or a no parse block let foundBody=false;let next=false;let parent=el.parent||el.parentNode;while(!foundBody){let parentName;if(!parent)parentName="body";else parentName=parent.nodeName?.toLowerCase()||parent.name;if(parentName==="loop"||parentName==="noparse"||parentName==="noteddy"){next=true;break}else if(parentName==="body")foundBody=true;else parent=parent.parent||parent.parentNode}if(next)continue; // get conditions let ifTrue;let ifFalse;if(browser)el.attribs=getAttribs(el);const args=[];for(const origAttr in el.attribs){let attr=origAttr;let val=el.attribs[attr];if(attr.includes("-teddyduplicate"))attr=attr.split("-teddyduplicate")[0];// the condition is a duplicate, so remove the `-teddyduplicate1` from `conditionName-teddyduplicate1`, `conditionName-teddyduplicate2`, etc if(val?.startsWith("{"))val=parseVars(val,model);if(attr.startsWith("if-")){const parts=attr.split("if-");if(val)args.push(`${parts[1]}=${val}`);else args.push(parts[1]);dom(el).removeAttr(origAttr)}else if(attr==="true"){ifTrue=val.replaceAll("&quot;",'"');// true="class='blah'" dom(el).removeAttr(origAttr)}else if(attr==="false"){ifFalse=val.replaceAll("&quot;",'"');// false="class='blah'" dom(el).removeAttr(origAttr)}else if(attr==="and"||attr==="or"||attr==="xor"){args.push(attr);dom(el).removeAttr(origAttr)}} // evaluate conditional if(evaluateConditional(args,model)){if(ifTrue){const parts=ifTrue.split("=");dom(el).attr(parts[0],parts[1]?parts[1].replace(/["']/g,""):"")}parsedTags++}else if(ifFalse){if(ifFalse){const parts=ifFalse.split("=");dom(el).attr(parts[0],parts[1]?parts[1].replace(/["']/g,""):"")}parsedTags++}}}while(parsedTags);return dom} // render <loop> tags function parseLoops(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("loop");if(tags.length>0)for(const el of tags){ // get attributes let loopThrough;let keyName;let valName;if(browser)el.attribs=getAttribs(el);for(const attr in el.attribs)if(attr==="through"){let attrVal=el.attribs[attr];if(attrVal.startsWith("{"))attrVal=parseVars(attrVal,model);loopThrough=getOrSetObjectByDotNotation(model,attrVal)}else if(attr==="key")keyName=el.attribs[attr];else if(attr==="val")valName=el.attribs[attr]; // reject the loop if it has invalid attributes if(!loopThrough){if(params.verbosity>1)console.warn("teddy encountered a loop without a through attribute.");dom(el).replaceWith("");continue}if(!keyName&&!valName){if(params.verbosity>1)console.warn("teddy encountered a loop without a key or a val attribute.");dom(el).replaceWith("");continue} // loop through model[loopThrough] and parse teddy tags within the loop's iteration against the local model let newMarkup="";let loopContents=dom(el).html();if(loopThrough instanceof Set)loopThrough=[...loopThrough];// convert Sets to arrays for(const key in loopThrough){const val=loopThrough[key];const localModel=Object.assign({},model);getOrSetObjectByDotNotation(localModel,keyName,key);getOrSetObjectByDotNotation(localModel,valName,val);const hasNoteddyLoopContents=loopContents.includes("</noteddy>");const hasNoparseLoopContents=loopContents.includes("</noparse>");const hasPreLoopContents=loopContents.includes("</pre>");const hasEscape=loopContents.includes("</escape>")||loopContents.includes("\x3c!--#");if(hasEscape)loopContents=parseEscapes(loopContents);if(hasNoteddyLoopContents||hasNoparseLoopContents||hasPreLoopContents){let localDom=(0,cheerio_slim__WEBPACK_IMPORTED_MODULE_2__.load)(loopContents,cheerioOptions);localDom=tagNoParseBlocks(localDom,localModel);loopContents=localDom.html()}const localMarkup=parseVars(loopContents,localModel)||"";const hasNoteddy=localMarkup.includes("</noteddy>");const hasNoparse=localMarkup.includes("</noparse>");const hasIf=localMarkup.includes("</if>");const hasUnless=localMarkup.includes("</unless>");const hasTrue=localMarkup.includes(" true=");const hasFalse=localMarkup.includes(" false=");const hasLoop=localMarkup.includes("</loop>");const hasInline=localMarkup.includes("</inline>");const hasSelected=localMarkup.includes(" selected-value=")||localMarkup.includes(" checked-value=");let localDom=(0,cheerio_slim__WEBPACK_IMPORTED_MODULE_2__.load)(localMarkup||"",cheerioOptions);if(hasNoteddy||hasNoparse)localDom=tagNoParseBlocks(localDom,localModel);if(hasIf||hasUnless)localDom=parseConditionals(localDom,localModel);if(hasTrue||hasFalse)localDom=parseOneLineConditionals(localDom,localModel);if(hasLoop)localDom=parseLoops(localDom,localModel);if(hasInline)localDom=parseInlines(localDom,localModel);if(hasSelected)localDom=parseSelectedAttributeValues(localDom,localModel);newMarkup+=localDom.html()}const newDom=(0,cheerio_slim__WEBPACK_IMPORTED_MODULE_2__.load)(newMarkup||"",cheerioOptions);dom(el).replaceWith(newDom.html());parsedTags++}}while(parsedTags);return dom} // render <inline> tags function parseInlines(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("inline");if(tags.length>0)for(const el of tags){ // get attributes let css;let js;if(browser)el.attribs=getAttribs(el);for(const attr in el.attribs)if(attr==="css")css=getOrSetObjectByDotNotation(model,el.attribs[attr]);else if(attr==="js")js=getOrSetObjectByDotNotation(model,el.attribs[attr]); // reject if it has invalid attributes if(!css&&!js){if(params.verbosity>1)console.warn("teddy encountered an <inline> element without a css or js attribute.");dom(el).replaceWith("");continue}let replaceWith="";if(css)replaceWith=`<style>${css}</style>`;else replaceWith=`<script>${js}<\/script>`;dom(el).replaceWith(replaceWith);parsedTags++}}while(parsedTags);return dom} // render <escape> tags function parseEscapes(templateString){return templateString.replace(/<escape>(.*?)<\/escape>/gs,((_,content)=>escapeEntities(content.trim())))} // render `selected-value` and `checked-value` attributes function parseSelectedAttributeValues(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("select[selected-value], [checked-value]");if(tags.length>0)for(const el of tags){ // get attributes if(browser)el.attribs=getAttribs(el);for(let attr in el.attribs){const origAttr=attr;if(attr.includes("-teddyduplicate"))attr=attr.split("-teddyduplicate")[0];if(attr==="selected-value"){const val=parseVars(el.attribs[origAttr],model)||el.attribs[origAttr];const children=dom(el).find("option[value]");for(const opt of children){if(browser)opt.attribs=getAttribs(opt);if(opt.attribs.value===val)dom(opt).attr("selected","selected")}dom(el).removeAttr(origAttr)}else if(attr==="checked-value"){const val=parseVars(el.attribs[origAttr],model)||el.attribs[origAttr];const children=dom(el).find('input[type="checkbox"][value], input[type="radio"][value]');for(const opt of children){if(browser)opt.attribs=getAttribs(opt);if(opt.attribs.value===val)dom(opt).attr("checked","checked")}dom(el).removeAttr(origAttr)}}parsedTags++}}while(parsedTags);return dom} // render {variables} function parseVars(templateString,model){let vars;try{vars=matchByDelimiter(templateString,"{","}")}catch(e){return templateString;// it will match {vars{withVarsInThem}} but if there are unbalanced brackets, just return the original text }const varsLength=vars.length;for(let i=0;i<varsLength;i++){let match=vars[i];if(match==="")continue;// empty {} if(!/^(\d+|[a-zA-Z_$][a-zA-Z0-9_$|{}.-]*(\.[a-zA-Z_$][a-zA-Z0-9_$|{}.-]*)*)$/.test(match)){if(params.verbosity>2)console.warn(`teddy.parseVars encountered a {variable} that could not be parsed: {${match}}`);continue;// skip invalid variables }if(match.includes("{")){ // there's a variable inside the variable name const originalMatch=match;match=parseVars(match,model);try{templateString=templateString.replace(new RegExp(`\${${originalMatch}}`,"i"),(()=>`\${${match}}`));templateString=templateString.replace(new RegExp(`{${originalMatch}}`,"i"),(()=>`{${match}}`))}catch(e){if(params.verbosity>2)console.warn(`teddy.parseVars encountered a {variable} that could not be parsed: {${originalMatch}}`)}}const lastSixChars=match.slice(-6);if(lastSixChars.includes("|p")){// no parse flag is set const originalMatch=match;match=match.substring(0,match.length-(lastSixChars.split("|").length-1)*2);// remove last 2-n chars let parsed=getOrSetObjectByDotNotation(model,match);if(!parsed&&!lastSixChars.includes("|d")&&(params.emptyVarBehavior==="hide"||lastSixChars.includes("|h")))parsed="";// display empty string instead of the variable text verbatim if this setting is set if(typeof parsed==="string"&&parsed.startsWith("{")&&parsed.includes("|d"))parsed=parsed.replace("|d","");if(parsed||parsed===""){const id=model._noTeddyBlocks.push(parsed)-1;try{try{templateString=templateString.replace(new RegExp(`\${${originalMatch}}`.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),"i"),`<noteddy id="${id}"></noteddy>`);templateString=templateString.replace(new RegExp(`{${originalMatch}}`.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),"i"),`<noteddy id="${id}"></noteddy>`)}catch(e){if(params.verbosity>2)console.warn(`teddy.parseVars encountered a {variable} that could not be parsed: {${originalMatch}}`)}}catch(e){return templateString}}}else if(lastSixChars.includes("|s")){// no escape flag is set const originalMatch=match;match=match.substring(0,match.length-(lastSixChars.split("|").length-1)*2);// remove last 2-n chars let parsed=getOrSetObjectByDotNotation(model,match);let skipTemplateLiteralReplacement=false;if(!parsed&&!lastSixChars.includes("|d")&&(params.emptyVarBehavior==="hide"||lastSixChars.includes("|h")))parsed="";// display empty string instead of the variable text verbatim if this setting is set else if(!parsed&&parsed!==""){skipTemplateLiteralReplacement=true;parsed=`{${originalMatch}}`}if(typeof parsed==="string"&&parsed.startsWith("{")&&parsed.includes("|d"))parsed=parsed.replace("|d","");try{if(!skipTemplateLiteralReplacement)templateString=templateString.replace(new RegExp(`\${${originalMatch}}`.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),"i"),(()=>parsed));templateString=templateString.replace(new RegExp(`{${originalMatch}}`.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),"i"),(()=>parsed))}catch(e){return templateString}}else{// no flags are set let parsed=getOrSetObjectByDotNotation(model,match);let skipTemplateLiteralReplacement=false;if(!parsed&&!lastSixChars.includes("|d")&&(params.emptyVarBehavior==="hide"||lastSixChars.includes("|h")))parsed="";// display empty string instead of the variable text verbatim if this setting is set else if(parsed||parsed==="")parsed=escapeEntities(parsed);else if(parsed===0)parsed="0";else{skipTemplateLiteralReplacement=true;parsed=`{${match}}`}if(typeof parsed==="string"&&parsed.startsWith("{")&&parsed.includes("|d"))parsed=parsed.replace("|d","");try{if(!skipTemplateLiteralReplacement)templateString=templateString.replace(new RegExp(`\${${match}}`.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),"i"),(()=>parsed));templateString=templateString.replace(new RegExp(`{${match}}`.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),"i"),(()=>parsed))}catch(e){return templateString}}}return templateString} // once the template is fully rendered, find all cache elements that still exist and cache their contents function defineNewCaches(dom,model){let parsedTags;do{parsedTags=0;const tags=dom("cache[defer]");if(tags.length>0)for(const el of tags){if(browser)el.attribs=getAttribs(el);const name=el.attribs.name;const key=el.attribs.key||"none";const maxAge=parseInt(el.attribs.maxAge||el.attribs.maxage)||0;const maxCaches=parseInt(el.attribs.maxCaches||el.attribs.maxcaches)||1e3;const timestamp=Date.now();const markup=dom(el).html();if(!caches[name])caches[name]={key,maxAge,maxCaches,entries:{}};caches[name].entries[el.attribs.key?getOrSetObjectByDotNotation(model,key):"none"]={lastAccessed:timestamp,created:timestamp,markup}; // invalidate oldest cache if we've reached max caches limit if(Object.keys(caches[name].entries).length>maxCaches){const lowestKeyVal=Object.keys(caches[name].entries).reduce(((a,b)=>caches[name].entries[a].lastAccessed<caches[name].entries[b].lastAccessed?a:b));delete caches[name].entries[lowestKeyVal]}dom(el).replaceWith(markup);parsedTags++}}while(parsedTags);return dom} // removes any remaining teddy tags from the dom before returning the parsed html to the user function cleanupStrayTeddyTags(dom){let parsedTags;do{parsedTags=0;const tags=dom("[teddydeferredonelineconditional], pre[parse], include, arg, if, unless, elseif, elseunless, else, loop, cache");if(tags.length>0)for(const el of tags){const tagName=browser?el.nodeName?.toLowerCase():el.name;if(tagName==="include"||tagName==="arg"||tagName==="if"||tagName==="unless"||tagName==="elseif"||tagName==="elseunless"||tagName==="else"||tagName==="loop"||tagName==="cache")dom(el).remove();if(browser)el.attribs=getAttribs(el);for(const attr in el.attribs)if(attr==="true"||attr==="false"||attr==="parse"||attr==="teddydeferredonelineconditional"||attr.startsWith("if-"))dom(el).removeAttr(attr)}}while(parsedTags);return dom} // escapes sensitive characters to prevent xss const escapeHtmlEntities={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&#34;","'":"&#39;"};const entityKeys=Object.keys(escapeHtmlEntities);const ekl=entityKeys.length;function escapeEntities(value){let escapedEntity=false;let newValue="";let i;let j;if(typeof value==="object"){// cannot escape on this value if(!value)return false;// it is falsey to return false else if(Array.isArray(value))if(value.length===0)return false;// empty arrays are falsey else return"[Array]";// print that it is an array with content in it, but do not print the contents return"[Object]";// just print that it is an object, do not print the contents }else if(value===void 0)return false;// cannot escape on this value; undefined is falsey else if(typeof value==="boolean"||typeof value==="number")return value;// cannot escape on these values; if it's already a boolean or a number just return it else // loop through value to find html entities for(i=0;i<value.length;i++){escapedEntity=false; // loop through list of html entities to escape for(j=0;j<ekl;j++)if(value[i]===entityKeys[j]){// alter value to show escaped html entities newValue+=escapeHtmlEntities[entityKeys[j]];escapedEntity=true;break}if(!escapedEntity)newValue+=value[i]}return newValue} // if an entity is double-encoded, this will fix that function reverseDoubleEncodedEntities(str){return str.replace(/&amp;(#\d+;|#x[0-9A-Fa-f]+;|[A-Za-z]+;)/g,"&$1")} // match strings by a custom delimiter function matchByDelimiter(input,openDelimiter,closeDelimiter){const stack=[];const result=[];const openLength=openDelimiter.length;const closeLength=closeDelimiter.length;for(let i=0;i<input.length;i++)if(input.substring(i,i+openLength)===openDelimiter){stack.push(i+openLength);i+=openLength-1}else if(input.substring(i,i+closeLength)===closeDelimiter){const start=stack.pop();if(stack.length===0)result.push(input.substring(start,i));i+=closeLength-1}const individualSegments=[];const regex=/{!([^{}]*)!}/g;let match;for(const segment of result){while((match=regex.exec(segment))!==null)individualSegments.push(match[1]);individualSegments.push(segment)}return individualSegments} // gets or sets an object by dot notation, e.g. thing.nestedThing.furtherNestedThing: two arguments gets, three arguments sets function getOrSetObjectByDotNotation(obj,dotNotation,value){if(!obj)return false;if(!dotNotation||typeof dotNotation==="boolean"||typeof dotNotation==="number")return dotNotation;if(typeof dotNotation==="string")return getOrSetObjectByDotNotation(obj,dotNotation.split("."),value);else if(dotNotation.length===1&&value!==void 0){obj[dotNotation[0]]=value;return obj[dotNotation[0]]}else if(dotNotation.length===0)return obj;else if(dotNotation.length===1){if(obj)return caseInsensitiveLookup(obj,dotNotation[0]);return false}else return getOrSetObjectByDotNotation(caseInsensitiveLookup(obj,dotNotation[0]),dotNotation.slice(1),value);function caseInsensitiveLookup(obj,key){if(key==="length")return obj.length;const lowerCaseKey=key.toLowerCase();const normalizedObj=Object.keys(obj).reduce(((acc,k)=>{acc[k.toLowerCase()]=obj[k];return acc}),{});return normalizedObj[lowerCaseKey]}} // cheerio polyfill function getAttribs(element){const attributes=element.attributes;const attributesObject={};for(let i=0;i<attributes.length;i++){const attr=attributes[i];attributesObject[attr.name]=attr.value}return attributesObject} // #endregion // #region public methods // set params to the defaults function setDefaultParams(){params.verbosity=1;params.templateRoot="./";params.maxPasses=1e3;params.emptyVarBehavior="display";// or 'hide' params.includeNotFoundBehavior="display";// or 'hide' } // mutator method to set verbosity param. takes human-readable string argument and converts it to an integer for more efficient checks against the setting function setVerbosity(v){switch(v){case"none":case 0:v=0;break;case"verbose":case 2:v=2;break;case"debug":case"DEBUG":case 3:v=3;break;default:// concise v=1}params.verbosity=v} // mutator method to set template root param; must be a string function setTemplateRoot(v){params.templateRoot=String(v)} // mutator method to set max passes param: the number of times the parser can iterate over the template function setMaxPasses(v){params.maxPasses=Number(v)} // mutator method to set empty var behavior param: whether to display {variables} that don't resolve as text ('display') or as an empty string ('hide') function setEmptyVarBehavior(v){if(v==="hide")params.emptyVarBehavior="hide";else params.emptyVarBehavior="display"} // mutator method to set include tag not found param: whether to display an error when an <include> tag src can't be found function setIncludeNotFoundBehavior(v){if(v==="hide")params.includeNotFoundBehavior="hide";else params.includeNotFoundBehavior="display"} // access templates function getTemplates(){return templates} // takes in a template string and outputs a function which when given data will render out html function compile(templateString){return function(model){return render(templateString,model)}} // mutator method to cache template function setTemplate(file,template){templates[file]=template} // mutator method to clear template cache entirely function clearTemplates(){templates={}}function setCache(params){if(!templateCaches[params.template])templateCaches[params.template]={};if(params.key)templateCaches[params.template][params.key]={maxAge:params.maxAge||params.maxage,maxCaches:params.maxCaches||params.maxcaches||1e3,entries:{}};else templateCaches[params.template].none={maxAge:params.maxAge||params.maxage,markup:null,created:null}} // delete one or more cached templates // 1 string argument deletes the whole cache at that name for template partial caches // 2 arguments deletes just the value at that keyVal for template partial caches // 1 object argument assumes we're clearing whole template level cache function clearCache(name,keyVal){if(typeof name==="string")if(keyVal)delete caches[name].entries[keyVal];else delete caches[name];else if(typeof name==="object"){const params=name;if(params.key)delete templateCaches[params.template][params.key];else delete templateCaches[params.template]}else if(params.verbosity>0)console.error("teddy: invalid params passed to clearCache.")} // parses a template function render(template,model,callback){ // ensure template is a string if(typeof template!=="string"){if(params.verbosity>1)console.warn("teddy.render attempted to render a template which is not a string.");if(typeof callback==="function")return callback(null,"");else return""} // ensure model is an object if(typeof model!=="object"){if(params.verbosity>1)console.warn("teddy.render was passed an invalid model.");model={};// allow the template to render if an invalid model is supplied, but it will have an empty model } // declare vars let dom;let renderedTemplate;model._noTeddyBlocks=[];// will store code blocks exempt from teddy parsing // express.js support if(model.settings&&model.settings.views&&path__WEBPACK_IMPORTED_MODULE_1__)params.templateRoot=path__WEBPACK_IMPORTED_MODULE_1__.resolve(model.settings.views); // remove templateRoot from template name if necessary if(template.slice(params.templateRoot.length)===params.templateRoot)template=template.replace(params.templateRoot,""); // whole template caching const templateCache=templateCaches[template];let cacheKey=null;let cacheKeyModelVal=null;if(templateCache){const singletonCache=templateCache.none;if(singletonCache) // check if the timestamp exceeds max age if(!singletonCache.created)cac