mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 02:01:56 -04:00
feat: Added first and last filters.
This commit is contained in:
@@ -18,8 +18,10 @@ Remember to polish existing features before moving on to new ones.
|
|||||||
- alt1: `layouts`, `partials`
|
- alt1: `layouts`, `partials`
|
||||||
- alt2: `templates/layouts`, `templates/partials`
|
- alt2: `templates/layouts`, `templates/partials`
|
||||||
- [ ] pipes
|
- [ ] pipes
|
||||||
- [ ] first
|
- [ ] first/last
|
||||||
- [ ] last
|
- [ ] warn when using negative numbers
|
||||||
|
- [ ] warn when using default arg on string
|
||||||
|
- [ ] add the above warnings to DIAGNOSTIC_TODOS.md
|
||||||
- [ ] sort_by
|
- [ ] sort_by
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
+68
-8
@@ -4,8 +4,8 @@ import "base:runtime"
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:reflect"
|
import "core:reflect"
|
||||||
|
import "core:strconv"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
|
||||||
|
|
||||||
import diags "../diagnostics"
|
import diags "../diagnostics"
|
||||||
import "core:time/datetime"
|
import "core:time/datetime"
|
||||||
@@ -22,6 +22,8 @@ DEFAULT_DATE_FORMAT :: "2 Jan 2006"
|
|||||||
Pipe_Op :: enum {
|
Pipe_Op :: enum {
|
||||||
Format,
|
Format,
|
||||||
Group_By,
|
Group_By,
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipe op names are derived from the enum via reflection (lowercased).
|
// Pipe op names are derived from the enum via reflection (lowercased).
|
||||||
@@ -133,11 +135,7 @@ parse_pipeline :: proc(
|
|||||||
pipe_count := strings.count(content, "|")
|
pipe_count := strings.count(content, "|")
|
||||||
if pipe_count > MAX_PIPES {
|
if pipe_count > MAX_PIPES {
|
||||||
return "", Error_Body {
|
return "", Error_Body {
|
||||||
msg = fmt.tprintf(
|
msg = fmt.tprintf("pipe expression has %d filters, max is %d", pipe_count, MAX_PIPES),
|
||||||
"pipe expression has %d filters, max is %d",
|
|
||||||
pipe_count,
|
|
||||||
MAX_PIPES,
|
|
||||||
),
|
|
||||||
pos = pos,
|
pos = pos,
|
||||||
kind = .Syntax,
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
@@ -267,8 +265,8 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
|
|||||||
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
||||||
}
|
}
|
||||||
return nil, Error_Body {
|
return nil, Error_Body {
|
||||||
msg = fmt.tprintf("unknown pipe op '%s'", filter.op),
|
msg = fmt.tprintf("unknown pipe op '%s'", filter.op),
|
||||||
pos = filter.op_pos,
|
pos = filter.op_pos,
|
||||||
span = len(filter.op),
|
span = len(filter.op),
|
||||||
kind = .Data,
|
kind = .Data,
|
||||||
hint = hint,
|
hint = hint,
|
||||||
@@ -316,6 +314,10 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
|
|||||||
} else {
|
} else {
|
||||||
return any{new_clone(str2, context.temp_allocator), typeid_of(string)}, nil
|
return any{new_clone(str2, context.temp_allocator), typeid_of(string)}, nil
|
||||||
}
|
}
|
||||||
|
case .First:
|
||||||
|
return take(value, filter.args[:], pos, from_start = true)
|
||||||
|
case .Last:
|
||||||
|
return take(value, filter.args[:], pos, from_start = false)
|
||||||
}
|
}
|
||||||
return {}, nil
|
return {}, nil
|
||||||
}
|
}
|
||||||
@@ -411,3 +413,61 @@ apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, er
|
|||||||
|
|
||||||
return groups, nil
|
return groups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
take :: proc(value: any, args: []string, pos: int, from_start: bool) -> (any, Error) {
|
||||||
|
elem_info, count, data := list_info(value)
|
||||||
|
is_list := elem_info != nil
|
||||||
|
from_start := from_start
|
||||||
|
|
||||||
|
str: string
|
||||||
|
if !is_list {
|
||||||
|
s, is_str := reflect.as_string(value)
|
||||||
|
if !is_str {
|
||||||
|
return nil, Error_Body{msg = "requires a list or string", pos = pos, kind = .Data}
|
||||||
|
}
|
||||||
|
str = s
|
||||||
|
count = len(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 1
|
||||||
|
if !is_list && len(args) == 0 {
|
||||||
|
return nil, Error_Body{msg = "requires an argument for strings", pos = pos, kind = .Data}
|
||||||
|
}
|
||||||
|
if len(args) > 0 {
|
||||||
|
parsed, ok := strconv.parse_int(args[0])
|
||||||
|
if !ok {
|
||||||
|
return nil, Error_Body {
|
||||||
|
msg = fmt.tprintf("invalid argument '%s'", args[0]),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 0 {
|
||||||
|
from_start = !from_start
|
||||||
|
n = 0 - n // abs(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
start: int
|
||||||
|
end: int
|
||||||
|
if from_start {
|
||||||
|
start = 0
|
||||||
|
end = min(count, n)
|
||||||
|
} else {
|
||||||
|
start = max(0, count - n)
|
||||||
|
end = count
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_list {
|
||||||
|
result := make([dynamic]any, 0, max(0, end - start), context.temp_allocator)
|
||||||
|
for j in start ..< end {
|
||||||
|
append(&result, extract_list_element(elem_info, data, j))
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
} else {
|
||||||
|
return str[start:end], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+143
-2
@@ -1,3 +1,4 @@
|
|||||||
|
#+feature dynamic-literals
|
||||||
#+test
|
#+test
|
||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
@@ -567,7 +568,10 @@ test_format_mst_no_offset_no_timezone :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_unknown_pipe_op_suggestion :: proc(t: ^testing.T) {
|
test_unknown_pipe_op_suggestion :: proc(t: ^testing.T) {
|
||||||
filter := Pipe_Filter{op = "formats", op_pos = 0}
|
filter := Pipe_Filter {
|
||||||
|
op = "formats",
|
||||||
|
op_pos = 0,
|
||||||
|
}
|
||||||
_, err := apply_filter("2026-01-15", &filter, 0, nil)
|
_, err := apply_filter("2026-01-15", &filter, 0, nil)
|
||||||
testing.expect(t, err != nil, "should error on unknown op")
|
testing.expect(t, err != nil, "should error on unknown op")
|
||||||
b := body(err)
|
b := body(err)
|
||||||
@@ -577,9 +581,146 @@ test_unknown_pipe_op_suggestion :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_unknown_pipe_op_no_suggestion :: proc(t: ^testing.T) {
|
test_unknown_pipe_op_no_suggestion :: proc(t: ^testing.T) {
|
||||||
filter := Pipe_Filter{op = "xyz", op_pos = 0}
|
filter := Pipe_Filter {
|
||||||
|
op = "xyz",
|
||||||
|
op_pos = 0,
|
||||||
|
}
|
||||||
_, err := apply_filter("2026-01-15", &filter, 0, nil)
|
_, err := apply_filter("2026-01-15", &filter, 0, nil)
|
||||||
testing.expect(t, err != nil, "should error on unknown op")
|
testing.expect(t, err != nil, "should error on unknown op")
|
||||||
b := body(err)
|
b := body(err)
|
||||||
testing.expect(t, b.hint == "", "no suggestion expected for 'xyz'")
|
testing.expect(t, b.hint == "", "no suggestion expected for 'xyz'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group 1: Lists
|
||||||
|
|
||||||
|
First_Last_List_Data: map[string][5]string = {
|
||||||
|
"items" = [5]string{"foo", "bar", "bat", "qux", "quz"},
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_first_list_no_args_equals_first_one :: proc(t: ^testing.T) {
|
||||||
|
tpl_a, _ := parse(
|
||||||
|
"{{#items | first}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result_a, _ := render(tpl_a, First_Last_List_Data, {}, context.temp_allocator)
|
||||||
|
|
||||||
|
tpl_b, _ := parse(
|
||||||
|
"{{#items | first 1}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result_b, _ := render(tpl_b, First_Last_List_Data, {}, context.temp_allocator)
|
||||||
|
|
||||||
|
testing.expect_value(t, result_a, "<li>foo</li>")
|
||||||
|
testing.expect_value(t, result_a, result_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_first_list_n :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_List_Data
|
||||||
|
tpl, _ := parse(
|
||||||
|
"{{#items | first 3}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "<li>foo</li><li>bar</li><li>bat</li>")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_first_list_negative_equals_last :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_List_Data
|
||||||
|
tpl, _ := parse(
|
||||||
|
"{{#items | first -2}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "<li>qux</li><li>quz</li>")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_last_list_no_args_equals_last_one :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_List_Data
|
||||||
|
tpl_a, _ := parse(
|
||||||
|
"{{#items | last}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result_a, _ := render(tpl_a, data, {}, context.temp_allocator)
|
||||||
|
|
||||||
|
tpl_b, _ := parse(
|
||||||
|
"{{#items | last 1}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result_b, _ := render(tpl_b, data, {}, context.temp_allocator)
|
||||||
|
|
||||||
|
testing.expect_value(t, result_a, "<li>quz</li>")
|
||||||
|
testing.expect_value(t, result_b, "<li>quz</li>")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_last_list_n :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_List_Data
|
||||||
|
tpl, _ := parse(
|
||||||
|
"{{#items | last 3}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "<li>bat</li><li>qux</li><li>quz</li>")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_last_list_negative_equals_first :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_List_Data
|
||||||
|
tpl, _ := parse(
|
||||||
|
"{{#items | last -2}}<li>{{.}}</li>{{/items}}",
|
||||||
|
"<test>",
|
||||||
|
allocator = context.temp_allocator,
|
||||||
|
)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "<li>foo</li><li>bar</li>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group 2: Strings
|
||||||
|
|
||||||
|
First_Last_String_Data := map[string]string {
|
||||||
|
"word" = "Hello, World!",
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_first_string_n :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_String_Data
|
||||||
|
tpl, _ := parse("{{word | first 5}}", "<test>", allocator = context.temp_allocator)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "Hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_last_string_n :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_String_Data
|
||||||
|
tpl, _ := parse("{{word | last 6}}", "<test>", allocator = context.temp_allocator)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "World!")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_first_string_negative_equals_last :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_String_Data
|
||||||
|
tpl, _ := parse("{{word | first -6}}", "<test>", allocator = context.temp_allocator)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "World!")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_last_string_negative_equals_first :: proc(t: ^testing.T) {
|
||||||
|
data := First_Last_String_Data
|
||||||
|
tpl, _ := parse("{{word | last -5}}", "<test>", allocator = context.temp_allocator)
|
||||||
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
|
testing.expect_value(t, result, "Hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user