Compare commits

12 Commits

Author SHA1 Message Date
PedroEdiaz
7124aeb94e Add partial use of comments 2025-06-19 22:01:03 -06:00
PedroEdiaz
0f76ad666a Pass section 2025-06-19 21:57:18 -06:00
PedroEdiaz
774d1fd8eb Clean up 2025-06-19 01:42:08 -06:00
PedroEdiaz
65c6a901d2 preprocess -> section 2025-06-19 00:35:42 -06:00
PedroEdiaz
f9f4f5d34a Add decoding of map 2025-06-19 00:35:24 -06:00
PedroEdiaz
03292a15c8 Add testing for spec 2025-06-19 00:34:16 -06:00
PedroEdiaz
b707360c85 Merge remote-tracking branch 'refs/remotes/origin/mustacheless' into mustacheless 2025-06-18 16:34:17 -06:00
PedroEdiaz
6388d52499 Add mustache spec 2025-06-18 15:59:50 -06:00
PedroEdiaz
31f2b1c5c9 Add Sections as mustache for arrays 2025-06-18 01:35:19 -06:00
PedroEdiaz
e4e797fe15 Add: Recursivity in decode 2025-06-17 19:43:31 -06:00
PedroEdiaz
c466e55169 Add Integer and Enum Decode 2025-06-17 18:46:30 -06:00
PedroEdiaz
fedfd70c50 Change map -> struct 2025-06-17 11:43:52 -06:00
7 changed files with 371 additions and 219 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "spec"]
path = spec
url = https://github.com/mustache/spec

View File

@@ -1,5 +1,5 @@
# Templateless # Mustacheless
A lightweight, logicless templating library for the A subset of {{mustache}}, logicless templating library for the
[Odin programming language](https://odin-lang.org/). This library allows you to [Odin programming language](https://odin-lang.org/). This library allows you to
define templates using placeholder variables within strings and replace them define templates using placeholder variables within strings and replace them
with values from a provided dictionary. with values from a provided dictionary.

155
decode.odin Normal file
View File

@@ -0,0 +1,155 @@
package mustache
import "base:runtime"
import "core:reflect"
import "core:strings"
import "core:mem"
import "core:log"
@private
_clean_html :: proc(s: string) -> string {
ret: strings.Builder
for c in s do switch c {
case '<':
strings.write_string(&ret, "&lt;")
case '>':
strings.write_string(&ret, "&gt;")
case '&':
strings.write_string(&ret, "&amp;")
case '"':
strings.write_string(&ret, "&quot;")
case '\'':
strings.write_string(&ret, "&apos;")
case:
strings.write_rune(&ret, c)
}
return strings.to_string(ret)
}
decode :: proc( v: any, key: string ) -> any {
ti := type_info_of(v.id)
variant := runtime.type_info_base(ti).variant
#partial switch i in variant {
case runtime.Type_Info_Enumerated_Array:
return decode(any{v.data, ti.id}, key)
case runtime.Type_Info_Union:
return decode(any{v.data, reflect.union_variant_typeid(v) }, key)
}
if key == "." {
return v
}
#partial switch i in variant {
case runtime.Type_Info_Struct:
newkey, err := strings.split_after_n(key, ".", 2)
if err != nil {
return nil
}
defer delete(newkey)
newkey_0 := newkey[0]
newkey_1 := "."
if len(newkey) != 2 {
newkey_0 = newkey[0][:len(newkey[0])-1]
newkey_1 = newkey[1]
}
return decode( reflect.struct_field_value_by_name(v, newkey_0), newkey_1 )
case runtime.Type_Info_Map:
newkey, err := strings.split_after_n(key, ".", 2)
if err!= nil {
return nil
}
defer delete(newkey)
newkey_0 := newkey[0]
newkey_1 := "."
if len(newkey) == 2 {
newkey_0 = newkey[0][:len(newkey[0])-1]
newkey_1 = newkey[1]
}
m := (^mem.Raw_Map)(v.data)
ks, vs, _, _, _ := runtime.map_kvh_data_dynamic(m^, i.map_info)
for j in 0..<runtime.map_cap(m^) {
key_data := runtime.map_cell_index_dynamic(ks, i.map_info.ks, uintptr(j))
ky, ok := reflect.as_string(any{rawptr(key_data), i.key.id})
if !ok {
return nil
}
if ky == newkey_0 {
t := any{rawptr(runtime.map_cell_index_dynamic(vs, i.map_info.vs, uintptr(j))), i.value.id}
return decode(t, newkey_1 )
}
}
return nil
}
return nil
}
@private
mustache_section :: proc( r: ^strings.Reader, v, p: any, section_key: string, inv: bool = false ) -> string {
tmp := mustache(r, v, section_key)
defer delete(tmp)
tmp2 := mustache(tmp, p, section_key)
if inv {
return tmp2
}
delete(tmp2)
return ""
}
section :: proc( r: ^strings.Reader, v: any, section_key: string, inv: bool = false ) -> string {
save := r.i
t := reflect.any_base(decode(v, section_key))
ti := type_info_of(t.id)
#partial switch i in runtime.type_info_base(ti).variant {
case runtime.Type_Info_Slice:
ret : string= ""
for i in 0..<reflect.length(t) {
elem :=reflect.index(t, i);
strings.reader_seek(r, save, .Start)
tmp := mustache_section(r, elem, v, section_key, !inv)
defer delete(tmp)
ret = strings.concatenate({ret, tmp})
}
return ret
case runtime.Type_Info_Boolean:
b, _ := reflect.as_bool(t)
return mustache_section(r, t, v, section_key, b~inv)
}
if t == nil {
return mustache_section(r, t, v, section_key, inv)
}
return mustache_section(r, t, v, section_key, !inv)
}

157
mustache.odin Normal file
View File

@@ -0,0 +1,157 @@
package mustache
import "core:fmt"
import "core:strings"
state :: enum {
writing,
reading_key,
open_bracket,
close_bracket,
}
mustache :: proc{mustache_reader, mustache_string}
mustache_string :: proc(fmt: string, data: any , section_key: string = "") -> string {
r : strings.Reader
strings.reader_init(&r, fmt)
return mustache(&r, data, section_key)
}
mustache_reader :: proc(r: ^strings.Reader, data: any, section_key: string = "" ) -> string {
/*
This is the main parser for mustache templates, it's a recursive decent parser, that works as a state machine,
it manipulates the processed template with data, `ret`, and the key to element on data `key`, according to the states.
This approach let us process the data as fast as posible.
*/
ret, key: strings.Builder
defer strings.builder_destroy(&key)
s:= state.writing
for {
c, _, err := strings.reader_read_rune(r);
if err != nil {
break
}
switch c {
case '{':
switch s {
case .open_bracket:
s=.reading_key
case .close_bracket:
strings.write_string(&ret, "}{" )
s=.writing
case .writing:
s=.open_bracket
case .reading_key:
strings.write_rune(&key, '{' )
}
case '}':
switch s {
case .open_bracket:
strings.write_string(&ret, "{}" )
s=.writing
case .close_bracket:
// Work with key
skey := strings.to_string(key)
strings.builder_reset(&key)
s=.writing
if len(skey) == 0 {
break
}
switch skey[0] {
case '/':
if skey[1:] == section_key {
return strings.to_string(ret)
}
case '#':
strings.write_string(&ret, section(r, data, skey[1:]) )
case '^':
strings.write_string(&ret, section(r, data, skey[1:], true) )
case '&':
strings.write_string(&ret, fmt.tprintf("%v",decode(data, skey[1:])) )
case '!':
case:
dec := decode(data, skey)
if dec == nil {
// If not decoded write as key
strings.write_string(&ret, "{{" )
strings.write_string(&ret, skey )
strings.write_string(&ret, "}}" )
break
}
clean := _clean_html(fmt.tprintf("%v", dec))
defer delete(clean)
strings.write_string(&ret, clean)
}
case .writing:
strings.write_rune(&ret, '}' )
s=.writing
case .reading_key:
s=.close_bracket
}
case:
switch s {
case .open_bracket:
strings.write_rune(&ret, '{' )
strings.write_rune(&ret, c )
s=.writing
case .close_bracket:
strings.write_rune(&key, '}' )
strings.write_rune(&key, c )
s=.reading_key
case .writing:
strings.write_rune(&ret, c )
s=.writing
case .reading_key:
strings.write_rune(&key, c )
}
}
}
switch s {
case .open_bracket:
strings.write_rune(&ret, '{' )
case .reading_key:
strings.write_string(&ret,"{{")
strings.write_string(&ret,strings.to_string(key))
case .close_bracket:
strings.write_string(&ret,"{{")
strings.write_string(&ret,strings.to_string(key))
strings.write_rune(&ret, '}' )
case .writing:
}
return strings.to_string(ret)
}
/*
1-Clause BSD NON-AI License
Copyright (c) 2025
NVIAM. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. The source code and any modifications made to it may not be used for the purpose of training or improving machine learning algorithms,
including but not limited to artificial intelligence, natural language processing, or data mining. This condition applies to any derivatives,
modifications, or updates based on the Software code. Any usage of the source code in an AI-training dataset is considered a breach of this License.
THIS SOFTWARE IS PROVIDED BY NVIAM AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL NVIAM BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

1
spec Submodule

Submodule spec added at 97c05b0652

View File

@@ -1,118 +0,0 @@
package template
import "core:strings"
@private
state :: enum {
writing,
reading_key,
open_bracket,
close_bracket,
}
/*
Input:
- fmt: A string with placeholders in the form `{{key}}`.
- dict: A map from string to string, where each key corresponds to a placeholder in the format string.
Returns:
A new string with all placeholders replaced by their corresponding values from
the dictionary. If a key is missing in the dictionary, the placeholder is
replaces with an empty string.
*/
template :: proc(fmt: string, dict: map[string]string ) -> string {
/*
template works as a state machine, it manipulates `b` (returned string)
and `key` (placeholder string), according to the states. No error
returns are needed because if `key` is not close it writes it as
it's not a placeholder
*/
b, key: strings.Builder
defer strings.builder_destroy(&key)
s:= state.writing
for c in fmt do switch c {
case '{':
switch s {
case .open_bracket:
s=.reading_key
case .close_bracket:
strings.write_string(&b, "}{" )
s=.writing
case .writing:
s=.open_bracket
case .reading_key:
strings.write_rune(&key, '{' )
}
case '}':
switch s {
case .open_bracket:
strings.write_string(&b, "{}" )
s=.writing
case .close_bracket:
strings.write_string(&b, dict[strings.to_string(key)] )
strings.builder_reset(&key)
s=.writing
case .writing:
strings.write_rune(&b, '}' )
s=.writing
case .reading_key:
s=.close_bracket
}
case:
switch s {
case .open_bracket:
strings.write_rune(&b, '{' )
strings.write_rune(&b, c )
s=.writing
case .close_bracket:
strings.write_rune(&key, '}' )
strings.write_rune(&key, c )
s=.reading_key
case .writing:
strings.write_rune(&b, c )
s=.writing
case .reading_key:
strings.write_rune(&key, c )
}
}
switch s {
case .open_bracket:
strings.write_rune(&b, '{' )
case .reading_key:
strings.write_string(&b,"{{")
strings.write_string(&b,strings.to_string(key))
case .close_bracket:
strings.write_string(&b,"{{")
strings.write_string(&b,strings.to_string(key))
strings.write_rune(&b, '}' )
case .writing:
}
return strings.to_string(b)
}
/*
1-Clause BSD NON-AI License
Copyright (c) 2025
NVIAM. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. The source code and any modifications made to it may not be used for the purpose of training or improving machine learning algorithms,
including but not limited to artificial intelligence, natural language processing, or data mining. This condition applies to any derivatives,
modifications, or updates based on the Software code. Any usage of the source code in an AI-training dataset is considered a breach of this License.
THIS SOFTWARE IS PROVIDED BY NVIAM AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL NVIAM BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

View File

@@ -1,108 +1,62 @@
#+feature dynamic-literals #+feature dynamic-literals
#+private #+private
package template package mustache
import "core:os"
import "core:log"
import "core:testing" import "core:testing"
import "core:encoding/json"
data_struct :: union {
map[string]data_struct,
[]data_struct,
string,
bool,
}
test_struct :: struct {
overview: string,
tests: []struct{
name, desc, template, expected: string,
data: data_struct
}
}
@(test) @(test)
test1 :: proc(t: ^testing.T){ spec_test :: proc(t: ^testing.T){
fmt := "{" test_files := []string {
tmp := template(fmt,{}) "./spec/specs/sections.json",
defer delete(tmp) "./spec/specs/interpolation.json",
testing.expect(t, tmp==fmt, tmp) "./spec/specs/inverted.json",
} "./spec/specs/comments.json",
@(test) /*
test2 :: proc(t: ^testing.T){ "./spec/specs/partials.json",
fmt := "}" "./spec/specs/delimiters.json",
tmp := template(fmt,{}) */
defer delete(tmp) }
testing.expect(t, tmp==fmt, tmp)
}
@(test)
test3 :: proc(t: ^testing.T){
fmt := "{{"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp==fmt, tmp)
}
@(test)
test4 :: proc(t: ^testing.T){
fmt := "{{}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp==fmt, tmp)
}
@(test)
test5 :: proc(t: ^testing.T){
fmt := "{{}}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp=="", tmp)
}
@(test)
test6 :: proc(t: ^testing.T){
fmt := "{{foo}}"
dict := map[string]string{"foo"="var"} for i in test_files {
defer delete(dict) data, err:=os.read_entire_file_from_filename_or_err(i)
defer delete(data)
tmp := template(fmt, dict) if err != nil {
defer delete(tmp) testing.expectf(t, false, "%v", err)
testing.expect(t, tmp=="var", tmp) }
}
@(test) test: test_struct
test7 :: proc(t: ^testing.T){
fmt := "{{{}}" json.unmarshal(data, &test, allocator=context.temp_allocator)
tmp := template(fmt,{})
defer delete(tmp) failed := 0
testing.expect(t, tmp=="", tmp) for j in test.tests {
} ret := mustache(j.template, j.data)
@(test) defer delete(ret)
test8 :: proc(t: ^testing.T){
fmt := "{{}}}" if ret!=j.expected {
tmp := template(fmt,{}) log.warnf( "[%s:%s]: %s", i, j.name, j.desc )
defer delete(tmp) failed += 1
testing.expect(t, tmp=="}", tmp) }
} }
@(test) log.infof( "[%s] Passed: (%d/%d)", i, len(test.tests)-failed, len(test.tests) )
test9 :: proc(t: ^testing.T){ }
fmt := "{{{}}}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp=="}", tmp)
}
@(test)
test10 :: proc(t: ^testing.T){
fmt := "{{} }}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp=="", tmp)
}
@(test)
test11 :: proc(t: ^testing.T){
fmt := "{{{} }}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp=="", tmp)
}
@(test)
test12 :: proc(t: ^testing.T){
fmt := " {{{} }}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp==" ", tmp)
}
@(test)
test13 :: proc(t: ^testing.T){
fmt := "{{{} }} "
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp==" ", tmp)
}
@(test)
test14 :: proc(t: ^testing.T){
fmt := "{{{}}}"
tmp := template(fmt,{})
defer delete(tmp)
testing.expect(t, tmp=="}", tmp)
} }