kempo-server 3.0.2 → 3.0.4
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.
- package/CHANGELOG.md +43 -0
- package/dist/templating/index.js +1 -1
- package/dist/templating/parse.js +1 -1
- package/docs/caching.html +18 -13
- package/docs/cli-utils.html +16 -13
- package/docs/configuration.html +18 -13
- package/docs/examples.html +16 -13
- package/docs/fs-utils.html +16 -13
- package/docs/getting-started.html +16 -13
- package/docs/index.html +16 -13
- package/docs/middleware.html +16 -13
- package/docs/request-response.html +18 -13
- package/docs/routing.html +16 -13
- package/docs/templating.html +87 -31
- package/docs-src/advanced-links.global.html +10 -0
- package/docs-src/caching.page.html +2 -0
- package/docs-src/configuration.page.html +2 -0
- package/docs-src/getting-started-links.global.html +7 -0
- package/docs-src/index.page.html +3 -0
- package/docs-src/nav.fragment.html +1 -13
- package/docs-src/request-response.page.html +2 -0
- package/docs-src/templating.page.html +71 -18
- package/package.json +2 -2
- package/src/templating/index.js +37 -4
- package/src/templating/parse.js +28 -5
- package/tests/templating-parse.node-test.js +43 -9
- package/tests/templating-render.node-test.js +91 -0
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
<li><a href="#overview">Overview</a></li>
|
|
9
9
|
<li><a href="#file-types">File Types</a></li>
|
|
10
10
|
<li><a href="#templates">Templates</a></li>
|
|
11
|
-
<li><a href="#pages">Pages</a
|
|
11
|
+
<li><a href="#pages">Pages</a>
|
|
12
|
+
<ul>
|
|
13
|
+
<li><a href="#frontmatter">Frontmatter</a></li>
|
|
14
|
+
</ul>
|
|
15
|
+
</li>
|
|
12
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>
|
|
13
19
|
<li><a href="#variables">Variables</a></li>
|
|
14
20
|
<li><a href="#conditionals">Conditionals</a></li>
|
|
15
21
|
<li><a href="#loops">Loops</a></li>
|
|
@@ -20,22 +26,23 @@
|
|
|
20
26
|
</nav>
|
|
21
27
|
|
|
22
28
|
<h2 id="overview">Overview</h2>
|
|
23
|
-
<p>The templating system uses
|
|
29
|
+
<p>The templating system uses four file types that work together:</p>
|
|
24
30
|
<ul>
|
|
25
31
|
<li><strong>Templates</strong> (<code>*.template.html</code>) — Shared page layouts with named content slots</li>
|
|
26
32
|
<li><strong>Pages</strong> (<code>*.page.html</code>) — Individual pages that fill template slots with content</li>
|
|
27
33
|
<li><strong>Fragments</strong> (<code>*.fragment.html</code>) — Reusable HTML partials included in templates or other fragments</li>
|
|
34
|
+
<li><strong>Globals</strong> (<code>*.global.html</code>) — Site-wide content blocks automatically injected into every page render</li>
|
|
28
35
|
</ul>
|
|
29
36
|
<p>All three file types are blocked from being served directly by the default <code>disallowedRegex</code> configuration.</p>
|
|
30
37
|
|
|
31
38
|
<h2 id="file-types">File Types</h2>
|
|
32
39
|
<p>A typical project structure:</p>
|
|
33
|
-
<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 />├─ 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>
|
|
40
|
+
<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>
|
|
34
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>
|
|
35
42
|
|
|
36
43
|
<h2 id="templates">Templates</h2>
|
|
37
44
|
<p>A template defines the shared HTML structure for your pages. Use <code><location></code> tags to define named content slots that pages will fill.</p>
|
|
38
|
-
<pre><code class="hljs xml"><span class="hljs-comment"><!-- default.template.html --></span><br /><span class="hljs-meta"><!DOCTYPE html></span><br /><span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">head</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /></span><br /> <span class="hljs-tag"><<span class="hljs-name">title</span>></span
|
|
45
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- default.template.html --></span><br /><span class="hljs-meta"><!DOCTYPE html></span><br /><span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">head</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /></span><br /> <span class="hljs-tag"><<span class="hljs-name">title</span>></span>{{title}}<span class="hljs-tag"></<span class="hljs-name">title</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">head</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">body</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">fragment</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"nav"</span> /></span><br /> <span class="hljs-tag"><<span class="hljs-name">main</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>{{pageName}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">location</span> /></span><br /> <span class="hljs-tag"></<span class="hljs-name">main</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">location</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"scripts"</span>></span><br /> <span class="hljs-comment"><!-- default scripts if page doesn't provide any --></span><br /> <span class="hljs-tag"></<span class="hljs-name">location</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">body</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">html</span>></span></code></pre>
|
|
39
46
|
|
|
40
47
|
<h3>Location Tags</h3>
|
|
41
48
|
<p>Locations are named slots in a template that pages fill with content. The <code>name</code> attribute is optional — a <code><location></code> without a name defaults to <code>"default"</code>.</p>
|
|
@@ -46,15 +53,22 @@
|
|
|
46
53
|
</ul>
|
|
47
54
|
|
|
48
55
|
<h2 id="pages">Pages</h2>
|
|
49
|
-
<p>A page file wraps its content in a <code><page></code> root element and uses <code><content></code> blocks to fill the template's locations. The <code>location</code> attribute is optional — a <code><content></code> block without one targets the <code>"default"</code> location. Multiple <code><content></code> blocks targeting the same location are
|
|
56
|
+
<p>A page file wraps its content in a <code><page></code> root element and uses <code><content></code> blocks to fill the template's locations. The <code>location</code> attribute is optional — a <code><content></code> block without one targets the <code>"default"</code> location. Multiple <code><content></code> blocks targeting the same location are merged and sorted by <code>priority</code> (higher first, default <code>0</code>).</p>
|
|
50
57
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- about.page.html → renders to about.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">page</span> <span class="hljs-attr">pageName</span>=<span class="hljs-string">"About Us"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"About - My Site"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">content</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">p</span>></span>Welcome to our about page.<span class="hljs-tag"></<span class="hljs-name">p</span>></span><br /> <span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">page</span>></span></code></pre>
|
|
51
58
|
|
|
52
59
|
<h3>Page Attributes</h3>
|
|
53
|
-
<p>Attributes on the <code><page></code> tag become template variables. In the example above, <code
|
|
60
|
+
<p>Attributes on the <code><page></code> tag become template variables. In the example above, <code>{{pageName}}</code> resolves to <code>About Us</code> and <code>{{title}}</code> resolves to <code>About - My Site</code>.</p>
|
|
54
61
|
<p>The special <code>template</code> attribute selects which template to use. It defaults to <code>default</code>, which looks for <code>default.template.html</code>.</p>
|
|
55
62
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- Uses blog.template.html instead of default.template.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">page</span> <span class="hljs-attr">template</span>=<span class="hljs-string">"blog"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"My Post"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"main"</span>></span>...<span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">page</span>></span></code></pre>
|
|
56
63
|
|
|
57
|
-
<
|
|
64
|
+
<h3 id="frontmatter">Frontmatter</h3>
|
|
65
|
+
<p>Any content before the opening <code><page></code> tag is ignored by the renderer. This makes it a natural place for author metadata, description, dates, or any notes that belong with the file but shouldn't appear in the output — similar to frontmatter in other static site generators.</p>
|
|
66
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!--
|
|
67
|
+
author: Dustin Poissant
|
|
68
|
+
date: 2026-04-10
|
|
69
|
+
description: Overview of the routing system
|
|
70
|
+
--></span><br /><span class="hljs-tag"><<span class="hljs-name">page</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Routing"</span>></span><br /> ...<br /><span class="hljs-tag"></<span class="hljs-name">page</span>></span></code></pre>
|
|
71
|
+
<p>The comment is purely for the author — nothing before <code><page></code> is parsed or rendered.</p>
|
|
58
72
|
<p>Fragments are reusable HTML partials. Include them in templates or other fragments using the <code><fragment></code> tag.</p>
|
|
59
73
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- nav.fragment.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">fragment</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">nav</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./"</span>></span>Home<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>></span>About<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /> <span class="hljs-tag"></<span class="hljs-name">nav</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">fragment</span>></span></code></pre>
|
|
60
74
|
<p>Include it in a template:</p>
|
|
@@ -68,10 +82,44 @@
|
|
|
68
82
|
<li>A subdirectory can provide its own version to override the parent</li>
|
|
69
83
|
</ul>
|
|
70
84
|
|
|
85
|
+
<h2 id="global-files">Global Files</h2>
|
|
86
|
+
<p>Any file named <code>*.global.html</code> anywhere under the root directory is automatically loaded and its <code><content></code> blocks are injected into every page render. This is the right place to put site-wide scripts, meta tags, nav links, or anything else that should appear across all pages without touching every template or page file.</p>
|
|
87
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- analytics.global.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"scripts"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span> <span class="hljs-attr">defer</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span></code></pre>
|
|
88
|
+
<p>The template just needs a matching <code><location></code> tag — the global file fills it automatically on every page.</p>
|
|
89
|
+
|
|
90
|
+
<h3>Priority</h3>
|
|
91
|
+
<p>When multiple <code><content></code> blocks — from global files, multiple global files, or the page itself — target the same location, a <code>priority</code> attribute controls their output order. Higher numbers come first. The default is <code>0</code>.</p>
|
|
92
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- framework.global.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"scripts"</span> <span class="hljs-attr">priority</span>=<span class="hljs-string">"10"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"framework.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><br /><span class="hljs-comment"><!-- analytics.global.html — priority defaults to 0 --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"scripts"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span> <span class="hljs-attr">defer</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span></code></pre>
|
|
93
|
+
<p>Output: <code>framework.js</code> first (priority 10), then <code>analytics.js</code> (priority 0). The same <code>priority</code> attribute works on <code><content></code> blocks in page files too, so a page can insert content before or after global content in the same location.</p>
|
|
94
|
+
|
|
95
|
+
<h3>Locations Inside Page Content</h3>
|
|
96
|
+
<p>You can place <code><location></code> tags inside a page's <code><content></code> block. Global content fills those locations before the block is inserted into the template. This lets a page pull in a global snippet at a specific spot without knowing what the snippet contains.</p>
|
|
97
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- nav-links.global.html --></span><br /><span class="hljs-tag"><<span class="hljs-name">content</span> <span class="hljs-attr">location</span>=<span class="hljs-string">"links"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./about.html"</span>></span>About<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">content</span>></span><br /><br /><span class="hljs-comment"><!-- nav.fragment.html — location tag pulls in the global links --></span><br /><span class="hljs-tag"><<span class="hljs-name">fragment</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">nav</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">location</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"links"</span> /></span><br /> <span class="hljs-tag"></<span class="hljs-name">nav</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">fragment</span>></span></code></pre>
|
|
98
|
+
|
|
99
|
+
<h2 id="fragments-vs-globals">Fragments vs. Global Files</h2>
|
|
100
|
+
<p>Fragments and global files both let you share HTML across pages, but they work in opposite directions.</p>
|
|
101
|
+
<p><strong>Fragments are a pull.</strong> The template (or another fragment) explicitly asks for one thing by name: <code><fragment name="nav" /></code>. The fragment file is a single reusable block of markup. Only what is asked for gets included, and where it lands is decided entirely by the caller.</p>
|
|
102
|
+
<p><strong>Global files are a push.</strong> A <code>*.global.html</code> file declares content and targets a location, and that content is automatically injected into every page render without the template needing to know it exists. You can have many global files each contributing to the same location, and a page can add its own content to that same location alongside them. The final output is all of those contributions merged and ordered by <code>priority</code>.</p>
|
|
103
|
+
<div class="table-wrapper mb">
|
|
104
|
+
<table>
|
|
105
|
+
<thead>
|
|
106
|
+
<tr><th></th><th>Fragments</th><th>Global Files</th></tr>
|
|
107
|
+
</thead>
|
|
108
|
+
<tbody>
|
|
109
|
+
<tr><td>Direction</td><td>Pull — caller decides what to include</td><td>Push — file injects itself automatically</td></tr>
|
|
110
|
+
<tr><td>How many per location</td><td>One (the named file)</td><td>Many — all globals targeting the same location are merged</td></tr>
|
|
111
|
+
<tr><td>Page can contribute too?</td><td>No — the fragment replaces the tag entirely</td><td>Yes — page <code><content></code> blocks merge with global content</td></tr>
|
|
112
|
+
<tr><td>Requires template change?</td><td>Yes — caller must add <code><fragment name="..." /></code></td><td>No — just add a <code><location></code> and the globals fill it</td></tr>
|
|
113
|
+
<tr><td>Good for</td><td>Reusable structural blocks (nav, footer, sidebar)</td><td>Site-wide injections (scripts, meta tags, nav links)</td></tr>
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
71
118
|
<h2 id="variables">Variables</h2>
|
|
72
|
-
<p>Use <code
|
|
119
|
+
<p>Use <code>{{variableName}}</code> syntax to insert dynamic values into templates and fragments.</p>
|
|
73
120
|
|
|
74
121
|
<h3>Built-in Variables</h3>
|
|
122
|
+
<div class="table-wrapper mb">
|
|
75
123
|
<table>
|
|
76
124
|
<thead>
|
|
77
125
|
<tr>
|
|
@@ -80,19 +128,20 @@
|
|
|
80
128
|
</tr>
|
|
81
129
|
</thead>
|
|
82
130
|
<tbody>
|
|
83
|
-
<tr><td><code
|
|
84
|
-
<tr><td><code
|
|
85
|
-
<tr><td><code
|
|
86
|
-
<tr><td><code
|
|
87
|
-
<tr><td><code
|
|
88
|
-
<tr><td><code
|
|
89
|
-
<tr><td><code
|
|
131
|
+
<tr><td><code>{{pathToRoot}}</code></td><td>Relative path from the page to the root directory (e.g. <code>./</code>, <code>../</code>, <code>../../</code>)</td></tr>
|
|
132
|
+
<tr><td><code>{{year}}</code></td><td>Current four-digit year</td></tr>
|
|
133
|
+
<tr><td><code>{{date}}</code></td><td>Current date in ISO format (<code>YYYY-MM-DD</code>)</td></tr>
|
|
134
|
+
<tr><td><code>{{datetime}}</code></td><td>Full ISO 8601 datetime string</td></tr>
|
|
135
|
+
<tr><td><code>{{timestamp}}</code></td><td>Unix timestamp in milliseconds</td></tr>
|
|
136
|
+
<tr><td><code>{{version}}</code></td><td>Version from the root <code>package.json</code></td></tr>
|
|
137
|
+
<tr><td><code>{{env}}</code></td><td>Value of <code>NODE_ENV</code></td></tr>
|
|
90
138
|
</tbody>
|
|
91
139
|
</table>
|
|
140
|
+
</div>
|
|
92
141
|
|
|
93
142
|
<h3>Page Attributes as Variables</h3>
|
|
94
143
|
<p>Any attribute on the <code><page></code> tag is available as a variable in the template:</p>
|
|
95
|
-
<pre><code class="hljs xml"><span class="hljs-comment"><!-- page file --></span><br /><span class="hljs-tag"><<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>></span>...<span class="hljs-tag"></<span class="hljs-name">page</span>></span><br /><br /><span class="hljs-comment"><!-- template file --></span><br /><span class="hljs-tag"><<span class="hljs-name">title</span>></span
|
|
144
|
+
<pre><code class="hljs xml"><span class="hljs-comment"><!-- page file --></span><br /><span class="hljs-tag"><<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>></span>...<span class="hljs-tag"></<span class="hljs-name">page</span>></span><br /><br /><span class="hljs-comment"><!-- template file --></span><br /><span class="hljs-tag"><<span class="hljs-name">title</span>></span>{{title}}<span class="hljs-tag"></<span class="hljs-name">title</span>></span><br /><span class="hljs-tag"><<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">"{{author}}"</span> /></span></code></pre>
|
|
96
145
|
|
|
97
146
|
<h3>Globals and State</h3>
|
|
98
147
|
<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>
|
|
@@ -101,7 +150,7 @@
|
|
|
101
150
|
|
|
102
151
|
<h3>Dot-Path Access</h3>
|
|
103
152
|
<p>Variables support dot notation for nested object access:</p>
|
|
104
|
-
<pre><code class="hljs xml"
|
|
153
|
+
<pre><code class="hljs xml">{{site.name}}<br />{{author.email}}</code></pre>
|
|
105
154
|
|
|
106
155
|
<h2 id="conditionals">Conditionals</h2>
|
|
107
156
|
<p>Use <code><if></code> blocks to conditionally include content based on variable values.</p>
|
|
@@ -109,6 +158,7 @@
|
|
|
109
158
|
|
|
110
159
|
<h3>Supported Operators</h3>
|
|
111
160
|
<p>Conditions support a full expression syntax:</p>
|
|
161
|
+
<div class="table-wrapper mb">
|
|
112
162
|
<table>
|
|
113
163
|
<thead>
|
|
114
164
|
<tr>
|
|
@@ -126,13 +176,14 @@
|
|
|
126
176
|
<tr><td><code>( )</code></td><td>Grouping</td></tr>
|
|
127
177
|
</tbody>
|
|
128
178
|
</table>
|
|
179
|
+
</div>
|
|
129
180
|
|
|
130
181
|
<h3>Examples</h3>
|
|
131
182
|
<pre><code class="hljs xml"><span class="hljs-comment"><!-- Truthy check --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isLoggedIn"</span>></span>Welcome back!<span class="hljs-tag"></<span class="hljs-name">if</span>></span><br /><br /><span class="hljs-comment"><!-- Negation --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"!isLoggedIn"</span>></span>Please log in.<span class="hljs-tag"></<span class="hljs-name">if</span>></span><br /><br /><span class="hljs-comment"><!-- String comparison --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"env === 'production'"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"analytics.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">if</span>></span><br /><br /><span class="hljs-comment"><!-- Compound conditions --></span><br /><span class="hljs-tag"><<span class="hljs-name">if</span> <span class="hljs-attr">condition</span>=<span class="hljs-string">"isAdmin && hasPermission"</span>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/admin"</span>></span>Admin Panel<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">if</span>></span></code></pre>
|
|
132
183
|
|
|
133
184
|
<h2 id="loops">Loops</h2>
|
|
134
185
|
<p>Use <code><foreach></code> blocks to iterate over arrays.</p>
|
|
135
|
-
<pre><code class="hljs xml"><span class="hljs-tag"><<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>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"
|
|
186
|
+
<pre><code class="hljs xml"><span class="hljs-tag"><<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>></span><br /> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{link.url}}"</span>></span>{{link.label}}<span class="hljs-tag"></<span class="hljs-name">a</span>></span><br /><span class="hljs-tag"></<span class="hljs-name">foreach</span>></span></code></pre>
|
|
136
187
|
<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>
|
|
137
188
|
|
|
138
189
|
<p>Provide the array through globals or state:</p>
|
|
@@ -162,6 +213,7 @@
|
|
|
162
213
|
|
|
163
214
|
<h2 id="configuration">Configuration</h2>
|
|
164
215
|
<p>All templating options live under the <code>templating</code> key in your configuration file.</p>
|
|
216
|
+
<div class="table-wrapper mb">
|
|
165
217
|
<table>
|
|
166
218
|
<thead>
|
|
167
219
|
<tr>
|
|
@@ -179,6 +231,7 @@
|
|
|
179
231
|
<tr><td><code>maxFragmentDepth</code></td><td><code>10</code></td><td>Maximum fragment nesting depth</td></tr>
|
|
180
232
|
</tbody>
|
|
181
233
|
</table>
|
|
234
|
+
</div>
|
|
182
235
|
|
|
183
236
|
<h3>Development vs Production</h3>
|
|
184
237
|
<p>A common pattern is to use separate config files:</p>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.4",
|
|
5
5
|
"description": "A lightweight, zero-dependency, file based routing server.",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./rescan": "./dist/rescan.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "node scripts/build.js",
|
|
18
18
|
"docs": "node dist/index.js -r ./docs",
|
|
19
|
-
"
|
|
19
|
+
"dev": "node dist/index.js -r ./docs-src",
|
|
20
20
|
"spa": "node dist/index.js -r ./spa",
|
|
21
21
|
"test": "npx kempo-test",
|
|
22
22
|
"test:gui": "npx kempo-test --gui",
|
package/src/templating/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import {
|
|
4
4
|
extractAttrs,
|
|
5
5
|
extractContentBlocks,
|
|
6
|
+
mergeContentBlocks,
|
|
6
7
|
replaceLocations,
|
|
7
8
|
resolveVars,
|
|
8
9
|
resolveIfs,
|
|
@@ -38,10 +39,33 @@ const loadVersion = rootDir => {
|
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
/*
|
|
43
|
+
Walk Directory for *.global.html Files
|
|
44
|
+
*/
|
|
45
|
+
const walkGlobals = async dir => {
|
|
46
|
+
const entries = await readdir(dir, {withFileTypes: true});
|
|
47
|
+
const results = [];
|
|
48
|
+
for(const entry of entries){
|
|
49
|
+
const full = path.join(dir, entry.name);
|
|
50
|
+
if(entry.isDirectory()){
|
|
51
|
+
results.push(...await walkGlobals(full));
|
|
52
|
+
} else if(entry.name.endsWith('.global.html')){
|
|
53
|
+
results.push(full);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const loadGlobalContent = async rootDir => {
|
|
60
|
+
const files = await walkGlobals(rootDir);
|
|
61
|
+
const maps = await Promise.all(files.map(async f => extractContentBlocks(await readFile(f, 'utf8'))));
|
|
62
|
+
return mergeContentBlocks(...maps);
|
|
63
|
+
};
|
|
64
|
+
|
|
41
65
|
/*
|
|
42
66
|
Render a Single Page
|
|
43
67
|
*/
|
|
44
|
-
const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDepth = 10) => {
|
|
68
|
+
const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDepth = 10, preloadedGlobalContent = null) => {
|
|
45
69
|
const pageContent = await readFile(pageFilePath, 'utf8');
|
|
46
70
|
const pageTagMatch = pageContent.match(/^[\s\S]*?<page\s([^>]*)>/);
|
|
47
71
|
if(!pageTagMatch) throw new Error(`Invalid page file: missing <page> root element in ${pageFilePath}`);
|
|
@@ -49,11 +73,20 @@ const renderPage = async (pageFilePath, rootDir, globals = {}, state = {}, maxDe
|
|
|
49
73
|
const templateName = pageAttrs.template || 'default';
|
|
50
74
|
delete pageAttrs.template;
|
|
51
75
|
|
|
52
|
-
const contentBlocks = extractContentBlocks(pageContent);
|
|
53
76
|
const pageDir = path.dirname(pageFilePath);
|
|
54
77
|
const templateFile = findFileUpSync(`${templateName}.template.html`, pageDir, rootDir);
|
|
55
78
|
if(!templateFile) throw new Error(`Template not found: ${templateName}.template.html (searched from ${pageDir} to ${rootDir})`);
|
|
56
79
|
|
|
80
|
+
const globalContent = preloadedGlobalContent ?? await loadGlobalContent(rootDir);
|
|
81
|
+
const rawPageBlocks = extractContentBlocks(pageContent);
|
|
82
|
+
|
|
83
|
+
// Allow <location> tags inside page content blocks to be filled by global content
|
|
84
|
+
const pageBlocks = {};
|
|
85
|
+
for(const [name, entries] of Object.entries(rawPageBlocks)){
|
|
86
|
+
pageBlocks[name] = entries.map(e => ({...e, html: replaceLocations(e.html, globalContent)}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const contentBlocks = mergeContentBlocks(pageBlocks, globalContent);
|
|
57
90
|
let templateHtml = readFileSync(templateFile, 'utf8');
|
|
58
91
|
|
|
59
92
|
const findFragmentFile = name => {
|
|
@@ -115,14 +148,14 @@ const walkPages = async dir => {
|
|
|
115
148
|
Render All Pages in a Directory
|
|
116
149
|
*/
|
|
117
150
|
const renderDir = async (inputDir, outputDir, globals = {}, state = {}, maxDepth = 10) => {
|
|
118
|
-
const pages = await walkPages(inputDir);
|
|
151
|
+
const [pages, globalContent] = await Promise.all([walkPages(inputDir), loadGlobalContent(inputDir)]);
|
|
119
152
|
let count = 0;
|
|
120
153
|
for(const page of pages){
|
|
121
154
|
const rel = path.relative(inputDir, page);
|
|
122
155
|
const outRel = rel.replace(/\.page\.html$/, '.html');
|
|
123
156
|
const outPath = path.join(outputDir, outRel);
|
|
124
157
|
await mkdir(path.dirname(outPath), {recursive: true});
|
|
125
|
-
const html = await renderPage(page, inputDir, globals, state, maxDepth);
|
|
158
|
+
const html = await renderPage(page, inputDir, globals, state, maxDepth, globalContent);
|
|
126
159
|
await writeFile(outPath, html, 'utf8');
|
|
127
160
|
count++;
|
|
128
161
|
}
|
package/src/templating/parse.js
CHANGED
|
@@ -16,25 +16,47 @@ const extractAttrs = tagString => {
|
|
|
16
16
|
*/
|
|
17
17
|
const extractContentBlocks = xml => {
|
|
18
18
|
const blocks = {};
|
|
19
|
-
const re = /<content(?:\s+
|
|
19
|
+
const re = /<content(?:\s+([^>]*))?\s*>([\s\S]*?)<\/content>/g;
|
|
20
20
|
let match;
|
|
21
21
|
while((match = re.exec(xml)) !== null){
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const attrs = extractAttrs(match[1] || '');
|
|
23
|
+
const name = attrs.location || 'default';
|
|
24
|
+
const priority = parseInt(attrs.priority || '0', 10);
|
|
25
|
+
if(!blocks[name]) blocks[name] = [];
|
|
26
|
+
blocks[name].push({html: match[2], priority});
|
|
24
27
|
}
|
|
25
28
|
return blocks;
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
/*
|
|
32
|
+
Content Block Merging
|
|
33
|
+
*/
|
|
34
|
+
const mergeContentBlocks = (...maps) => {
|
|
35
|
+
const merged = {};
|
|
36
|
+
for(const map of maps){
|
|
37
|
+
for(const [name, entries] of Object.entries(map)){
|
|
38
|
+
if(!merged[name]) merged[name] = [];
|
|
39
|
+
merged[name].push(...entries);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return merged;
|
|
43
|
+
};
|
|
44
|
+
|
|
28
45
|
/*
|
|
29
46
|
Location Replacement
|
|
30
47
|
*/
|
|
48
|
+
const resolveLocation = (entries) => {
|
|
49
|
+
if(!entries?.length) return null;
|
|
50
|
+
return [...entries].sort((a, b) => b.priority - a.priority).map(e => e.html).join('');
|
|
51
|
+
};
|
|
52
|
+
|
|
31
53
|
const replaceLocations = (html, contentMap) =>
|
|
32
54
|
html
|
|
33
55
|
.replace(/<location(?:\s+name="([^"]*)")?>([\s\S]*?)<\/location>/g, (_, name, fallback) =>
|
|
34
|
-
contentMap[name || 'default'] ?? fallback
|
|
56
|
+
resolveLocation(contentMap[name || 'default']) ?? fallback
|
|
35
57
|
)
|
|
36
58
|
.replace(/<location(?:\s+name="([^"]*)")?\s*\/>/g, (_, name) =>
|
|
37
|
-
contentMap[name || 'default'] ?? ''
|
|
59
|
+
resolveLocation(contentMap[name || 'default']) ?? ''
|
|
38
60
|
);
|
|
39
61
|
|
|
40
62
|
/*
|
|
@@ -274,6 +296,7 @@ const evalCondition = (expression, vars) => {
|
|
|
274
296
|
export {
|
|
275
297
|
extractAttrs,
|
|
276
298
|
extractContentBlocks,
|
|
299
|
+
mergeContentBlocks,
|
|
277
300
|
replaceLocations,
|
|
278
301
|
stripFragmentWrapper,
|
|
279
302
|
resolveVars,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
extractAttrs,
|
|
3
3
|
extractContentBlocks,
|
|
4
|
+
mergeContentBlocks,
|
|
4
5
|
replaceLocations,
|
|
5
6
|
stripFragmentWrapper,
|
|
6
7
|
resolveVars,
|
|
@@ -26,13 +27,33 @@ export default {
|
|
|
26
27
|
'extractContentBlocks extracts named blocks': ({pass, fail}) => {
|
|
27
28
|
const xml = '<content location="main">Hello</content><content location="sidebar">World</content>';
|
|
28
29
|
const blocks = extractContentBlocks(xml);
|
|
29
|
-
if(blocks.main !== 'Hello') return fail('main wrong');
|
|
30
|
-
if(blocks.sidebar !== 'World') return fail('sidebar wrong');
|
|
30
|
+
if(!Array.isArray(blocks.main) || blocks.main[0].html !== 'Hello') return fail('main wrong');
|
|
31
|
+
if(!Array.isArray(blocks.sidebar) || blocks.sidebar[0].html !== 'World') return fail('sidebar wrong');
|
|
32
|
+
pass();
|
|
33
|
+
},
|
|
34
|
+
'extractContentBlocks captures priority': ({pass, fail}) => {
|
|
35
|
+
const xml = '<content location="main" priority="5">hi</content>';
|
|
36
|
+
const blocks = extractContentBlocks(xml);
|
|
37
|
+
if(blocks.main[0].priority !== 5) return fail(`priority wrong: ${blocks.main[0].priority}`);
|
|
38
|
+
pass();
|
|
39
|
+
},
|
|
40
|
+
'extractContentBlocks defaults priority to 0': ({pass, fail}) => {
|
|
41
|
+
const xml = '<content location="main">hi</content>';
|
|
42
|
+
const blocks = extractContentBlocks(xml);
|
|
43
|
+
if(blocks.main[0].priority !== 0) return fail(`priority wrong: ${blocks.main[0].priority}`);
|
|
44
|
+
pass();
|
|
45
|
+
},
|
|
46
|
+
'mergeContentBlocks combines maps': ({pass, fail}) => {
|
|
47
|
+
const a = {main: [{html: 'A', priority: 0}]};
|
|
48
|
+
const b = {main: [{html: 'B', priority: 0}], sidebar: [{html: 'S', priority: 0}]};
|
|
49
|
+
const merged = mergeContentBlocks(a, b);
|
|
50
|
+
if(merged.main.length !== 2) return fail(`main length wrong: ${merged.main.length}`);
|
|
51
|
+
if(!merged.sidebar) return fail('sidebar missing');
|
|
31
52
|
pass();
|
|
32
53
|
},
|
|
33
54
|
'replaceLocations fills named locations': ({pass, fail}) => {
|
|
34
55
|
const html = '<location name="main" />';
|
|
35
|
-
const result = replaceLocations(html, {main: '<p>Hi</p>'});
|
|
56
|
+
const result = replaceLocations(html, {main: [{html: '<p>Hi</p>', priority: 0}]});
|
|
36
57
|
if(result !== '<p>Hi</p>') return fail(`got: ${result}`);
|
|
37
58
|
pass();
|
|
38
59
|
},
|
|
@@ -44,10 +65,21 @@ export default {
|
|
|
44
65
|
},
|
|
45
66
|
'replaceLocations uses content over fallback': ({pass, fail}) => {
|
|
46
67
|
const html = '<location name="main">fallback</location>';
|
|
47
|
-
const result = replaceLocations(html, {main: 'real'});
|
|
68
|
+
const result = replaceLocations(html, {main: [{html: 'real', priority: 0}]});
|
|
48
69
|
if(result !== 'real') return fail(`got: ${result}`);
|
|
49
70
|
pass();
|
|
50
71
|
},
|
|
72
|
+
'replaceLocations orders by priority descending': ({pass, fail}) => {
|
|
73
|
+
const html = '<location name="scripts" />';
|
|
74
|
+
const entries = [
|
|
75
|
+
{html: 'low', priority: 1},
|
|
76
|
+
{html: 'high', priority: 10},
|
|
77
|
+
{html: 'mid', priority: 5}
|
|
78
|
+
];
|
|
79
|
+
const result = replaceLocations(html, {scripts: entries});
|
|
80
|
+
if(result !== 'highmidlow') return fail(`got: ${result}`);
|
|
81
|
+
pass();
|
|
82
|
+
},
|
|
51
83
|
'stripFragmentWrapper removes wrapping fragment tag': ({pass, fail}) => {
|
|
52
84
|
const result = stripFragmentWrapper('<fragment name="nav"><nav>Hi</nav></fragment>');
|
|
53
85
|
if(result !== '<nav>Hi</nav>') return fail(`got: ${result}`);
|
|
@@ -210,28 +242,30 @@ export default {
|
|
|
210
242
|
},
|
|
211
243
|
'extractContentBlocks defaults location to default': ({pass, fail}) => {
|
|
212
244
|
const blocks = extractContentBlocks('<content>Hello</content>');
|
|
213
|
-
if(blocks.default !== 'Hello') return fail(`got: ${blocks.default}`);
|
|
245
|
+
if(!Array.isArray(blocks.default) || blocks.default[0].html !== 'Hello') return fail(`got: ${JSON.stringify(blocks.default)}`);
|
|
214
246
|
pass();
|
|
215
247
|
},
|
|
216
248
|
'extractContentBlocks concatenates multiple contents to same location': ({pass, fail}) => {
|
|
217
249
|
const xml = '<content location="main">A</content><content location="main">B</content>';
|
|
218
250
|
const blocks = extractContentBlocks(xml);
|
|
219
|
-
if(blocks.main !==
|
|
251
|
+
if(blocks.main.length !== 2) return fail(`expected 2 entries, got: ${JSON.stringify(blocks.main)}`);
|
|
252
|
+
if(blocks.main[0].html !== 'A' || blocks.main[1].html !== 'B') return fail(`got: ${JSON.stringify(blocks.main)}`);
|
|
220
253
|
pass();
|
|
221
254
|
},
|
|
222
255
|
'extractContentBlocks concatenates default contents': ({pass, fail}) => {
|
|
223
256
|
const xml = '<content>A</content><content>B</content>';
|
|
224
257
|
const blocks = extractContentBlocks(xml);
|
|
225
|
-
if(blocks.default !==
|
|
258
|
+
if(blocks.default.length !== 2) return fail(`expected 2 entries, got: ${JSON.stringify(blocks.default)}`);
|
|
259
|
+
if(blocks.default[0].html !== 'A' || blocks.default[1].html !== 'B') return fail(`got: ${JSON.stringify(blocks.default)}`);
|
|
226
260
|
pass();
|
|
227
261
|
},
|
|
228
262
|
'replaceLocations defaults nameless location to default': ({pass, fail}) => {
|
|
229
|
-
const result = replaceLocations('<location />', {default: 'Hi'});
|
|
263
|
+
const result = replaceLocations('<location />', {default: [{html: 'Hi', priority: 0}]});
|
|
230
264
|
if(result !== 'Hi') return fail(`got: ${result}`);
|
|
231
265
|
pass();
|
|
232
266
|
},
|
|
233
267
|
'replaceLocations defaults nameless block location to default': ({pass, fail}) => {
|
|
234
|
-
const result = replaceLocations('<location>fallback</location>', {default: 'Hi'});
|
|
268
|
+
const result = replaceLocations('<location>fallback</location>', {default: [{html: 'Hi', priority: 0}]});
|
|
235
269
|
if(result !== 'Hi') return fail(`got: ${result}`);
|
|
236
270
|
pass();
|
|
237
271
|
},
|
|
@@ -184,5 +184,96 @@ export default {
|
|
|
184
184
|
if(!html.includes(String(new Date().getFullYear()))) return fail(`year missing: ${html}`);
|
|
185
185
|
pass();
|
|
186
186
|
});
|
|
187
|
+
},
|
|
188
|
+
'renderPage injects global content into template location': async ({pass, fail}) => {
|
|
189
|
+
await withTempDir(async dir => {
|
|
190
|
+
await setupFiles(dir, {
|
|
191
|
+
'default.template.html': '<head><location name="head" /></head><body><location name="main" /></body>',
|
|
192
|
+
'site.global.html': '<content location="head"><meta charset="utf-8"></content>',
|
|
193
|
+
'index.page.html': '<page template="default"><content location="main">hello</content></page>'
|
|
194
|
+
});
|
|
195
|
+
const html = await renderPage(path.join(dir, 'index.page.html'), dir);
|
|
196
|
+
if(!html.includes('<meta charset="utf-8">')) return fail(`global head missing: ${html}`);
|
|
197
|
+
if(!html.includes('hello')) return fail(`page content missing: ${html}`);
|
|
198
|
+
pass();
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
'renderPage merges page and global content into same location': async ({pass, fail}) => {
|
|
202
|
+
await withTempDir(async dir => {
|
|
203
|
+
await setupFiles(dir, {
|
|
204
|
+
'default.template.html': '<location name="scripts" />',
|
|
205
|
+
'site.global.html': '<content location="scripts"><script src="analytics.js"></script></content>',
|
|
206
|
+
'index.page.html': '<page template="default"><content location="scripts"><script src="page.js"></script></content></page>'
|
|
207
|
+
});
|
|
208
|
+
const html = await renderPage(path.join(dir, 'index.page.html'), dir);
|
|
209
|
+
if(!html.includes('analytics.js')) return fail(`global script missing: ${html}`);
|
|
210
|
+
if(!html.includes('page.js')) return fail(`page script missing: ${html}`);
|
|
211
|
+
pass();
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
'renderPage respects priority ordering — higher number first': async ({pass, fail}) => {
|
|
215
|
+
await withTempDir(async dir => {
|
|
216
|
+
await setupFiles(dir, {
|
|
217
|
+
'default.template.html': '<location name="scripts" />',
|
|
218
|
+
'site.global.html': '<content location="scripts" priority="10">FIRST</content><content location="scripts" priority="1">LAST</content>',
|
|
219
|
+
'index.page.html': '<page template="default"></page>'
|
|
220
|
+
});
|
|
221
|
+
const html = await renderPage(path.join(dir, 'index.page.html'), dir);
|
|
222
|
+
if(html.indexOf('FIRST') > html.indexOf('LAST')) return fail(`wrong order: ${html}`);
|
|
223
|
+
pass();
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
'renderPage page content priority beats global at same location': async ({pass, fail}) => {
|
|
227
|
+
await withTempDir(async dir => {
|
|
228
|
+
await setupFiles(dir, {
|
|
229
|
+
'default.template.html': '<location name="scripts" />',
|
|
230
|
+
'site.global.html': '<content location="scripts" priority="0">global</content>',
|
|
231
|
+
'index.page.html': '<page template="default"><content location="scripts" priority="5">page</content></page>'
|
|
232
|
+
});
|
|
233
|
+
const html = await renderPage(path.join(dir, 'index.page.html'), dir);
|
|
234
|
+
if(html.indexOf('page') > html.indexOf('global')) return fail(`page should come before global: ${html}`);
|
|
235
|
+
pass();
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
'renderPage fills locations inside page content from globals': async ({pass, fail}) => {
|
|
239
|
+
await withTempDir(async dir => {
|
|
240
|
+
await setupFiles(dir, {
|
|
241
|
+
'default.template.html': '<location name="main" />',
|
|
242
|
+
'site.global.html': '<content location="badge"><span class="badge">NEW</span></content>',
|
|
243
|
+
'index.page.html': '<page template="default"><content location="main"><h1>Title</h1><location name="badge" /></content></page>'
|
|
244
|
+
});
|
|
245
|
+
const html = await renderPage(path.join(dir, 'index.page.html'), dir);
|
|
246
|
+
if(!html.includes('<span class="badge">NEW</span>')) return fail(`badge missing: ${html}`);
|
|
247
|
+
pass();
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
'renderPage loads globals from subdirectories': async ({pass, fail}) => {
|
|
251
|
+
await withTempDir(async dir => {
|
|
252
|
+
await setupFiles(dir, {
|
|
253
|
+
'default.template.html': '<location name="head" /><location name="main" />',
|
|
254
|
+
'sub/site.global.html': '<content location="head">subglobal</content>',
|
|
255
|
+
'index.page.html': '<page template="default"><content location="main">x</content></page>'
|
|
256
|
+
});
|
|
257
|
+
const html = await renderPage(path.join(dir, 'index.page.html'), dir);
|
|
258
|
+
if(!html.includes('subglobal')) return fail(`subdir global missing: ${html}`);
|
|
259
|
+
pass();
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
'renderDir applies global content to all pages': async ({pass, fail}) => {
|
|
263
|
+
await withTempDir(async dir => {
|
|
264
|
+
await setupFiles(dir, {
|
|
265
|
+
'default.template.html': '<location name="head" /><location name="main" />',
|
|
266
|
+
'site.global.html': '<content location="head"><meta name="global"></content>',
|
|
267
|
+
'index.page.html': '<page template="default"><content location="main">home</content></page>',
|
|
268
|
+
'about.page.html': '<page template="default"><content location="main">about</content></page>'
|
|
269
|
+
});
|
|
270
|
+
const outDir = path.join(dir, 'out');
|
|
271
|
+
await renderDir(dir, outDir);
|
|
272
|
+
const home = await readFile(path.join(outDir, 'index.html'), 'utf8');
|
|
273
|
+
const about = await readFile(path.join(outDir, 'about.html'), 'utf8');
|
|
274
|
+
if(!home.includes('<meta name="global">')) return fail(`home missing global: ${home}`);
|
|
275
|
+
if(!about.includes('<meta name="global">')) return fail(`about missing global: ${about}`);
|
|
276
|
+
pass();
|
|
277
|
+
});
|
|
187
278
|
}
|
|
188
279
|
};
|