mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 10:11:55 -04:00
test: Converted DIAGNOSTIC_TODOS.yaml into a test suite.
This commit is contained in:
+177
-80
@@ -1,4 +1,8 @@
|
|||||||
|
# This file contains tests for all known mustache syntax errors and their
|
||||||
|
# desired error messages. Each one is converted into an Odin test.
|
||||||
name: Missing 2nd closing brace
|
name: Missing 2nd closing brace
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<menu>
|
<menu>
|
||||||
<a class="goto-top opacity-0" href="#">{{> icons/chevron_up}</a>
|
<a class="goto-top opacity-0" href="#">{{> icons/chevron_up}</a>
|
||||||
@@ -7,20 +11,20 @@ input: |
|
|||||||
{{/params.social}}
|
{{/params.social}}
|
||||||
</menu>
|
</menu>
|
||||||
<p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
<p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
||||||
|
expected: |
|
||||||
expected: Somthing that points to the opening and lack of close.
|
Missing closing brace
|
||||||
actual: |
|
--> <test>:1:1
|
||||||
[ERROR] --- [383:load_partials()] unexpected {{/params.social}}
|
1 | <menu>
|
||||||
--> /home/spencer/github.com/sbrow.github.io/layouts/partials/footer.html:6:5
|
2 | <a class="goto-top opacity-0" href="#">{{> icons/chevron_up}</a>
|
||||||
|
| ^^ opened ^ never closed
|
||||||
|
3 | {{#params.social}}
|
||||||
|
4 | <a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
||||||
|
5 | {{/params.social}}
|
||||||
|
|
|
|
||||||
4 | {{#params.social}}
|
|
||||||
5 | <a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
|
||||||
6 | {{/params.social}}
|
|
||||||
| ^^^^^^^^^^^^^^^^^^
|
|
||||||
7 | </menu>
|
|
||||||
8 | <p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
|
||||||
---
|
---
|
||||||
name: Missing 2nd opening brace
|
name: Missing 2nd opening brace
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<menu>
|
<menu>
|
||||||
<a class="goto-top opacity-0" href="#">{> icons/chevron_up}}</a>
|
<a class="goto-top opacity-0" href="#">{> icons/chevron_up}}</a>
|
||||||
@@ -29,10 +33,21 @@ input: |
|
|||||||
{{/params.social}}
|
{{/params.social}}
|
||||||
</menu>
|
</menu>
|
||||||
<p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
<p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
||||||
expected: Somthing that shows the whole block, and points out the missing opening brace
|
expected: |
|
||||||
actual: Nothing. The page compiles with no warnings or errors
|
Missing opening brace
|
||||||
|
--> <test>:1:1
|
||||||
|
1 | <menu>
|
||||||
|
2 | <a class="goto-top opacity-0" href="#">{> icons/chevron_up}}</a>
|
||||||
|
| ^ opened ^^ closed
|
||||||
|
3 | {{#params.social}}
|
||||||
|
4 | <a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
||||||
|
5 | {{/params.social}}
|
||||||
|
|
|
||||||
---
|
---
|
||||||
|
# TODO: make it work
|
||||||
name: Invalid timezone
|
name: Invalid timezone
|
||||||
|
enable: false
|
||||||
|
should: warn
|
||||||
input: |
|
input: |
|
||||||
{
|
{
|
||||||
"date": {
|
"date": {
|
||||||
@@ -44,6 +59,8 @@ actual: |
|
|||||||
[WARN ] --- [148:init_site()] unable to load timezone 'America/New_Yorkside'
|
[WARN ] --- [148:init_site()] unable to load timezone 'America/New_Yorkside'
|
||||||
---
|
---
|
||||||
name: Mismatched section tags
|
name: Mismatched section tags
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -57,19 +74,20 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{now | format "2006" }} {{params.author.name}}</p>
|
<p><small>©</small> {{now | format "2006" }} {{params.author.name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: Mostly the same, but with rust style markers at the opening and close
|
expected: |
|
||||||
actual: |
|
Mismatched section: 'ocial' opened, 'social' closed
|
||||||
[ERROR] --- [383:load_partials()] expected {{/params.ocial}}, got {{/params.social}}
|
--> <test>:4:5
|
||||||
--> /home/spencer/github.com/sbrow.github.io/layouts/partials/footer.html:6:5
|
|
||||||
|
|
|
|
||||||
4 | {{#params.ocial}}
|
4 | {{#params.ocial}}
|
||||||
|
| ^^^^^ opened — did you mean 'social'?
|
||||||
5 | <a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
5 | <a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
||||||
6 | {{/params.social}}
|
6 | {{/params.social}}
|
||||||
| ^^^^^^^^^^^^^^^^^^
|
| ^^^^^^ expected {{/params.ocial}}, got {{/params.social}}
|
||||||
7 | </menu>
|
|
|
||||||
8 | <p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
|
||||||
---
|
---
|
||||||
name: Missing closing section
|
name: Missing closing section
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -83,10 +101,20 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{now | format "2006" }} {{params.author.name}}</p>
|
<p><small>©</small> {{now | format "2006" }} {{params.author.name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: "Missing closing tag '{{/params.social}}' ...opened <here> ... expected close <here>"
|
expected: |
|
||||||
actual: Nothing. The page compiles with no warnings or errors
|
Missing closing tag '{{/params.social}}'
|
||||||
|
--> <test>:4:5
|
||||||
|
|
|
||||||
|
4 | {{#params.social}}
|
||||||
|
| ^^^^^^^^^^^^^^^^^ opened
|
||||||
|
5 | <a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
||||||
|
6 | <!-- {{/params.social}} -->
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ closing tag is commented out
|
||||||
|
|
|
||||||
---
|
---
|
||||||
name: empty tag
|
name: empty tag
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -100,18 +128,20 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{}} {{params.author.name}}</p>
|
<p><small>©</small> {{}} {{params.author.name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: "Found an empty tag at <line:col>"
|
expected: |
|
||||||
actual: |
|
Empty tag
|
||||||
[WARN ] --- [920:warn_unknown_key()] unknown key ''
|
--> <test>:11:28
|
||||||
--> /home/spencer/github.com/sbrow.github.io/layouts/partials/footer.html:1:1
|
|
|
||||||
|
|
9 | href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>
|
||||||
1 | <footer>
|
10 | </p>
|
||||||
| ^
|
11 | <p><small>©</small> {{}} {{params.author.name}}</p>
|
||||||
2 | <menu>
|
| ^^^^ tag must contain a key
|
||||||
3 | <a class="goto-top opacity-0" href="#">{{> icons/chevron_up}}</a>
|
12 | </footer>
|
||||||
|
|
|
|
||||||
---
|
---
|
||||||
name: Missing pipe arguement
|
name: Missing pipe arguement
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -125,18 +155,20 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{now | group_by }} {{params.author.name}}</p>
|
<p><small>©</small> {{now | group_by }} {{params.author.name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: "Missing filter argument <pointing to exact location>"
|
expected: |
|
||||||
actual: |
|
group_by expects 1 argument, got 0
|
||||||
[ERROR] --- [152:render_template()] group_by expects 1 argument, got 0
|
--> <test>:11:36
|
||||||
--> /home/spencer/github.com/sbrow.github.io/layouts/partials/footer.html:1:1
|
|
||||||
|
|
|
||||||
1 | <footer>
|
|
||||||
| ^
|
|
||||||
2 | <menu>
|
|
||||||
3 | <a class="goto-top opacity-0" href="#">{{> icons/chevron_up}}</a>
|
|
||||||
|
|
|
|
||||||
|
9 | href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>
|
||||||
|
10 | </p>
|
||||||
|
11 | <p><small>©</small> {{now | group_by }} {{params.author.name}}</p>
|
||||||
|
| ^^^^^^^^^ missing argument
|
||||||
|
12 | </footer>
|
||||||
|
|
|
||||||
---
|
---
|
||||||
name: Double dot access
|
name: Double dot access
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -150,10 +182,20 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{now | format "2006" }} {{params..name}}</p>
|
<p><small>©</small> {{now | format "2006" }} {{params..name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: Error "Invalid access key"
|
expected: |
|
||||||
actual: Nothing. The page compiles with no warnings or errors
|
Invalid key path: consecutive dots
|
||||||
|
--> <test>:11:60
|
||||||
|
|
|
||||||
|
9 | href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>
|
||||||
|
10 | </p>
|
||||||
|
11 | <p><small>©</small> {{now | format "2006" }} {{params..name}}</p>
|
||||||
|
| ^^ missing path name
|
||||||
|
12 | </footer>
|
||||||
|
|
|
||||||
---
|
---
|
||||||
name: Too many opening braces
|
name: Too many opening braces
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -167,18 +209,20 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{{{now | format "2006" }}} {{params.author.name}}</p>
|
<p><small>©</small> {{{{now | format "2006" }}} {{params.author.name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: "Too many opening braces <point to location>"
|
expected: |
|
||||||
actual: |
|
Too many opening braces
|
||||||
[WARN ] --- [920:warn_unknown_key()] unknown key '{now'
|
--> <test>:11:28
|
||||||
--> /home/spencer/github.com/sbrow.github.io/layouts/partials/footer.html:1:1
|
|
|
||||||
|
|
9 | href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>
|
||||||
1 | <footer>
|
10 | </p>
|
||||||
| ^ did you mean 'now'?
|
11 | <p><small>©</small> {{{{now | format "2006" }}} {{params.author.name}}</p>
|
||||||
2 | <menu>
|
| ^^^^ expected {{{, got {{{{
|
||||||
3 | <a class="goto-top opacity-0" href="#">{{> icons/chevron_up}}</a>
|
12 | </footer>
|
||||||
|
|
|
|
||||||
---
|
---
|
||||||
name: Too many closing braces
|
name: Too many closing braces
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<footer>
|
<footer>
|
||||||
<menu>
|
<menu>
|
||||||
@@ -192,10 +236,21 @@ input: |
|
|||||||
</p>
|
</p>
|
||||||
<p><small>©</small> {{now | format "2006" }}}} {{params.author.name}}</p>
|
<p><small>©</small> {{now | format "2006" }}}} {{params.author.name}}</p>
|
||||||
</footer>
|
</footer>
|
||||||
expected: "Too many closing braces <Point to location>"
|
expected: |
|
||||||
actual: Nothing. The page compiles with no warnings or errors
|
Too many closing braces
|
||||||
|
--> <test>:11:50
|
||||||
|
|
|
||||||
|
9 | href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>
|
||||||
|
10 | </p>
|
||||||
|
11 | <p><small>©</small> {{now | format "2006" }}}} {{params.author.name}}</p>
|
||||||
|
| ^^^^ expected }}, got }}}
|
||||||
|
12 | </footer>
|
||||||
|
|
|
||||||
---
|
---
|
||||||
|
# TODO: Implement
|
||||||
name: Frontmatter missing closing quote on key
|
name: Frontmatter missing closing quote on key
|
||||||
|
enable: false
|
||||||
|
should: warn
|
||||||
input: |
|
input: |
|
||||||
{
|
{
|
||||||
"title: Docs"
|
"title: Docs"
|
||||||
@@ -208,7 +263,10 @@ expected: 'TODO:'
|
|||||||
actual: |
|
actual: |
|
||||||
[ERROR] --- [42:parse_frontmatter()] failed to parse frontmatter JSON: Expected_Colon_After_Key
|
[ERROR] --- [42:parse_frontmatter()] failed to parse frontmatter JSON: Expected_Colon_After_Key
|
||||||
---
|
---
|
||||||
|
# TODO: Implement
|
||||||
name: Fronmatter missing opening quote on value
|
name: Fronmatter missing opening quote on value
|
||||||
|
should: warn
|
||||||
|
enable: false
|
||||||
input: |
|
input: |
|
||||||
{
|
{
|
||||||
"title": Docs"
|
"title": Docs"
|
||||||
@@ -220,48 +278,87 @@ input: |
|
|||||||
expected: 'detailed diagnostic pointing to the point of failure and suggesting the addition of a quote'
|
expected: 'detailed diagnostic pointing to the point of failure and suggesting the addition of a quote'
|
||||||
actual: Nothing. The page compiles with no warnings or errors
|
actual: Nothing. The page compiles with no warnings or errors
|
||||||
---
|
---
|
||||||
name: No error when missing index page.
|
name: No error when missing index page
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
expected: 'Your site has no index page! Please create an index.md or index.html file in ./content/'
|
expected: 'Your site has no index page! Please create an index.md or index.html file in ./content/'
|
||||||
actual: Nothing. The site compiles with no warnings or errors
|
|
||||||
---
|
---
|
||||||
|
# FIXME: Doesn't work properly
|
||||||
name: format pipe with no date format set
|
name: format pipe with no date format set
|
||||||
|
enable: true
|
||||||
|
should: warn
|
||||||
|
input: '{{ now | format }}'
|
||||||
expected: |
|
expected: |
|
||||||
Show where the error was triggered. Should also show the format that will be used
|
no date format configured — set date.format in thor.json or specify a format, e.g. {{key | format "2 Jan 2006"}}
|
||||||
actual: |
|
--> <test>:1:10
|
||||||
[ERROR] --- [333:apply_format()] format pipe used but no date format configured (set date.format in thor.json) Default will be used
|
|
|
||||||
|
1 | {{now | format}}
|
||||||
|
| ^^^^^^ defaulting to "2 Jan 2006"
|
||||||
---
|
---
|
||||||
|
# TODO: called First/last, but only tests first
|
||||||
name: first/last with zero argument
|
name: first/last with zero argument
|
||||||
|
enable: true
|
||||||
|
should: error
|
||||||
input: |
|
input: |
|
||||||
<ul>{{#items | first 0}}<li>{{.}}</li>{{/items}}</ul>
|
<ul>{{#items | first 0}}<li>{{.}}</li>{{/items}}</ul>
|
||||||
expected: Error with source location — "argument must not be zero"
|
expected: |
|
||||||
actual: |
|
argument must not be zero
|
||||||
[ERROR] --- [render_template()] argument must not be zero
|
--> <test>:1:5
|
||||||
--> <file>:<line>:<col>
|
|
||||||
|
|
|
|
||||||
N | <ul>{{#items | first 0}}<li>{{.}}</li>{{/items}}</ul>
|
1 | <ul>{{#items | first 0}}<li>{{.}}</li>{{/items}}</ul>
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
|
|
||||||
---
|
---
|
||||||
|
# TODO: called First/last, but only tests first
|
||||||
name: first/last with negative argument
|
name: first/last with negative argument
|
||||||
|
enable: true
|
||||||
|
should: warn
|
||||||
input: |
|
input: |
|
||||||
<ul>{{#items | first -3}}<li>{{.}}</li>{{/items}}</ul>
|
<ul>{{#items | first -3}}<li>{{.}}</li>{{/items}}</ul>
|
||||||
expected: Warning with source location — "first -3 is equivalent to last 3 — consider using the clearer form"
|
expected: |
|
||||||
actual: |
|
first -3 is equivalent to last 3
|
||||||
[WARN ] --- [take()] first -3 is equivalent to last 3
|
--> <test>:1:5
|
||||||
--> <file>:<line>:<col>
|
|
||||||
|
|
|
|
||||||
N | <ul>{{#items | first -3}}<li>{{.}}</li>{{/items}}</ul>
|
1 | <ul>{{#items | first -3}}<li>{{.}}</li>{{/items}}</ul>
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ consider using the clearer form
|
| ^^^^^^^^ consider using the clearer form
|
||||||
|
|
|
|
||||||
---
|
---
|
||||||
|
# TODO: called First/last, but only tests first
|
||||||
name: first/last on string with no argument
|
name: first/last on string with no argument
|
||||||
input: |
|
# enable: true
|
||||||
{{word | first}}
|
input: '{{word | first}}'
|
||||||
expected: Warning with source location — "defaulting to n=1 for string; consider specifying n explicitly"
|
should: warn
|
||||||
actual: |
|
expected: |
|
||||||
[WARN ] --- [take()] defaulting to n=1 for string
|
defaulting to n=1 for string
|
||||||
--> <file>:<line>:<col>
|
--> <test>:1:1
|
||||||
|
|
|
|
||||||
N | {{word | first}}
|
1 | {{word | first}}
|
||||||
| ^^^^^^^^^^^^^^^ consider specifying n explicitly
|
| ^^^^^ consider specifying n explicitly
|
||||||
|
|
|
|
||||||
|
---
|
||||||
|
name: "first succeeds on params.author.name"
|
||||||
|
enable: true
|
||||||
|
input: '{{ params.author.name | first 3 }}'
|
||||||
|
should: ok
|
||||||
|
expected: "foo"
|
||||||
|
---
|
||||||
|
# TODO: called First/last, but only tests first
|
||||||
|
name: "first/last in partial"
|
||||||
|
enable: true
|
||||||
|
input: "{{> footer }}"
|
||||||
|
should: error
|
||||||
|
expected: |
|
||||||
|
first -3 is equivalent to last 3
|
||||||
|
--> /home/spencer/github.com/sbrow.github.io/layouts/partials/footer.html:1:1
|
||||||
|
|
|
||||||
|
1 | <footer>
|
||||||
|
| ^ consider using the clearer form
|
||||||
|
2 | <menu>
|
||||||
|
3 | <a class="goto-top opacity-0" href="#">{{> icons/chevron_up}}</a>
|
||||||
|
|
|
||||||
|
---
|
||||||
|
name: "Pipes are ignored in comments"
|
||||||
|
enable: true
|
||||||
|
should: ok
|
||||||
|
input: 'Hello, {{! now | format "2006" }}'
|
||||||
|
expected: 'Hello, '
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
- [ ] merge `render_{section,home_html,page_html}` procs.
|
- [ ] merge `render_{section,home_html,page_html}` procs.
|
||||||
- [ ] try to combine render_page_html and render_home_html?
|
- [ ] try to combine render_page_html and render_home_html?
|
||||||
- [ ] make `parse` an overload of `parse_text/parse_inline` and `parse_file`, or something.
|
- [ ] make `parse` an overload of `parse_text/parse_inline` and `parse_file`, or something.
|
||||||
|
- [ ] refactor `mustache.load_partials` to accept an iterator so we don't copy `main.load_partials`
|
||||||
|
|
||||||
### Menus
|
### Menus
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Warning Counter Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
When the same warning fires multiple times across a single page render, show it once with a `[×N]` count prefix instead of N identical diagnostic blocks.
|
||||||
|
|
||||||
|
```
|
||||||
|
[×5] first -3 is equivalent to last 3 — consider using the clearer form
|
||||||
|
--> layouts/home.html:12:1
|
||||||
|
|
|
||||||
|
12 | <ul>{{#items | first -3}}<li>{{.}}</li>{{/items}}</ul>
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ consider using the clearer form
|
||||||
|
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
Module-level buffer in `pipes.odin`. Warnings are collected during render, deduplicated + counted + logged after the page completes. No parameter threading through `render_nodes`.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### `mustache/pipes.odin`
|
||||||
|
|
||||||
|
**New module-level state:**
|
||||||
|
```odin
|
||||||
|
_pending_warnings: [dynamic]Error
|
||||||
|
```
|
||||||
|
|
||||||
|
**New procs:**
|
||||||
|
```odin
|
||||||
|
reset_warnings :: proc() {
|
||||||
|
clear(&_pending_warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_warnings :: proc(colorize: bool = false) {
|
||||||
|
if len(_pending_warnings) == 0 do return
|
||||||
|
|
||||||
|
counts := make(map[string]int, context.temp_allocator)
|
||||||
|
first: map[string]Error = make(map[string]Error, context.temp_allocator)
|
||||||
|
defer delete(counts)
|
||||||
|
defer delete(first)
|
||||||
|
|
||||||
|
for w in _pending_warnings {
|
||||||
|
b := body(w)
|
||||||
|
if _, seen := first[b.msg]; !seen {
|
||||||
|
first[b.msg] = w
|
||||||
|
}
|
||||||
|
counts[b.msg] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg, w in first {
|
||||||
|
c := counts[msg]
|
||||||
|
b := body(w)
|
||||||
|
path := b.path != "" ? b.path : "<input>"
|
||||||
|
prefix := c > 1 ? fmt.tprintf("[×%d] ", c) : ""
|
||||||
|
diag := diags.format_error(
|
||||||
|
path, b.source, b.pos, b.msg,
|
||||||
|
hint = b.hint,
|
||||||
|
colorize = colorize,
|
||||||
|
)
|
||||||
|
log.warnf("%s%s", prefix, diag)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(&_pending_warnings)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`apply_pipeline` change:**
|
||||||
|
|
||||||
|
Replace the current warning handling (which logs immediately):
|
||||||
|
```odin
|
||||||
|
if warnings != nil {
|
||||||
|
append(warnings, ferr)
|
||||||
|
} else {
|
||||||
|
log.warnf("%s", format_render_error(ferr, tmpl))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
```odin
|
||||||
|
append(&_pending_warnings, ferr)
|
||||||
|
```
|
||||||
|
|
||||||
|
Drop the `warnings: ^[dynamic]Error` parameter — no longer needed.
|
||||||
|
|
||||||
|
### `render.odin`
|
||||||
|
|
||||||
|
In `render_template`, wrap the render call:
|
||||||
|
```odin
|
||||||
|
mustache.reset_warnings()
|
||||||
|
result, err := mustache.render(content_tpl, []any{ctx.site, ctx.page, ctx}, partials)
|
||||||
|
mustache.flush_warnings(colorize = diags.should_colorize())
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mustache/pipes.odin` cleanup
|
||||||
|
|
||||||
|
- Remove `warnings` parameter from `apply_pipeline` signature
|
||||||
|
- Remove `format_render_error` import/call from `apply_pipeline` (formatting moves to `flush_warnings`)
|
||||||
|
- Remove the `log` import if no longer used directly in `apply_pipeline`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
~25 lines new code (`reset_warnings` + `flush_warnings`), ~3 lines changed in `apply_pipeline`, ~2 lines added in `render.odin`. Total ~30 lines.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Module-level state means warnings don't survive across page renders (by design — `reset_warnings` is called per page)
|
||||||
|
- Not thread-safe (fine for sequential rendering)
|
||||||
|
- Counting is by exact `msg` string — warnings with different messages but same root cause are counted separately
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package mustache
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
Diag_Case :: struct {
|
||||||
|
name: string,
|
||||||
|
enable: bool,
|
||||||
|
should: string, // "error" | "warn" | "pass"
|
||||||
|
expected: string, // exact formatted diagnostic output
|
||||||
|
input: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
load_diag_cases :: proc(path: string, allocator := context.temp_allocator) -> []Diag_Case {
|
||||||
|
dirs := [?]string{#directory, path}
|
||||||
|
full_path, _ := filepath.join(dirs[:], allocator)
|
||||||
|
raw_data, err := os.read_entire_file(full_path, allocator)
|
||||||
|
if err != nil {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(raw_data)
|
||||||
|
lines := strings.split(content, "\n", allocator)
|
||||||
|
|
||||||
|
cases := make([dynamic]Diag_Case, 0, 16, allocator)
|
||||||
|
|
||||||
|
current: Diag_Case
|
||||||
|
has_current := false
|
||||||
|
|
||||||
|
// Multiline block state
|
||||||
|
in_block := false
|
||||||
|
block_key: string
|
||||||
|
block_indent := -1
|
||||||
|
block_sb: strings.Builder
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
raw := strings.trim_right(line, "\r")
|
||||||
|
|
||||||
|
if in_block {
|
||||||
|
// Check if this line belongs to the multiline block
|
||||||
|
is_indented := len(raw) == 0 || raw[0] == ' ' || raw[0] == '\t'
|
||||||
|
if is_indented {
|
||||||
|
if block_indent < 0 && len(raw) > 0 {
|
||||||
|
// Determine indentation from first content line
|
||||||
|
block_indent = 0
|
||||||
|
for block_indent < len(raw) && raw[block_indent] == ' ' {
|
||||||
|
block_indent += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Strip block indentation
|
||||||
|
stripped := raw
|
||||||
|
if block_indent > 0 && len(stripped) >= block_indent {
|
||||||
|
stripped = stripped[block_indent:]
|
||||||
|
}
|
||||||
|
if strings.builder_len(block_sb) > 0 {
|
||||||
|
strings.write_string(&block_sb, "\n")
|
||||||
|
}
|
||||||
|
strings.write_string(&block_sb, stripped)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// End of multiline block
|
||||||
|
in_block = false
|
||||||
|
strings.write_string(&block_sb, "\n") // YAML | trailing newline
|
||||||
|
value := strings.clone(strings.to_string(block_sb), allocator)
|
||||||
|
strings.builder_destroy(&block_sb)
|
||||||
|
set_field(¤t, block_key, value)
|
||||||
|
block_indent = -1
|
||||||
|
// Fall through to process this line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed := strings.trim_space(raw)
|
||||||
|
|
||||||
|
// Entry separator
|
||||||
|
if trimmed == "---" {
|
||||||
|
if has_current {
|
||||||
|
append(&cases, current)
|
||||||
|
current = {}
|
||||||
|
has_current = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip blank lines outside blocks
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiline block start: "key: |"
|
||||||
|
if strings.has_suffix(trimmed, ": |") {
|
||||||
|
colon := strings.last_index(trimmed, ":")
|
||||||
|
if colon > 0 {
|
||||||
|
block_key = strings.trim_space(trimmed[:colon])
|
||||||
|
strings.builder_init(&block_sb, allocator)
|
||||||
|
in_block = true
|
||||||
|
has_current = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple key: value
|
||||||
|
colon := strings.index(trimmed, ":")
|
||||||
|
if colon > 0 {
|
||||||
|
key := strings.trim_space(trimmed[:colon])
|
||||||
|
value := strings.trim_space(trimmed[colon + 1:])
|
||||||
|
// Strip matching outer quotes
|
||||||
|
if len(value) >= 2 {
|
||||||
|
if (value[0] == '"' && value[len(value) - 1] == '"') ||
|
||||||
|
(value[0] == '\'' && value[len(value) - 1] == '\'') {
|
||||||
|
value = value[1:len(value) - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_field(¤t, key, value)
|
||||||
|
has_current = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush trailing multiline block
|
||||||
|
if in_block {
|
||||||
|
strings.write_string(&block_sb, "\n") // YAML | trailing newline
|
||||||
|
value := strings.clone(strings.to_string(block_sb), allocator)
|
||||||
|
strings.builder_destroy(&block_sb)
|
||||||
|
set_field(¤t, block_key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush last entry
|
||||||
|
if has_current {
|
||||||
|
append(&cases, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cases[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
set_field :: proc(c: ^Diag_Case, key, value: string) {
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
c.name = value
|
||||||
|
case "enable":
|
||||||
|
c.enable = value == "true"
|
||||||
|
case "should":
|
||||||
|
c.should = value
|
||||||
|
case "expected":
|
||||||
|
c.expected = value
|
||||||
|
case "input":
|
||||||
|
c.input = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
#+test
|
#+test
|
||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
@@ -475,3 +478,243 @@ test_parse_error_pipe_parse_in_inverted_keeps_double_braces :: proc(t: ^testing.
|
|||||||
fmt.tprintf("msg should contain literal '{{^', got %q", b.msg),
|
fmt.tprintf("msg should contain literal '{{^', got %q", b.msg),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data-driven diagnostic cases (from DIAGNOSTIC_TODOS.yaml)
|
||||||
|
//
|
||||||
|
// write_diagnostic_tests generates diagnostic_test_cases_test.odin from the
|
||||||
|
// enabled cases in DIAGNOSTIC_TODOS.yaml. On the first run (or when the YAML
|
||||||
|
// changes), it regenerates the file and fails. On the second run, each case
|
||||||
|
// runs as an individual test via run_diag_case.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Diag_Context :: struct {
|
||||||
|
items: [5]string,
|
||||||
|
word: string,
|
||||||
|
params: json.Object,
|
||||||
|
now: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
diag_ctx :: proc() -> Diag_Context {
|
||||||
|
params_raw := `{ "author": { "name": "foo bar"}}`
|
||||||
|
params_val, err := json.parse(params_raw, allocator = context.temp_allocator)
|
||||||
|
assert(err == nil)
|
||||||
|
params, ok := params_val.(json.Object)
|
||||||
|
assert(ok)
|
||||||
|
return Diag_Context {
|
||||||
|
items = {"a", "b", "c", "d", "e"},
|
||||||
|
word = "Hello, World!",
|
||||||
|
params = params,
|
||||||
|
now = "2026-08-10T16:21:55-04:00",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diag_partials :: proc() -> map[string]Template {
|
||||||
|
dirs := [?]string{#directory, "fixtures/layouts/partials"}
|
||||||
|
partials_dir, _ := filepath.join(dirs[:], context.temp_allocator)
|
||||||
|
return load_partials(partials_dir, allocator = context.temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
diag_slug :: proc(name: string) -> string {
|
||||||
|
sb: strings.Builder
|
||||||
|
strings.builder_init(&sb, context.temp_allocator)
|
||||||
|
prev_underscore := false
|
||||||
|
for r in name {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||||
|
strings.write_rune(&sb, r)
|
||||||
|
prev_underscore = false
|
||||||
|
} else if r >= 'A' && r <= 'Z' {
|
||||||
|
strings.write_rune(&sb, r + 32)
|
||||||
|
prev_underscore = false
|
||||||
|
} else {
|
||||||
|
if !prev_underscore && strings.builder_len(sb) > 0 {
|
||||||
|
strings.write_rune(&sb, '_')
|
||||||
|
prev_underscore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s := strings.to_string(sb)
|
||||||
|
if len(s) > 0 && s[len(s) - 1] == '_' {
|
||||||
|
s = s[:len(s) - 1]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
run_diag_case :: proc(t: ^testing.T, name: string) {
|
||||||
|
cases := load_diag_cases("../DIAGNOSTIC_TODOS.yaml")
|
||||||
|
ctx := diag_ctx()
|
||||||
|
partials := diag_partials()
|
||||||
|
|
||||||
|
for c in cases {
|
||||||
|
if c.name != name do continue
|
||||||
|
|
||||||
|
switch c.should {
|
||||||
|
case "error":
|
||||||
|
tmpl, perr := parse(c.input, "<test>", allocator = context.temp_allocator)
|
||||||
|
if perr != nil {
|
||||||
|
formatted := format_render_error(perr, tmpl, colorize = false)
|
||||||
|
testing.expect_value(t, formatted, c.expected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, rerr := render(tmpl, ctx, partials, context.temp_allocator)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
rerr != nil,
|
||||||
|
fmt.tprintf("[%s] should error but render succeeded", c.name),
|
||||||
|
)
|
||||||
|
if rerr == nil do return
|
||||||
|
|
||||||
|
formatted := format_render_error(rerr, tmpl, colorize = false)
|
||||||
|
testing.expect_value(t, formatted, c.expected)
|
||||||
|
|
||||||
|
case "pass":
|
||||||
|
tmpl, perr := parse(c.input, "<test>", allocator = context.temp_allocator)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
perr == nil,
|
||||||
|
fmt.tprintf(
|
||||||
|
"[%s] should parse, got: %s",
|
||||||
|
c.name,
|
||||||
|
perr != nil ? format_render_error(perr, tmpl, colorize = false) : "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if perr != nil do return
|
||||||
|
|
||||||
|
_, rerr := render(tmpl, ctx, partials, context.temp_allocator)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
rerr == nil,
|
||||||
|
fmt.tprintf(
|
||||||
|
"[%s] should not error, got: %s",
|
||||||
|
c.name,
|
||||||
|
rerr != nil ? format_render_error(rerr, tmpl, colorize = false) : "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case "ok":
|
||||||
|
tmpl, perr := parse(c.input, "<test>", allocator = context.temp_allocator)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
perr == nil,
|
||||||
|
fmt.tprintf(
|
||||||
|
"[%s] should parse, got: %s",
|
||||||
|
c.name,
|
||||||
|
perr != nil ? format_render_error(perr, tmpl, colorize = false) : "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if perr != nil do return
|
||||||
|
|
||||||
|
result, rerr := render(tmpl, ctx, partials, context.temp_allocator)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
rerr == nil,
|
||||||
|
fmt.tprintf(
|
||||||
|
"[%s] should not error, got: %s",
|
||||||
|
c.name,
|
||||||
|
rerr != nil ? format_render_error(rerr, tmpl, colorize = false) : "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if rerr != nil do return
|
||||||
|
|
||||||
|
testing.expect_value(t, result, c.expected)
|
||||||
|
|
||||||
|
case "warn":
|
||||||
|
tmpl, perr := parse(c.input, "<test>", allocator = context.temp_allocator)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
perr == nil,
|
||||||
|
fmt.tprintf(
|
||||||
|
"[%s] should parse, got: %s",
|
||||||
|
c.name,
|
||||||
|
perr != nil ? format_render_error(perr, tmpl, colorize = false) : "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if perr != nil do return
|
||||||
|
|
||||||
|
warnings := make([dynamic]Error, 0, 4, context.temp_allocator)
|
||||||
|
_, rerr := render(tmpl, ctx, partials, context.temp_allocator, &warnings)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
rerr == nil,
|
||||||
|
fmt.tprintf(
|
||||||
|
"[%s] should not error, got: %s",
|
||||||
|
c.name,
|
||||||
|
rerr != nil ? format_render_error(rerr, tmpl, colorize = false) : "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if rerr != nil do return
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
len(warnings) > 0,
|
||||||
|
fmt.tprintf("[%s] should produce a warning", c.name),
|
||||||
|
)
|
||||||
|
if len(warnings) == 0 do return
|
||||||
|
|
||||||
|
formatted := format_render_error(warnings[0], tmpl, colorize = false)
|
||||||
|
testing.expect_value(t, formatted, c.expected)
|
||||||
|
|
||||||
|
case:
|
||||||
|
testing.expectf(t, false, "[%s] unknown should value: '%s'", c.name, c.should)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expectf(t, false, "case '%s' not found in DIAGNOSTIC_TODOS.yaml", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
write_diagnostic_tests :: proc(t: ^testing.T) {
|
||||||
|
cases := load_diag_cases("../DIAGNOSTIC_TODOS.yaml")
|
||||||
|
testing.expect(t, len(cases) > 0, "should load diagnostic cases")
|
||||||
|
|
||||||
|
sb: strings.Builder
|
||||||
|
strings.builder_init(&sb, context.temp_allocator)
|
||||||
|
strings.write_string(&sb, "#+test\n")
|
||||||
|
strings.write_string(&sb, "package mustache\n\n")
|
||||||
|
strings.write_string(&sb, "import \"core:testing\"\n\n")
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for c in cases {
|
||||||
|
if !c.enable do continue
|
||||||
|
if c.should == "" {
|
||||||
|
testing.expectf(t, false, "[%s] has enable: true but no should value", c.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count += 1
|
||||||
|
slug := diag_slug(c.name)
|
||||||
|
strings.write_string(&sb, "@(test)\n")
|
||||||
|
strings.write_string(&sb, "test_diag_")
|
||||||
|
strings.write_string(&sb, slug)
|
||||||
|
strings.write_string(&sb, " :: proc(t: ^testing.T) {\n")
|
||||||
|
strings.write_string(&sb, "\trun_diag_case(t, \"")
|
||||||
|
strings.write_string(&sb, c.name)
|
||||||
|
strings.write_string(&sb, "\")\n}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expect(t, count > 0, "at least one case should be enabled")
|
||||||
|
|
||||||
|
generated := strings.to_string(sb)
|
||||||
|
|
||||||
|
dirs := [?]string{#directory, "diagnostic_test_cases_test.odin"}
|
||||||
|
gen_path, _ := filepath.join(dirs[:], context.temp_allocator)
|
||||||
|
|
||||||
|
existing_data, _ := os.read_entire_file_from_path(gen_path, context.temp_allocator)
|
||||||
|
existing := string(existing_data)
|
||||||
|
|
||||||
|
if existing == generated do return
|
||||||
|
|
||||||
|
werr := os.write_entire_file(gen_path, transmute([]byte)generated)
|
||||||
|
if werr != nil {
|
||||||
|
testing.expectf(t, false, "failed to write %s: %v", gen_path, werr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testing.expectf(
|
||||||
|
t,
|
||||||
|
false,
|
||||||
|
"diagnostic_test_cases_test.odin regenerated (%d cases), re-run tests to see results",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#+test
|
||||||
|
package mustache
|
||||||
|
|
||||||
|
import "core:testing"
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_missing_2nd_closing_brace :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Missing 2nd closing brace")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_missing_2nd_opening_brace :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Missing 2nd opening brace")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_mismatched_section_tags :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Mismatched section tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_missing_closing_section :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Missing closing section")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_empty_tag :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "empty tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_missing_pipe_arguement :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Missing pipe arguement")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_double_dot_access :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Double dot access")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_too_many_opening_braces :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Too many opening braces")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_too_many_closing_braces :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Too many closing braces")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_no_error_when_missing_index_page :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "No error when missing index page")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_format_pipe_with_no_date_format_set :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "format pipe with no date format set")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_first_last_with_zero_argument :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "first/last with zero argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_first_last_with_negative_argument :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "first/last with negative argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_first_succeeds_on_params_author_name :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "first succeeds on params.author.name")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_first_last_in_partial :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "first/last in partial")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_diag_pipes_are_ignored_in_comments :: proc(t: ^testing.T) {
|
||||||
|
run_diag_case(t, "Pipes are ignored in comments")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<footer>
|
||||||
|
<menu>
|
||||||
|
<a class="goto-top opacity-0" href="#">{{> icons/chevron_up}}</a>
|
||||||
|
{{#params.social}}
|
||||||
|
<a href="{{url}}" target="_blank" rel="noopener noreferrer me" title="{{name}}">{{>* icon}}</a>
|
||||||
|
{{/params.social}}
|
||||||
|
</menu>
|
||||||
|
<p>Proudly built with <a href="https://github.com/sbrow/thor/">Thor</a> and <a
|
||||||
|
href="https://edwardtufte.github.io/tufte-css/">Tufte CSS</a>
|
||||||
|
</p>
|
||||||
|
<p><small>©</small> {{! now | format "2006" }} {{params.author.name | first -3 }}</p>
|
||||||
|
</footer>
|
||||||
+120
-46
@@ -23,9 +23,9 @@ Context_Stack :: [dynamic]any
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Error_Kind :: enum {
|
Error_Kind :: enum {
|
||||||
Syntax, // parse-time: malformed template
|
Syntax, // parse-time: malformed template
|
||||||
Data, // render-time: template fine, data wrong (e.g. filter misuse)
|
Data, // render-time: template fine, data wrong (e.g. filter misuse)
|
||||||
Warning, // non-fatal: operation succeeded but user should know
|
Warning, // non-fatal: operation succeeded but user should know
|
||||||
}
|
}
|
||||||
|
|
||||||
Error_Body :: struct {
|
Error_Body :: struct {
|
||||||
@@ -60,13 +60,13 @@ tag_error :: proc(err: Error, tmpl: Template) -> Error {
|
|||||||
if err == nil do return nil
|
if err == nil do return nil
|
||||||
b := body(err)
|
b := body(err)
|
||||||
return Error_Body {
|
return Error_Body {
|
||||||
msg = b.msg,
|
msg = b.msg,
|
||||||
pos = b.pos,
|
pos = b.pos,
|
||||||
span = b.span,
|
span = b.span,
|
||||||
hint = b.hint,
|
hint = b.hint,
|
||||||
kind = b.kind,
|
kind = b.kind,
|
||||||
source = tmpl.source,
|
source = tmpl.source,
|
||||||
path = tmpl.path,
|
path = tmpl.path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,18 +156,22 @@ parse :: proc(
|
|||||||
tmpl: Template,
|
tmpl: Template,
|
||||||
err: Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
|
tmpl.source = source
|
||||||
|
tmpl.path = path
|
||||||
|
|
||||||
tokens, terr := tokenize(source, tokens_allocator)
|
tokens, terr := tokenize(source, tokens_allocator)
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
return {}, terr
|
return tmpl, terr
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl.nodes, err = parse_tokens(tokens[:], source, allocator)
|
tmpl.nodes, err = parse_tokens(tokens[:], source, allocator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
delete(tmpl.nodes)
|
delete(tmpl.nodes)
|
||||||
return {}, err
|
e := body(err)
|
||||||
|
e.source = source
|
||||||
|
e.path = path
|
||||||
|
return {}, e
|
||||||
}
|
}
|
||||||
tmpl.source = source
|
|
||||||
tmpl.path = path
|
|
||||||
deindent_blocks(tmpl.nodes[:], allocator)
|
deindent_blocks(tmpl.nodes[:], allocator)
|
||||||
return tmpl, nil
|
return tmpl, nil
|
||||||
}
|
}
|
||||||
@@ -269,7 +273,12 @@ parse_section :: proc(
|
|||||||
case .Variable:
|
case .Variable:
|
||||||
idx := len(nodes)
|
idx := len(nodes)
|
||||||
append(nodes, Node{kind = .Variable})
|
append(nodes, Node{kind = .Variable})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos, tag_content_base(source, tok.pos))
|
pipe_key, perr := parse_pipeline(
|
||||||
|
tok.value,
|
||||||
|
&nodes[idx].filters,
|
||||||
|
tok.pos,
|
||||||
|
tag_content_base(source, tok.pos),
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Error_Body {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{%s}}}}': %v", tok.value, perr),
|
||||||
@@ -283,7 +292,12 @@ parse_section :: proc(
|
|||||||
case .Unescaped:
|
case .Unescaped:
|
||||||
idx := len(nodes)
|
idx := len(nodes)
|
||||||
append(nodes, Node{kind = .Unescaped})
|
append(nodes, Node{kind = .Unescaped})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos, tag_content_base(source, tok.pos))
|
pipe_key, perr := parse_pipeline(
|
||||||
|
tok.value,
|
||||||
|
&nodes[idx].filters,
|
||||||
|
tok.pos,
|
||||||
|
tag_content_base(source, tok.pos),
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Error_Body {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{&%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{&%s}}}}': %v", tok.value, perr),
|
||||||
@@ -303,7 +317,12 @@ parse_section :: proc(
|
|||||||
content_start := 0
|
content_start := 0
|
||||||
if pos^ < len(tokens) {content_start = tokens[pos^].pos}
|
if pos^ < len(tokens) {content_start = tokens[pos^].pos}
|
||||||
append(nodes, Node{kind = .Section, pos = tok.pos})
|
append(nodes, Node{kind = .Section, pos = tok.pos})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos, tag_content_base(source, tok.pos))
|
pipe_key, perr := parse_pipeline(
|
||||||
|
tok.value,
|
||||||
|
&nodes[idx].filters,
|
||||||
|
tok.pos,
|
||||||
|
tag_content_base(source, tok.pos),
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Error_Body {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{#%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{#%s}}}}': %v", tok.value, perr),
|
||||||
@@ -324,7 +343,12 @@ parse_section :: proc(
|
|||||||
content_start := 0
|
content_start := 0
|
||||||
if pos^ < len(tokens) {content_start = tokens[pos^].pos}
|
if pos^ < len(tokens) {content_start = tokens[pos^].pos}
|
||||||
append(nodes, Node{kind = .Inverted, pos = tok.pos})
|
append(nodes, Node{kind = .Inverted, pos = tok.pos})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos, tag_content_base(source, tok.pos))
|
pipe_key, perr := parse_pipeline(
|
||||||
|
tok.value,
|
||||||
|
&nodes[idx].filters,
|
||||||
|
tok.pos,
|
||||||
|
tag_content_base(source, tok.pos),
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Error_Body {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{^%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{^%s}}}}': %v", tok.value, perr),
|
||||||
@@ -625,7 +649,14 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:], current, warnings)
|
transformed, perr := apply_pipeline(
|
||||||
|
val,
|
||||||
|
node.filters[:],
|
||||||
|
node.pos,
|
||||||
|
ctx[:],
|
||||||
|
current,
|
||||||
|
warnings,
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
}
|
}
|
||||||
@@ -644,7 +675,14 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:], current, warnings)
|
transformed, perr := apply_pipeline(
|
||||||
|
val,
|
||||||
|
node.filters[:],
|
||||||
|
node.pos,
|
||||||
|
ctx[:],
|
||||||
|
current,
|
||||||
|
warnings,
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
}
|
}
|
||||||
@@ -659,7 +697,14 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:], current, warnings)
|
transformed, perr := apply_pipeline(
|
||||||
|
val,
|
||||||
|
node.filters[:],
|
||||||
|
node.pos,
|
||||||
|
ctx[:],
|
||||||
|
current,
|
||||||
|
warnings,
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
}
|
}
|
||||||
@@ -673,16 +718,16 @@ render_nodes :: proc(
|
|||||||
elem := extract_list_element(elem_info, data, j)
|
elem := extract_list_element(elem_info, data, j)
|
||||||
context_push(ctx, elem, current, node)
|
context_push(ctx, elem, current, node)
|
||||||
defer pop(ctx)
|
defer pop(ctx)
|
||||||
render_nodes(
|
render_nodes(
|
||||||
current,
|
current,
|
||||||
children,
|
children,
|
||||||
ctx,
|
ctx,
|
||||||
partials,
|
partials,
|
||||||
b,
|
b,
|
||||||
blocks,
|
blocks,
|
||||||
indent_state,
|
indent_state,
|
||||||
warnings,
|
warnings,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
context_push(ctx, val, current, node)
|
context_push(ctx, val, current, node)
|
||||||
@@ -707,24 +752,31 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:], current, warnings)
|
transformed, perr := apply_pipeline(
|
||||||
|
val,
|
||||||
|
node.filters[:],
|
||||||
|
node.pos,
|
||||||
|
ctx[:],
|
||||||
|
current,
|
||||||
|
warnings,
|
||||||
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
}
|
}
|
||||||
val = transformed
|
val = transformed
|
||||||
}
|
}
|
||||||
if !is_truthy(val) {
|
if !is_truthy(val) {
|
||||||
render_nodes(
|
render_nodes(
|
||||||
current,
|
current,
|
||||||
node.children,
|
node.children,
|
||||||
ctx,
|
ctx,
|
||||||
partials,
|
partials,
|
||||||
b,
|
b,
|
||||||
blocks,
|
blocks,
|
||||||
indent_state,
|
indent_state,
|
||||||
warnings,
|
warnings,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
i += 1 + len(node.children)
|
i += 1 + len(node.children)
|
||||||
|
|
||||||
case .Partial:
|
case .Partial:
|
||||||
@@ -863,7 +915,14 @@ warn_unknown_key :: proc(current: Template, ctx: []any, node: Node) {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_tag_error(path, current.source, node.pos, msg, hint, colorize = diags.should_colorize())
|
diag := format_tag_error(
|
||||||
|
path,
|
||||||
|
current.source,
|
||||||
|
node.pos,
|
||||||
|
msg,
|
||||||
|
hint,
|
||||||
|
colorize = diags.should_colorize(),
|
||||||
|
)
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,7 +945,14 @@ warn_missing_partial :: proc(
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_tag_error(path, current.source, node.pos, msg, hint, colorize = diags.should_colorize())
|
diag := format_tag_error(
|
||||||
|
path,
|
||||||
|
current.source,
|
||||||
|
node.pos,
|
||||||
|
msg,
|
||||||
|
hint,
|
||||||
|
colorize = diags.should_colorize(),
|
||||||
|
)
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,6 +1031,14 @@ warn_context_depth :: proc(current: Template, node: Node) {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_tag_error(path, current.source, node.pos, msg, "", colorize = diags.should_colorize())
|
diag := format_tag_error(
|
||||||
|
path,
|
||||||
|
current.source,
|
||||||
|
node.pos,
|
||||||
|
msg,
|
||||||
|
"",
|
||||||
|
colorize = diags.should_colorize(),
|
||||||
|
)
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package mustache
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
load_partials :: proc(
|
||||||
|
dir: string,
|
||||||
|
base: string = "",
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> map[string]Template {
|
||||||
|
partials := make(map[string]Template, allocator)
|
||||||
|
|
||||||
|
entries, err := os.read_all_directory_by_path(dir, context.temp_allocator)
|
||||||
|
if err != nil {
|
||||||
|
log.warnf("cannot read partials dir %s: %v", dir, err)
|
||||||
|
return partials
|
||||||
|
}
|
||||||
|
defer os.file_info_slice_delete(entries, context.temp_allocator)
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
if entry.type == .Regular {
|
||||||
|
if !strings.has_suffix(entry.name, ".html") do continue
|
||||||
|
stripped := entry.name[:len(entry.name) - len(".html")]
|
||||||
|
key := fmt.tprintf("%s%s", base, stripped)
|
||||||
|
data, ferr := os.read_entire_file_from_path(entry.fullpath, allocator)
|
||||||
|
if ferr != nil do continue
|
||||||
|
source := string(data)
|
||||||
|
path := strings.clone(entry.fullpath, allocator)
|
||||||
|
tmpl, perr := parse(source, path, allocator = allocator)
|
||||||
|
if perr != nil {
|
||||||
|
b := body(perr)
|
||||||
|
log.warnf("failed to parse partial %s: %s", entry.fullpath, b.msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
partials[key] = tmpl
|
||||||
|
} else if entry.type == .Directory {
|
||||||
|
sub_base := fmt.tprintf("%s%s/", base, entry.name)
|
||||||
|
sub := load_partials(entry.fullpath, sub_base, allocator)
|
||||||
|
for k, v in sub {
|
||||||
|
partials[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return partials
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user