kempo-server 3.0.5 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{readFile,writeFile,mkdir,readdir}from"fs/promises";import path from"path";import{extractAttrs,extractContentBlocks,mergeContentBlocks,replaceLocations,resolveVars,resolveIfs,resolveForeach,resolveFragmentTags}from"./parse.js";import{readFileSync,statSync}from"fs";const findFileUpSync=(filename,startDir,rootDir)=>{let dir=startDir;const root=path.resolve(rootDir);for(;;){const candidate=path.join(dir,filename);try{return statSync(candidate),candidate}catch(e){}if(path.resolve(dir)===root)return null;const parent=path.dirname(dir);if(parent===dir)return null;dir=parent}},loadVersion=rootDir=>{try{return JSON.parse(readFileSync(path.join(rootDir,"package.json"),"utf8")).version||""}catch(e){return""}},walkGlobals=async dir=>{const entries=await readdir(dir,{withFileTypes:!0}),results=[];for(const entry of entries){const full=path.join(dir,entry.name);entry.isDirectory()?results.push(...await walkGlobals(full)):entry.name.endsWith(".global.html")&&results.push(full)}return results},loadGlobalContent=async rootDir=>{const files=await walkGlobals(rootDir),maps=await Promise.all(files.map(async f=>extractContentBlocks(await readFile(f,"utf8"))));return mergeContentBlocks(...maps)},renderPage=async(pageFilePath,rootDir,globals={},state={},maxDepth=10,preloadedGlobalContent=null)=>{const pageContent=await readFile(pageFilePath,"utf8"),pageTagMatch=pageContent.match(/^[\s\S]*?<page\s([^>]*)>/);if(!pageTagMatch)throw new Error(`Invalid page file: missing <page> root element in ${pageFilePath}`);const pageAttrs=extractAttrs(pageTagMatch[1]),templateName=pageAttrs.template||"default";delete pageAttrs.template;const pageDir=path.dirname(pageFilePath),templateFile=findFileUpSync(`${templateName}.template.html`,pageDir,rootDir);if(!templateFile)throw new Error(`Template not found: ${templateName}.template.html (searched from ${pageDir} to ${rootDir})`);const globalContent=preloadedGlobalContent??await loadGlobalContent(rootDir),rawPageBlocks=extractContentBlocks(pageContent),pageBlocks={};for(const[name,entries]of Object.entries(rawPageBlocks))pageBlocks[name]=entries.map(e=>({...e,html:replaceLocations(e.html,globalContent)}));const contentBlocks=mergeContentBlocks(pageBlocks,globalContent);let templateHtml=readFileSync(templateFile,"utf8");templateHtml=resolveFragmentTags(templateHtml,name=>{const filePath=findFileUpSync(name+".fragment.html",pageDir,rootDir);return filePath?readFileSync(filePath,"utf8"):null},0,maxDepth),templateHtml=replaceLocations(templateHtml,contentBlocks);const rel=path.relative(rootDir,path.dirname(pageFilePath)),depth=rel?rel.split(path.sep).length:0,now=new Date,vars={pathToRoot:depth>0?"../".repeat(depth):"./",year:String(now.getFullYear()),date:now.toISOString().slice(0,10),datetime:now.toISOString(),timestamp:String(Date.now()),version:loadVersion(rootDir),env:process.env.NODE_ENV||"",...globals,...state,...pageAttrs};for(const[key,val]of Object.entries(vars))"function"==typeof val&&(vars[key]=val());return templateHtml=resolveIfs(templateHtml,vars),templateHtml=resolveForeach(templateHtml,vars),templateHtml=resolveVars(templateHtml,vars),templateHtml},walkPages=async dir=>{const entries=await readdir(dir,{withFileTypes:!0}),results=[];for(const entry of entries){const full=path.join(dir,entry.name);entry.isDirectory()?results.push(...await walkPages(full)):entry.name.endsWith(".page.html")&&results.push(full)}return results},renderDir=async(inputDir,outputDir,globals={},state={},maxDepth=10)=>{const[pages,globalContent]=await Promise.all([walkPages(inputDir),loadGlobalContent(inputDir)]);let count=0;for(const page of pages){const outRel=path.relative(inputDir,page).replace(/\.page\.html$/,".html"),outPath=path.join(outputDir,outRel);await mkdir(path.dirname(outPath),{recursive:!0});const html=await renderPage(page,inputDir,globals,state,maxDepth,globalContent);await writeFile(outPath,html,"utf8"),count++}return count};export{renderPage,renderDir};
1
+ import{readFile,writeFile,mkdir,readdir}from"fs/promises";import path from"path";import{extractAttrs,extractContentBlocks,mergeContentBlocks,replaceLocations,resolveVars,resolveIfs,resolveForeach,resolveFragmentTags}from"./parse.js";import{readFileSync,statSync}from"fs";const findFileUpSync=(filename,startDir,rootDir)=>{let dir=startDir;const root=path.resolve(rootDir);for(;;){const candidate=path.join(dir,filename);try{return statSync(candidate),candidate}catch(e){}if(path.resolve(dir)===root)return null;const parent=path.dirname(dir);if(parent===dir)return null;dir=parent}},loadVersion=rootDir=>{try{return JSON.parse(readFileSync(path.join(rootDir,"package.json"),"utf8")).version||""}catch(e){return""}},walkGlobals=async dir=>{const entries=await readdir(dir,{withFileTypes:!0}),results=[];for(const entry of entries){const full=path.join(dir,entry.name);entry.isDirectory()?results.push(...await walkGlobals(full)):entry.name.endsWith(".global.html")&&results.push(full)}return results},loadGlobalContent=async rootDir=>{const files=await walkGlobals(rootDir),maps=await Promise.all(files.map(async f=>extractContentBlocks(await readFile(f,"utf8"))));return mergeContentBlocks(...maps)},renderPage=async(pageFilePath,rootDir,globals={},state={},maxDepth=10,preloadedGlobalContent=null)=>{const pageContent=await readFile(pageFilePath,"utf8"),pageTagMatch=pageContent.match(/^[\s\S]*?<page((?:[^>"']|"[^"]*"|'[^']*')*)>/);if(!pageTagMatch)throw new Error(`Invalid page file: missing <page> root element in ${pageFilePath}`);const pageAttrs=extractAttrs(pageTagMatch[1]),templateName=pageAttrs.template||"default";delete pageAttrs.template;const pageDir=path.dirname(pageFilePath),templateFile=findFileUpSync(`${templateName}.template.html`,pageDir,rootDir);if(!templateFile)throw new Error(`Template not found: ${templateName}.template.html (searched from ${pageDir} to ${rootDir})`);const globalContent=preloadedGlobalContent??await loadGlobalContent(rootDir),rawPageBlocks=extractContentBlocks(pageContent),pageBlocks={};for(const[name,entries]of Object.entries(rawPageBlocks))pageBlocks[name]=entries.map(e=>({...e,html:replaceLocations(e.html,globalContent)}));const contentBlocks=mergeContentBlocks(pageBlocks,globalContent);let templateHtml=readFileSync(templateFile,"utf8");templateHtml=resolveFragmentTags(templateHtml,name=>{const filePath=findFileUpSync(name+".fragment.html",pageDir,rootDir);return filePath?readFileSync(filePath,"utf8"):null},0,maxDepth),templateHtml=replaceLocations(templateHtml,contentBlocks);const rel=path.relative(rootDir,path.dirname(pageFilePath)),depth=rel?rel.split(path.sep).length:0,now=new Date,vars={pathToRoot:depth>0?"../".repeat(depth):"./",year:String(now.getFullYear()),date:now.toISOString().slice(0,10),datetime:now.toISOString(),timestamp:String(Date.now()),version:loadVersion(rootDir),env:process.env.NODE_ENV||"",...globals,...state,...pageAttrs};for(const[key,val]of Object.entries(vars))"function"==typeof val&&(vars[key]=val());return templateHtml=resolveIfs(templateHtml,vars),templateHtml=resolveForeach(templateHtml,vars),templateHtml=resolveVars(templateHtml,vars),templateHtml},walkPages=async dir=>{const entries=await readdir(dir,{withFileTypes:!0}),results=[];for(const entry of entries){const full=path.join(dir,entry.name);entry.isDirectory()?results.push(...await walkPages(full)):entry.name.endsWith(".page.html")&&results.push(full)}return results},renderDir=async(inputDir,outputDir,globals={},state={},maxDepth=10)=>{const[pages,globalContent]=await Promise.all([walkPages(inputDir),loadGlobalContent(inputDir)]);let count=0;for(const page of pages){const outRel=path.relative(inputDir,page).replace(/\.page\.html$/,".html"),outPath=path.join(outputDir,outRel);await mkdir(path.dirname(outPath),{recursive:!0});const html=await renderPage(page,inputDir,globals,state,maxDepth,globalContent);await writeFile(outPath,html,"utf8"),count++}return count};export{renderPage,renderDir};
@@ -1 +1 @@
1
- const extractAttrs=tagString=>{const attrs={},re=/(\w[\w-]*)=(?:"([^"]*)"|'([^']*)')/g;let match;for(;null!==(match=re.exec(tagString));)attrs[match[1]]=match[2]??match[3];return attrs},extractContentBlocks=xml=>{const blocks={},re=/<content(?:\s+([^>]*))?\s*>([\s\S]*?)<\/content>/g;let match;for(;null!==(match=re.exec(xml));){const attrs=extractAttrs(match[1]||""),name=attrs.location||"default",priority=parseInt(attrs.priority||"0",10);blocks[name]||(blocks[name]=[]),blocks[name].push({html:match[2],priority:priority})}return blocks},mergeContentBlocks=(...maps)=>{const merged={};for(const map of maps)for(const[name,entries]of Object.entries(map))merged[name]||(merged[name]=[]),merged[name].push(...entries);return merged},resolveLocation=entries=>entries?.length?[...entries].sort((a,b)=>b.priority-a.priority).map(e=>e.html).join(""):null,replaceLocations=(html,contentMap)=>html.replace(/<location(?:\s+name="([^"]*)")?>([\s\S]*?)<\/location>/g,(_,name,fallback)=>resolveLocation(contentMap[name||"default"])??fallback).replace(/<location(?:\s+name="([^"]*)")?\s*\/>/g,(_,name)=>resolveLocation(contentMap[name||"default"])??""),stripFragmentWrapper=xml=>{const match=xml.match(/^\s*<fragment\b[^>]*>([\s\S]*)<\/fragment>\s*$/);return match?match[1]:xml},resolvePath=(obj,dotPath)=>dotPath.split(".").reduce((cur,key)=>cur?.[key],obj),resolveVars=(html,vars)=>html.replace(/\{\{([^}]+)\}\}/g,(_,key)=>{const trimmed=key.trim(),val=resolvePath(vars,trimmed);return"function"==typeof val?val():val??""}),resolveIfs=(html,vars)=>{const re=/<if\s+condition="([^"]+)">([\s\S]*?)<\/if>/g;let prev,result=html;do{prev=result,result=result.replace(re,(_,condition,inner)=>evalCondition(condition,vars)?inner:"")}while(result!==prev);return result},resolveForeach=(html,vars)=>{const re=/<foreach\s+in="([^"]+)"\s+as="([^"]+)">([\s\S]*?)<\/foreach>/g;let prev,result=html;do{prev=result,result=result.replace(re,(_,inAttr,asAttr,inner)=>{const arr=resolvePath(vars,inAttr.trim());return Array.isArray(arr)?arr.map(item=>{const scopedVars={...vars,[asAttr]:item};return resolveVars(inner,scopedVars)}).join(""):""})}while(result!==prev);return result},resolveFragmentTags=(html,findFragmentFile,depth,maxDepth)=>{if(depth>maxDepth)throw new Error(`Fragment depth exceeded maximum of ${maxDepth}`);return html.replace(/<fragment\s+name="([^"]+)"(?:\s*\/>|>([\s\S]*?)<\/fragment>)/g,(_,name,fallback)=>{const content=findFragmentFile(name);if(null===content)return fallback??"";const stripped=stripFragmentWrapper(content);return resolveFragmentTags(stripped,findFragmentFile,depth+1,maxDepth)})},TOKEN_TYPES_NUMBER="NUMBER",TOKEN_TYPES_STRING="STRING",TOKEN_TYPES_BOOLEAN="BOOLEAN",TOKEN_TYPES_IDENTIFIER="IDENTIFIER",TOKEN_TYPES_OPERATOR="OPERATOR",TOKEN_TYPES_NOT="NOT",TOKEN_TYPES_LPAREN="LPAREN",TOKEN_TYPES_RPAREN="RPAREN",evalCondition=(expression,vars)=>!!((tokens,vars)=>{let pos=0;const peek=()=>tokens[pos],advance=()=>tokens[pos++],parsePrimary=()=>{const tok=peek();if(!tok)throw new Error("Unexpected end of expression");if(tok.type===TOKEN_TYPES_NOT)return advance(),!parsePrimary();if(tok.type===TOKEN_TYPES_LPAREN){advance();const val=parseOr();if(!peek()||peek().type!==TOKEN_TYPES_RPAREN)throw new Error("Missing closing parenthesis");return advance(),val}if(tok.type===TOKEN_TYPES_NUMBER||tok.type===TOKEN_TYPES_STRING||tok.type===TOKEN_TYPES_BOOLEAN)return advance(),tok.value;if(tok.type===TOKEN_TYPES_IDENTIFIER)return advance(),resolvePath(vars,tok.value);throw new Error(`Unexpected token: ${JSON.stringify(tok)}`)},parseComparison=()=>{let left=parsePrimary();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&["===","!==",">","<",">=","<="].includes(peek().value);){const op=advance().value,right=parsePrimary();switch(op){case"===":left=left===right;break;case"!==":left=left!==right;break;case">":left=left>right;break;case"<":left=left<right;break;case">=":left=left>=right;break;case"<=":left=left<=right}}return left},parseAnd=()=>{let left=parseComparison();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&"&&"===peek().value;){advance();const right=parseComparison();left=left&&right}return left},parseOr=()=>{let left=parseAnd();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&"||"===peek().value;){advance();const right=parseAnd();left=left||right}return left},result=parseOr();if(pos<tokens.length)throw new Error(`Unexpected token after expression: ${JSON.stringify(tokens[pos])}`);return result})((expr=>{const tokens=[];let i=0;for(;i<expr.length;){if(/\s/.test(expr[i])){i++;continue}if("("===expr[i]){tokens.push({type:TOKEN_TYPES_LPAREN}),i++;continue}if(")"===expr[i]){tokens.push({type:TOKEN_TYPES_RPAREN}),i++;continue}if("!"===expr[i]&&"="!==expr[i+1]){tokens.push({type:TOKEN_TYPES_NOT}),i++;continue}const opMatch=expr.slice(i).match(/^(===|!==|>=|<=|&&|\|\||>|<)/);if(opMatch){tokens.push({type:TOKEN_TYPES_OPERATOR,value:opMatch[1]}),i+=opMatch[1].length;continue}if('"'===expr[i]||"'"===expr[i]){const quote=expr[i];let str="";for(i++;i<expr.length&&expr[i]!==quote;)str+=expr[i],i++;if(i>=expr.length)throw new Error(`Unterminated string in condition: ${expr}`);i++,tokens.push({type:TOKEN_TYPES_STRING,value:str});continue}const numMatch=expr.slice(i).match(/^(\d+(\.\d+)?)/);if(numMatch){tokens.push({type:TOKEN_TYPES_NUMBER,value:Number(numMatch[1])}),i+=numMatch[1].length;continue}const idMatch=expr.slice(i).match(/^([a-zA-Z_$][\w$.]*)/);if(idMatch){const id=idMatch[1];"true"===id||"false"===id?tokens.push({type:TOKEN_TYPES_BOOLEAN,value:"true"===id}):tokens.push({type:TOKEN_TYPES_IDENTIFIER,value:id}),i+=id.length;continue}throw new Error(`Unexpected character '${expr[i]}' in condition: ${expr}`)}return tokens})(expression),vars);export{extractAttrs,extractContentBlocks,mergeContentBlocks,replaceLocations,stripFragmentWrapper,resolveVars,resolveIfs,resolveForeach,resolveFragmentTags,evalCondition,resolvePath};
1
+ const extractAttrs=tagString=>{const attrs={},re=/(\w[\w-]*)=(?:"([^"]*)"|'([^']*)')/g;let match;for(;null!==(match=re.exec(tagString));)attrs[match[1]]=match[2]??match[3];return attrs},extractContentBlocks=xml=>{const blocks={},re=/<content((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/content>/g;let match;for(;null!==(match=re.exec(xml));){const attrs=extractAttrs(match[1]||""),name=attrs.location||"default",priority=parseInt(attrs.priority||"0",10);blocks[name]||(blocks[name]=[]),blocks[name].push({html:match[2],priority:priority})}return blocks},mergeContentBlocks=(...maps)=>{const merged={};for(const map of maps)for(const[name,entries]of Object.entries(map))merged[name]||(merged[name]=[]),merged[name].push(...entries);return merged},replaceLocations=(html,contentMap)=>html.replace(/<location((?:[^>"']|"[^"]*"|'[^']*')*?)(?:\s*\/>|>([\s\S]*?)<\/location>)/g,(_,attrStr,fallback)=>{return entries=contentMap[extractAttrs(attrStr).name||"default"],(entries?.length?[...entries].sort((a,b)=>b.priority-a.priority).map(e=>e.html).join(""):null)??fallback??"";var entries}),stripFragmentWrapper=xml=>{const match=xml.match(/^\s*<fragment(?:[^>"']|"[^"]*"|'[^']*')*>([\s\S]*)<\/fragment>\s*$/);return match?match[1]:xml},resolvePath=(obj,dotPath)=>dotPath.split(".").reduce((cur,key)=>cur?.[key],obj),resolveVars=(html,vars)=>html.replace(/\{\{([^}]+)\}\}/g,(_,key)=>{const trimmed=key.trim(),val=resolvePath(vars,trimmed);return"function"==typeof val?val():val??""}),resolveIfs=(html,vars)=>{const re=/<if((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/if>/g;let prev,result=html;do{prev=result,result=result.replace(re,(_,attrStr,inner)=>evalCondition(extractAttrs(attrStr).condition||"",vars)?inner:"")}while(result!==prev);return result},resolveForeach=(html,vars)=>{const re=/<foreach((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/foreach>/g;let prev,result=html;do{prev=result,result=result.replace(re,(_,attrStr,inner)=>{const attrs=extractAttrs(attrStr),arr=resolvePath(vars,(attrs.in||"").trim());return Array.isArray(arr)?arr.map(item=>{const scopedVars={...vars,[attrs.as]:item};return resolveVars(inner,scopedVars)}).join(""):""})}while(result!==prev);return result},resolveFragmentTags=(html,findFragmentFile,depth,maxDepth)=>{if(depth>maxDepth)throw new Error(`Fragment depth exceeded maximum of ${maxDepth}`);return html.replace(/<fragment((?:[^>"']|"[^"]*"|'[^']*')*?)(?:\s*\/>|>([\s\S]*?)<\/fragment>)/g,(_,attrStr,fallback)=>{const name=extractAttrs(attrStr||"").name,content=findFragmentFile(name);if(null===content)return fallback??"";const stripped=stripFragmentWrapper(content);return resolveFragmentTags(stripped,findFragmentFile,depth+1,maxDepth)})},TOKEN_TYPES_NUMBER="NUMBER",TOKEN_TYPES_STRING="STRING",TOKEN_TYPES_BOOLEAN="BOOLEAN",TOKEN_TYPES_IDENTIFIER="IDENTIFIER",TOKEN_TYPES_OPERATOR="OPERATOR",TOKEN_TYPES_NOT="NOT",TOKEN_TYPES_LPAREN="LPAREN",TOKEN_TYPES_RPAREN="RPAREN",evalCondition=(expression,vars)=>!!((tokens,vars)=>{let pos=0;const peek=()=>tokens[pos],advance=()=>tokens[pos++],parsePrimary=()=>{const tok=peek();if(!tok)throw new Error("Unexpected end of expression");if(tok.type===TOKEN_TYPES_NOT)return advance(),!parsePrimary();if(tok.type===TOKEN_TYPES_LPAREN){advance();const val=parseOr();if(!peek()||peek().type!==TOKEN_TYPES_RPAREN)throw new Error("Missing closing parenthesis");return advance(),val}if(tok.type===TOKEN_TYPES_NUMBER||tok.type===TOKEN_TYPES_STRING||tok.type===TOKEN_TYPES_BOOLEAN)return advance(),tok.value;if(tok.type===TOKEN_TYPES_IDENTIFIER)return advance(),resolvePath(vars,tok.value);throw new Error(`Unexpected token: ${JSON.stringify(tok)}`)},parseComparison=()=>{let left=parsePrimary();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&["===","!==",">","<",">=","<="].includes(peek().value);){const op=advance().value,right=parsePrimary();switch(op){case"===":left=left===right;break;case"!==":left=left!==right;break;case">":left=left>right;break;case"<":left=left<right;break;case">=":left=left>=right;break;case"<=":left=left<=right}}return left},parseAnd=()=>{let left=parseComparison();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&"&&"===peek().value;){advance();const right=parseComparison();left=left&&right}return left},parseOr=()=>{let left=parseAnd();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&"||"===peek().value;){advance();const right=parseAnd();left=left||right}return left},result=parseOr();if(pos<tokens.length)throw new Error(`Unexpected token after expression: ${JSON.stringify(tokens[pos])}`);return result})((expr=>{const tokens=[];let i=0;for(;i<expr.length;){if(/\s/.test(expr[i])){i++;continue}if("("===expr[i]){tokens.push({type:TOKEN_TYPES_LPAREN}),i++;continue}if(")"===expr[i]){tokens.push({type:TOKEN_TYPES_RPAREN}),i++;continue}if("!"===expr[i]&&"="!==expr[i+1]){tokens.push({type:TOKEN_TYPES_NOT}),i++;continue}const opMatch=expr.slice(i).match(/^(===|!==|>=|<=|&&|\|\||>|<)/);if(opMatch){tokens.push({type:TOKEN_TYPES_OPERATOR,value:opMatch[1]}),i+=opMatch[1].length;continue}if('"'===expr[i]||"'"===expr[i]){const quote=expr[i];let str="";for(i++;i<expr.length&&expr[i]!==quote;)str+=expr[i],i++;if(i>=expr.length)throw new Error(`Unterminated string in condition: ${expr}`);i++,tokens.push({type:TOKEN_TYPES_STRING,value:str});continue}const numMatch=expr.slice(i).match(/^(\d+(\.\d+)?)/);if(numMatch){tokens.push({type:TOKEN_TYPES_NUMBER,value:Number(numMatch[1])}),i+=numMatch[1].length;continue}const idMatch=expr.slice(i).match(/^([a-zA-Z_$][\w$.]*)/);if(idMatch){const id=idMatch[1];"true"===id||"false"===id?tokens.push({type:TOKEN_TYPES_BOOLEAN,value:"true"===id}):tokens.push({type:TOKEN_TYPES_IDENTIFIER,value:id}),i+=id.length;continue}throw new Error(`Unexpected character '${expr[i]}' in condition: ${expr}`)}return tokens})(expression),vars);export{extractAttrs,extractContentBlocks,mergeContentBlocks,replaceLocations,stripFragmentWrapper,resolveVars,resolveIfs,resolveForeach,resolveFragmentTags,evalCondition,resolvePath};
package/docs/caching.html CHANGED
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -323,7 +323,7 @@
323
323
  <div style="height:25vh"></div>
324
324
  <script
325
325
  type="module"
326
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
326
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
327
327
  ></script>
328
328
 
329
329
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -172,7 +172,7 @@
172
172
  <div style="height:25vh"></div>
173
173
  <script
174
174
  type="module"
175
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
175
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
176
176
  ></script>
177
177
 
178
178
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -413,7 +413,7 @@
413
413
  <div style="height:25vh"></div>
414
414
  <script
415
415
  type="module"
416
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
416
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
417
417
  ></script>
418
418
 
419
419
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -293,7 +293,7 @@
293
293
  <div style="height:25vh"></div>
294
294
  <script
295
295
  type="module"
296
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
296
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
297
297
  ></script>
298
298
 
299
299
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -203,7 +203,7 @@ async function backupProject() {
203
203
  <div style="height:25vh"></div>
204
204
  <script
205
205
  type="module"
206
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
206
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
207
207
  ></script>
208
208
 
209
209
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -164,7 +164,7 @@
164
164
  <div style="height:25vh"></div>
165
165
  <script
166
166
  type="module"
167
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
167
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
168
168
  ></script>
169
169
 
170
170
  </body>
package/docs/index.html CHANGED
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -181,7 +181,7 @@
181
181
  <div style="height:25vh"></div>
182
182
  <script
183
183
  type="module"
184
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
184
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
185
185
  ></script>
186
186
 
187
187
  <script type="module">
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -234,7 +234,7 @@
234
234
  <div style="height:25vh"></div>
235
235
  <script
236
236
  type="module"
237
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
237
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
238
238
  ></script>
239
239
 
240
240
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -199,7 +199,7 @@
199
199
  <div style="height:25vh"></div>
200
200
  <script
201
201
  type="module"
202
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
202
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
203
203
  ></script>
204
204
 
205
205
  </body>
package/docs/routing.html CHANGED
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -174,7 +174,7 @@
174
174
  <div style="height:25vh"></div>
175
175
  <script
176
176
  type="module"
177
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
177
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
178
178
  ></script>
179
179
 
180
180
  </body>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -73,11 +73,11 @@
73
73
 
74
74
  </menu>
75
75
  </k-aside>
76
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
79
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
80
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
78
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
79
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
81
81
  <script>
82
82
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
83
83
  await window.customElements.whenDefined('k-aside');
@@ -102,28 +102,33 @@
102
102
 
103
103
  <p>Kempo Server includes a built-in XML-based templating system for building static sites with shared layouts, reusable fragments, and dynamic content. Pages can be pre-rendered at build time or server-side rendered on each request.</p>
104
104
 
105
- <nav class="b r mb p">
106
- <h4 class="mt0">On This Page</h4>
107
- <ul>
108
- <li><a href="#overview">Overview</a></li>
109
- <li><a href="#file-types">File Types</a></li>
110
- <li><a href="#templates">Templates</a></li>
111
- <li><a href="#pages">Pages</a>
112
- <ul>
113
- <li><a href="#frontmatter">Frontmatter</a></li>
114
- </ul>
115
- </li>
116
- <li><a href="#fragments">Fragments</a></li>
117
- <li><a href="#global-files">Global Files</a></li>
118
- <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
119
- <li><a href="#variables">Variables</a></li>
120
- <li><a href="#conditionals">Conditionals</a></li>
121
- <li><a href="#loops">Loops</a></li>
122
- <li><a href="#rendering">Rendering</a></li>
123
- <li><a href="#ssr">Server-Side Rendering</a></li>
124
- <li><a href="#configuration">Configuration</a></li>
125
- </ul>
126
- </nav>
105
+ <k-accordion persistent-id="templating-toc" class="mb">
106
+ <k-accordion-header for-panel="toc">Table of Contents</k-accordion-header>
107
+ <k-accordion-panel name="toc">
108
+ <ul class="m">
109
+ <li><a href="#overview">Overview</a></li>
110
+ <li><a href="#templates">Templates</a></li>
111
+ <li><a href="#pages">Pages</a>
112
+ <ul>
113
+ <li><a href="#frontmatter">Frontmatter</a></li>
114
+ </ul>
115
+ </li>
116
+ <li><a href="#fragments">Fragments</a></li>
117
+ <li><a href="#global-files">Global Files</a></li>
118
+ <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
119
+ <li><a href="#template-syntax">Template Syntax</a>
120
+ <ul>
121
+ <li><a href="#variables">Variables</a></li>
122
+ <li><a href="#conditionals">Conditionals</a></li>
123
+ <li><a href="#loops">Loops</a></li>
124
+ </ul>
125
+ </li>
126
+ <li><a href="#rendering">Rendering</a></li>
127
+ <li><a href="#ssr">Server-Side Rendering</a></li>
128
+ <li><a href="#configuration">Configuration</a></li>
129
+ </ul>
130
+ </k-accordion-panel>
131
+ </k-accordion>
127
132
 
128
133
  <h2 id="overview">Overview</h2>
129
134
  <p>The templating system uses four file types that work together:</p>
@@ -133,12 +138,23 @@
133
138
  <li><strong>Fragments</strong> (<code>*.fragment.html</code>) &mdash; Reusable HTML partials included in templates or other fragments</li>
134
139
  <li><strong>Globals</strong> (<code>*.global.html</code>) &mdash; Site-wide content blocks automatically injected into every page render</li>
135
140
  </ul>
136
- <p>All three file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration.</p>
137
-
138
- <h2 id="file-types">File Types</h2>
139
141
  <p>A typical project structure:</p>
140
142
  <pre><code class="hljs markdown">my-site/<br />├─ default.template.html # Shared layout<br />├─ nav.fragment.html # Reusable navigation<br />├─ footer.fragment.html # Reusable footer<br />├─ analytics.global.html # Site-wide scripts injected into every page<br />├─ index.page.html # Homepage → renders to index.html<br />├─ about.page.html # About → renders to about.html<br />├─ blog/<br />│ ├─ index.page.html # Blog index → blog/index.html<br />│ ├─ post-1.page.html # Blog post → blog/post-1.html<br /></code></pre>
141
- <p>Templates and fragments are resolved by walking up the directory tree from the page file to the root, so subdirectories can override them by providing their own versions.</p>
143
+ <p>All four file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration. Each type is located differently at startup:</p>
144
+ <div class="table-wrapper mb">
145
+ <table>
146
+ <thead>
147
+ <tr><th>File Type</th><th>How It Is Found</th></tr>
148
+ </thead>
149
+ <tbody>
150
+ <tr><td><code>*.page.html</code></td><td>Recursive scan of the entire root directory tree &mdash; pages in any subdirectory are discovered automatically.</td></tr>
151
+ <tr><td><code>*.global.html</code></td><td>Recursive scan of the entire root directory tree &mdash; globals in any subdirectory are merged together automatically.</td></tr>
152
+ <tr><td><code>*.template.html</code></td><td>Walks <strong>up</strong> the directory tree from the page file&rsquo;s directory toward the root. The nearest match wins. A subdirectory template overrides the root one for pages below it.</td></tr>
153
+ <tr><td><code>*.fragment.html</code></td><td>Same up-walk as templates &mdash; starts at the including file&rsquo;s directory and searches each ancestor up to the root. Nearest match wins.</td></tr>
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ <p>Because pages and globals use a downward recursive scan, you can freely organize them into subdirectories and they will still be found. Templates and fragments use an upward walk, so they must live in the same directory as the files that reference them, or in a parent directory. Placing a template or fragment in a sibling or cousin directory will not be found.</p>
142
158
 
143
159
  <h2 id="templates">Templates</h2>
144
160
  <p>A template defines the shared HTML structure for your pages. Use <code>&lt;location&gt;</code> tags to define named content slots that pages will fill.</p>
@@ -169,13 +185,15 @@
169
185
  description: Overview of the routing system
170
186
  --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Routing"</span>&gt;</span><br /> ...<br /><span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span></code></pre>
171
187
  <p>The comment is purely for the author &mdash; nothing before <code>&lt;page&gt;</code> is parsed or rendered.</p>
188
+
189
+ <h2 id="fragments">Fragments</h2>
172
190
  <p>Fragments are reusable HTML partials. Include them in templates or other fragments using the <code>&lt;fragment&gt;</code> tag.</p>
173
191
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- nav.fragment.html --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./"</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
174
192
  <p>Include it in a template:</p>
175
193
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Self-closing: renders empty if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span> /&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- With fallback content if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>Fallback Nav<span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
176
194
  <p>Fragments can include other fragments. The maximum nesting depth is controlled by <code>maxFragmentDepth</code> (default: 10).</p>
177
195
 
178
- <h3>File Resolution</h3>
196
+ <h3>Directory Overrides</h3>
179
197
  <p>When a page references a template or a template includes a fragment, the system searches for the file starting in the page's directory and walking up to the root. This means:</p>
180
198
  <ul>
181
199
  <li>Templates and fragments placed at the root apply to all pages</li>
@@ -215,10 +233,13 @@
215
233
  </table>
216
234
  </div>
217
235
 
218
- <h2 id="variables">Variables</h2>
236
+ <h2 id="template-syntax">Template Syntax</h2>
237
+ <p>Use these tags and expressions inside templates, fragments, and pages to produce dynamic output.</p>
238
+
239
+ <h3 id="variables">Variables</h3>
219
240
  <p>Use <code>&#123;&#123;variableName&#125;&#125;</code> syntax to insert dynamic values into templates and fragments.</p>
220
241
 
221
- <h3>Built-in Variables</h3>
242
+ <h4>Built-in Variables</h4>
222
243
  <div class="table-wrapper mb">
223
244
  <table>
224
245
  <thead>
@@ -239,24 +260,24 @@
239
260
  </table>
240
261
  </div>
241
262
 
242
- <h3>Page Attributes as Variables</h3>
263
+ <h4>Page Attributes as Variables</h4>
243
264
  <p>Any attribute on the <code>&lt;page&gt;</code> tag is available as a variable in the template:</p>
244
265
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- page file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"My Page"</span> <span class="hljs-attr">author</span>=<span class="hljs-string">"Dustin"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- template file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>&#123;&#123;title&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"author"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"&#123;&#123;author&#125;&#125;"</span> /&gt;</span></code></pre>
245
266
 
246
- <h3>Globals and State</h3>
267
+ <h4>Globals and State</h4>
247
268
  <p>Additional variables can be provided through the <code>globals</code> and <code>state</code> configuration objects. Globals and state are merged with page attributes, with page attributes taking priority.</p>
248
269
  <pre><code class="hljs javascript"><span class="hljs-comment">// .config.js</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: {<br /> <span class="hljs-attr">globals</span>: {<br /> <span class="hljs-attr">siteName</span>: <span class="hljs-string">'My Site'</span>,<br /> <span class="hljs-attr">copyright</span>: <span class="hljs-string">'© 2026 My Company'</span><br /> },<br /> <span class="hljs-attr">state</span>: {<br /> <span class="hljs-attr">buildId</span>: () =&gt; <span class="hljs-built_in">Date</span>.now().toString(<span class="hljs-number">36</span>)<br /> }<br /> }<br />};</code></pre>
249
270
  <p>Function values in globals or state are called at render time, allowing dynamic values.</p>
250
271
 
251
- <h3>Dot-Path Access</h3>
272
+ <h4>Dot-Path Access</h4>
252
273
  <p>Variables support dot notation for nested object access:</p>
253
274
  <pre><code class="hljs xml">&#123;&#123;site.name&#125;&#125;<br />&#123;&#123;author.email&#125;&#125;</code></pre>
254
275
 
255
- <h2 id="conditionals">Conditionals</h2>
276
+ <h3 id="conditionals">Conditionals</h3>
256
277
  <p>Use <code>&lt;if&gt;</code> blocks to conditionally include content based on variable values.</p>
257
278
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"showBanner"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"banner"</span>&gt;</span>Welcome!<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
258
279
 
259
- <h3>Supported Operators</h3>
280
+ <h4>Supported Operators</h4>
260
281
  <p>Conditions support a full expression syntax:</p>
261
282
  <div class="table-wrapper mb">
262
283
  <table>
@@ -278,10 +299,10 @@
278
299
  </table>
279
300
  </div>
280
301
 
281
- <h3>Examples</h3>
302
+ <h4>Examples</h4>
282
303
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Truthy check --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isLoggedIn"</span>&gt;</span>Welcome back!<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Negation --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"!isLoggedIn"</span>&gt;</span>Please log in.<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- String comparison --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"env === 'production'"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Compound conditions --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isAdmin &amp;&amp; hasPermission"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/admin"</span>&gt;</span>Admin Panel<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
283
304
 
284
- <h2 id="loops">Loops</h2>
305
+ <h3 id="loops">Loops</h3>
285
306
  <p>Use <code>&lt;foreach&gt;</code> blocks to iterate over arrays.</p>
286
307
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">foreach</span> <span class="hljs-attr">in</span>=<span class="hljs-string">"navLinks"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"link"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"&#123;&#123;link.url&#125;&#125;"</span>&gt;</span>&#123;&#123;link.label&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">foreach</span>&gt;</span></code></pre>
287
308
  <p>The <code>in</code> attribute references an array variable and <code>as</code> names the loop variable. The loop variable supports dot-path access for object items.</p>
@@ -342,8 +363,10 @@
342
363
  <div style="height:25vh"></div>
343
364
  <script
344
365
  type="module"
345
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
366
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
346
367
  ></script>
347
368
 
369
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Accordion.js" type="module"></script>
370
+
348
371
  </body>
349
372
  </html>
@@ -17,7 +17,7 @@
17
17
  <link rel="stylesheet" href="./theme.css" />
18
18
  <script>
19
19
  window.litDisableBundleWarning = true;
20
- window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
20
+ window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.3/icons/'] };
21
21
  </script>
22
22
  </head>
23
23
  <body>
@@ -29,7 +29,7 @@
29
29
  <div style="height:25vh"></div>
30
30
  <script
31
31
  type="module"
32
- src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
32
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
33
33
  ></script>
34
34
  <location name="scripts"></location>
35
35
  </body>
@@ -35,11 +35,11 @@
35
35
  <location name="links" />
36
36
  </menu>
37
37
  </k-aside>
38
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
39
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
40
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
41
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
42
- <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
38
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Aside.js" type="module"></script>
39
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Main.js" type="module"></script>
40
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Nav.js" type="module"></script>
41
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Icon.js" type="module"></script>
42
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/ThemeSwitcher.js" type="module"></script>
43
43
  <script>
44
44
  document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
45
45
  await window.customElements.whenDefined('k-aside');
@@ -2,28 +2,33 @@
2
2
  <content>
3
3
  <p>Kempo Server includes a built-in XML-based templating system for building static sites with shared layouts, reusable fragments, and dynamic content. Pages can be pre-rendered at build time or server-side rendered on each request.</p>
4
4
 
5
- <nav class="b r mb p">
6
- <h4 class="mt0">On This Page</h4>
7
- <ul>
8
- <li><a href="#overview">Overview</a></li>
9
- <li><a href="#file-types">File Types</a></li>
10
- <li><a href="#templates">Templates</a></li>
11
- <li><a href="#pages">Pages</a>
12
- <ul>
13
- <li><a href="#frontmatter">Frontmatter</a></li>
14
- </ul>
15
- </li>
16
- <li><a href="#fragments">Fragments</a></li>
17
- <li><a href="#global-files">Global Files</a></li>
18
- <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
19
- <li><a href="#variables">Variables</a></li>
20
- <li><a href="#conditionals">Conditionals</a></li>
21
- <li><a href="#loops">Loops</a></li>
22
- <li><a href="#rendering">Rendering</a></li>
23
- <li><a href="#ssr">Server-Side Rendering</a></li>
24
- <li><a href="#configuration">Configuration</a></li>
25
- </ul>
26
- </nav>
5
+ <k-accordion persistent-id="templating-toc" class="mb">
6
+ <k-accordion-header for-panel="toc">Table of Contents</k-accordion-header>
7
+ <k-accordion-panel name="toc">
8
+ <ul class="m">
9
+ <li><a href="#overview">Overview</a></li>
10
+ <li><a href="#templates">Templates</a></li>
11
+ <li><a href="#pages">Pages</a>
12
+ <ul>
13
+ <li><a href="#frontmatter">Frontmatter</a></li>
14
+ </ul>
15
+ </li>
16
+ <li><a href="#fragments">Fragments</a></li>
17
+ <li><a href="#global-files">Global Files</a></li>
18
+ <li><a href="#fragments-vs-globals">Fragments vs. Global Files</a></li>
19
+ <li><a href="#template-syntax">Template Syntax</a>
20
+ <ul>
21
+ <li><a href="#variables">Variables</a></li>
22
+ <li><a href="#conditionals">Conditionals</a></li>
23
+ <li><a href="#loops">Loops</a></li>
24
+ </ul>
25
+ </li>
26
+ <li><a href="#rendering">Rendering</a></li>
27
+ <li><a href="#ssr">Server-Side Rendering</a></li>
28
+ <li><a href="#configuration">Configuration</a></li>
29
+ </ul>
30
+ </k-accordion-panel>
31
+ </k-accordion>
27
32
 
28
33
  <h2 id="overview">Overview</h2>
29
34
  <p>The templating system uses four file types that work together:</p>
@@ -33,12 +38,23 @@
33
38
  <li><strong>Fragments</strong> (<code>*.fragment.html</code>) &mdash; Reusable HTML partials included in templates or other fragments</li>
34
39
  <li><strong>Globals</strong> (<code>*.global.html</code>) &mdash; Site-wide content blocks automatically injected into every page render</li>
35
40
  </ul>
36
- <p>All three file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration.</p>
37
-
38
- <h2 id="file-types">File Types</h2>
39
41
  <p>A typical project structure:</p>
40
42
  <pre><code class="hljs markdown">my-site/<br />├─ default.template.html # Shared layout<br />├─ nav.fragment.html # Reusable navigation<br />├─ footer.fragment.html # Reusable footer<br />├─ analytics.global.html # Site-wide scripts injected into every page<br />├─ index.page.html # Homepage → renders to index.html<br />├─ about.page.html # About → renders to about.html<br />├─ blog/<br />│ ├─ index.page.html # Blog index → blog/index.html<br />│ ├─ post-1.page.html # Blog post → blog/post-1.html<br /></code></pre>
41
- <p>Templates and fragments are resolved by walking up the directory tree from the page file to the root, so subdirectories can override them by providing their own versions.</p>
43
+ <p>All four file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration. Each type is located differently at startup:</p>
44
+ <div class="table-wrapper mb">
45
+ <table>
46
+ <thead>
47
+ <tr><th>File Type</th><th>How It Is Found</th></tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr><td><code>*.page.html</code></td><td>Recursive scan of the entire root directory tree &mdash; pages in any subdirectory are discovered automatically.</td></tr>
51
+ <tr><td><code>*.global.html</code></td><td>Recursive scan of the entire root directory tree &mdash; globals in any subdirectory are merged together automatically.</td></tr>
52
+ <tr><td><code>*.template.html</code></td><td>Walks <strong>up</strong> the directory tree from the page file&rsquo;s directory toward the root. The nearest match wins. A subdirectory template overrides the root one for pages below it.</td></tr>
53
+ <tr><td><code>*.fragment.html</code></td><td>Same up-walk as templates &mdash; starts at the including file&rsquo;s directory and searches each ancestor up to the root. Nearest match wins.</td></tr>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ <p>Because pages and globals use a downward recursive scan, you can freely organize them into subdirectories and they will still be found. Templates and fragments use an upward walk, so they must live in the same directory as the files that reference them, or in a parent directory. Placing a template or fragment in a sibling or cousin directory will not be found.</p>
42
58
 
43
59
  <h2 id="templates">Templates</h2>
44
60
  <p>A template defines the shared HTML structure for your pages. Use <code>&lt;location&gt;</code> tags to define named content slots that pages will fill.</p>
@@ -69,13 +85,15 @@
69
85
  description: Overview of the routing system
70
86
  --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Routing"</span>&gt;</span><br /> ...<br /><span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span></code></pre>
71
87
  <p>The comment is purely for the author &mdash; nothing before <code>&lt;page&gt;</code> is parsed or rendered.</p>
88
+
89
+ <h2 id="fragments">Fragments</h2>
72
90
  <p>Fragments are reusable HTML partials. Include them in templates or other fragments using the <code>&lt;fragment&gt;</code> tag.</p>
73
91
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- nav.fragment.html --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./"</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /> <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
74
92
  <p>Include it in a template:</p>
75
93
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Self-closing: renders empty if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span> /&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- With fallback content if fragment file not found --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>Fallback Nav<span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">fragment</span>&gt;</span></code></pre>
76
94
  <p>Fragments can include other fragments. The maximum nesting depth is controlled by <code>maxFragmentDepth</code> (default: 10).</p>
77
95
 
78
- <h3>File Resolution</h3>
96
+ <h3>Directory Overrides</h3>
79
97
  <p>When a page references a template or a template includes a fragment, the system searches for the file starting in the page's directory and walking up to the root. This means:</p>
80
98
  <ul>
81
99
  <li>Templates and fragments placed at the root apply to all pages</li>
@@ -115,10 +133,13 @@
115
133
  </table>
116
134
  </div>
117
135
 
118
- <h2 id="variables">Variables</h2>
136
+ <h2 id="template-syntax">Template Syntax</h2>
137
+ <p>Use these tags and expressions inside templates, fragments, and pages to produce dynamic output.</p>
138
+
139
+ <h3 id="variables">Variables</h3>
119
140
  <p>Use <code>&#123;&#123;variableName&#125;&#125;</code> syntax to insert dynamic values into templates and fragments.</p>
120
141
 
121
- <h3>Built-in Variables</h3>
142
+ <h4>Built-in Variables</h4>
122
143
  <div class="table-wrapper mb">
123
144
  <table>
124
145
  <thead>
@@ -139,24 +160,24 @@
139
160
  </table>
140
161
  </div>
141
162
 
142
- <h3>Page Attributes as Variables</h3>
163
+ <h4>Page Attributes as Variables</h4>
143
164
  <p>Any attribute on the <code>&lt;page&gt;</code> tag is available as a variable in the template:</p>
144
165
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- page file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"My Page"</span> <span class="hljs-attr">author</span>=<span class="hljs-string">"Dustin"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">page</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- template file --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>&#123;&#123;title&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"author"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"&#123;&#123;author&#125;&#125;"</span> /&gt;</span></code></pre>
145
166
 
146
- <h3>Globals and State</h3>
167
+ <h4>Globals and State</h4>
147
168
  <p>Additional variables can be provided through the <code>globals</code> and <code>state</code> configuration objects. Globals and state are merged with page attributes, with page attributes taking priority.</p>
148
169
  <pre><code class="hljs javascript"><span class="hljs-comment">// .config.js</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: {<br /> <span class="hljs-attr">globals</span>: {<br /> <span class="hljs-attr">siteName</span>: <span class="hljs-string">'My Site'</span>,<br /> <span class="hljs-attr">copyright</span>: <span class="hljs-string">'© 2026 My Company'</span><br /> },<br /> <span class="hljs-attr">state</span>: {<br /> <span class="hljs-attr">buildId</span>: () =&gt; <span class="hljs-built_in">Date</span>.now().toString(<span class="hljs-number">36</span>)<br /> }<br /> }<br />};</code></pre>
149
170
  <p>Function values in globals or state are called at render time, allowing dynamic values.</p>
150
171
 
151
- <h3>Dot-Path Access</h3>
172
+ <h4>Dot-Path Access</h4>
152
173
  <p>Variables support dot notation for nested object access:</p>
153
174
  <pre><code class="hljs xml">&#123;&#123;site.name&#125;&#125;<br />&#123;&#123;author.email&#125;&#125;</code></pre>
154
175
 
155
- <h2 id="conditionals">Conditionals</h2>
176
+ <h3 id="conditionals">Conditionals</h3>
156
177
  <p>Use <code>&lt;if&gt;</code> blocks to conditionally include content based on variable values.</p>
157
178
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"showBanner"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"banner"</span>&gt;</span>Welcome!<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
158
179
 
159
- <h3>Supported Operators</h3>
180
+ <h4>Supported Operators</h4>
160
181
  <p>Conditions support a full expression syntax:</p>
161
182
  <div class="table-wrapper mb">
162
183
  <table>
@@ -178,10 +199,10 @@
178
199
  </table>
179
200
  </div>
180
201
 
181
- <h3>Examples</h3>
202
+ <h4>Examples</h4>
182
203
  <pre><code class="hljs xml"><span class="hljs-comment">&lt;!-- Truthy check --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isLoggedIn"</span>&gt;</span>Welcome back!<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Negation --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"!isLoggedIn"</span>&gt;</span>Please log in.<span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- String comparison --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"env === 'production'"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span><br /><br /><span class="hljs-comment">&lt;!-- Compound conditions --&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isAdmin &amp;&amp; hasPermission"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/admin"</span>&gt;</span>Admin Panel<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">if</span>&gt;</span></code></pre>
183
204
 
184
- <h2 id="loops">Loops</h2>
205
+ <h3 id="loops">Loops</h3>
185
206
  <p>Use <code>&lt;foreach&gt;</code> blocks to iterate over arrays.</p>
186
207
  <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">foreach</span> <span class="hljs-attr">in</span>=<span class="hljs-string">"navLinks"</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"link"</span>&gt;</span><br /> <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"&#123;&#123;link.url&#125;&#125;"</span>&gt;</span>&#123;&#123;link.label&#125;&#125;<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br /><span class="hljs-tag">&lt;/<span class="hljs-name">foreach</span>&gt;</span></code></pre>
187
208
  <p>The <code>in</code> attribute references an array variable and <code>as</code> names the loop variable. The loop variable supports dot-path access for object items.</p>
@@ -238,4 +259,7 @@
238
259
  <pre><code class="hljs javascript"><span class="hljs-comment">// dev.config.js — live rendering, no rebuild needed</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: { <span class="hljs-attr">ssr</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">ssrPriority</span>: <span class="hljs-literal">true</span> }<br />};<br /><br /><span class="hljs-comment">// prod.config.js — static files only</span><br /><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {<br /> <span class="hljs-attr">templating</span>: { <span class="hljs-attr">ssr</span>: <span class="hljs-literal">false</span> }<br />};</code></pre>
239
260
  <pre><code class="hljs bash"><span class="hljs-comment"># Development</span><br />node dist/index.js -r ./docs/src -c dev.config.js<br /><br /><span class="hljs-comment"># Production (serve pre-rendered output)</span><br />node dist/index.js -r ./docs/dist</code></pre>
240
261
  </content>
262
+ <content location="scripts">
263
+ <script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/src/components/Accordion.js" type="module"></script>
264
+ </content>
241
265
  </page>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "3.0.5",
4
+ "version": "3.0.7",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "exports": {
7
7
  "./rescan": "./dist/rescan.js",
@@ -67,7 +67,7 @@ const loadGlobalContent = async rootDir => {
67
67
  */
68
68
  const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDepth = 10, preloadedGlobalContent = null) => {
69
69
  const pageContent = await readFile(pageFilePath, 'utf8');
70
- const pageTagMatch = pageContent.match(/^[\s\S]*?<page\s([^>]*)>/);
70
+ const pageTagMatch = pageContent.match(/^[\s\S]*?<page((?:[^>"']|"[^"]*"|'[^']*')*)>/);
71
71
  if(!pageTagMatch) throw new Error(`Invalid page file: missing <page> root element in ${pageFilePath}`);
72
72
  const pageAttrs = extractAttrs(pageTagMatch[1]);
73
73
  const templateName = pageAttrs.template || 'default';
@@ -16,7 +16,7 @@ const extractAttrs = tagString => {
16
16
  */
17
17
  const extractContentBlocks = xml => {
18
18
  const blocks = {};
19
- const re = /<content(?:\s+([^>]*))?\s*>([\s\S]*?)<\/content>/g;
19
+ const re = /<content((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/content>/g;
20
20
  let match;
21
21
  while((match = re.exec(xml)) !== null){
22
22
  const attrs = extractAttrs(match[1] || '');
@@ -51,19 +51,15 @@ const resolveLocation = (entries) => {
51
51
  };
52
52
 
53
53
  const replaceLocations = (html, contentMap) =>
54
- html
55
- .replace(/<location(?:\s+name="([^"]*)")?>([\s\S]*?)<\/location>/g, (_, name, fallback) =>
56
- resolveLocation(contentMap[name || 'default']) ?? fallback
57
- )
58
- .replace(/<location(?:\s+name="([^"]*)")?\s*\/>/g, (_, name) =>
59
- resolveLocation(contentMap[name || 'default']) ?? ''
60
- );
54
+ html.replace(/<location((?:[^>"']|"[^"]*"|'[^']*')*?)(?:\s*\/>|>([\s\S]*?)<\/location>)/g, (_, attrStr, fallback) =>
55
+ resolveLocation(contentMap[extractAttrs(attrStr).name || 'default']) ?? fallback ?? ''
56
+ );
61
57
 
62
58
  /*
63
59
  Fragment Wrapper Stripping
64
60
  */
65
61
  const stripFragmentWrapper = xml => {
66
- const match = xml.match(/^\s*<fragment\b[^>]*>([\s\S]*)<\/fragment>\s*$/);
62
+ const match = xml.match(/^\s*<fragment(?:[^>"']|"[^"]*"|'[^']*')*>([\s\S]*)<\/fragment>\s*$/);
67
63
  return match ? match[1] : xml;
68
64
  };
69
65
 
@@ -88,13 +84,13 @@ const resolveVars = (html, vars) =>
88
84
  If-Block Resolution
89
85
  */
90
86
  const resolveIfs = (html, vars) => {
91
- const re = /<if\s+condition="([^"]+)">([\s\S]*?)<\/if>/g;
87
+ const re = /<if((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/if>/g;
92
88
  let result = html;
93
89
  let prev;
94
90
  do {
95
91
  prev = result;
96
- result = result.replace(re, (_, condition, inner) =>
97
- evalCondition(condition, vars) ? inner : ''
92
+ result = result.replace(re, (_, attrStr, inner) =>
93
+ evalCondition(extractAttrs(attrStr).condition || '', vars) ? inner : ''
98
94
  );
99
95
  } while(result !== prev);
100
96
  return result;
@@ -104,16 +100,17 @@ const resolveIfs = (html, vars) => {
104
100
  Foreach-Block Resolution
105
101
  */
106
102
  const resolveForeach = (html, vars) => {
107
- const re = /<foreach\s+in="([^"]+)"\s+as="([^"]+)">([\s\S]*?)<\/foreach>/g;
103
+ const re = /<foreach((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/foreach>/g;
108
104
  let result = html;
109
105
  let prev;
110
106
  do {
111
107
  prev = result;
112
- result = result.replace(re, (_, inAttr, asAttr, inner) => {
113
- const arr = resolvePath(vars, inAttr.trim());
108
+ result = result.replace(re, (_, attrStr, inner) => {
109
+ const attrs = extractAttrs(attrStr);
110
+ const arr = resolvePath(vars, (attrs['in'] || '').trim());
114
111
  if(!Array.isArray(arr)) return '';
115
112
  return arr.map(item => {
116
- const scopedVars = {...vars, [asAttr]: item};
113
+ const scopedVars = {...vars, [attrs.as]: item};
117
114
  return resolveVars(inner, scopedVars);
118
115
  }).join('');
119
116
  });
@@ -126,8 +123,9 @@ const resolveForeach = (html, vars) => {
126
123
  */
127
124
  const resolveFragmentTags = (html, findFragmentFile, depth, maxDepth) => {
128
125
  if(depth > maxDepth) throw new Error(`Fragment depth exceeded maximum of ${maxDepth}`);
129
- const re = /<fragment\s+name="([^"]+)"(?:\s*\/>|>([\s\S]*?)<\/fragment>)/g;
130
- return html.replace(re, (_, name, fallback) => {
126
+ const re = /<fragment((?:[^>"']|"[^"]*"|'[^']*')*?)(?:\s*\/>|>([\s\S]*?)<\/fragment>)/g;
127
+ return html.replace(re, (_, attrStr, fallback) => {
128
+ const name = extractAttrs(attrStr || '').name;
131
129
  const content = findFragmentFile(name);
132
130
  if(content === null) return fallback ?? '';
133
131
  const stripped = stripFragmentWrapper(content);
@@ -273,5 +273,86 @@ export default {
273
273
  const result = replaceLocations('<location>fallback</location>', {});
274
274
  if(result !== 'fallback') return fail(`got: ${result}`);
275
275
  pass();
276
+ },
277
+ 'replaceLocations handles custom attributes on self-closing location': ({pass, fail}) => {
278
+ const result = replaceLocations('<location name="main" label="Main Content" />', {main: [{html: '<p>Hi</p>', priority: 0}]});
279
+ if(result !== '<p>Hi</p>') return fail(`got: ${result}`);
280
+ pass();
281
+ },
282
+ 'replaceLocations handles custom attributes on block location': ({pass, fail}) => {
283
+ const result = replaceLocations('<location name="sidebar" label="Sidebar" editable="true">fallback</location>', {sidebar: [{html: 'real', priority: 0}]});
284
+ if(result !== 'real') return fail(`got: ${result}`);
285
+ pass();
286
+ },
287
+ 'replaceLocations handles custom attributes with fallback': ({pass, fail}) => {
288
+ const result = replaceLocations('<location name="missing" label="Test">fallback</location>', {});
289
+ if(result !== 'fallback') return fail(`got: ${result}`);
290
+ pass();
291
+ },
292
+ 'replaceLocations handles custom attributes on nameless location': ({pass, fail}) => {
293
+ const result = replaceLocations('<location label="Default" />', {default: [{html: 'content', priority: 0}]});
294
+ if(result !== 'content') return fail(`got: ${result}`);
295
+ pass();
296
+ },
297
+ 'resolveFragmentTags handles custom attributes on fragment': ({pass, fail}) => {
298
+ const html = '<fragment name="nav" label="Navigation" />';
299
+ const finder = name => name === 'nav' ? '<nav>Link</nav>' : null;
300
+ const result = resolveFragmentTags(html, finder, 0, 10);
301
+ if(result !== '<nav>Link</nav>') return fail(`got: ${result}`);
302
+ pass();
303
+ },
304
+ 'resolveFragmentTags handles custom attributes with fallback': ({pass, fail}) => {
305
+ const html = '<fragment name="missing" label="Test">fallback</fragment>';
306
+ const finder = () => null;
307
+ const result = resolveFragmentTags(html, finder, 0, 10);
308
+ if(result !== 'fallback') return fail(`got: ${result}`);
309
+ pass();
310
+ },
311
+ 'resolveFragmentTags handles excessive whitespace': ({pass, fail}) => {
312
+ const html = '<fragment name="nav" />';
313
+ const finder = name => name === 'nav' ? '<nav>Link</nav>' : null;
314
+ const result = resolveFragmentTags(html, finder, 0, 10);
315
+ if(result !== '<nav>Link</nav>') return fail(`got: ${result}`);
316
+ pass();
317
+ },
318
+ 'replaceLocations handles excessive whitespace on self-closing': ({pass, fail}) => {
319
+ const result = replaceLocations('<location name="main" />', {main: [{html: 'hi', priority: 0}]});
320
+ if(result !== 'hi') return fail(`got: ${result}`);
321
+ pass();
322
+ },
323
+ 'replaceLocations handles excessive whitespace on block location': ({pass, fail}) => {
324
+ const result = replaceLocations('<location name="main" >fallback</location>', {main: [{html: 'hi', priority: 0}]});
325
+ if(result !== 'hi') return fail(`got: ${result}`);
326
+ pass();
327
+ },
328
+ 'resolveIfs handles extra attributes before condition': ({pass, fail}) => {
329
+ const result = resolveIfs('<if label="test" condition="show">visible</if>', {show: true});
330
+ if(result !== 'visible') return fail(`got: ${result}`);
331
+ pass();
332
+ },
333
+ 'resolveIfs handles extra attributes after condition': ({pass, fail}) => {
334
+ const result = resolveIfs('<if condition="show" label="test">visible</if>', {show: false});
335
+ if(result !== '') return fail(`got: ${result}`);
336
+ pass();
337
+ },
338
+ 'resolveForeach handles reversed attribute order': ({pass, fail}) => {
339
+ const result = resolveForeach('<foreach as="item" in="items">{{item}},</foreach>', {items: ['a', 'b']});
340
+ if(result !== 'a,b,') return fail(`got: ${result}`);
341
+ pass();
342
+ },
343
+ 'resolveForeach handles extra attributes': ({pass, fail}) => {
344
+ const result = resolveForeach('<foreach in="items" as="item" label="List">{{item}}</foreach>', {items: ['x', 'y']});
345
+ if(result !== 'xy') return fail(`got: ${result}`);
346
+ pass();
347
+ },
348
+ 'extractContentBlocks handles excessive whitespace': ({pass, fail}) => {
349
+ const blocks = extractContentBlocks('<content location="main" >Hello</content>');
350
+ if(!Array.isArray(blocks.main) || blocks.main[0].html !== 'Hello') return fail(`got: ${JSON.stringify(blocks.main)}`);
351
+ pass();
352
+ },
353
+ 'extractContentBlocks handles extra custom attributes': ({pass, fail}) => {
354
+ const blocks = extractContentBlocks('<content location="main" label="Main" editable="true">Hello</content>');
355
+ if(!Array.isArray(blocks.main) || blocks.main[0].html !== 'Hello') return fail(`got: ${JSON.stringify(blocks.main)}`);
356
+ pass();
276
357
  }
277
358
  };