mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Warns the user if they exceed 16 context frames.
This commit is contained in:
@@ -2,18 +2,25 @@
|
|||||||
|
|
||||||
- Polish existing features before moving on to new ones.
|
- Polish existing features before moving on to new ones.
|
||||||
- [ ] Improve diagnostics
|
- [ ] Improve diagnostics
|
||||||
|
- [ ] show "stack traces" in template error diagnostics
|
||||||
|
- [ ] better diagnostics for syntax errors in treesitter.
|
||||||
|
- [ ] Ensure diagnostics for MAX_CONTEXT_DEPTH are good.
|
||||||
|
- [ ] show a proper diagnostic for timezones
|
||||||
|
- currently "unable to load timezone 'America/New_Yorkskie'"
|
||||||
|
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
|
||||||
- [x] Simplify / unify template context stack. Come up with a name for it.
|
- [x] Simplify / unify template context stack. Come up with a name for it.
|
||||||
- [x] `render_template` should accept `Template_Context`, not `any`
|
- [x] `render_template` should accept `Template_Context`, not `any`
|
||||||
- [ ] Load grammars dynamically
|
- [ ] Load grammars dynamically
|
||||||
- [ ] consider adding a limit to the context stack in mustache.
|
- [x] consider adding a limit to the context stack in mustache.
|
||||||
- [ ] better diagnostics for syntax errors in treesitter.
|
|
||||||
- [x] Add heading ids as a default on extension.
|
- [x] Add heading ids as a default on extension.
|
||||||
- [ ] show "stack traces" in template error diagnostics
|
|
||||||
- [ ] starred must be a param.
|
- [ ] starred must be a param.
|
||||||
- [ ] Add a `#config(MAX_CONTEXT_DEPTH, 16?)` to `mustache`.
|
- [x] Add a `#config(MAX_CONTEXT_DEPTH, 16?)` to `mustache`.
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] talk about the context stack (and its limit).
|
||||||
- [ ] menu system
|
- [ ] menu system
|
||||||
- [ ] like Hugo's, but warn(/fail?) if menus are defined in the config *and* pages.
|
- [ ] like Hugo's, but warn(/fail?) if menus are defined in the config *and* pages.
|
||||||
- i.e. force the user to choose one or the other.
|
- i.e. force the user to choose one or the other.
|
||||||
|
- [ ] Don't show annoying log output in tests.
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
@@ -69,9 +76,6 @@
|
|||||||
- [ ] display an error when no part of the date appears in the output.
|
- [ ] display an error when no part of the date appears in the output.
|
||||||
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
||||||
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
||||||
- [ ] show a proper diagnostic for timezones
|
|
||||||
- currently "unable to load timezone 'America/New_Yorkskie'"
|
|
||||||
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- [ ] get rid of the global variables in the `treesitter` package.
|
- [ ] get rid of the global variables in the `treesitter` package.
|
||||||
|
|||||||
+76
-31
@@ -3,9 +3,19 @@ package mustache
|
|||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:reflect"
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
|
// A hypothetical maximum context depth. Trying to pass more than this many items
|
||||||
|
// to render(tpl, data), or nesting templates further than this depth would be
|
||||||
|
// an error.
|
||||||
|
// May be enforced in a later version (for performance)
|
||||||
|
MAX_CONTEXT_DEPTH :: #config(MAX_CONTEXT_DEPTH, 16)
|
||||||
|
|
||||||
|
// Context_Stack is the growable stack of data frames walked top-to-bottom by
|
||||||
|
// resolve_name. Frames are pushed on section descent and popped on exit; the
|
||||||
|
// root data (or each element of a root []any) forms the base frames.
|
||||||
|
Context_Stack :: [dynamic]any
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Error types
|
// Error types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -22,14 +32,18 @@ Error_Body :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error is nil when no error occurred.
|
// Error is nil when no error occurred.
|
||||||
Error :: union { Error_Body }
|
Error :: union {
|
||||||
|
Error_Body,
|
||||||
|
}
|
||||||
|
|
||||||
// body unwraps the Error_Body from a non-nil Error.
|
// body unwraps the Error_Body from a non-nil Error.
|
||||||
// Precondition: err != nil.
|
// Precondition: err != nil.
|
||||||
body :: proc(err: Error) -> Error_Body {
|
body :: proc(err: Error) -> Error_Body {
|
||||||
switch e in err {
|
switch e in err {
|
||||||
case Error_Body: return e
|
case Error_Body:
|
||||||
case: return {}
|
return e
|
||||||
|
case:
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,11 +157,9 @@ render :: proc(
|
|||||||
err: Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
builder: strings.Builder
|
builder: strings.Builder
|
||||||
strings.builder_init(&builder, allocator)
|
strings.builder_init(&builder, context.temp_allocator)
|
||||||
defer strings.builder_destroy(&builder)
|
|
||||||
|
|
||||||
ctx := make([dynamic]any, 0, 4, allocator)
|
ctx := make(Context_Stack, 0, 4, context.temp_allocator)
|
||||||
defer delete(ctx)
|
|
||||||
|
|
||||||
// If data is a []any, expand into individual context frames.
|
// If data is a []any, expand into individual context frames.
|
||||||
// Otherwise, push as a single frame.
|
// Otherwise, push as a single frame.
|
||||||
@@ -331,12 +343,7 @@ parse_section :: proc(
|
|||||||
idx := len(nodes)
|
idx := len(nodes)
|
||||||
append(
|
append(
|
||||||
nodes,
|
nodes,
|
||||||
Node {
|
Node{kind = .Parent, key = tok.value, indent = tok.indent, pos = tok.pos},
|
||||||
kind = .Parent,
|
|
||||||
key = tok.value,
|
|
||||||
indent = tok.indent,
|
|
||||||
pos = tok.pos,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
||||||
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
||||||
@@ -344,15 +351,7 @@ parse_section :: proc(
|
|||||||
case .Block_Open:
|
case .Block_Open:
|
||||||
pos^ += 1
|
pos^ += 1
|
||||||
idx := len(nodes)
|
idx := len(nodes)
|
||||||
append(
|
append(nodes, Node{kind = .Block, key = tok.value, indent = tok.indent, pos = tok.pos})
|
||||||
nodes,
|
|
||||||
Node {
|
|
||||||
kind = .Block,
|
|
||||||
key = tok.value,
|
|
||||||
indent = tok.indent,
|
|
||||||
pos = tok.pos,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
||||||
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
||||||
}
|
}
|
||||||
@@ -499,21 +498,29 @@ remove_line_indent :: proc(s: string, indent: string, allocator := context.alloc
|
|||||||
|
|
||||||
render_template :: proc(
|
render_template :: proc(
|
||||||
pt: Template,
|
pt: Template,
|
||||||
ctx: ^[dynamic]any,
|
ctx: ^Context_Stack,
|
||||||
partials: map[string]Template,
|
partials: map[string]Template,
|
||||||
b: ^strings.Builder,
|
b: ^strings.Builder,
|
||||||
blocks: map[string]Block_Override,
|
blocks: map[string]Block_Override,
|
||||||
indent: string,
|
indent: string,
|
||||||
) -> Error {
|
) -> Error {
|
||||||
if len(indent) > 0 {
|
if len(indent) > 0 {
|
||||||
state := Indent_State{indent = indent, at_line_start = false}
|
state := Indent_State {
|
||||||
|
indent = indent,
|
||||||
|
at_line_start = false,
|
||||||
|
}
|
||||||
strings.write_string(b, indent) // first line always gets indent
|
strings.write_string(b, indent) // first line always gets indent
|
||||||
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state)
|
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state)
|
||||||
}
|
}
|
||||||
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil)
|
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_line_start: ^bool) {
|
write_indented :: proc(
|
||||||
|
b: ^strings.Builder,
|
||||||
|
indent: string,
|
||||||
|
content: string,
|
||||||
|
at_line_start: ^bool,
|
||||||
|
) {
|
||||||
if len(indent) == 0 || len(content) == 0 {
|
if len(indent) == 0 || len(content) == 0 {
|
||||||
strings.write_string(b, content)
|
strings.write_string(b, content)
|
||||||
return
|
return
|
||||||
@@ -544,7 +551,7 @@ write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_
|
|||||||
render_nodes :: proc(
|
render_nodes :: proc(
|
||||||
current: Template,
|
current: Template,
|
||||||
nodes: []Node,
|
nodes: []Node,
|
||||||
ctx: ^[dynamic]any,
|
ctx: ^Context_Stack,
|
||||||
partials: map[string]Template,
|
partials: map[string]Template,
|
||||||
b: ^strings.Builder,
|
b: ^strings.Builder,
|
||||||
blocks: map[string]Block_Override = nil,
|
blocks: map[string]Block_Override = nil,
|
||||||
@@ -682,7 +689,7 @@ render_nodes :: proc(
|
|||||||
if elem_info != nil {
|
if elem_info != nil {
|
||||||
for j in 0 ..< count {
|
for j in 0 ..< count {
|
||||||
elem := extract_list_element(elem_info, data, j)
|
elem := extract_list_element(elem_info, data, j)
|
||||||
append(ctx, elem)
|
context_push(ctx, elem, current, node)
|
||||||
defer pop(ctx)
|
defer pop(ctx)
|
||||||
render_nodes(
|
render_nodes(
|
||||||
current,
|
current,
|
||||||
@@ -695,9 +702,17 @@ render_nodes :: proc(
|
|||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
append(ctx, val)
|
context_push(ctx, val, current, node)
|
||||||
defer pop(ctx)
|
defer pop(ctx)
|
||||||
render_nodes(current, children, ctx, partials, b, blocks, indent_state) or_return
|
render_nodes(
|
||||||
|
current,
|
||||||
|
children,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
b,
|
||||||
|
blocks,
|
||||||
|
indent_state,
|
||||||
|
) or_return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i += 1 + len(node.children)
|
i += 1 + len(node.children)
|
||||||
@@ -715,7 +730,15 @@ render_nodes :: proc(
|
|||||||
val = transformed
|
val = transformed
|
||||||
}
|
}
|
||||||
if !is_truthy(val) {
|
if !is_truthy(val) {
|
||||||
render_nodes(current, node.children, ctx, partials, b, blocks, indent_state) or_return
|
render_nodes(
|
||||||
|
current,
|
||||||
|
node.children,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
b,
|
||||||
|
blocks,
|
||||||
|
indent_state,
|
||||||
|
) or_return
|
||||||
}
|
}
|
||||||
i += 1 + len(node.children)
|
i += 1 + len(node.children)
|
||||||
|
|
||||||
@@ -937,3 +960,25 @@ warn_unmatched_block_overrides :: proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context_push :: proc(ctx: ^Context_Stack, val: any, current: Template, node: Node) {
|
||||||
|
append(ctx, val)
|
||||||
|
if len(ctx^) == MAX_CONTEXT_DEPTH + 1 {
|
||||||
|
warn_context_depth(current, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn_context_depth emits a diagnostic warning pointing at the section tag
|
||||||
|
// whose push carried the context stack past MAX_CONTEXT_DEPTH.
|
||||||
|
warn_context_depth :: proc(current: Template, node: Node) {
|
||||||
|
msg := fmt.tprintf(
|
||||||
|
"context stack depth exceeded %d (possible recursive section/partial)",
|
||||||
|
MAX_CONTEXT_DEPTH,
|
||||||
|
)
|
||||||
|
path := current.path
|
||||||
|
if path == "" {
|
||||||
|
path = "<input>"
|
||||||
|
}
|
||||||
|
diag := format_error(path, current.source, node.pos, msg, "", colorize = should_colorize())
|
||||||
|
log.warnf("%s", diag)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#+test
|
#+test
|
||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
|
import "core:log"
|
||||||
import "core:mem"
|
import "core:mem"
|
||||||
|
import "core:strings"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -124,3 +126,39 @@ leak_repeated_render :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A deeply nested map pushes the context stack past MAX_CONTEXT_DEPTH (16).
|
||||||
|
// The depth warning must be non-fatal: rendering still succeeds.
|
||||||
|
@(test)
|
||||||
|
test_context_depth_warns :: proc(t: ^testing.T) {
|
||||||
|
context.logger = log.nil_logger()
|
||||||
|
|
||||||
|
AMT :: MAX_CONTEXT_DEPTH + 2
|
||||||
|
|
||||||
|
// Build AMT nested {x: {...}} levels; the innermost holds `leaf`.
|
||||||
|
data := make(map[string]any, context.temp_allocator)
|
||||||
|
data["leaf"] = "found"
|
||||||
|
for _ in 0 ..< AMT {
|
||||||
|
outer := make(map[string]any, context.temp_allocator)
|
||||||
|
outer["x"] = data
|
||||||
|
data = outer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template: AMT nested {{#x}} sections around {{leaf}}.
|
||||||
|
src: strings.Builder
|
||||||
|
strings.builder_init(&src, context.temp_allocator)
|
||||||
|
for _ in 0 ..< AMT do strings.write_string(&src, "{{#x}}")
|
||||||
|
strings.write_string(&src, "{{leaf}}")
|
||||||
|
for _ in 0 ..< AMT do strings.write_string(&src, "{{/x}}")
|
||||||
|
template := strings.to_string(src)
|
||||||
|
|
||||||
|
tmpl, perr := parse(template, "<depth-test>", context.temp_allocator)
|
||||||
|
testing.expect(t, perr == nil, "should parse")
|
||||||
|
if perr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, rerr := render(tmpl, data, allocator = context.temp_allocator)
|
||||||
|
testing.expect(t, rerr == nil, "depth warning must be non-fatal")
|
||||||
|
testing.expect_value(t, result, "found")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,10 +153,7 @@ suggest_correction :: proc(available: []string, missing: string) -> string {
|
|||||||
if len(available) == 0 || len(missing) == 0 {
|
if len(available) == 0 || len(missing) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
threshold := 2
|
threshold := max(2, len(missing) / 3)
|
||||||
if len(missing) > 8 {
|
|
||||||
threshold = len(missing) / 4
|
|
||||||
}
|
|
||||||
|
|
||||||
best: string
|
best: string
|
||||||
best_dist := threshold + 1
|
best_dist := threshold + 1
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ Inner :: struct {
|
|||||||
bar: int,
|
bar: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Page :: struct {
|
||||||
|
title: string,
|
||||||
|
}
|
||||||
|
|
||||||
Outer :: struct {
|
Outer :: struct {
|
||||||
title: string,
|
title: string,
|
||||||
page.title: string,
|
page: Page,
|
||||||
inner: Inner,
|
inner: Inner,
|
||||||
numbers: [3]int,
|
numbers: [3]int,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user