From 526a2e7e51fb5c9e66357a5c3926ed2979cdf83d Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Thu, 24 Apr 2025 14:08:40 +0300 Subject: [PATCH 01/18] RESET: Fresh start, working and tested Expressions + Context --- Makefile | 2 +- cli/cli.go | 54 +----- example/components/icon.html | 1 + example/components/root.html | 89 +++++++++ example/components/unmarked-link.html | 5 + example/header.html | 4 - example/icon.svg | 4 - example/index.html | 7 +- example/root.html | 35 ---- go.mod | 6 +- go.sum | 11 ++ lang/context/context.go | 237 +++++++++++++++++++++++ lang/context/context_test.go | 170 +++++++++++++++++ lang/context/parse.go | 41 ++++ lang/expressions/parse.go | 42 +++++ lang/printer/printer.go | 18 ++ main.go | 13 +- mend/assert/assert.go | 34 ---- mend/attrs/attributes.go | 105 ----------- mend/attrs/attributes_render.go | 23 --- mend/attrs/attributes_sort.go | 33 ---- mend/attrs/list.go | 47 ----- mend/attrs/writer.go | 8 - mend/expression.go | 80 -------- mend/expression_compute.go | 174 ----------------- mend/settings/settings.go | 16 -- mend/tags/custom_extend.go | 46 ----- mend/tags/custom_if.go | 50 ----- mend/tags/custom_range.go | 31 ---- mend/tags/node.go | 126 ------------- mend/tags/node_comment.go | 47 ----- mend/tags/node_doctype.go | 43 ----- mend/tags/node_root.go | 26 --- mend/tags/node_tag.go | 63 ------- mend/tags/node_text.go | 46 ----- mend/tags/node_void.go | 48 ----- mend/tags/tags.go | 17 -- mend/tags/writer.go | 8 - mend/template.go | 63 ------- mend/template_branch.go | 34 ---- mend/template_errors.go | 41 ---- mend/template_find.go | 32 ---- mend/template_parse.go | 258 -------------------------- 43 files changed, 637 insertions(+), 1601 deletions(-) create mode 100644 example/components/icon.html create mode 100644 example/components/root.html create mode 100644 example/components/unmarked-link.html delete mode 100644 example/header.html delete mode 100644 example/icon.svg delete mode 100644 example/root.html create mode 100644 lang/context/context.go create mode 100644 lang/context/context_test.go create mode 100644 lang/context/parse.go create mode 100644 lang/expressions/parse.go create mode 100644 lang/printer/printer.go delete mode 100644 mend/assert/assert.go delete mode 100644 mend/attrs/attributes.go delete mode 100644 mend/attrs/attributes_render.go delete mode 100644 mend/attrs/attributes_sort.go delete mode 100644 mend/attrs/list.go delete mode 100644 mend/attrs/writer.go delete mode 100644 mend/expression.go delete mode 100644 mend/expression_compute.go delete mode 100644 mend/settings/settings.go delete mode 100644 mend/tags/custom_extend.go delete mode 100644 mend/tags/custom_if.go delete mode 100644 mend/tags/custom_range.go delete mode 100644 mend/tags/node.go delete mode 100644 mend/tags/node_comment.go delete mode 100644 mend/tags/node_doctype.go delete mode 100644 mend/tags/node_root.go delete mode 100644 mend/tags/node_tag.go delete mode 100644 mend/tags/node_text.go delete mode 100644 mend/tags/node_void.go delete mode 100644 mend/tags/tags.go delete mode 100644 mend/tags/writer.go delete mode 100644 mend/template.go delete mode 100644 mend/template_branch.go delete mode 100644 mend/template_errors.go delete mode 100644 mend/template_find.go delete mode 100644 mend/template_parse.go diff --git a/Makefile b/Makefile index 156781d..b29bada 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION := v0.2.0-alpha +VERSION := v1.0.0-alpha BINARY_NAME := mend run: diff --git a/cli/cli.go b/cli/cli.go index b4b0383..ccece5a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,62 +1,24 @@ package cli import ( - "os" - "path/filepath" "strings" - "github.com/bbfh-dev/mend/mend" - "github.com/bbfh-dev/mend/mend/settings" + "github.com/bbfh-dev/mend/lang/printer" ) var Options struct { - Input string `alt:"i" desc:"Set global input parameters"` - Indent int `desc:"Set amount of spaces to indent with. Gets ignored if --tabs is used"` Tabs bool `alt:"t" desc:"Use tabs for indentation"` - StripComments bool `desc:"Strips away any comments"` + Indent int `default:"4" desc:"The amount of spaces to be used for indentation (overwriten by --tabs)"` + StripComments bool `desc:"Strips away HTML comments from the output"` + Input string `desc:"Provide input to the provided files in the following format: 'attr1=value1,attr2=value2,...'"` } func Main(args []string) error { - if len(args) == 0 { - return nil - } - - settings.KeepComments = !Options.StripComments + printer.StripComments = Options.StripComments if Options.Tabs { - settings.IndentWith = "\t" - } else if Options.Indent != 0 { - settings.IndentWith = strings.Repeat(" ", Options.Indent) - } - - var err error - for _, filename := range args { - filename, err = filepath.Abs(filename) - if err != nil { - return err - } - if _, err := os.Stat(filename); os.IsNotExist(err) { - return err - } - - file, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) - if err != nil { - return err - } - defer file.Close() - - params := "{}" - if Options.Input != "" { - params = Options.Input - } - settings.GlobalParams = params - - template := mend.NewTemplate(filename, params) - err = template.Parse(file) - if err != nil { - return err - } - - template.Root.Render(os.Stdout, 0) + printer.IndentString = "\t" + } else { + printer.IndentString = strings.Repeat(" ", Options.Indent) } return nil diff --git a/example/components/icon.html b/example/components/icon.html new file mode 100644 index 0000000..b52cd7a --- /dev/null +++ b/example/components/icon.html @@ -0,0 +1 @@ +<:include :src="./icons/[[ this.name ]].svg" class="type-icon" /> diff --git a/example/components/root.html b/example/components/root.html new file mode 100644 index 0000000..4392bdf --- /dev/null +++ b/example/components/root.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + [[ this.title ]] — Smithed™ + + + + + + + +
+ + +

Smithed

+
+ + + + Weld + + + <:if true="[[ this.searchbar ]]"> + + <:if false="[[ this.searchbar ]]"> +
+ + +
+ + +
+
+ + Inbox + + + +
+ +
+ <:slot /> +
+ + + + + diff --git a/example/components/unmarked-link.html b/example/components/unmarked-link.html new file mode 100644 index 0000000..6f0a790 --- /dev/null +++ b/example/components/unmarked-link.html @@ -0,0 +1,5 @@ + + <:slot> + [[ this.label? ]] + + diff --git a/example/header.html b/example/header.html deleted file mode 100644 index 3e76dde..0000000 --- a/example/header.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - This is a header! -
diff --git a/example/icon.svg b/example/icon.svg deleted file mode 100644 index ceb1cb0..0000000 --- a/example/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/example/index.html b/example/index.html index a6aa880..2483cf7 100644 --- a/example/index.html +++ b/example/index.html @@ -1,5 +1,4 @@ - -
- [[ .title || "Hello World" ]] -
+{{ define "index" }} + +{{ end }} diff --git a/example/root.html b/example/root.html deleted file mode 100644 index c0f2c4d..0000000 --- a/example/root.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - [[ -to-lower -to-snake-case -capitalize -invert ^.page.title ]] ([[ -get-length ^.page.title ]]) - - - - - -
- -
- - FIRST! - - - NOT FIRST! - - [[ -quote @.name ]] -
- This is not always here: [[ -quote @.count ||]] -
-
-
-
- - - diff --git a/go.mod b/go.mod index 11752dc..288e5ae 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,17 @@ go 1.24.1 toolchain go1.24.2 require ( - github.com/bbfh-dev/parsex/v2 v2.0.1-beta + github.com/bbfh-dev/parsex/v2 v2.0.2-beta github.com/iancoleman/strcase v0.3.0 github.com/tidwall/gjson v1.18.0 golang.org/x/net v0.37.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6a5ccfb..ded9384 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ github.com/bbfh-dev/parsex/v2 v2.0.1-beta h1:AJoVHPLVGlSk6Hiyr9KAE4QM+3S4rY+uiJswx4SWyU4= github.com/bbfh-dev/parsex/v2 v2.0.1-beta/go.mod h1:mUhq/3DUhv062sQz7wTDdMmzvtomf12iF5o3w7D2a7k= +github.com/bbfh-dev/parsex/v2 v2.0.2-beta h1:ro60ULutX+kXpmK7EnxfrPuc6/WNxR5QlF4UVgB2DIA= +github.com/bbfh-dev/parsex/v2 v2.0.2-beta/go.mod h1:mUhq/3DUhv062sQz7wTDdMmzvtomf12iF5o3w7D2a7k= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -11,3 +19,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lang/context/context.go b/lang/context/context.go new file mode 100644 index 0000000..60e7b1d --- /dev/null +++ b/lang/context/context.go @@ -0,0 +1,237 @@ +package context + +import ( + "fmt" + "slices" + "strings" + "unicode" + + "github.com/iancoleman/strcase" +) + +var GlobalContext *Context + +type Context struct { + Values map[string]any +} + +func New() *Context { + return &Context{ + Values: map[string]any{}, + } +} + +func (ctx *Context) Query(path []string) (string, error) { + if len(path) == 0 { + return ctx.String(), nil + } + + value, ok := ctx.Values[path[0]] + if !ok { + return "", fmt.Errorf("undefined property %q in %q", path[0], ctx.String()) + } + + switch value := value.(type) { + case *Context: + return value.Query(path[1:]) + default: + return fmt.Sprintf("%v", value), nil + } +} + +func (ctx *Context) Set(path []string, newValue string) { + switch len(path) { + case 0: + return + case 1: + ctx.Values[path[0]] = newValue + return + } + + value, ok := ctx.Values[path[0]] + if !ok { + value = New() + ctx.Values[path[0]] = value + } + + switch value := value.(type) { + case *Context: + value.Set(path[1:], newValue) + case string: + ctx.Values[path[0]] = newValue + } +} + +func (ctx *Context) String() string { + var builder strings.Builder + + for key, value := range ctx.Values { + switch value := value.(type) { + case string: + fmt.Fprintf(&builder, "%s='%s' ", key, value) + default: + fmt.Fprintf(&builder, "%s=%s ", key, value) + } + } + + str := builder.String() + if len(str) == 0 { + return "{}" + } + return "{" + str[:len(str)-1] + "}" +} + +func (ctx *Context) Compute(expression string) (string, error) { + fields := getFields(expression) + switch len(fields) { + case 0: + return "", nil + case 1: + return ctx.queryPath(fields[0]) + } + + variable, err := ctx.queryPath(fields[0]) + if err != nil && !slices.Contains(fields, "||") { + return "", err + } + + if len(fields) < 3 { + return "", fmt.Errorf("expression requires a value to the right of %q", fields[1]) + } + + var result string + switch fields[1] { + case "==": + result = fmt.Sprintf("%v", variable == fields[2]) + case "!=": + result = fmt.Sprintf("%v", variable != fields[2]) + case "has": + result = fmt.Sprintf("%v", strings.Contains(variable, fields[2])) + case "lacks": + result = fmt.Sprintf("%v", !strings.Contains(variable, fields[2])) + case "||": + if err != nil { + result = fields[2] + break + } + result = variable + default: + return "", fmt.Errorf("unknown operation %q", fields[1]) + } + + if len(fields) < 5 { + return result, nil + } + + switch fields[3] { + case "||": + if err != nil { + result = fields[4] + break + } + default: + return "", fmt.Errorf( + "unsupported after-operation %q, only || is supported for chaining", + fields[3], + ) + } + + return result, nil +} + +func (ctx *Context) queryPath(field string) (result string, err error) { + segments := strings.Split(field, ".")[1:] + path := make([]string, 0, len(segments)) + operations := make([]string, 0, len(segments)) + + for _, segment := range segments { + if strings.HasSuffix(segment, "()") { + operations = append(operations, segment[:len(segment)-2]) + continue + } + path = append(path, segment) + } + + if strings.HasPrefix(field, "this.") { + result, err = ctx.Query(path) + } + if strings.HasPrefix(field, "root.") { + result, err = GlobalContext.Query(path) + } + + if err != nil { + return "", fmt.Errorf("%s: %w", field, err) + } + + for _, operation := range operations { + switch operation { + case "to_lower": + result = strings.ToLower(result) + case "to_upper": + result = strings.ToUpper(result) + case "to_pascal_case": + result = strcase.ToCamel(result) + case "to_camel_case": + result = strcase.ToLowerCamel(result) + case "to_snake_case": + result = strcase.ToSnake(result) + case "to_kebab_case": + result = strcase.ToKebab(result) + case "capitalize": + result = strings.ToUpper(result[:1]) + result[1:] + case "length": + result = fmt.Sprintf("%d", len(result)) + default: + return "", fmt.Errorf("%s: unknown operation %q on %q", field, operation+"()", result) + } + } + + return result, nil +} + +func getFields(text string) (out []string) { + var ( + current []rune + inQuote bool + quoteChar rune + ) + + flush := func() { + if len(current) > 0 { + out = append(out, string(current)) + current = current[:0] + } + } + + for _, char := range text { + switch { + case (char == '"' || char == '\''): + if inQuote { + // closing quote? + if char == quoteChar { + inQuote = false + quoteChar = 0 + } else { + // different quote inside a quote: treat literally + current = append(current, char) + } + } else { + inQuote = true + quoteChar = char + } + + case unicode.IsSpace(char): + if inQuote { + current = append(current, char) + } else { + flush() + } + + default: + current = append(current, char) + } + } + + flush() + return +} diff --git a/lang/context/context_test.go b/lang/context/context_test.go new file mode 100644 index 0000000..f37d08d --- /dev/null +++ b/lang/context/context_test.go @@ -0,0 +1,170 @@ +package context_test + +import ( + "strings" + "testing" + + "github.com/bbfh-dev/mend/lang/context" + "github.com/stretchr/testify/assert" +) + +func TestContextGetAndSet(test *testing.T) { + assert := assert.New(test) + + ctx := context.New() + assert.NotNil(ctx) + + ctx.Set([]string{"a", "b", "c"}, "Hello World!") + ctx.Set([]string{"a", "b", "d"}, "Another one!") + ctx.Set([]string{"e"}, "1") + + value, err := ctx.Query([]string{"a", "b", "c"}) + assert.NoError(err) + assert.Equal("Hello World!", value) + + value, err = ctx.Query([]string{"a", "b", "d"}) + assert.NoError(err) + assert.Equal("Another one!", value) + + value, err = ctx.Query([]string{"e"}) + assert.NoError(err) + assert.Equal("1", value) + + value, err = ctx.Query([]string{"unknown"}) + assert.Error(err) + assert.Equal("", value) +} + +func TestContextExpressions(test *testing.T) { + assert := assert.New(test) + + ctx := context.New() + assert.NotNil(ctx) + + ctx.Set([]string{"a", "b", "c"}, "Hello World!") + + var cases = []struct { + Expression string + ExpectErr bool + Expect string + }{ + { + Expression: "this.a.b.c", + ExpectErr: false, + Expect: "Hello World!", + }, + { + Expression: "this.a.b.c == 'Hello World!'", + ExpectErr: false, + Expect: "true", + }, + { + Expression: "this.a.b.c != 'Hello World!'", + ExpectErr: false, + Expect: "false", + }, + { + Expression: "this.a.unknown == 'Hello World!'", + ExpectErr: true, + }, + { + Expression: "this.a.b.c has 'Hello'", + ExpectErr: false, + Expect: "true", + }, + { + Expression: "this.a.b.c lacks 'Hello'", + ExpectErr: false, + Expect: "false", + }, + { + Expression: "this.a.b.c || 'Fallback'", + ExpectErr: false, + Expect: "Hello World!", + }, + { + Expression: "this.a.unknown || 'Fallback'", + ExpectErr: false, + Expect: "Fallback", + }, + { + Expression: "this.a.b.c.to_lower()", + ExpectErr: false, + Expect: "hello world!", + }, + { + Expression: "this.a.b.c.to_upper()", + ExpectErr: false, + Expect: "HELLO WORLD!", + }, + { + Expression: "this.a.b.c.to_pascal_case()", + ExpectErr: false, + Expect: "HelloWorld", + }, + { + Expression: "this.a.b.c.to_camel_case()", + ExpectErr: false, + Expect: "helloWorld", + }, + { + Expression: "this.a.b.c.to_snake_case()", + ExpectErr: false, + Expect: "hello_world!", + }, + { + Expression: "this.a.b.c.to_kebab_case()", + ExpectErr: false, + Expect: "hello-world!", + }, + { + Expression: "this.a.b.c.to_lower().capitalize()", + ExpectErr: false, + Expect: "Hello world!", + }, + { + Expression: "this.a.b.c.length()", + ExpectErr: false, + Expect: "12", + }, + { + Expression: "this.a.b.c.length() == this.a.b.c.to_camel_case().length()", + ExpectErr: false, + Expect: "false", + }, + { + Expression: "this.a.b.unknown == 69", + ExpectErr: true, + }, + { + Expression: "this.a.b.unknown == 69 || true", + ExpectErr: false, + Expect: "true", + }, + { + Expression: "this.a.b.c has '!' || 'Fallback'", + ExpectErr: false, + Expect: "true", + }, + { + Expression: "", + ExpectErr: false, + Expect: "", + }, + } + + for _, testCase := range cases { + test.Run( + strings.ReplaceAll(testCase.Expression, " ", "__"), + func(test *testing.T) { + result, err := ctx.Compute(testCase.Expression) + if testCase.ExpectErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal(testCase.Expect, result) + }, + ) + } +} diff --git a/lang/context/parse.go b/lang/context/parse.go new file mode 100644 index 0000000..1ab3f5a --- /dev/null +++ b/lang/context/parse.go @@ -0,0 +1,41 @@ +package context + +import ( + "strings" + + "golang.org/x/net/html" +) + +func IsContextKey(key string) bool { + return strings.HasPrefix(key, ":") +} + +func ParseAttrs(attrs []html.Attribute) *Context { + ctx := New() + + for _, attr := range attrs { + if !IsContextKey(attr.Key) { + continue + } + key := attr.Key[1:] + value := strings.TrimSpace(attr.Val) + + if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") { + ctx.Values[key] = parseDict(value) + continue + } + + ctx.Set([]string{key}, value) + } + + return ctx +} + +func parseDict(str string) *Context { + dict := New() + if len(str) == 2 { + return dict + } + + return dict +} diff --git a/lang/expressions/parse.go b/lang/expressions/parse.go new file mode 100644 index 0000000..cf5a891 --- /dev/null +++ b/lang/expressions/parse.go @@ -0,0 +1,42 @@ +package expressions + +import "strings" + +const bracketOpen = "[[" +const bracketClose = "]]" + +func Parse(text string, callback func(string) (string, error)) (string, error) { + var builder strings.Builder + i := 0 + + for { + start := strings.Index(text[i:], bracketOpen) + if start == -1 { + // No more expressions; write the remaining text. + builder.WriteString(text[i:]) + break + } + + builder.WriteString(text[i : i+start]) + i += start + len(bracketOpen) + + // Find the closing bracket. + end := strings.Index(text[i:], bracketClose) + if end == -1 { + // Unmatched bracket; treat the remainder as plain text. + builder.WriteString(bracketOpen) + builder.WriteString(text[i:]) + break + } + + // Evaluate the expression inside the brackets. + expr, err := callback(strings.TrimSpace(text[i : i+end])) + if err != nil { + return builder.String(), err + } + builder.WriteString(expr) + i += end + len(bracketClose) + } + + return builder.String(), nil +} diff --git a/lang/printer/printer.go b/lang/printer/printer.go new file mode 100644 index 0000000..83637ed --- /dev/null +++ b/lang/printer/printer.go @@ -0,0 +1,18 @@ +package printer + +import ( + "io" + "strings" +) + +var IndentString string +var StripComments bool + +type Writer interface { + io.Writer + io.StringWriter +} + +func WriteIndent(writer Writer, indent int) { + writer.WriteString(strings.Repeat(IndentString, indent)) +} diff --git a/main.go b/main.go index c4eb432..ea67527 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,23 @@ package main import ( + "fmt" "os" "github.com/bbfh-dev/mend/cli" "github.com/bbfh-dev/parsex/v2" ) -var CLI = parsex.Program{ +var Runtime = parsex.Program{ Data: &cli.Options, Name: "mend", - Desc: "Mend is a simple HTML template processor designed to, but not limited to be used to generate static websites", + Desc: "HTML template processor designed to, but not limited to be used to generate static websites", Exec: cli.Main, -}.Runtime(). - SetVersion("0.2.0-alpha"). - SetPosArgs("html files...") +}.Runtime().SetVersion("1.0.0-alpha").SetPosArgs("html files...") func main() { - err := CLI.Run(os.Args[1:]) + err := Runtime.Run(os.Args[1:]) if err != nil { - os.Stderr.WriteString(err.Error() + "\n") + fmt.Fprintln(os.Stderr, err.Error()) } } diff --git a/mend/assert/assert.go b/mend/assert/assert.go deleted file mode 100644 index 3bda0d9..0000000 --- a/mend/assert/assert.go +++ /dev/null @@ -1,34 +0,0 @@ -package assert - -import ( - "fmt" - "reflect" -) - -const prefix = "(assertion failed)" - -func NotNil[T any](value any, message string, args ...any) { - if value == nil { - panic( - fmt.Sprintf( - "%s: %q is nil: %s", - prefix, - reflect.TypeOf(new(T)).String(), - fmt.Sprintf(message, args...), - ), - ) - } -} - -func NotEmpty[T any](value []T, message string, args ...any) { - if len(value) == 0 { - panic( - fmt.Sprintf( - "%s: %q is empty: %s", - prefix, - "[]"+reflect.TypeOf(new(T)).String(), - fmt.Sprintf(message, args...), - ), - ) - } -} diff --git a/mend/attrs/attributes.go b/mend/attrs/attributes.go deleted file mode 100644 index 84bba0b..0000000 --- a/mend/attrs/attributes.go +++ /dev/null @@ -1,105 +0,0 @@ -package attrs - -import ( - "strings" - - "golang.org/x/net/html" -) - -// Sorted HTML tag attributes key="value" -type Attributes struct { - order []string - values map[string]string -} - -func New(fromAttrs []html.Attribute) Attributes { - out := Attributes{ - order: []string{}, - values: map[string]string{}, - } - - for _, attr := range fromAttrs { - out.order = append(out.order, attr.Key) - out.values[attr.Key] = attr.Val - } - - return out.sort() -} - -func (attrs Attributes) IsEmpty() bool { - return len(attrs.order) == 0 -} - -func (attrs Attributes) ParamKeys() map[string]string { - out := map[string]string{} - for _, key := range attrs.order { - if strings.HasPrefix(key, ":") { - out[key[1:]] = attrs.values[key] - } - } - return out -} - -func (attrs Attributes) InheritAttributes() Attributes { - out := New([]html.Attribute{}) - for _, key := range attrs.order { - if strings.HasPrefix(key, "@") { - out.order = append(out.order, key[1:]) - out.values[key[1:]] = attrs.values[key] - } - } - return out -} - -func (attrs Attributes) Merge(overwrite Attributes) Attributes { - for _, key := range overwrite.order { - _, ok := attrs.values[key] - if !ok { - attrs.order = append(attrs.order, key) - } - attrs.values[key] = overwrite.values[key] - } - return attrs -} - -func (attrs Attributes) ParseExpressions( - source string, - fn func(string, string) (string, error), -) (Attributes, error) { - for key, value := range attrs.values { - newValue, err := fn(source, value) - if err != nil { - return attrs, err - } - attrs.values[key] = newValue - } - return attrs, nil -} - -func (attrs Attributes) ReplaceText(text string, with string) Attributes { - clone := Attributes{ - order: attrs.order, - values: map[string]string{}, - } - for key, value := range attrs.values { - clone.values[key] = strings.ReplaceAll(value, text, with) - } - return clone -} - -func (attrs Attributes) Get(key string) string { - return attrs.values[key] -} - -func (attrs Attributes) Contains(key string) bool { - _, ok := attrs.values[key] - return ok -} - -func (attrs Attributes) GetOrFallback(key string, fallback string) string { - value, ok := attrs.values[key] - if ok { - return value - } - return fallback -} diff --git a/mend/attrs/attributes_render.go b/mend/attrs/attributes_render.go deleted file mode 100644 index 33770e5..0000000 --- a/mend/attrs/attributes_render.go +++ /dev/null @@ -1,23 +0,0 @@ -package attrs - -import "fmt" - -func (attrs Attributes) Render(output writer) { - last := len(attrs.order) - 1 - - for i, key := range attrs.order { - attrs.renderKey(output, key) - if i != last { - output.WriteString(" ") - } - } -} - -func (attrs Attributes) renderKey(output writer, key string) { - if len(attrs.values[key]) == 0 { - output.WriteString(key) - return - } - - fmt.Fprintf(output, "%s=%q", key, attrs.values[key]) -} diff --git a/mend/attrs/attributes_sort.go b/mend/attrs/attributes_sort.go deleted file mode 100644 index bbe224e..0000000 --- a/mend/attrs/attributes_sort.go +++ /dev/null @@ -1,33 +0,0 @@ -package attrs - -import "sort" - -func (attrs Attributes) sort() Attributes { - order := make(map[string]int) - for i, s := range AttrSortOrder { - order[s] = i - } - - // Ort a while keeping the order for equal elements. - sort.SliceStable(attrs.order, func(i, j int) bool { - iIdx, iInB := order[attrs.order[i]] - jIdx, jInB := order[attrs.order[j]] - - // Both elements are in b: sort by the order in b. - if iInB && jInB { - return iIdx < jIdx - } - // Only a[i] is in b: it comes first. - if iInB { - return true - } - // Only a[j] is in b: it comes first. - if jInB { - return false - } - // Neither element is in b: keep original order. - return false - }) - - return attrs -} diff --git a/mend/attrs/list.go b/mend/attrs/list.go deleted file mode 100644 index 05d5ca6..0000000 --- a/mend/attrs/list.go +++ /dev/null @@ -1,47 +0,0 @@ -package attrs - -var SelfClosingTags = []string{ - "area", "base", "br", "col", - "embed", "hr", "img", "input", - "link", "meta", "param", "source", - "track", "wbr", "path", "rect", - "polygon", "stop", "ellipse", -} - -var AttrSortOrder = []string{ - "id", "class", "name", - - "src", "rel", "type", "href", "action", "formaction", "open", "download", - - "width", "height", "cols", "rows", "colspan", "rowspan", "align", "border", "bgcolor", - - "label", "for", "tabindex", "accesskey", "style", "title", "alt", - - "value", "placeholder", "checked", "disabled", "readonly", - "autocomplete", "autofocus", "novalidate", "form", "enctype", "method", - - "srcdoc", "poster", "controls", "autoplay", "muted", "loop", "preload", "media", "ismap", - - "accept", "accept-charset", "charset", "color", "cite", "content", - "contenteditable", "coords", "data", "datetime", "default", "defer", "dir", - "dirname", "draggable", "enterkeyhint", "headers", "hidden", "high", - "hreflang", "http-equiv", "inert", "inputmode", "kind", "list", "low", - "max", "maxlength", "min", "multiple", "optimum", "pattern", "popover", - "popovertarget", "popovertargetaction", "reversed", "sandbox", - "scope", "selected", "shape", "size", "sizes", "span", "spellcheck", "srclang", - "srcset", "start", "step", "translate", "usemap", "wrap", - - "onabort", "onafterprint", "onbeforeprint", "onbeforeunload", "onblur", - "oncanplay", "oncanplaythrough", "onchange", "onclick", "oncontextmenu", - "oncopy", "oncuechange", "oncut", "ondblclick", "ondrag", "ondragend", - "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", - "ondurationchange", "onemptied", "onended", "onerror", "onfocus", - "onhashchange", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", - "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onmousedown", - "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", - "onoffline", "ononline", "onpagehide", "onpageshow", "onpaste", "onpause", - "onplay", "onplaying", "onpopstate", "onprogress", "onratechange", "onreset", - "onresize", "onscroll", "onsearch", "onseeked", "onseeking", "onselect", - "onstalled", "onstorage", "onsubmit", "onsuspend", "ontimeupdate", "ontoggle", - "onunload", "onvolumechange", "onwaiting", "onwheel", -} diff --git a/mend/attrs/writer.go b/mend/attrs/writer.go deleted file mode 100644 index 2427b07..0000000 --- a/mend/attrs/writer.go +++ /dev/null @@ -1,8 +0,0 @@ -package attrs - -import "io" - -type writer interface { - io.Writer - io.StringWriter -} diff --git a/mend/expression.go b/mend/expression.go deleted file mode 100644 index 995441b..0000000 --- a/mend/expression.go +++ /dev/null @@ -1,80 +0,0 @@ -package mend - -import ( - "fmt" - "strings" - - "github.com/tidwall/gjson" -) - -const bracketOpen = "[[" -const bracketClose = "]]" - -type modifier func(original string) string - -type Expression struct { - Variable string - DataSource string - Modifiers []modifier - Fallback string - IsNumber bool - notFound bool -} - -func (expression *Expression) String() string { - result := gjson.Get(expression.DataSource, expression.Variable) - if !result.Exists() { - expression.notFound = true - return expression.Fallback - } - value := result.String() - - for _, modify := range expression.Modifiers { - value = modify(value) - } - - return value -} - -func (expression *Expression) Err() error { - if expression.notFound && expression.Fallback == "" { - return fmt.Errorf("undefined parameter %q", expression.Variable) - } - return nil -} - -func ParseForExpressions(source string, text string) (string, error) { - var builder strings.Builder - i := 0 - - for { - start := strings.Index(text[i:], bracketOpen) - if start == -1 { - // No more expressions; write the remaining text. - builder.WriteString(text[i:]) - break - } - - builder.WriteString(text[i : i+start]) - i += start + len(bracketOpen) - - // Find the closing bracket. - end := strings.Index(text[i:], bracketClose) - if end == -1 { - // Unmatched bracket; treat the remainder as plain text. - builder.WriteString(bracketOpen) - builder.WriteString(text[i:]) - break - } - - // Evaluate the expression inside the brackets. - expr, err := ComputeExpression(source, strings.TrimSpace(text[i:i+end])) - if err != nil { - return builder.String(), err - } - builder.WriteString(expr) - i += end + len(bracketClose) - } - - return builder.String(), nil -} diff --git a/mend/expression_compute.go b/mend/expression_compute.go deleted file mode 100644 index bb5f54a..0000000 --- a/mend/expression_compute.go +++ /dev/null @@ -1,174 +0,0 @@ -package mend - -import ( - "fmt" - "strconv" - "strings" - "unicode" - - "github.com/bbfh-dev/mend/mend/settings" - "github.com/iancoleman/strcase" -) - -const tokenGlobal = "^" -const tokenVar = "." -const tokenModifier = "-" - -func ComputeExpression(source string, str string) (string, error) { - expression := &Expression{ - Variable: "", - DataSource: source, - Modifiers: []modifier{}, - Fallback: "", - IsNumber: false, - notFound: false, - } - - tokens := strings.Fields(str) - for i, token := range tokens { - if strings.HasPrefix(token, "@") { - // Why are we even computing an unparsed expression? - return "", nil - } - - if _, err := strconv.Atoi(token); err == nil { - expression.Variable = token - expression.IsNumber = true - continue - } - - if strings.HasPrefix(token, tokenGlobal) { - expression.DataSource = settings.GlobalParams - token = token[1:] - } - - if strings.HasPrefix(token, tokenVar) { - if expression.Variable != "" { - return "", fmt.Errorf( - "expression variable is set to %q but attempting to change it to %q", - expression.Variable, - token[1:], - ) - } - expression.Variable = token[1:] - continue - } - - if strings.HasPrefix(token, tokenModifier) { - switch token[1:] { - - case "capitalize": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - if len(original) == 0 { - return "" - } - return strings.ToUpper(original[:1]) + original[1:] - }) - - case "invert": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strings.Map(func(char rune) rune { - switch { - case unicode.IsLower(char): - return unicode.ToUpper(char) - case unicode.IsUpper(char): - return unicode.ToLower(char) - } - return char - }, original) - }) - - case "quote": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return fmt.Sprintf("%q", original) - }) - - case "get-length": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return fmt.Sprintf("%d", len(original)) - }) - - case "get-lines": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return fmt.Sprintf("%d", strings.Count(original, "\n")) - }) - - case "get-fields": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return fmt.Sprintf("%d", len(strings.Fields(original))) - }) - - case "to-upper": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strings.ToUpper(original) - }) - - case "to-lower": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strings.ToLower(original) - }) - - case "to-snake-case": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strcase.ToSnake(original) - }) - - case "to-camel-case": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strcase.ToLowerCamel(original) - }) - - case "to-pascal-case": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strcase.ToCamel(original) - }) - - case "to-kebab-case": - expression.Modifiers = append(expression.Modifiers, func(original string) string { - return strcase.ToKebab(original) - }) - - default: - return "", fmt.Errorf("unknown modifier %q", token) - - } - continue - } - - if token == "==" || token == "!=" { - if len(tokens[i+1:]) == 0 { - return "", fmt.Errorf( - "expecting a value to the right of the equation of %q", - expression.Variable, - ) - } - compare := expression.Variable - if !expression.IsNumber { - compare = expression.String() - } - - var result = compare == tokens[i+1] - if token == "!=" { - result = !result - } - return fmt.Sprintf("%v", result), nil - } - - if token == "||" { - if len(tokens[i+1:]) == 0 { - expression.Fallback = " " - } else { - expression.Fallback = strings.Join(tokens[i+1:], " ") - } - break - } - - return "", fmt.Errorf("unexpected token %q", token) - } - - if expression.Variable == "" { - return "", fmt.Errorf("expression {{ %s }} does not contain any variables!", str) - } - - return expression.String(), expression.Err() -} diff --git a/mend/settings/settings.go b/mend/settings/settings.go deleted file mode 100644 index 1cd54b8..0000000 --- a/mend/settings/settings.go +++ /dev/null @@ -1,16 +0,0 @@ -package settings - -import ( - "io" - "strings" -) - -var GlobalParams string - -var KeepComments bool - -var IndentWith string = strings.Repeat(" ", 4) - -func WriteIndent(output io.StringWriter, indent int) { - output.WriteString(strings.Repeat(IndentWith, indent)) -} diff --git a/mend/tags/custom_extend.go b/mend/tags/custom_extend.go deleted file mode 100644 index e0348e7..0000000 --- a/mend/tags/custom_extend.go +++ /dev/null @@ -1,46 +0,0 @@ -package tags - -import ( - "errors" - "fmt" -) - -type CustomExtendNode struct { - *pairedNode - // Store all the nodes from the file it extends - Inner *pairedNode - // The node where contents will be placed - Slot NodeWithChildren -} - -func NewCustomExtendNode() *CustomExtendNode { - return &CustomExtendNode{ - pairedNode: newPairedNode(), - Inner: newPairedNode(), - } -} - -func (node *CustomExtendNode) Render(out writer, indent int) { - node.Inner.renderMinimal(out, indent) -} - -func (node *CustomExtendNode) Visible() bool { - return true -} - -func (node *CustomExtendNode) Clone() Node { - clone := *node - clone.pairedNode = clone.pairedNode.Clone() - return &clone -} - -func (node *CustomExtendNode) ParseExpressions(source string, fn expressionFunc) (err error) { - errs := make([]error, 0, len(node.Inner.Children)) - for _, child := range node.Inner.Children { - err = child.ParseExpressions(source, fn) - if err != nil { - errs = append(errs, fmt.Errorf(": %w", err)) - } - } - return errors.Join(errs...) -} diff --git a/mend/tags/custom_if.go b/mend/tags/custom_if.go deleted file mode 100644 index 8b613fc..0000000 --- a/mend/tags/custom_if.go +++ /dev/null @@ -1,50 +0,0 @@ -package tags - -import ( - "errors" - "strings" -) - -type CustomIfNode struct { - *pairedNode - Value string - Expect bool -} - -func NewCustomIfNode(value string, expect bool) *CustomIfNode { - return &CustomIfNode{ - pairedNode: newPairedNode(), - Value: value, - Expect: expect, - } -} - -func (node *CustomIfNode) check() bool { - return (node.Value == "true") == node.Expect -} - -func (node *CustomIfNode) Render(out writer, indent int) { - if node.check() { - node.renderMinimal(out, indent) - } -} - -func (node *CustomIfNode) Visible() bool { - return node.check() -} - -func (node *CustomIfNode) Clone() Node { - clone := *node - clone.pairedNode = clone.pairedNode.Clone() - return &clone -} - -func (node *CustomIfNode) ParseExpressions(source string, fn expressionFunc) (err error) { - node.Value, err = fn(source, node.Value) - return errors.Join(err, node.pairedNode.ParseExpressions(source, fn)) -} - -func (node *CustomIfNode) ReplaceText(text string, with string) { - node.Value = strings.ReplaceAll(node.Value, text, with) - node.pairedNode.ReplaceText(text, with) -} diff --git a/mend/tags/custom_range.go b/mend/tags/custom_range.go deleted file mode 100644 index 22d01fb..0000000 --- a/mend/tags/custom_range.go +++ /dev/null @@ -1,31 +0,0 @@ -package tags - -import ( - "github.com/tidwall/gjson" -) - -type CustomRangeNode struct { - *pairedNode - Name string - Values gjson.Result -} - -func NewCustomRangeNode(name string, values gjson.Result) *CustomRangeNode { - return &CustomRangeNode{ - pairedNode: newPairedNode(), - Name: name, - Values: values, - } -} - -func (node *CustomRangeNode) Render(out writer, indent int) {} - -func (node *CustomRangeNode) Visible() bool { - return false -} - -func (node *CustomRangeNode) Clone() Node { - clone := *node - clone.pairedNode = clone.pairedNode.Clone() - return &clone -} diff --git a/mend/tags/node.go b/mend/tags/node.go deleted file mode 100644 index d7c9e1e..0000000 --- a/mend/tags/node.go +++ /dev/null @@ -1,126 +0,0 @@ -package tags - -import ( - "errors" - "fmt" - - "github.com/bbfh-dev/mend/mend/attrs" -) - -type expressionFunc func(source, text string) (string, error) - -type Node interface { - Render(out writer, indent int) - Visible() bool - ParseExpressions(string, expressionFunc) error - ReplaceText(text string, with string) - Clone() Node - MergeAttributes(attrs attrs.Attributes) (ok bool) -} - -type NodeWithChildren interface { - Node - Add(...Node) -} - -// NOTE: Doesn't print the '>' in the end to allow for it to be used for both block and void tags -func renderOpeningTag(out writer, tag string, attributes attrs.Attributes) { - if attributes.IsEmpty() { - fmt.Fprintf(out, "<%s", tag) - return - } - - fmt.Fprintf(out, "<%s ", tag) - attributes.Render(out) -} - -func renderClosingTag(out writer, tag string) { - fmt.Fprintf(out, "", tag) -} - -// Shared fields/methods for paired nodes -// -// A paired node is: -type pairedNode struct { - Children []Node -} - -func newPairedNode() *pairedNode { - return &pairedNode{ - Children: []Node{}, - } -} - -func (node *pairedNode) renderMinimal(out writer, indent int) { - last := len(node.Children) - 1 - for i, child := range node.Children { - if !child.Visible() { - continue - } - child.Render(out, indent) - if i != last { - out.WriteString("\n") - } - } -} - -func (node *pairedNode) renderList(out writer, indent int) { - for _, child := range node.Children { - if !child.Visible() { - continue - } - child.Render(out, indent+1) - out.WriteString("\n") - } -} - -func (node *pairedNode) renderPadded(out writer, indent int) { - out.WriteString("\n") - for _, child := range node.Children { - if !child.Visible() { - continue - } - child.Render(out, indent) - out.WriteString("\n\n") - } -} - -func (node *pairedNode) Add(nodes ...Node) { - node.Children = append(node.Children, nodes...) -} - -func (node *pairedNode) ParseExpressions(source string, fn expressionFunc) (err error) { - errs := make([]error, 0, len(node.Children)) - for _, child := range node.Children { - err = child.ParseExpressions(source, fn) - if err != nil { - errs = append(errs, err) - } - } - return errors.Join(errs...) -} - -func (node *pairedNode) ReplaceText(text string, with string) { - for _, child := range node.Children { - child.ReplaceText(text, with) - } -} - -func (node *pairedNode) Clone() *pairedNode { - clone := *node - children := make([]Node, len(clone.Children)) - for i, child := range clone.Children { - children[i] = child.Clone() - } - clone.Children = children - return &clone -} - -func (node *pairedNode) MergeAttributes(attrs attrs.Attributes) bool { - for _, child := range node.Children { - if child.MergeAttributes(attrs) { - return true - } - } - return false -} diff --git a/mend/tags/node_comment.go b/mend/tags/node_comment.go deleted file mode 100644 index 4ed87b9..0000000 --- a/mend/tags/node_comment.go +++ /dev/null @@ -1,47 +0,0 @@ -package tags - -import ( - "fmt" - "strings" - - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/settings" -) - -// Represents a block of text -type CommentNode struct { - Comment string -} - -func NewCommentNode(comment string) *CommentNode { - return &CommentNode{ - Comment: comment, - } -} - -func (node *CommentNode) Render(out writer, indent int) { - settings.WriteIndent(out, indent) - fmt.Fprintf(out, "", node.Comment) -} - -func (node *CommentNode) Visible() bool { - return true -} - -func (node *CommentNode) ParseExpressions(source string, fn expressionFunc) (err error) { - node.Comment, err = fn(source, node.Comment) - return err -} - -func (node *CommentNode) ReplaceText(text string, with string) { - node.Comment = strings.ReplaceAll(node.Comment, text, with) -} - -func (node *CommentNode) Clone() Node { - clone := *node - return &clone -} - -func (node *CommentNode) MergeAttributes(attrs attrs.Attributes) bool { - return false -} diff --git a/mend/tags/node_doctype.go b/mend/tags/node_doctype.go deleted file mode 100644 index 3c63558..0000000 --- a/mend/tags/node_doctype.go +++ /dev/null @@ -1,43 +0,0 @@ -package tags - -import ( - "fmt" - - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/settings" -) - -type DoctypeNode struct { - Doctype string -} - -func NewDoctypeNode(doctype string) *DoctypeNode { - return &DoctypeNode{ - Doctype: doctype, - } -} - -func (node *DoctypeNode) Render(out writer, indent int) { - settings.WriteIndent(out, indent) - fmt.Fprintf(out, "", node.Doctype) -} - -func (node *DoctypeNode) Visible() bool { - return true -} - -func (node *DoctypeNode) ParseExpressions(source string, fn expressionFunc) (err error) { - return nil -} - -func (node *DoctypeNode) ReplaceText(text string, with string) { -} - -func (node *DoctypeNode) Clone() Node { - clone := *node - return &clone -} - -func (node *DoctypeNode) MergeAttributes(attrs attrs.Attributes) bool { - return false -} diff --git a/mend/tags/node_root.go b/mend/tags/node_root.go deleted file mode 100644 index 122bb40..0000000 --- a/mend/tags/node_root.go +++ /dev/null @@ -1,26 +0,0 @@ -package tags - -// Represents a simple paired node -type RootNode struct { - *pairedNode -} - -func NewRootNode() *RootNode { - return &RootNode{ - pairedNode: newPairedNode(), - } -} - -func (node *RootNode) Render(out writer, indent int) { - node.renderMinimal(out, indent) -} - -func (node *RootNode) Visible() bool { - return true -} - -func (node *RootNode) Clone() Node { - clone := *node - clone.pairedNode = clone.pairedNode.Clone() - return &clone -} diff --git a/mend/tags/node_tag.go b/mend/tags/node_tag.go deleted file mode 100644 index 6bd0a99..0000000 --- a/mend/tags/node_tag.go +++ /dev/null @@ -1,63 +0,0 @@ -package tags - -import ( - "errors" - - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/settings" -) - -// Represents a regular HTML paired node -type TagNode struct { - *pairedNode - Tag string - Attributes attrs.Attributes -} - -func NewTagNode(tag string, attributes attrs.Attributes) *TagNode { - return &TagNode{ - Tag: tag, - Attributes: attributes, - pairedNode: newPairedNode(), - } -} - -func (node *TagNode) Render(out writer, indent int) { - settings.WriteIndent(out, indent) - renderOpeningTag(out, node.Tag, node.Attributes) - out.WriteString(">\n") - - if node.Tag == "html" { - node.renderPadded(out, indent) - } else { - node.renderList(out, indent) - } - - settings.WriteIndent(out, indent) - renderClosingTag(out, node.Tag) -} - -func (node *TagNode) Visible() bool { - return true -} - -func (node *TagNode) ParseExpressions(source string, fn expressionFunc) (err error) { - node.Attributes, err = node.Attributes.ParseExpressions(source, fn) - return errors.Join(err, node.pairedNode.ParseExpressions(source, fn)) -} - -func (node *TagNode) ReplaceText(text string, with string) { - node.Attributes = node.Attributes.ReplaceText(text, with) - node.pairedNode.ReplaceText(text, with) -} - -func (node *TagNode) Clone() Node { - clone := *node - clone.pairedNode = clone.pairedNode.Clone() - return &clone -} - -func (node *TagNode) MergeAttributes(attrs attrs.Attributes) bool { - node.Attributes = node.Attributes.Merge(attrs) - return true -} diff --git a/mend/tags/node_text.go b/mend/tags/node_text.go deleted file mode 100644 index 38aa8fc..0000000 --- a/mend/tags/node_text.go +++ /dev/null @@ -1,46 +0,0 @@ -package tags - -import ( - "strings" - - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/settings" -) - -// Represents a block of text -type TextNode struct { - Text string -} - -func NewTextNode(text string) *TextNode { - return &TextNode{ - Text: text, - } -} - -func (node *TextNode) Render(out writer, indent int) { - settings.WriteIndent(out, indent) - out.WriteString(node.Text) -} - -func (node *TextNode) Visible() bool { - return true -} - -func (node *TextNode) ParseExpressions(source string, fn expressionFunc) (err error) { - node.Text, err = fn(source, node.Text) - return err -} - -func (node *TextNode) ReplaceText(text string, with string) { - node.Text = strings.ReplaceAll(node.Text, text, with) -} - -func (node *TextNode) Clone() Node { - clone := *node - return &clone -} - -func (node *TextNode) MergeAttributes(attrs attrs.Attributes) bool { - return false -} diff --git a/mend/tags/node_void.go b/mend/tags/node_void.go deleted file mode 100644 index 595cba8..0000000 --- a/mend/tags/node_void.go +++ /dev/null @@ -1,48 +0,0 @@ -package tags - -import ( - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/settings" -) - -// Represents a self-closing HTML tag -type VoidNode struct { - Void string - Attributes attrs.Attributes -} - -func NewVoidNode(tag string, attrs attrs.Attributes) *VoidNode { - return &VoidNode{ - Void: tag, - Attributes: attrs, - } -} - -func (node *VoidNode) Render(out writer, indent int) { - settings.WriteIndent(out, indent) - renderOpeningTag(out, node.Void, node.Attributes) - out.WriteString(" />") -} - -func (node *VoidNode) Visible() bool { - return true -} - -func (node *VoidNode) ParseExpressions(source string, fn expressionFunc) (err error) { - node.Attributes, err = node.Attributes.ParseExpressions(source, fn) - return err -} - -func (node *VoidNode) ReplaceText(text string, with string) { - node.Attributes = node.Attributes.ReplaceText(text, with) -} - -func (node *VoidNode) Clone() Node { - clone := *node - return &clone -} - -func (node *VoidNode) MergeAttributes(attrs attrs.Attributes) bool { - node.Attributes = node.Attributes.Merge(attrs) - return true -} diff --git a/mend/tags/tags.go b/mend/tags/tags.go deleted file mode 100644 index 6414f37..0000000 --- a/mend/tags/tags.go +++ /dev/null @@ -1,17 +0,0 @@ -package tags - -const TAG_INCLUDE = "include" -const TAG_SLOT = "slot" -const TAG_EXTEND = "extend" -const TAG_RANGE = "range" -const TAG_IF = "if" -const TAG_UNLESS = "unless" - -var AllTags = []string{ - TAG_INCLUDE, - TAG_SLOT, - TAG_EXTEND, - TAG_RANGE, - TAG_IF, - TAG_UNLESS, -} diff --git a/mend/tags/writer.go b/mend/tags/writer.go deleted file mode 100644 index 5550568..0000000 --- a/mend/tags/writer.go +++ /dev/null @@ -1,8 +0,0 @@ -package tags - -import "io" - -type writer interface { - io.Writer - io.StringWriter -} diff --git a/mend/template.go b/mend/template.go deleted file mode 100644 index ea72c10..0000000 --- a/mend/template.go +++ /dev/null @@ -1,63 +0,0 @@ -package mend - -import ( - "github.com/bbfh-dev/mend/mend/assert" - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/tags" - "golang.org/x/net/html" -) - -const MEND_PREFIX = "mend:" -const PKG_PREFIX = "pkg:" - -type Template struct { - Name string - Params string - - Root tags.NodeWithChildren - Slot tags.NodeWithChildren - - currentLine int - currentToken html.Token - currentText string - currentAttrs attrs.Attributes - // A list of current parents from greatest to closest - breadcrumbs []tags.NodeWithChildren -} - -func NewTemplate(name string, params string) *Template { - root := tags.NewRootNode() - return &Template{ - Name: name, - Params: params, - Root: root, - Slot: nil, - currentLine: 1, - currentToken: html.Token{}, - currentAttrs: attrs.Attributes{}, - currentText: "", - breadcrumbs: []tags.NodeWithChildren{root}, - } -} - -func (template *Template) lastBreadcrumb() tags.NodeWithChildren { - assert.NotEmpty(template.breadcrumbs, "template must never have no breadcrumbs left") - return template.breadcrumbs[len(template.breadcrumbs)-1] -} - -func (template *Template) grandParent() tags.NodeWithChildren { - switch len(template.breadcrumbs) { - case 0, 1: - return template.Root - } - return template.breadcrumbs[len(template.breadcrumbs)-2] -} - -func (template *Template) append(nodes ...tags.Node) { - template.lastBreadcrumb().Add(nodes...) -} - -func (template *Template) appendLevel(node tags.NodeWithChildren) { - template.append(node) - template.breadcrumbs = append(template.breadcrumbs, node) -} diff --git a/mend/template_branch.go b/mend/template_branch.go deleted file mode 100644 index ee56713..0000000 --- a/mend/template_branch.go +++ /dev/null @@ -1,34 +0,0 @@ -package mend - -import ( - "encoding/json" - "io/fs" - "os" -) - -func (template *Template) branchOut(src string) (*Template, error) { - var file fs.File - var err error - - file, err = os.OpenFile(src, os.O_RDONLY, os.ModePerm) - - if err != nil { - return nil, err - } - defer file.Close() - - data, err := json.Marshal(template.currentAttrs.ParamKeys()) - if err != nil { - return nil, template.errInternal(err) - } - - branch := NewTemplate(src, string(data)) - err = branch.Parse(file) - if err != nil { - return nil, template.errBranch(err) - } - - branch.Root.MergeAttributes(template.currentAttrs.InheritAttributes()) - - return branch, nil -} diff --git a/mend/template_errors.go b/mend/template_errors.go deleted file mode 100644 index 1f829a9..0000000 --- a/mend/template_errors.go +++ /dev/null @@ -1,41 +0,0 @@ -package mend - -import ( - "fmt" - "strings" - - "github.com/bbfh-dev/mend/mend/tags" -) - -func (template *Template) errUnknownTag() error { - var help strings.Builder - for _, tag := range tags.AllTags { - fmt.Fprintf(&help, "<%s> ", tag) - } - - return fmt.Errorf( - "unknown custom tag <%s>.\nPossible tags are: %s", - template.currentText, - help.String(), - ) -} - -func (template *Template) errInternal(err error) error { - return fmt.Errorf("(internal) %w", err) -} - -func (template *Template) errBranch(err error) error { - return fmt.Errorf("-> %w", err) -} - -func (template *Template) errMissingAttribute(attr string) error { - return fmt.Errorf( - "tag <%s> is missing attribute %q", - template.currentText, - attr, - ) -} - -func (template *Template) errUndefinedParam(param string) error { - return fmt.Errorf("undefined parameter %q", param) -} diff --git a/mend/template_find.go b/mend/template_find.go deleted file mode 100644 index b53f0bb..0000000 --- a/mend/template_find.go +++ /dev/null @@ -1,32 +0,0 @@ -package mend - -import ( - "io/fs" - "path/filepath" -) - -func (template *Template) Find(name string) (string, bool) { - var result string - - filepath.WalkDir( - filepath.Dir(template.Name), - func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - - if entry.IsDir() { - return nil - } - - if entry.Name() == name { - result = path - return filepath.SkipAll - } - - return nil - }, - ) - - return result, result != "" -} diff --git a/mend/template_parse.go b/mend/template_parse.go deleted file mode 100644 index 28d55a5..0000000 --- a/mend/template_parse.go +++ /dev/null @@ -1,258 +0,0 @@ -package mend - -import ( - "fmt" - "io" - "path/filepath" - "slices" - "strings" - - "github.com/bbfh-dev/mend/mend/attrs" - "github.com/bbfh-dev/mend/mend/settings" - "github.com/bbfh-dev/mend/mend/tags" - "github.com/tidwall/gjson" - "golang.org/x/net/html" -) - -func (template *Template) Parse(reader io.Reader) error { - tokenizer := html.NewTokenizer(reader) - -loop: - for { - tokenType := tokenizer.Next() - switch tokenType { - - case html.ErrorToken: - if tokenizer.Err() == io.EOF { - break loop - } - return fmt.Errorf( - "(%s) %w", - filepath.Base(template.Name), - tokenizer.Err(), - ) - - case html.TextToken: - template.currentLine += strings.Count(template.currentToken.Data, "\n") - - } - - template.currentToken = tokenizer.Token() - template.currentAttrs = attrs.New(template.currentToken.Attr) - template.currentText = strings.TrimSpace(template.currentToken.Data) - - err := template.process(tokenType) - if err != nil { - return fmt.Errorf( - "(%s:%d) %w", - filepath.Base(template.Name), - template.currentLine, - err, - ) - } - } - - err := template.Root.ParseExpressions(template.Params, ParseForExpressions) - if err != nil { - return fmt.Errorf("(%s | Expression) %w", filepath.Base(template.Name), err) - } - - return nil -} - -func (template *Template) process(tokenType html.TokenType) error { - switch tokenType { - - case html.DoctypeToken: - template.append(tags.NewDoctypeNode(template.currentText)) - - case html.CommentToken: - if settings.KeepComments { - template.append(tags.NewCommentNode(template.currentText)) - } - - case html.TextToken: - if len(template.currentText) == 0 { - break - } - - var builder strings.Builder - lines := strings.Split(template.currentText, "\n") - lastLine := len(lines) - 1 - for i, line := range lines { - builder.WriteString(strings.TrimSpace(line)) - if i != lastLine { - builder.WriteString(" ") - } - } - template.append(tags.NewTextNode(builder.String())) - - case html.SelfClosingTagToken: - switch { - - case strings.HasPrefix(template.currentText, MEND_PREFIX): - switch strings.TrimPrefix(template.currentText, MEND_PREFIX) { - - case tags.TAG_INCLUDE: - if !template.currentAttrs.Contains("src") { - return template.errMissingAttribute("src") - } - src := template.currentAttrs.Get("src") - src = filepath.Join(filepath.Dir(template.Name), src) - branch, err := template.branchOut(src) - if err != nil { - return err - } - template.append(branch.Root) - - case tags.TAG_SLOT: - node := tags.NewRootNode() - template.append(node) - template.Slot = node - - default: - return template.errUnknownTag() - } - - case strings.HasPrefix(template.currentText, PKG_PREFIX): - tag := strings.TrimPrefix(template.currentText, PKG_PREFIX) - location, exists := template.Find(tag + ".html") - if !exists { - return fmt.Errorf( - "can't resolve <%s> inside of %s", - template.currentText, - filepath.Dir(template.Name), - ) - } - branch, err := template.branchOut(location) - if err != nil { - return err - } - template.append(branch.Root) - - default: - node := tags.NewVoidNode(template.currentText, template.currentAttrs) - template.append(node) - return nil - } - - case html.StartTagToken: - switch { - - case strings.HasPrefix(template.currentText, MEND_PREFIX): - switch strings.TrimPrefix(template.currentText, MEND_PREFIX) { - - case tags.TAG_EXTEND: - if !template.currentAttrs.Contains("src") { - return template.errMissingAttribute("src") - } - src := template.currentAttrs.Get("src") - src = filepath.Join(filepath.Dir(template.Name), src) - branch, err := template.branchOut(src) - if err != nil { - return err - } - node := tags.NewCustomExtendNode() - node.Inner.Add(branch.Root) - node.Slot = branch.Slot - template.appendLevel(node) - - case tags.TAG_RANGE: - if !template.currentAttrs.Contains("for") { - return template.errMissingAttribute("for") - } - variable := template.currentAttrs.Get("for") - - var result gjson.Result - if strings.HasPrefix(variable, "^.") { - result = gjson.Get(settings.GlobalParams, variable[2:]) - } else { - result = gjson.Get(template.Params, variable) - } - - if !result.Exists() { - return template.errUndefinedParam(variable) - } - if !result.IsArray() { - return fmt.Errorf( - "parameter %q is not an array! It's set to: `%s`", - variable, - result.String(), - ) - } - node := tags.NewCustomRangeNode(variable, result) - template.appendLevel(node) - - case tags.TAG_IF: - node := tags.NewCustomIfNode( - template.currentAttrs.GetOrFallback("value", "true"), - true, - ) - template.appendLevel(node) - - case tags.TAG_UNLESS: - node := tags.NewCustomIfNode( - template.currentAttrs.GetOrFallback("value", "true"), - false, - ) - template.appendLevel(node) - - default: - return template.errUnknownTag() - } - - case strings.HasPrefix(template.currentText, PKG_PREFIX): - tag := strings.TrimPrefix(template.currentText, PKG_PREFIX) - location, exists := template.Find(tag + ".html") - if !exists { - return fmt.Errorf( - "can't resolve <%s> inside of %s", - template.currentText, - filepath.Dir(template.Name), - ) - } - branch, err := template.branchOut(location) - if err != nil { - return err - } - node := tags.NewCustomExtendNode() - node.Inner.Add(branch.Root) - node.Slot = branch.Slot - template.appendLevel(node) - - default: - // Is it actually a self-closing tag with wrong syntax? - if slices.Contains(attrs.SelfClosingTags, template.currentText) { - return template.process(html.SelfClosingTagToken) - } - - node := tags.NewTagNode(template.currentText, template.currentAttrs) - template.appendLevel(node) - return nil - } - - case html.EndTagToken: - if len(template.breadcrumbs) == 1 { - break - } - switch node := template.lastBreadcrumb().(type) { - - case *tags.CustomExtendNode: - if node.Slot != nil { - node.Slot.Add(node.Children...) - } - - case *tags.CustomRangeNode: - for i := range node.Values.Array() { - clone := node.Clone().(*tags.CustomRangeNode) - clone.ReplaceText("@index", fmt.Sprintf("%d", i)) - clone.ReplaceText("@.", fmt.Sprintf("%s.%d.", node.Name, i)) - template.grandParent().Add(clone.Children...) - } - - } - template.breadcrumbs = template.breadcrumbs[:len(template.breadcrumbs)-1] - } - - return nil -} From 0eecb172aabe4c72480fdea2132d1ef9bbcddccd Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Thu, 24 Apr 2025 14:51:50 +0300 Subject: [PATCH 02/18] docs: Expand example to use custom layout elements --- example/index.html | 28 ++++++++++++++++++++++++++++ example/layout/grid.html | 3 +++ example/layout/heading.html | 3 +++ example/layout/separator.html | 5 +++++ 4 files changed, 39 insertions(+) create mode 100644 example/layout/grid.html create mode 100644 example/layout/heading.html create mode 100644 example/layout/separator.html diff --git a/example/index.html b/example/index.html index 2483cf7..8e78274 100644 --- a/example/index.html +++ b/example/index.html @@ -1,4 +1,32 @@ {{ define "index" }} + +

Welcome!

+

+ Smithed is an open-source platform for exploring, sharing
+ and supercharging minecraft data & resource packs. +

+
+ + +
+
+ {{ range .Cards }} + {{ template "pack_card" .Trending }} + {{ end }} +
+
+
+
+ {{ range .Cards }} + {{ template "pack_card" .Newest }} + {{ end }} +
+
+
+ + + What is Smithed exactly? +
{{ end }} diff --git a/example/layout/grid.html b/example/layout/grid.html new file mode 100644 index 0000000..0d74628 --- /dev/null +++ b/example/layout/grid.html @@ -0,0 +1,3 @@ +
+ <:slot /> +
diff --git a/example/layout/heading.html b/example/layout/heading.html new file mode 100644 index 0000000..1c50360 --- /dev/null +++ b/example/layout/heading.html @@ -0,0 +1,3 @@ +
+ <:slot /> +
diff --git a/example/layout/separator.html b/example/layout/separator.html new file mode 100644 index 0000000..6bcf6b9 --- /dev/null +++ b/example/layout/separator.html @@ -0,0 +1,5 @@ +
+
+ <:slot /> +
+
From 76f6a8a5cc92c968b47e9f8360f0988f3e49de87 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Thu, 24 Apr 2025 14:52:07 +0300 Subject: [PATCH 03/18] feat: Add attrs.Attribute and everything related to it --- lang/attrs/attrs.go | 43 ++++++++++++++++++++++++++++++++++++ lang/attrs/attrs_sort.go | 33 ++++++++++++++++++++++++++++ lang/attrs/sorting.go | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 lang/attrs/attrs.go create mode 100644 lang/attrs/attrs_sort.go create mode 100644 lang/attrs/sorting.go diff --git a/lang/attrs/attrs.go b/lang/attrs/attrs.go new file mode 100644 index 0000000..6e8bc45 --- /dev/null +++ b/lang/attrs/attrs.go @@ -0,0 +1,43 @@ +package attrs + +import ( + "fmt" + + "github.com/bbfh-dev/mend/lang/printer" + "golang.org/x/net/html" +) + +type Attributes struct { + order []string + Values map[string]string +} + +func New(sourceAttrs []html.Attribute) *Attributes { + attrs := &Attributes{ + order: []string{}, + Values: map[string]string{}, + } + + for _, attr := range sourceAttrs { + attrs.order = append(attrs.order, attr.Key) + attrs.Values[attr.Key] = attr.Val + } + + return attrs.Sort() +} + +func (attrs *Attributes) Render(out printer.Writer) { + for _, key := range attrs.order { + out.WriteString(" ") + attrs.renderKey(out, key) + } +} + +func (attrs *Attributes) renderKey(out printer.Writer, key string) { + if len(attrs.Values[key]) == 0 { + out.WriteString(key) + return + } + + fmt.Fprintf(out, "%s=%q", key, attrs.Values[key]) +} diff --git a/lang/attrs/attrs_sort.go b/lang/attrs/attrs_sort.go new file mode 100644 index 0000000..53b006a --- /dev/null +++ b/lang/attrs/attrs_sort.go @@ -0,0 +1,33 @@ +package attrs + +import "sort" + +func (attrs *Attributes) Sort() *Attributes { + order := make(map[string]int) + for i, s := range AttrSortOrder { + order[s] = i + } + + // Ort a while keeping the order for equal elements. + sort.SliceStable(attrs.order, func(i, j int) bool { + iIdx, iInB := order[attrs.order[i]] + jIdx, jInB := order[attrs.order[j]] + + // Both elements are in b: sort by the order in b. + if iInB && jInB { + return iIdx < jIdx + } + // Only a[i] is in b: it comes first. + if iInB { + return true + } + // Only a[j] is in b: it comes first. + if jInB { + return false + } + // Neither element is in b: keep original order. + return false + }) + + return attrs +} diff --git a/lang/attrs/sorting.go b/lang/attrs/sorting.go new file mode 100644 index 0000000..a630e85 --- /dev/null +++ b/lang/attrs/sorting.go @@ -0,0 +1,47 @@ +package attrs + +var SelfClosingTags = []string{ + "area", "base", "br", "col", + "embed", "hr", "img", "input", + "link", "meta", "param", "source", + "track", "wbr", "path", "rect", + "polygon", "stop", "ellipse", +} + +var AttrSortOrder = []string{ + "id", "class", "name", + + "src", "rel", "type", "href", "action", "formaction", "open", "download", + + "width", "height", "cols", "rows", "colspan", "rowspan", "align", "border", "bgcolor", + + "label", "for", "tabindex", "accesskey", "style", "title", "alt", + + "value", "placeholder", "checked", "disabled", "readonly", + "autocomplete", "autofocus", "novalidate", "form", "enctype", "method", + + "srcdoc", "poster", "controls", "autoplay", "muted", "loop", "preload", "media", "ismap", + + "http-equiv", "accept", "accept-charset", "charset", "color", "cite", "content", + "contenteditable", "coords", "data", "datetime", "default", "defer", "dir", + "dirname", "draggable", "enterkeyhint", "headers", "hidden", "high", + "hreflang", "inert", "inputmode", "kind", "list", "low", + "max", "maxlength", "min", "multiple", "optimum", "pattern", "popover", + "popovertarget", "popovertargetaction", "reversed", "sandbox", + "scope", "selected", "shape", "size", "sizes", "span", "spellcheck", "srclang", + "srcset", "start", "step", "translate", "usemap", "wrap", + + "onabort", "onafterprint", "onbeforeprint", "onbeforeunload", "onblur", + "oncanplay", "oncanplaythrough", "onchange", "onclick", "oncontextmenu", + "oncopy", "oncuechange", "oncut", "ondblclick", "ondrag", "ondragend", + "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", + "ondurationchange", "onemptied", "onended", "onerror", "onfocus", + "onhashchange", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", + "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onmousedown", + "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", + "onoffline", "ononline", "onpagehide", "onpageshow", "onpaste", "onpause", + "onplay", "onplaying", "onpopstate", "onprogress", "onratechange", "onreset", + "onresize", "onscroll", "onsearch", "onseeked", "onseeking", "onselect", + "onstalled", "onstorage", "onsubmit", "onsuspend", "ontimeupdate", "ontoggle", + "onunload", "onvolumechange", "onwaiting", "onwheel", +} From 9fe96ee69f17bf65c6429aff7b3bd3a9008fad99 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Thu, 24 Apr 2025 14:52:22 +0300 Subject: [PATCH 04/18] feat: Add all templating tags --- lang/templating/tag.go | 27 +++++++++++++ lang/templating/tag_base.go | 34 +++++++++++++++++ lang/templating/tag_base_default.go | 8 ++++ lang/templating/tag_base_paired.go | 42 +++++++++++++++++++++ lang/templating/tag_comment.go | 24 ++++++++++++ lang/templating/tag_default.go | 40 ++++++++++++++++++++ lang/templating/tag_default_root.go | 27 +++++++++++++ lang/templating/tag_default_self_closing.go | 31 +++++++++++++++ lang/templating/tag_doctype.go | 24 ++++++++++++ lang/templating/tag_mend_extend.go | 25 ++++++++++++ lang/templating/tag_mend_slot.go | 15 ++++++++ lang/templating/tag_text.go | 33 ++++++++++++++++ 12 files changed, 330 insertions(+) create mode 100644 lang/templating/tag.go create mode 100644 lang/templating/tag_base.go create mode 100644 lang/templating/tag_base_default.go create mode 100644 lang/templating/tag_base_paired.go create mode 100644 lang/templating/tag_comment.go create mode 100644 lang/templating/tag_default.go create mode 100644 lang/templating/tag_default_root.go create mode 100644 lang/templating/tag_default_self_closing.go create mode 100644 lang/templating/tag_doctype.go create mode 100644 lang/templating/tag_mend_extend.go create mode 100644 lang/templating/tag_mend_slot.go create mode 100644 lang/templating/tag_text.go diff --git a/lang/templating/tag.go b/lang/templating/tag.go new file mode 100644 index 0000000..8933b42 --- /dev/null +++ b/lang/templating/tag.go @@ -0,0 +1,27 @@ +package templating + +import ( + "github.com/bbfh-dev/mend/lang/printer" +) + +type visibility int + +const ( + INVISIBLE visibility = iota + VISIBLE + INLINE +) + +type Tag interface { + Render(writer printer.Writer) + Visibility() visibility + Indent() int + Shift(offset int) + Clone() Tag +} + +type PairedTag interface { + Tag + SetChildren(tags []Tag) + Append(tags ...Tag) +} diff --git a/lang/templating/tag_base.go b/lang/templating/tag_base.go new file mode 100644 index 0000000..8635095 --- /dev/null +++ b/lang/templating/tag_base.go @@ -0,0 +1,34 @@ +package templating + +import "github.com/bbfh-dev/mend/lang/printer" + +type BaseTag struct { + indent int +} + +func NewBase(indent int) *BaseTag { + return &BaseTag{ + indent: indent, + } +} + +func (tag *BaseTag) Render(writer printer.Writer) { + printer.WriteIndent(writer, tag.indent) +} + +func (tag *BaseTag) Visibility() visibility { + return VISIBLE +} + +func (tag *BaseTag) Indent() int { + return tag.indent +} + +func (tag *BaseTag) Shift(offset int) { + tag.indent += offset +} + +func (tag *BaseTag) Clone() Tag { + clone := *tag + return &clone +} diff --git a/lang/templating/tag_base_default.go b/lang/templating/tag_base_default.go new file mode 100644 index 0000000..50921cd --- /dev/null +++ b/lang/templating/tag_base_default.go @@ -0,0 +1,8 @@ +package templating + +import "github.com/bbfh-dev/mend/lang/attrs" + +type BaseDefaultTag struct { + Name string + Attrs *attrs.Attributes +} diff --git a/lang/templating/tag_base_paired.go b/lang/templating/tag_base_paired.go new file mode 100644 index 0000000..e0ee4d5 --- /dev/null +++ b/lang/templating/tag_base_paired.go @@ -0,0 +1,42 @@ +package templating + +import "github.com/bbfh-dev/mend/lang/printer" + +type BasePairedTag struct { + *BaseTag + Children []Tag +} + +func NewPairedBase(indent int) *BasePairedTag { + return &BasePairedTag{ + BaseTag: NewBase(indent), + Children: []Tag{}, + } +} + +func (tag *BasePairedTag) Render(writer printer.Writer) { + for _, child := range tag.Children { + switch child.Visibility() { + case VISIBLE: + child.Render(writer) + writer.WriteString("\n") + case INLINE: + child.Render(writer) + } + } +} + +func (tag *BasePairedTag) Offset(offset int) { + tag.BaseTag.Shift(offset) + for _, child := range tag.Children { + child.Shift(offset) + } +} + +func (tag *BasePairedTag) SetChildren(tags []Tag) { + tag.Children = tags +} + +func (tag *BasePairedTag) Append(tags ...Tag) { + tag.Children = append(tag.Children, tags...) +} diff --git a/lang/templating/tag_comment.go b/lang/templating/tag_comment.go new file mode 100644 index 0000000..99bca9d --- /dev/null +++ b/lang/templating/tag_comment.go @@ -0,0 +1,24 @@ +package templating + +import ( + "fmt" + + "github.com/bbfh-dev/mend/lang/printer" +) + +type CommentTag struct { + *BaseTag + Comment string +} + +func NewComment(indent int, comment string) *CommentTag { + return &CommentTag{ + BaseTag: NewBase(indent), + Comment: comment, + } +} + +func (tag *CommentTag) Render(writer printer.Writer) { + tag.BaseTag.Render(writer) + fmt.Fprintf(writer, "", tag.Comment) +} diff --git a/lang/templating/tag_default.go b/lang/templating/tag_default.go new file mode 100644 index 0000000..38fbb9d --- /dev/null +++ b/lang/templating/tag_default.go @@ -0,0 +1,40 @@ +package templating + +import ( + "fmt" + + "github.com/bbfh-dev/mend/lang/attrs" + "github.com/bbfh-dev/mend/lang/printer" +) + +type DefaultTag struct { + *BasePairedTag + BaseDefaultTag +} + +func NewDefault(indent int, name string, attrs *attrs.Attributes) *DefaultTag { + return &DefaultTag{ + BasePairedTag: NewPairedBase(indent), + BaseDefaultTag: BaseDefaultTag{ + Name: name, + Attrs: attrs, + }, + } +} + +func (tag *DefaultTag) Render(writer printer.Writer) { + tag.BaseTag.Render(writer) + + fmt.Fprintf(writer, "<%s", tag.Name) + tag.Attrs.Render(writer) + writer.WriteString(">\n") + + tag.RenderBody(writer) + + tag.BaseTag.Render(writer) + fmt.Fprintf(writer, "", tag.Name) +} + +func (tag *DefaultTag) RenderBody(writer printer.Writer) { + tag.BasePairedTag.Render(writer) +} diff --git a/lang/templating/tag_default_root.go b/lang/templating/tag_default_root.go new file mode 100644 index 0000000..1386ac4 --- /dev/null +++ b/lang/templating/tag_default_root.go @@ -0,0 +1,27 @@ +package templating + +import ( + "github.com/bbfh-dev/mend/lang/attrs" + "github.com/bbfh-dev/mend/lang/printer" +) + +type DefaultRootTag struct { + *DefaultTag +} + +func NewDefaultRoot(indent int, name string, attrs *attrs.Attributes) *DefaultRootTag { + return &DefaultRootTag{ + DefaultTag: NewDefault(indent, name, attrs), + } +} + +func (tag *DefaultRootTag) RenderBody(writer printer.Writer) { + writer.WriteString("\n") + for _, child := range tag.Children { + if child.Visibility() != INVISIBLE { + child.Shift(-1) + child.Render(writer) + writer.WriteString("\n\n") + } + } +} diff --git a/lang/templating/tag_default_self_closing.go b/lang/templating/tag_default_self_closing.go new file mode 100644 index 0000000..5658293 --- /dev/null +++ b/lang/templating/tag_default_self_closing.go @@ -0,0 +1,31 @@ +package templating + +import ( + "fmt" + + "github.com/bbfh-dev/mend/lang/attrs" + "github.com/bbfh-dev/mend/lang/printer" +) + +type SelfClosingTag struct { + *BaseTag + BaseDefaultTag +} + +func NewSelfClosing(indent int, name string, attrs *attrs.Attributes) *SelfClosingTag { + return &SelfClosingTag{ + BaseTag: NewBase(indent), + BaseDefaultTag: BaseDefaultTag{ + Name: name, + Attrs: attrs, + }, + } +} + +func (tag *SelfClosingTag) Render(writer printer.Writer) { + tag.BaseTag.Render(writer) + + fmt.Fprintf(writer, "<%s", tag.Name) + tag.Attrs.Render(writer) + writer.WriteString(" />") +} diff --git a/lang/templating/tag_doctype.go b/lang/templating/tag_doctype.go new file mode 100644 index 0000000..e686fd9 --- /dev/null +++ b/lang/templating/tag_doctype.go @@ -0,0 +1,24 @@ +package templating + +import ( + "fmt" + + "github.com/bbfh-dev/mend/lang/printer" +) + +type DoctypeTag struct { + *BaseTag + Doctype string +} + +func NewDoctype(indent int, doctype string) *DoctypeTag { + return &DoctypeTag{ + BaseTag: NewBase(indent), + Doctype: doctype, + } +} + +func (tag *DoctypeTag) Render(writer printer.Writer) { + tag.BaseTag.Render(writer) + fmt.Fprintf(writer, "", tag.Doctype) +} diff --git a/lang/templating/tag_mend_extend.go b/lang/templating/tag_mend_extend.go new file mode 100644 index 0000000..113168e --- /dev/null +++ b/lang/templating/tag_mend_extend.go @@ -0,0 +1,25 @@ +package templating + +import "github.com/bbfh-dev/mend/lang/printer" + +type MendExtendTag struct { + *BasePairedTag + Root PairedTag + Slot *MendSlotTag +} + +func NewExtendSlot(indent int, root PairedTag, slot *MendSlotTag) *MendExtendTag { + return &MendExtendTag{ + BasePairedTag: NewPairedBase(indent), + Root: root, + Slot: slot, + } +} + +func (tag *MendExtendTag) Render(writer printer.Writer) { + tag.Root.Render(writer) +} + +func (tag *MendExtendTag) Visibility() visibility { + return INLINE +} diff --git a/lang/templating/tag_mend_slot.go b/lang/templating/tag_mend_slot.go new file mode 100644 index 0000000..d3bca6a --- /dev/null +++ b/lang/templating/tag_mend_slot.go @@ -0,0 +1,15 @@ +package templating + +type MendSlotTag struct { + *BasePairedTag +} + +func NewMendSlot(indent int) *MendSlotTag { + return &MendSlotTag{ + BasePairedTag: NewPairedBase(indent), + } +} + +func (tag *MendSlotTag) Visibility() visibility { + return INLINE +} diff --git a/lang/templating/tag_text.go b/lang/templating/tag_text.go new file mode 100644 index 0000000..22aabca --- /dev/null +++ b/lang/templating/tag_text.go @@ -0,0 +1,33 @@ +package templating + +import ( + "strings" + + "github.com/bbfh-dev/mend/lang/printer" +) + +type TextTag struct { + *BaseTag + Text string +} + +func NewText(indent int, text string) *TextTag { + return &TextTag{ + BaseTag: NewBase(indent), + Text: text, + } +} + +func (tag *TextTag) Render(writer printer.Writer) { + tag.BaseTag.Render(writer) + + lines := strings.Split(tag.Text, "\n") + lastLine := len(lines) - 1 + for i, line := range lines { + line = strings.TrimSpace(line) + writer.WriteString(line) + if i != lastLine { + writer.WriteString(" ") + } + } +} From ba1783abc216979fb2462e19f6cac0e9b3824469 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Thu, 24 Apr 2025 14:52:33 +0300 Subject: [PATCH 05/18] feat: Add Template{} --- lang/template.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 lang/template.go diff --git a/lang/template.go b/lang/template.go new file mode 100644 index 0000000..5e9415f --- /dev/null +++ b/lang/template.go @@ -0,0 +1,65 @@ +package lang + +import ( + "fmt" + + "github.com/bbfh-dev/mend/lang/attrs" + "github.com/bbfh-dev/mend/lang/context" + "github.com/bbfh-dev/mend/lang/templating" + "golang.org/x/net/html" +) + +const MEND_PREFIX = ":" +const PKG_PREFIX = "pkg:" + +type Template struct { + Dir string + Name string + Context *context.Context + + Breadcrumbs []templating.PairedTag + Slot *templating.MendSlotTag + + thisToken html.Token + thisText string + thisAttrs *attrs.Attributes + thisLineIndex int +} + +func New(indent int, ctx *context.Context, dir, name string) *Template { + return &Template{ + Dir: dir, + Name: name, + Context: ctx, + Breadcrumbs: []templating.PairedTag{templating.NewPairedBase(indent)}, + Slot: nil, + thisToken: html.Token{}, + thisText: "", + thisAttrs: nil, + thisLineIndex: 0, + } +} + +func (template *Template) Cursor() string { + return fmt.Sprintf("%s:%d", template.Name, template.thisLineIndex+1) +} + +func (template *Template) Root() templating.PairedTag { + return template.Breadcrumbs[0] +} + +func (template *Template) Pivot() templating.PairedTag { + return template.Breadcrumbs[len(template.Breadcrumbs)-1] +} + +func (template *Template) EnterPivot(tag templating.PairedTag) { + template.Pivot().Append(tag) + template.Breadcrumbs = append(template.Breadcrumbs, tag) +} + +func (template *Template) ExitPivot() { + if len(template.Breadcrumbs) == 1 { + return + } + template.Breadcrumbs = template.Breadcrumbs[:len(template.Breadcrumbs)-1] +} From 1ee90ad32d9040cf970c1bb2eac7184ef8466a5e Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Thu, 24 Apr 2025 18:48:49 +0300 Subject: [PATCH 06/18] feat: This is somewhat working but wrongly formatted --- cli/cli.go | 39 ++- example/components/button.html | 4 + example/components/icon.html | 2 +- .../components/icons/notification-unread.svg | 1 + example/components/icons/popout.svg | 1 + example/components/icons/search.svg | 1 + example/components/icons/smithed.svg | 1 + example/components/icons/user.svg | 1 + example/components/root.html | 94 ++++---- example/components/unmarked-link.html | 4 +- example/layout/grid.html | 2 +- example/layout/heading.html | 2 +- example/layout/separator.html | 2 +- lang/context/context.go | 15 +- lang/context/context_test.go | 11 + lang/context/parse.go | 51 ++++ lang/template.go | 20 +- lang/template_branch.go | 67 ++++++ lang/template_build.go | 223 ++++++++++++++++++ lang/templating/tag_base_paired.go | 7 +- lang/templating/tag_default.go | 6 +- lang/templating/tag_default_root.go | 13 +- lang/templating/tag_mend_extend.go | 6 +- 23 files changed, 506 insertions(+), 67 deletions(-) create mode 100644 example/components/button.html create mode 100644 example/components/icons/notification-unread.svg create mode 100644 example/components/icons/popout.svg create mode 100644 example/components/icons/search.svg create mode 100644 example/components/icons/smithed.svg create mode 100644 example/components/icons/user.svg create mode 100644 lang/template_branch.go create mode 100644 lang/template_build.go diff --git a/cli/cli.go b/cli/cli.go index ccece5a..3182f5f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,8 +1,13 @@ package cli import ( + "errors" + "os" + "path/filepath" "strings" + "github.com/bbfh-dev/mend/lang" + "github.com/bbfh-dev/mend/lang/context" "github.com/bbfh-dev/mend/lang/printer" ) @@ -10,7 +15,7 @@ var Options struct { Tabs bool `alt:"t" desc:"Use tabs for indentation"` Indent int `default:"4" desc:"The amount of spaces to be used for indentation (overwriten by --tabs)"` StripComments bool `desc:"Strips away HTML comments from the output"` - Input string `desc:"Provide input to the provided files in the following format: 'attr1=value1,attr2=value2,...'"` + Input string `desc:"Provide input to the provided files in the following format: 'attr1=value1,attr2.a.b.c=value2,...'"` } func Main(args []string) error { @@ -21,5 +26,37 @@ func Main(args []string) error { printer.IndentString = strings.Repeat(" ", Options.Indent) } + context.GlobalContext = context.New() + if len(Options.Input) != 0 { + for prop := range strings.SplitSeq(Options.Input, ",") { + pair := strings.SplitN(prop, "=", 2) + if len(pair) != 2 { + return errors.New("input format must be: 'attr1=value1,attr2.a.b.c=value2,...'") + } + key, value := pair[0], pair[1] + context.GlobalContext.Set(strings.Split(key, "."), value) + } + } + + for _, filename := range args { + dir := filepath.Dir(filename) + base := filepath.Base(filename) + filename, _ := filepath.Abs(filename) + context.GlobalContext.Set([]string{"@file"}, filename) + + file, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) + if err != nil { + return err + } + defer file.Close() + + template := lang.New(0, context.GlobalContext, dir, base) + if err := template.Build(file); err != nil { + return err + } + + template.Root().Render(os.Stdout) + } + return nil } diff --git a/example/components/button.html b/example/components/button.html new file mode 100644 index 0000000..58d2e32 --- /dev/null +++ b/example/components/button.html @@ -0,0 +1,4 @@ + + + [[ this.label ]] + diff --git a/example/components/icon.html b/example/components/icon.html index b52cd7a..2c76078 100644 --- a/example/components/icon.html +++ b/example/components/icon.html @@ -1 +1 @@ -<:include :src="./icons/[[ this.name ]].svg" class="type-icon" /> + diff --git a/example/components/icons/notification-unread.svg b/example/components/icons/notification-unread.svg new file mode 100644 index 0000000..3e8fcac --- /dev/null +++ b/example/components/icons/notification-unread.svg @@ -0,0 +1 @@ + diff --git a/example/components/icons/popout.svg b/example/components/icons/popout.svg new file mode 100644 index 0000000..3e8fcac --- /dev/null +++ b/example/components/icons/popout.svg @@ -0,0 +1 @@ + diff --git a/example/components/icons/search.svg b/example/components/icons/search.svg new file mode 100644 index 0000000..3e8fcac --- /dev/null +++ b/example/components/icons/search.svg @@ -0,0 +1 @@ + diff --git a/example/components/icons/smithed.svg b/example/components/icons/smithed.svg new file mode 100644 index 0000000..3e8fcac --- /dev/null +++ b/example/components/icons/smithed.svg @@ -0,0 +1 @@ + diff --git a/example/components/icons/user.svg b/example/components/icons/user.svg new file mode 100644 index 0000000..3e8fcac --- /dev/null +++ b/example/components/icons/user.svg @@ -0,0 +1 @@ + diff --git a/example/components/root.html b/example/components/root.html index 4392bdf..64f54e4 100644 --- a/example/components/root.html +++ b/example/components/root.html @@ -13,7 +13,7 @@ - + @@ -28,17 +28,17 @@

Smithed

Weld - <:if true="[[ this.searchbar ]]"> - - <:if false="[[ this.searchbar ]]"> -
- - -
- - -
-
+ + + + + + + + + + + Inbox @@ -47,43 +47,43 @@

Smithed

- <:slot /> +
-
-
-
-

- ABOUT -

- - - -
-
-

- COMMUNITY -

- - - -
-
-

- LEGAL -

- - - -
-
-
- Copyright © 2018-2025 Smithed -

- Not an official Minecraft product. Not approved by or associated with Mojang Studios -

-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/components/unmarked-link.html b/example/components/unmarked-link.html index 6f0a790..252b269 100644 --- a/example/components/unmarked-link.html +++ b/example/components/unmarked-link.html @@ -1,5 +1,5 @@ - <:slot> + [[ this.label? ]] - + diff --git a/example/layout/grid.html b/example/layout/grid.html index 0d74628..caae756 100644 --- a/example/layout/grid.html +++ b/example/layout/grid.html @@ -1,3 +1,3 @@
- <:slot /> +
diff --git a/example/layout/heading.html b/example/layout/heading.html index 1c50360..3720934 100644 --- a/example/layout/heading.html +++ b/example/layout/heading.html @@ -1,3 +1,3 @@
- <:slot /> +
diff --git a/example/layout/separator.html b/example/layout/separator.html index 6bcf6b9..e9ad10e 100644 --- a/example/layout/separator.html +++ b/example/layout/separator.html @@ -1,5 +1,5 @@

- <:slot /> +
diff --git a/lang/context/context.go b/lang/context/context.go index 60e7b1d..4da9333 100644 --- a/lang/context/context.go +++ b/lang/context/context.go @@ -2,6 +2,7 @@ package context import ( "fmt" + "path/filepath" "slices" "strings" "unicode" @@ -139,7 +140,8 @@ func (ctx *Context) Compute(expression string) (string, error) { return result, nil } -func (ctx *Context) queryPath(field string) (result string, err error) { +func (ctx *Context) queryPath(fieldPath string) (result string, err error) { + field := strings.TrimSuffix(fieldPath, "?") segments := strings.Split(field, ".")[1:] path := make([]string, 0, len(segments)) operations := make([]string, 0, len(segments)) @@ -160,6 +162,9 @@ func (ctx *Context) queryPath(field string) (result string, err error) { } if err != nil { + if strings.HasSuffix(fieldPath, "?") { + return "", nil + } return "", fmt.Errorf("%s: %w", field, err) } @@ -181,6 +186,14 @@ func (ctx *Context) queryPath(field string) (result string, err error) { result = strings.ToUpper(result[:1]) + result[1:] case "length": result = fmt.Sprintf("%d", len(result)) + case "filename": + result = filepath.Base(result) + case "dir": + result = filepath.Dir(result) + case "extension": + result = filepath.Ext(result) + case "trim_extension": + result = strings.TrimSuffix(result, filepath.Ext(result)) default: return "", fmt.Errorf("%s: unknown operation %q on %q", field, operation+"()", result) } diff --git a/lang/context/context_test.go b/lang/context/context_test.go index f37d08d..e25709c 100644 --- a/lang/context/context_test.go +++ b/lang/context/context_test.go @@ -42,6 +42,7 @@ func TestContextExpressions(test *testing.T) { assert.NotNil(ctx) ctx.Set([]string{"a", "b", "c"}, "Hello World!") + ctx.Set([]string{"a", "path"}, "/tmp/some/filename.html") var cases = []struct { Expression string @@ -132,6 +133,16 @@ func TestContextExpressions(test *testing.T) { ExpectErr: false, Expect: "false", }, + { + Expression: "this.a.path.extension()", + ExpectErr: false, + Expect: ".html", + }, + { + Expression: "this.a.path.filename().trim_extension()", + ExpectErr: false, + Expect: "filename", + }, { Expression: "this.a.b.unknown == 69", ExpectErr: true, diff --git a/lang/context/parse.go b/lang/context/parse.go index 1ab3f5a..17f39e7 100644 --- a/lang/context/parse.go +++ b/lang/context/parse.go @@ -37,5 +37,56 @@ func parseDict(str string) *Context { return dict } + content := strings.TrimSpace(str[1 : len(str)-1]) + for len(content) > 0 { + // key up to '=' + eq := strings.Index(content, "=") + if eq < 0 { + break + } + key := content[:eq] + content = content[eq+1:] + if len(content) == 0 { + break + } + + switch { + // nested dict + case content[0] == '{': + depth := 1 + i := 1 + for ; i < len(content) && depth > 0; i++ { + switch content[i] { + case '{': + depth++ + case '}': + depth-- + } + } + sub := content[:i] + dict.Values[key] = parseDict(sub) + content = strings.TrimLeft(content[i:], " ") + + // quoted string + case content[0] == '\'': + i := 1 + for ; i < len(content) && content[i] != '\''; i++ { + } + dict.Values[key] = content[1:i] + content = strings.TrimLeft(content[i+1:], " ") + + // bare word + default: + i := strings.Index(content, " ") + if i < 0 { + dict.Values[key] = content + content = "" + } else { + dict.Values[key] = content[:i] + content = strings.TrimLeft(content[i+1:], " ") + } + } + } + return dict } diff --git a/lang/template.go b/lang/template.go index 5e9415f..2282857 100644 --- a/lang/template.go +++ b/lang/template.go @@ -9,7 +9,7 @@ import ( "golang.org/x/net/html" ) -const MEND_PREFIX = ":" +const MEND_PREFIX = "mend:" const PKG_PREFIX = "pkg:" type Template struct { @@ -24,6 +24,7 @@ type Template struct { thisText string thisAttrs *attrs.Attributes thisLineIndex int + thisIndent int } func New(indent int, ctx *context.Context, dir, name string) *Template { @@ -31,12 +32,13 @@ func New(indent int, ctx *context.Context, dir, name string) *Template { Dir: dir, Name: name, Context: ctx, - Breadcrumbs: []templating.PairedTag{templating.NewPairedBase(indent)}, + Breadcrumbs: []templating.PairedTag{templating.NewMendSlot(indent)}, Slot: nil, thisToken: html.Token{}, thisText: "", thisAttrs: nil, thisLineIndex: 0, + thisIndent: indent, } } @@ -55,6 +57,7 @@ func (template *Template) Pivot() templating.PairedTag { func (template *Template) EnterPivot(tag templating.PairedTag) { template.Pivot().Append(tag) template.Breadcrumbs = append(template.Breadcrumbs, tag) + template.thisIndent++ } func (template *Template) ExitPivot() { @@ -62,4 +65,17 @@ func (template *Template) ExitPivot() { return } template.Breadcrumbs = template.Breadcrumbs[:len(template.Breadcrumbs)-1] + template.thisIndent-- +} + +func (template *Template) requireAttr(key string) (string, error) { + src, ok := template.thisAttrs.Values[key] + if !ok { + return "", fmt.Errorf( + "<%s> requires an `:%s=\"...\"` attribute", + template.thisText, + key, + ) + } + return src, nil } diff --git a/lang/template_branch.go b/lang/template_branch.go new file mode 100644 index 0000000..e4cf942 --- /dev/null +++ b/lang/template_branch.go @@ -0,0 +1,67 @@ +package lang + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/bbfh-dev/mend/lang/context" +) + +func (template *Template) BranchOut(location string) (*Template, error) { + file, err := os.OpenFile(location, os.O_RDONLY, os.ModePerm) + if err != nil { + return nil, err + } + defer file.Close() + + branch := New( + template.thisIndent, + context.ParseAttrs(template.thisToken.Attr), + filepath.Dir(location), + filepath.Base(location), + ) + if err := branch.Build(file); err != nil { + return nil, err + } + + return branch, nil +} + +func (template *Template) Find(name string) (string, bool) { + var result string + + filepath.WalkDir(template.Dir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + if entry.IsDir() { + return nil + } + + if entry.Name() == name { + result = path + return filepath.SkipAll + } + + return nil + }) + + return result, result != "" +} + +func (template *Template) locateTemplate(tag string) (string, error) { + location, found := template.Find(tag + ".html") + if !found { + abs, _ := filepath.Abs(template.Dir) + return "", fmt.Errorf( + "Can't find template <%s%s> in %s/*", + PKG_PREFIX, + tag, + abs, + ) + } + return location, nil +} diff --git a/lang/template_build.go b/lang/template_build.go new file mode 100644 index 0000000..4d8fd50 --- /dev/null +++ b/lang/template_build.go @@ -0,0 +1,223 @@ +package lang + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/bbfh-dev/mend/lang/attrs" + "github.com/bbfh-dev/mend/lang/expressions" + "github.com/bbfh-dev/mend/lang/printer" + "github.com/bbfh-dev/mend/lang/templating" + "golang.org/x/net/html" +) + +func (template *Template) Build(reader io.Reader) error { + tokenizer := html.NewTokenizer(reader) + + for { + tokenType := tokenizer.Next() + if tokenType == html.ErrorToken { + err := tokenizer.Err() + if err == io.EOF { + return nil + } + return fmt.Errorf("(%s): %w", template.Cursor(), err) + } + + template.thisToken = tokenizer.Token() + template.thisText = strings.TrimSpace(template.thisToken.Data) + template.thisAttrs = attrs.New(template.thisToken.Attr) + for key, value := range template.thisAttrs.Values { + text, err := expressions.Parse(value, template.Context.Compute) + if err != nil { + return fmt.Errorf("%s (expression): %w", template.Cursor(), err) + } + template.thisAttrs.Values[key] = text + } + + if tokenType == html.TextToken { + template.thisLineIndex += strings.Count(template.thisText, "\n") + } + + if err := template.buildToken(tokenType); err != nil { + return fmt.Errorf("(%s): %w", template.Cursor(), err) + } + } +} + +func (template *Template) buildToken(tokenType html.TokenType) error { + switch tokenType { + + case html.DoctypeToken: + template.Pivot().Append(templating.NewDoctype( + template.thisIndent, + template.thisText, + )) + + case html.CommentToken: + if printer.StripComments { + break + } + template.Pivot().Append(templating.NewComment( + template.thisIndent, + template.thisText, + )) + + case html.TextToken: + if len(template.thisText) == 0 { + break + } + text, err := expressions.Parse(template.thisText, template.Context.Compute) + if err != nil { + return fmt.Errorf("(expression): %w", err) + } + template.Pivot().Append(templating.NewText( + template.thisIndent, + text, + )) + + case html.SelfClosingTagToken: + switch { + + case strings.HasPrefix(template.thisText, MEND_PREFIX): + name := strings.TrimPrefix(template.thisText, MEND_PREFIX) + switch name { + + case "slot": + tag := templating.NewMendSlot(template.thisIndent) + template.Pivot().Append(tag) + template.Slot = tag + + case "include": + src, err := template.requireAttr(":src") + if err != nil { + return err + } + + branch, err := template.BranchOut(filepath.Join(template.Dir, src)) + if err != nil { + return err + } + + template.Pivot().Append(branch.Root()) + + default: + return fmt.Errorf("unknown tag <%s%s />", MEND_PREFIX, name) + } + + case strings.HasPrefix(template.thisText, PKG_PREFIX): + name := strings.TrimPrefix(template.thisText, PKG_PREFIX) + location, err := template.locateTemplate(name) + if err != nil { + return err + } + + branch, err := template.BranchOut(location) + if err != nil { + return err + } + + template.Pivot().Append(branch.Root()) + + default: + template.Pivot().Append(templating.NewSelfClosing( + template.thisIndent, + template.thisText, + template.thisAttrs, + )) + } + + case html.StartTagToken: + switch { + + case strings.HasPrefix(template.thisText, MEND_PREFIX): + name := strings.TrimPrefix(template.thisText, MEND_PREFIX) + switch name { + + case "slot": + tag := templating.NewMendSlot(template.thisIndent) + template.EnterPivot(tag) + template.Slot = tag + + case "if": + + case "extend": + src, err := template.requireAttr(":src") + if err != nil { + return err + } + + branch, err := template.BranchOut(filepath.Join(template.Dir, src)) + if err != nil { + return err + } + + template.EnterPivot(templating.NewMendExtend( + template.thisIndent, + branch.Root(), + branch.Slot, + )) + + default: + return fmt.Errorf("unknown tag <%s%s>", MEND_PREFIX, name) + } + + case strings.HasPrefix(template.thisText, PKG_PREFIX): + name := strings.TrimPrefix(template.thisText, PKG_PREFIX) + location, err := template.locateTemplate(name) + if err != nil { + return err + } + + branch, err := template.BranchOut(location) + if err != nil { + return err + } + + template.EnterPivot(templating.NewMendExtend( + template.thisIndent, + branch.Root(), + branch.Slot, + )) + + case template.thisText == "html": + template.EnterPivot( + templating.NewDefaultRoot( + template.thisIndent, + template.thisText, + template.thisAttrs, + ), + ) + + default: + template.EnterPivot( + templating.NewDefault( + template.thisIndent, + template.thisText, + template.thisAttrs, + ), + ) + } + + case html.EndTagToken: + switch tag := template.Pivot().(type) { + case *templating.MendExtendTag: + if tag.Slot == nil { + fmt.Fprintf( + os.Stderr, + "WARN: (%s) couldn't find block inside of extended file. Skipping body\n", + template.Cursor(), + ) + } else { + tag.Slot.SetChildren(tag.Children) + tag.Slot.Shift(template.thisIndent) + } + } + template.ExitPivot() + } + + return nil +} diff --git a/lang/templating/tag_base_paired.go b/lang/templating/tag_base_paired.go index e0ee4d5..37ea0a1 100644 --- a/lang/templating/tag_base_paired.go +++ b/lang/templating/tag_base_paired.go @@ -1,6 +1,8 @@ package templating -import "github.com/bbfh-dev/mend/lang/printer" +import ( + "github.com/bbfh-dev/mend/lang/printer" +) type BasePairedTag struct { *BaseTag @@ -16,6 +18,7 @@ func NewPairedBase(indent int) *BasePairedTag { func (tag *BasePairedTag) Render(writer printer.Writer) { for _, child := range tag.Children { + // fmt.Fprintf(writer, "\n", reflect.TypeOf(child), child) switch child.Visibility() { case VISIBLE: child.Render(writer) @@ -26,7 +29,7 @@ func (tag *BasePairedTag) Render(writer printer.Writer) { } } -func (tag *BasePairedTag) Offset(offset int) { +func (tag *BasePairedTag) Shift(offset int) { tag.BaseTag.Shift(offset) for _, child := range tag.Children { child.Shift(offset) diff --git a/lang/templating/tag_default.go b/lang/templating/tag_default.go index 38fbb9d..6463ed5 100644 --- a/lang/templating/tag_default.go +++ b/lang/templating/tag_default.go @@ -29,12 +29,8 @@ func (tag *DefaultTag) Render(writer printer.Writer) { tag.Attrs.Render(writer) writer.WriteString(">\n") - tag.RenderBody(writer) + tag.BasePairedTag.Render(writer) tag.BaseTag.Render(writer) fmt.Fprintf(writer, "", tag.Name) } - -func (tag *DefaultTag) RenderBody(writer printer.Writer) { - tag.BasePairedTag.Render(writer) -} diff --git a/lang/templating/tag_default_root.go b/lang/templating/tag_default_root.go index 1386ac4..f5d0b6b 100644 --- a/lang/templating/tag_default_root.go +++ b/lang/templating/tag_default_root.go @@ -1,6 +1,8 @@ package templating import ( + "fmt" + "github.com/bbfh-dev/mend/lang/attrs" "github.com/bbfh-dev/mend/lang/printer" ) @@ -15,7 +17,13 @@ func NewDefaultRoot(indent int, name string, attrs *attrs.Attributes) *DefaultRo } } -func (tag *DefaultRootTag) RenderBody(writer printer.Writer) { +func (tag *DefaultRootTag) Render(writer printer.Writer) { + tag.BaseTag.Render(writer) + + fmt.Fprintf(writer, "<%s", tag.Name) + tag.Attrs.Render(writer) + writer.WriteString(">\n") + writer.WriteString("\n") for _, child := range tag.Children { if child.Visibility() != INVISIBLE { @@ -24,4 +32,7 @@ func (tag *DefaultRootTag) RenderBody(writer printer.Writer) { writer.WriteString("\n\n") } } + + tag.BaseTag.Render(writer) + fmt.Fprintf(writer, "", tag.Name) } diff --git a/lang/templating/tag_mend_extend.go b/lang/templating/tag_mend_extend.go index 113168e..af9cb0e 100644 --- a/lang/templating/tag_mend_extend.go +++ b/lang/templating/tag_mend_extend.go @@ -1,6 +1,8 @@ package templating -import "github.com/bbfh-dev/mend/lang/printer" +import ( + "github.com/bbfh-dev/mend/lang/printer" +) type MendExtendTag struct { *BasePairedTag @@ -8,7 +10,7 @@ type MendExtendTag struct { Slot *MendSlotTag } -func NewExtendSlot(indent int, root PairedTag, slot *MendSlotTag) *MendExtendTag { +func NewMendExtend(indent int, root PairedTag, slot *MendSlotTag) *MendExtendTag { return &MendExtendTag{ BasePairedTag: NewPairedBase(indent), Root: root, From 64bebb706f8f83eab5583adb8c9ef97fef50c6ec Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 12:27:15 +0300 Subject: [PATCH 07/18] fix: Indentation isnt broken because it's handled by Render() now --- cli/cli.go | 2 +- lang/printer/printer.go | 3 +++ lang/template.go | 2 +- lang/template_build.go | 13 ++---------- lang/templating/tag.go | 4 +--- lang/templating/tag_base.go | 23 +++++++-------------- lang/templating/tag_base_paired.go | 17 +++++---------- lang/templating/tag_comment.go | 8 +++---- lang/templating/tag_default.go | 12 +++++------ lang/templating/tag_default_root.go | 13 ++++++------ lang/templating/tag_default_self_closing.go | 8 +++---- lang/templating/tag_doctype.go | 8 +++---- lang/templating/tag_mend_extend.go | 8 +++---- lang/templating/tag_mend_slot.go | 4 ++-- lang/templating/tag_text.go | 8 +++---- 15 files changed, 54 insertions(+), 79 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 3182f5f..7c1715a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -55,7 +55,7 @@ func Main(args []string) error { return err } - template.Root().Render(os.Stdout) + template.Root().Render(os.Stdout, -1) } return nil diff --git a/lang/printer/printer.go b/lang/printer/printer.go index 83637ed..d2b8134 100644 --- a/lang/printer/printer.go +++ b/lang/printer/printer.go @@ -14,5 +14,8 @@ type Writer interface { } func WriteIndent(writer Writer, indent int) { + if indent < 0 { + return + } writer.WriteString(strings.Repeat(IndentString, indent)) } diff --git a/lang/template.go b/lang/template.go index 2282857..357d65c 100644 --- a/lang/template.go +++ b/lang/template.go @@ -32,7 +32,7 @@ func New(indent int, ctx *context.Context, dir, name string) *Template { Dir: dir, Name: name, Context: ctx, - Breadcrumbs: []templating.PairedTag{templating.NewMendSlot(indent)}, + Breadcrumbs: []templating.PairedTag{templating.NewMendSlot()}, Slot: nil, thisToken: html.Token{}, thisText: "", diff --git a/lang/template_build.go b/lang/template_build.go index 4d8fd50..d2074d2 100644 --- a/lang/template_build.go +++ b/lang/template_build.go @@ -53,7 +53,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { case html.DoctypeToken: template.Pivot().Append(templating.NewDoctype( - template.thisIndent, template.thisText, )) @@ -62,7 +61,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { break } template.Pivot().Append(templating.NewComment( - template.thisIndent, template.thisText, )) @@ -75,7 +73,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { return fmt.Errorf("(expression): %w", err) } template.Pivot().Append(templating.NewText( - template.thisIndent, text, )) @@ -87,7 +84,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { switch name { case "slot": - tag := templating.NewMendSlot(template.thisIndent) + tag := templating.NewMendSlot() template.Pivot().Append(tag) template.Slot = tag @@ -124,7 +121,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { default: template.Pivot().Append(templating.NewSelfClosing( - template.thisIndent, template.thisText, template.thisAttrs, )) @@ -138,7 +134,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { switch name { case "slot": - tag := templating.NewMendSlot(template.thisIndent) + tag := templating.NewMendSlot() template.EnterPivot(tag) template.Slot = tag @@ -156,7 +152,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { } template.EnterPivot(templating.NewMendExtend( - template.thisIndent, branch.Root(), branch.Slot, )) @@ -178,7 +173,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { } template.EnterPivot(templating.NewMendExtend( - template.thisIndent, branch.Root(), branch.Slot, )) @@ -186,7 +180,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { case template.thisText == "html": template.EnterPivot( templating.NewDefaultRoot( - template.thisIndent, template.thisText, template.thisAttrs, ), @@ -195,7 +188,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { default: template.EnterPivot( templating.NewDefault( - template.thisIndent, template.thisText, template.thisAttrs, ), @@ -213,7 +205,6 @@ func (template *Template) buildToken(tokenType html.TokenType) error { ) } else { tag.Slot.SetChildren(tag.Children) - tag.Slot.Shift(template.thisIndent) } } template.ExitPivot() diff --git a/lang/templating/tag.go b/lang/templating/tag.go index 8933b42..84e8d28 100644 --- a/lang/templating/tag.go +++ b/lang/templating/tag.go @@ -13,10 +13,8 @@ const ( ) type Tag interface { - Render(writer printer.Writer) + Render(writer printer.Writer, indent int) Visibility() visibility - Indent() int - Shift(offset int) Clone() Tag } diff --git a/lang/templating/tag_base.go b/lang/templating/tag_base.go index 8635095..29d991e 100644 --- a/lang/templating/tag_base.go +++ b/lang/templating/tag_base.go @@ -1,33 +1,24 @@ package templating -import "github.com/bbfh-dev/mend/lang/printer" +import ( + "github.com/bbfh-dev/mend/lang/printer" +) type BaseTag struct { - indent int } -func NewBase(indent int) *BaseTag { - return &BaseTag{ - indent: indent, - } +func NewBase() *BaseTag { + return &BaseTag{} } -func (tag *BaseTag) Render(writer printer.Writer) { - printer.WriteIndent(writer, tag.indent) +func (tag *BaseTag) Render(writer printer.Writer, indent int) { + printer.WriteIndent(writer, indent) } func (tag *BaseTag) Visibility() visibility { return VISIBLE } -func (tag *BaseTag) Indent() int { - return tag.indent -} - -func (tag *BaseTag) Shift(offset int) { - tag.indent += offset -} - func (tag *BaseTag) Clone() Tag { clone := *tag return &clone diff --git a/lang/templating/tag_base_paired.go b/lang/templating/tag_base_paired.go index 37ea0a1..2c38672 100644 --- a/lang/templating/tag_base_paired.go +++ b/lang/templating/tag_base_paired.go @@ -9,33 +9,26 @@ type BasePairedTag struct { Children []Tag } -func NewPairedBase(indent int) *BasePairedTag { +func NewPairedBase() *BasePairedTag { return &BasePairedTag{ - BaseTag: NewBase(indent), + BaseTag: NewBase(), Children: []Tag{}, } } -func (tag *BasePairedTag) Render(writer printer.Writer) { +func (tag *BasePairedTag) Render(writer printer.Writer, indent int) { for _, child := range tag.Children { // fmt.Fprintf(writer, "\n", reflect.TypeOf(child), child) switch child.Visibility() { case VISIBLE: - child.Render(writer) + child.Render(writer, indent+1) writer.WriteString("\n") case INLINE: - child.Render(writer) + child.Render(writer, indent) } } } -func (tag *BasePairedTag) Shift(offset int) { - tag.BaseTag.Shift(offset) - for _, child := range tag.Children { - child.Shift(offset) - } -} - func (tag *BasePairedTag) SetChildren(tags []Tag) { tag.Children = tags } diff --git a/lang/templating/tag_comment.go b/lang/templating/tag_comment.go index 99bca9d..95d7eb0 100644 --- a/lang/templating/tag_comment.go +++ b/lang/templating/tag_comment.go @@ -11,14 +11,14 @@ type CommentTag struct { Comment string } -func NewComment(indent int, comment string) *CommentTag { +func NewComment(comment string) *CommentTag { return &CommentTag{ - BaseTag: NewBase(indent), + BaseTag: NewBase(), Comment: comment, } } -func (tag *CommentTag) Render(writer printer.Writer) { - tag.BaseTag.Render(writer) +func (tag *CommentTag) Render(writer printer.Writer, indent int) { + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "", tag.Comment) } diff --git a/lang/templating/tag_default.go b/lang/templating/tag_default.go index 6463ed5..52643c8 100644 --- a/lang/templating/tag_default.go +++ b/lang/templating/tag_default.go @@ -12,9 +12,9 @@ type DefaultTag struct { BaseDefaultTag } -func NewDefault(indent int, name string, attrs *attrs.Attributes) *DefaultTag { +func NewDefault(name string, attrs *attrs.Attributes) *DefaultTag { return &DefaultTag{ - BasePairedTag: NewPairedBase(indent), + BasePairedTag: NewPairedBase(), BaseDefaultTag: BaseDefaultTag{ Name: name, Attrs: attrs, @@ -22,15 +22,15 @@ func NewDefault(indent int, name string, attrs *attrs.Attributes) *DefaultTag { } } -func (tag *DefaultTag) Render(writer printer.Writer) { - tag.BaseTag.Render(writer) +func (tag *DefaultTag) Render(writer printer.Writer, indent int) { + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "<%s", tag.Name) tag.Attrs.Render(writer) writer.WriteString(">\n") - tag.BasePairedTag.Render(writer) + tag.BasePairedTag.Render(writer, indent) - tag.BaseTag.Render(writer) + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "", tag.Name) } diff --git a/lang/templating/tag_default_root.go b/lang/templating/tag_default_root.go index f5d0b6b..7d5a6c2 100644 --- a/lang/templating/tag_default_root.go +++ b/lang/templating/tag_default_root.go @@ -11,14 +11,14 @@ type DefaultRootTag struct { *DefaultTag } -func NewDefaultRoot(indent int, name string, attrs *attrs.Attributes) *DefaultRootTag { +func NewDefaultRoot(name string, attrs *attrs.Attributes) *DefaultRootTag { return &DefaultRootTag{ - DefaultTag: NewDefault(indent, name, attrs), + DefaultTag: NewDefault(name, attrs), } } -func (tag *DefaultRootTag) Render(writer printer.Writer) { - tag.BaseTag.Render(writer) +func (tag *DefaultRootTag) Render(writer printer.Writer, indent int) { + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "<%s", tag.Name) tag.Attrs.Render(writer) @@ -27,12 +27,11 @@ func (tag *DefaultRootTag) Render(writer printer.Writer) { writer.WriteString("\n") for _, child := range tag.Children { if child.Visibility() != INVISIBLE { - child.Shift(-1) - child.Render(writer) + child.Render(writer, indent) writer.WriteString("\n\n") } } - tag.BaseTag.Render(writer) + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "", tag.Name) } diff --git a/lang/templating/tag_default_self_closing.go b/lang/templating/tag_default_self_closing.go index 5658293..9cfb7df 100644 --- a/lang/templating/tag_default_self_closing.go +++ b/lang/templating/tag_default_self_closing.go @@ -12,9 +12,9 @@ type SelfClosingTag struct { BaseDefaultTag } -func NewSelfClosing(indent int, name string, attrs *attrs.Attributes) *SelfClosingTag { +func NewSelfClosing(name string, attrs *attrs.Attributes) *SelfClosingTag { return &SelfClosingTag{ - BaseTag: NewBase(indent), + BaseTag: NewBase(), BaseDefaultTag: BaseDefaultTag{ Name: name, Attrs: attrs, @@ -22,8 +22,8 @@ func NewSelfClosing(indent int, name string, attrs *attrs.Attributes) *SelfClosi } } -func (tag *SelfClosingTag) Render(writer printer.Writer) { - tag.BaseTag.Render(writer) +func (tag *SelfClosingTag) Render(writer printer.Writer, indent int) { + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "<%s", tag.Name) tag.Attrs.Render(writer) diff --git a/lang/templating/tag_doctype.go b/lang/templating/tag_doctype.go index e686fd9..c03d50e 100644 --- a/lang/templating/tag_doctype.go +++ b/lang/templating/tag_doctype.go @@ -11,14 +11,14 @@ type DoctypeTag struct { Doctype string } -func NewDoctype(indent int, doctype string) *DoctypeTag { +func NewDoctype(doctype string) *DoctypeTag { return &DoctypeTag{ - BaseTag: NewBase(indent), + BaseTag: NewBase(), Doctype: doctype, } } -func (tag *DoctypeTag) Render(writer printer.Writer) { - tag.BaseTag.Render(writer) +func (tag *DoctypeTag) Render(writer printer.Writer, indent int) { + tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "", tag.Doctype) } diff --git a/lang/templating/tag_mend_extend.go b/lang/templating/tag_mend_extend.go index af9cb0e..003566c 100644 --- a/lang/templating/tag_mend_extend.go +++ b/lang/templating/tag_mend_extend.go @@ -10,16 +10,16 @@ type MendExtendTag struct { Slot *MendSlotTag } -func NewMendExtend(indent int, root PairedTag, slot *MendSlotTag) *MendExtendTag { +func NewMendExtend(root PairedTag, slot *MendSlotTag) *MendExtendTag { return &MendExtendTag{ - BasePairedTag: NewPairedBase(indent), + BasePairedTag: NewPairedBase(), Root: root, Slot: slot, } } -func (tag *MendExtendTag) Render(writer printer.Writer) { - tag.Root.Render(writer) +func (tag *MendExtendTag) Render(writer printer.Writer, indent int) { + tag.Root.Render(writer, indent) } func (tag *MendExtendTag) Visibility() visibility { diff --git a/lang/templating/tag_mend_slot.go b/lang/templating/tag_mend_slot.go index d3bca6a..a5b2acd 100644 --- a/lang/templating/tag_mend_slot.go +++ b/lang/templating/tag_mend_slot.go @@ -4,9 +4,9 @@ type MendSlotTag struct { *BasePairedTag } -func NewMendSlot(indent int) *MendSlotTag { +func NewMendSlot() *MendSlotTag { return &MendSlotTag{ - BasePairedTag: NewPairedBase(indent), + BasePairedTag: NewPairedBase(), } } diff --git a/lang/templating/tag_text.go b/lang/templating/tag_text.go index 22aabca..2594986 100644 --- a/lang/templating/tag_text.go +++ b/lang/templating/tag_text.go @@ -11,15 +11,15 @@ type TextTag struct { Text string } -func NewText(indent int, text string) *TextTag { +func NewText(text string) *TextTag { return &TextTag{ - BaseTag: NewBase(indent), + BaseTag: NewBase(), Text: text, } } -func (tag *TextTag) Render(writer printer.Writer) { - tag.BaseTag.Render(writer) +func (tag *TextTag) Render(writer printer.Writer, indent int) { + tag.BaseTag.Render(writer, indent) lines := strings.Split(tag.Text, "\n") lastLine := len(lines) - 1 From 467463173a45fe0dba7f00c63aaae02b859932de Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 12:39:41 +0300 Subject: [PATCH 08/18] fix: Make parser report accurate line of the current token --- lang/template_build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/template_build.go b/lang/template_build.go index d2074d2..6383739 100644 --- a/lang/template_build.go +++ b/lang/template_build.go @@ -39,7 +39,7 @@ func (template *Template) Build(reader io.Reader) error { } if tokenType == html.TextToken { - template.thisLineIndex += strings.Count(template.thisText, "\n") + template.thisLineIndex += strings.Count(template.thisToken.Data, "\n") } if err := template.buildToken(tokenType); err != nil { From 5ca42618313265ff7dc8dae6a3eb24ca2045394f Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 12:40:12 +0300 Subject: [PATCH 09/18] feat: Implement --- example/components/root.html | 95 +++++++++++++++++--------------- lang/template_build.go | 19 +++++++ lang/templating/tag_mend_void.go | 15 +++++ 3 files changed, 84 insertions(+), 45 deletions(-) create mode 100644 lang/templating/tag_mend_void.go diff --git a/example/components/root.html b/example/components/root.html index 64f54e4..66e65af 100644 --- a/example/components/root.html +++ b/example/components/root.html @@ -22,27 +22,32 @@

Smithed

+ + + Weld - - - - - - - - - - - + + +
+ + +
+
+ + +
+
+ Inbox + @@ -50,40 +55,40 @@

Smithed

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+
+

+ ABOUT +

+ + + +
+
+

+ COMMUNITY +

+ + + +
+
+

+ LEGAL +

+ + + +
+
+
+ Copyright © 2018-2025 Smithed +

+ Not an official Minecraft product. Not approved by or associated with Mojang Studios +

+
+
diff --git a/lang/template_build.go b/lang/template_build.go index 6383739..4c21264 100644 --- a/lang/template_build.go +++ b/lang/template_build.go @@ -139,6 +139,25 @@ func (template *Template) buildToken(tokenType html.TokenType) error { template.Slot = tag case "if": + checkTrue, okTrue := template.thisAttrs.Values[":true"] + checkFalse, okFalse := template.thisAttrs.Values[":false"] + switch { + case okTrue: + if checkTrue != "true" { + template.EnterPivot(templating.NewMendVoid()) + return nil + } + case okFalse: + if checkFalse != "false" { + template.EnterPivot(templating.NewMendVoid()) + return nil + } + default: + return fmt.Errorf( + " requires a `:true=\"...\"` or `:false=\"...\"` attribute", + ) + } + template.EnterPivot(templating.NewMendSlot()) case "extend": src, err := template.requireAttr(":src") diff --git a/lang/templating/tag_mend_void.go b/lang/templating/tag_mend_void.go new file mode 100644 index 0000000..ff74a88 --- /dev/null +++ b/lang/templating/tag_mend_void.go @@ -0,0 +1,15 @@ +package templating + +type MendVoidTag struct { + *BasePairedTag +} + +func NewMendVoid() *MendVoidTag { + return &MendVoidTag{ + BasePairedTag: NewPairedBase(), + } +} + +func (tag *MendVoidTag) Visibility() visibility { + return INVISIBLE +} From f6019ec577ff46c1a695fe0a134223347e5cecf5 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 12:40:22 +0300 Subject: [PATCH 10/18] feat: Make icons be actual icons and not an empty svg --- example/components/icons/notification-unread.svg | 4 +++- example/components/icons/popout.svg | 5 ++++- example/components/icons/search.svg | 6 +++++- example/components/icons/user.svg | 6 +++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/example/components/icons/notification-unread.svg b/example/components/icons/notification-unread.svg index 3e8fcac..a0b5072 100644 --- a/example/components/icons/notification-unread.svg +++ b/example/components/icons/notification-unread.svg @@ -1 +1,3 @@ - + + + diff --git a/example/components/icons/popout.svg b/example/components/icons/popout.svg index 3e8fcac..2f33168 100644 --- a/example/components/icons/popout.svg +++ b/example/components/icons/popout.svg @@ -1 +1,4 @@ - + + + diff --git a/example/components/icons/search.svg b/example/components/icons/search.svg index 3e8fcac..e08725f 100644 --- a/example/components/icons/search.svg +++ b/example/components/icons/search.svg @@ -1 +1,5 @@ - + + + diff --git a/example/components/icons/user.svg b/example/components/icons/user.svg index 3e8fcac..e299092 100644 --- a/example/components/icons/user.svg +++ b/example/components/icons/user.svg @@ -1 +1,5 @@ - + + + From 7d1925d5351b089b0e9eebda616f5c1e6458b81c Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 14:36:48 +0300 Subject: [PATCH 11/18] refactor: Remove BaseDefaultTag and just do a little code repetition --- lang/templating/tag_base_default.go | 8 -------- lang/templating/tag_default.go | 9 ++++----- lang/templating/tag_default_self_closing.go | 9 ++++----- 3 files changed, 8 insertions(+), 18 deletions(-) delete mode 100644 lang/templating/tag_base_default.go diff --git a/lang/templating/tag_base_default.go b/lang/templating/tag_base_default.go deleted file mode 100644 index 50921cd..0000000 --- a/lang/templating/tag_base_default.go +++ /dev/null @@ -1,8 +0,0 @@ -package templating - -import "github.com/bbfh-dev/mend/lang/attrs" - -type BaseDefaultTag struct { - Name string - Attrs *attrs.Attributes -} diff --git a/lang/templating/tag_default.go b/lang/templating/tag_default.go index 52643c8..854e558 100644 --- a/lang/templating/tag_default.go +++ b/lang/templating/tag_default.go @@ -9,16 +9,15 @@ import ( type DefaultTag struct { *BasePairedTag - BaseDefaultTag + Name string + Attrs *attrs.Attributes } func NewDefault(name string, attrs *attrs.Attributes) *DefaultTag { return &DefaultTag{ BasePairedTag: NewPairedBase(), - BaseDefaultTag: BaseDefaultTag{ - Name: name, - Attrs: attrs, - }, + Name: name, + Attrs: attrs, } } diff --git a/lang/templating/tag_default_self_closing.go b/lang/templating/tag_default_self_closing.go index 9cfb7df..8506536 100644 --- a/lang/templating/tag_default_self_closing.go +++ b/lang/templating/tag_default_self_closing.go @@ -9,16 +9,15 @@ import ( type SelfClosingTag struct { *BaseTag - BaseDefaultTag + Name string + Attrs *attrs.Attributes } func NewSelfClosing(name string, attrs *attrs.Attributes) *SelfClosingTag { return &SelfClosingTag{ BaseTag: NewBase(), - BaseDefaultTag: BaseDefaultTag{ - Name: name, - Attrs: attrs, - }, + Name: name, + Attrs: attrs, } } From 054dd1ef21527726345537090b5c2a77104f4b01 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 14:37:09 +0300 Subject: [PATCH 12/18] feat: Add feature of attributes being overriden/expanded --- lang/attrs/attrs.go | 12 ++++++++++++ lang/template_branch.go | 8 ++++++++ lang/templating/tag.go | 1 + lang/templating/tag_base.go | 4 ++++ lang/templating/tag_base_paired.go | 9 +++++++++ lang/templating/tag_default.go | 5 +++++ lang/templating/tag_default_self_closing.go | 5 +++++ 7 files changed, 44 insertions(+) diff --git a/lang/attrs/attrs.go b/lang/attrs/attrs.go index 6e8bc45..371e809 100644 --- a/lang/attrs/attrs.go +++ b/lang/attrs/attrs.go @@ -33,6 +33,18 @@ func (attrs *Attributes) Render(out printer.Writer) { } } +func (attrs *Attributes) OverrideAttr(key string, value string) { + original, ok := attrs.Values[key] + if !ok { + attrs.order = append(attrs.order, key) + attrs.Values[key] = value + attrs.Sort() + return + } + + attrs.Values[key] = fmt.Sprintf(value, original) +} + func (attrs *Attributes) renderKey(out printer.Writer, key string) { if len(attrs.Values[key]) == 0 { out.WriteString(key) diff --git a/lang/template_branch.go b/lang/template_branch.go index e4cf942..03327d3 100644 --- a/lang/template_branch.go +++ b/lang/template_branch.go @@ -5,6 +5,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "github.com/bbfh-dev/mend/lang/context" ) @@ -26,6 +27,13 @@ func (template *Template) BranchOut(location string) (*Template, error) { return nil, err } + for key, attr := range template.thisAttrs.Values { + if strings.HasPrefix(key, ":") { + continue + } + branch.Root().OverrideAttr(key, attr) + } + return branch, nil } diff --git a/lang/templating/tag.go b/lang/templating/tag.go index 84e8d28..cdb32d6 100644 --- a/lang/templating/tag.go +++ b/lang/templating/tag.go @@ -16,6 +16,7 @@ type Tag interface { Render(writer printer.Writer, indent int) Visibility() visibility Clone() Tag + OverrideAttr(key string, value string) bool } type PairedTag interface { diff --git a/lang/templating/tag_base.go b/lang/templating/tag_base.go index 29d991e..1583af7 100644 --- a/lang/templating/tag_base.go +++ b/lang/templating/tag_base.go @@ -23,3 +23,7 @@ func (tag *BaseTag) Clone() Tag { clone := *tag return &clone } + +func (tag *BaseTag) OverrideAttr(key string, value string) bool { + return false +} diff --git a/lang/templating/tag_base_paired.go b/lang/templating/tag_base_paired.go index 2c38672..6b8ed15 100644 --- a/lang/templating/tag_base_paired.go +++ b/lang/templating/tag_base_paired.go @@ -36,3 +36,12 @@ func (tag *BasePairedTag) SetChildren(tags []Tag) { func (tag *BasePairedTag) Append(tags ...Tag) { tag.Children = append(tag.Children, tags...) } + +func (tag *BasePairedTag) OverrideAttr(key string, value string) bool { + for _, child := range tag.Children { + if child.OverrideAttr(key, value) { + return true + } + } + return false +} diff --git a/lang/templating/tag_default.go b/lang/templating/tag_default.go index 854e558..04970b7 100644 --- a/lang/templating/tag_default.go +++ b/lang/templating/tag_default.go @@ -33,3 +33,8 @@ func (tag *DefaultTag) Render(writer printer.Writer, indent int) { tag.BaseTag.Render(writer, indent) fmt.Fprintf(writer, "", tag.Name) } + +func (tag *DefaultTag) OverrideAttr(key, value string) bool { + tag.Attrs.OverrideAttr(key, value) + return true +} diff --git a/lang/templating/tag_default_self_closing.go b/lang/templating/tag_default_self_closing.go index 8506536..8ae4ef4 100644 --- a/lang/templating/tag_default_self_closing.go +++ b/lang/templating/tag_default_self_closing.go @@ -28,3 +28,8 @@ func (tag *SelfClosingTag) Render(writer printer.Writer, indent int) { tag.Attrs.Render(writer) writer.WriteString(" />") } + +func (tag *SelfClosingTag) OverrideAttr(key, value string) bool { + tag.Attrs.OverrideAttr(key, value) + return true +} From bd885a3c22aaf1578e717e19fde72da9260a9f30 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 14:39:51 +0300 Subject: [PATCH 13/18] refactor: Rename package 'templating' -> 'tags' --- lang/{templating => tags}/tag.go | 2 +- lang/{templating => tags}/tag_base.go | 2 +- lang/tags/tag_base_default.go | 5 ++++ lang/{templating => tags}/tag_base_paired.go | 2 +- lang/{templating => tags}/tag_comment.go | 2 +- lang/{templating => tags}/tag_default.go | 2 +- lang/{templating => tags}/tag_default_root.go | 2 +- .../tag_default_self_closing.go | 2 +- lang/{templating => tags}/tag_doctype.go | 2 +- lang/{templating => tags}/tag_mend_extend.go | 2 +- lang/{templating => tags}/tag_mend_slot.go | 2 +- lang/{templating => tags}/tag_mend_void.go | 2 +- lang/{templating => tags}/tag_text.go | 2 +- lang/template.go | 14 ++++----- lang/template_build.go | 30 +++++++++---------- 15 files changed, 39 insertions(+), 34 deletions(-) rename lang/{templating => tags}/tag.go (95%) rename lang/{templating => tags}/tag_base.go (95%) create mode 100644 lang/tags/tag_base_default.go rename lang/{templating => tags}/tag_base_paired.go (98%) rename lang/{templating => tags}/tag_comment.go (95%) rename lang/{templating => tags}/tag_default.go (97%) rename lang/{templating => tags}/tag_default_root.go (97%) rename lang/{templating => tags}/tag_default_self_closing.go (97%) rename lang/{templating => tags}/tag_doctype.go (95%) rename lang/{templating => tags}/tag_mend_extend.go (96%) rename lang/{templating => tags}/tag_mend_slot.go (91%) rename lang/{templating => tags}/tag_mend_void.go (91%) rename lang/{templating => tags}/tag_text.go (96%) diff --git a/lang/templating/tag.go b/lang/tags/tag.go similarity index 95% rename from lang/templating/tag.go rename to lang/tags/tag.go index cdb32d6..5baafec 100644 --- a/lang/templating/tag.go +++ b/lang/tags/tag.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "github.com/bbfh-dev/mend/lang/printer" diff --git a/lang/templating/tag_base.go b/lang/tags/tag_base.go similarity index 95% rename from lang/templating/tag_base.go rename to lang/tags/tag_base.go index 1583af7..3a9c44f 100644 --- a/lang/templating/tag_base.go +++ b/lang/tags/tag_base.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "github.com/bbfh-dev/mend/lang/printer" diff --git a/lang/tags/tag_base_default.go b/lang/tags/tag_base_default.go new file mode 100644 index 0000000..5fbbc2f --- /dev/null +++ b/lang/tags/tag_base_default.go @@ -0,0 +1,5 @@ +package tags + +type BaseDefaultTag struct { + *BasePairedTag +} diff --git a/lang/templating/tag_base_paired.go b/lang/tags/tag_base_paired.go similarity index 98% rename from lang/templating/tag_base_paired.go rename to lang/tags/tag_base_paired.go index 6b8ed15..2764ee2 100644 --- a/lang/templating/tag_base_paired.go +++ b/lang/tags/tag_base_paired.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "github.com/bbfh-dev/mend/lang/printer" diff --git a/lang/templating/tag_comment.go b/lang/tags/tag_comment.go similarity index 95% rename from lang/templating/tag_comment.go rename to lang/tags/tag_comment.go index 95d7eb0..bba62a8 100644 --- a/lang/templating/tag_comment.go +++ b/lang/tags/tag_comment.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "fmt" diff --git a/lang/templating/tag_default.go b/lang/tags/tag_default.go similarity index 97% rename from lang/templating/tag_default.go rename to lang/tags/tag_default.go index 04970b7..2099ebf 100644 --- a/lang/templating/tag_default.go +++ b/lang/tags/tag_default.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "fmt" diff --git a/lang/templating/tag_default_root.go b/lang/tags/tag_default_root.go similarity index 97% rename from lang/templating/tag_default_root.go rename to lang/tags/tag_default_root.go index 7d5a6c2..83aefb8 100644 --- a/lang/templating/tag_default_root.go +++ b/lang/tags/tag_default_root.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "fmt" diff --git a/lang/templating/tag_default_self_closing.go b/lang/tags/tag_default_self_closing.go similarity index 97% rename from lang/templating/tag_default_self_closing.go rename to lang/tags/tag_default_self_closing.go index 8ae4ef4..24d3691 100644 --- a/lang/templating/tag_default_self_closing.go +++ b/lang/tags/tag_default_self_closing.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "fmt" diff --git a/lang/templating/tag_doctype.go b/lang/tags/tag_doctype.go similarity index 95% rename from lang/templating/tag_doctype.go rename to lang/tags/tag_doctype.go index c03d50e..c09564a 100644 --- a/lang/templating/tag_doctype.go +++ b/lang/tags/tag_doctype.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "fmt" diff --git a/lang/templating/tag_mend_extend.go b/lang/tags/tag_mend_extend.go similarity index 96% rename from lang/templating/tag_mend_extend.go rename to lang/tags/tag_mend_extend.go index 003566c..14707ec 100644 --- a/lang/templating/tag_mend_extend.go +++ b/lang/tags/tag_mend_extend.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "github.com/bbfh-dev/mend/lang/printer" diff --git a/lang/templating/tag_mend_slot.go b/lang/tags/tag_mend_slot.go similarity index 91% rename from lang/templating/tag_mend_slot.go rename to lang/tags/tag_mend_slot.go index a5b2acd..d35f80f 100644 --- a/lang/templating/tag_mend_slot.go +++ b/lang/tags/tag_mend_slot.go @@ -1,4 +1,4 @@ -package templating +package tags type MendSlotTag struct { *BasePairedTag diff --git a/lang/templating/tag_mend_void.go b/lang/tags/tag_mend_void.go similarity index 91% rename from lang/templating/tag_mend_void.go rename to lang/tags/tag_mend_void.go index ff74a88..a70c69d 100644 --- a/lang/templating/tag_mend_void.go +++ b/lang/tags/tag_mend_void.go @@ -1,4 +1,4 @@ -package templating +package tags type MendVoidTag struct { *BasePairedTag diff --git a/lang/templating/tag_text.go b/lang/tags/tag_text.go similarity index 96% rename from lang/templating/tag_text.go rename to lang/tags/tag_text.go index 2594986..b6785f6 100644 --- a/lang/templating/tag_text.go +++ b/lang/tags/tag_text.go @@ -1,4 +1,4 @@ -package templating +package tags import ( "strings" diff --git a/lang/template.go b/lang/template.go index 357d65c..b3db844 100644 --- a/lang/template.go +++ b/lang/template.go @@ -5,7 +5,7 @@ import ( "github.com/bbfh-dev/mend/lang/attrs" "github.com/bbfh-dev/mend/lang/context" - "github.com/bbfh-dev/mend/lang/templating" + "github.com/bbfh-dev/mend/lang/tags" "golang.org/x/net/html" ) @@ -17,8 +17,8 @@ type Template struct { Name string Context *context.Context - Breadcrumbs []templating.PairedTag - Slot *templating.MendSlotTag + Breadcrumbs []tags.PairedTag + Slot *tags.MendSlotTag thisToken html.Token thisText string @@ -32,7 +32,7 @@ func New(indent int, ctx *context.Context, dir, name string) *Template { Dir: dir, Name: name, Context: ctx, - Breadcrumbs: []templating.PairedTag{templating.NewMendSlot()}, + Breadcrumbs: []tags.PairedTag{tags.NewMendSlot()}, Slot: nil, thisToken: html.Token{}, thisText: "", @@ -46,15 +46,15 @@ func (template *Template) Cursor() string { return fmt.Sprintf("%s:%d", template.Name, template.thisLineIndex+1) } -func (template *Template) Root() templating.PairedTag { +func (template *Template) Root() tags.PairedTag { return template.Breadcrumbs[0] } -func (template *Template) Pivot() templating.PairedTag { +func (template *Template) Pivot() tags.PairedTag { return template.Breadcrumbs[len(template.Breadcrumbs)-1] } -func (template *Template) EnterPivot(tag templating.PairedTag) { +func (template *Template) EnterPivot(tag tags.PairedTag) { template.Pivot().Append(tag) template.Breadcrumbs = append(template.Breadcrumbs, tag) template.thisIndent++ diff --git a/lang/template_build.go b/lang/template_build.go index 4c21264..20d0a5e 100644 --- a/lang/template_build.go +++ b/lang/template_build.go @@ -10,7 +10,7 @@ import ( "github.com/bbfh-dev/mend/lang/attrs" "github.com/bbfh-dev/mend/lang/expressions" "github.com/bbfh-dev/mend/lang/printer" - "github.com/bbfh-dev/mend/lang/templating" + "github.com/bbfh-dev/mend/lang/tags" "golang.org/x/net/html" ) @@ -52,7 +52,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { switch tokenType { case html.DoctypeToken: - template.Pivot().Append(templating.NewDoctype( + template.Pivot().Append(tags.NewDoctype( template.thisText, )) @@ -60,7 +60,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { if printer.StripComments { break } - template.Pivot().Append(templating.NewComment( + template.Pivot().Append(tags.NewComment( template.thisText, )) @@ -72,7 +72,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { if err != nil { return fmt.Errorf("(expression): %w", err) } - template.Pivot().Append(templating.NewText( + template.Pivot().Append(tags.NewText( text, )) @@ -84,7 +84,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { switch name { case "slot": - tag := templating.NewMendSlot() + tag := tags.NewMendSlot() template.Pivot().Append(tag) template.Slot = tag @@ -120,7 +120,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { template.Pivot().Append(branch.Root()) default: - template.Pivot().Append(templating.NewSelfClosing( + template.Pivot().Append(tags.NewSelfClosing( template.thisText, template.thisAttrs, )) @@ -134,7 +134,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { switch name { case "slot": - tag := templating.NewMendSlot() + tag := tags.NewMendSlot() template.EnterPivot(tag) template.Slot = tag @@ -144,12 +144,12 @@ func (template *Template) buildToken(tokenType html.TokenType) error { switch { case okTrue: if checkTrue != "true" { - template.EnterPivot(templating.NewMendVoid()) + template.EnterPivot(tags.NewMendVoid()) return nil } case okFalse: if checkFalse != "false" { - template.EnterPivot(templating.NewMendVoid()) + template.EnterPivot(tags.NewMendVoid()) return nil } default: @@ -157,7 +157,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { " requires a `:true=\"...\"` or `:false=\"...\"` attribute", ) } - template.EnterPivot(templating.NewMendSlot()) + template.EnterPivot(tags.NewMendSlot()) case "extend": src, err := template.requireAttr(":src") @@ -170,7 +170,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { return err } - template.EnterPivot(templating.NewMendExtend( + template.EnterPivot(tags.NewMendExtend( branch.Root(), branch.Slot, )) @@ -191,14 +191,14 @@ func (template *Template) buildToken(tokenType html.TokenType) error { return err } - template.EnterPivot(templating.NewMendExtend( + template.EnterPivot(tags.NewMendExtend( branch.Root(), branch.Slot, )) case template.thisText == "html": template.EnterPivot( - templating.NewDefaultRoot( + tags.NewDefaultRoot( template.thisText, template.thisAttrs, ), @@ -206,7 +206,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { default: template.EnterPivot( - templating.NewDefault( + tags.NewDefault( template.thisText, template.thisAttrs, ), @@ -215,7 +215,7 @@ func (template *Template) buildToken(tokenType html.TokenType) error { case html.EndTagToken: switch tag := template.Pivot().(type) { - case *templating.MendExtendTag: + case *tags.MendExtendTag: if tag.Slot == nil { fmt.Fprintf( os.Stderr, From 3940ef764030686f4ab8881b6ea6be7ecc6b4de1 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 18:03:16 +0300 Subject: [PATCH 14/18] feat: Handle self-closing tags that use wrong syntax --- lang/template_build.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lang/template_build.go b/lang/template_build.go index 20d0a5e..9f935cd 100644 --- a/lang/template_build.go +++ b/lang/template_build.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "slices" "strings" "github.com/bbfh-dev/mend/lang/attrs" @@ -205,6 +206,10 @@ func (template *Template) buildToken(tokenType html.TokenType) error { ) default: + // Is it actually a self-closing tag with wrong syntax? + if slices.Contains(attrs.SelfClosingTags, template.thisText) { + return template.buildToken(html.SelfClosingTagToken) + } template.EnterPivot( tags.NewDefault( template.thisText, From 4bccad2efb7054cb56919ee957b38bb27232b89e Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 18:05:08 +0300 Subject: [PATCH 15/18] feat: Move context computing to context_compute.go --- lang/context/context.go | 174 +------------------------------ lang/context/context_compute.go | 178 ++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 172 deletions(-) create mode 100644 lang/context/context_compute.go diff --git a/lang/context/context.go b/lang/context/context.go index 4da9333..3cb1da3 100644 --- a/lang/context/context.go +++ b/lang/context/context.go @@ -2,16 +2,13 @@ package context import ( "fmt" - "path/filepath" - "slices" "strings" - "unicode" - - "github.com/iancoleman/strcase" ) +// The 'root.' context var GlobalContext *Context +// Template's context that's accessed using `this.` type Context struct { Values map[string]any } @@ -81,170 +78,3 @@ func (ctx *Context) String() string { } return "{" + str[:len(str)-1] + "}" } - -func (ctx *Context) Compute(expression string) (string, error) { - fields := getFields(expression) - switch len(fields) { - case 0: - return "", nil - case 1: - return ctx.queryPath(fields[0]) - } - - variable, err := ctx.queryPath(fields[0]) - if err != nil && !slices.Contains(fields, "||") { - return "", err - } - - if len(fields) < 3 { - return "", fmt.Errorf("expression requires a value to the right of %q", fields[1]) - } - - var result string - switch fields[1] { - case "==": - result = fmt.Sprintf("%v", variable == fields[2]) - case "!=": - result = fmt.Sprintf("%v", variable != fields[2]) - case "has": - result = fmt.Sprintf("%v", strings.Contains(variable, fields[2])) - case "lacks": - result = fmt.Sprintf("%v", !strings.Contains(variable, fields[2])) - case "||": - if err != nil { - result = fields[2] - break - } - result = variable - default: - return "", fmt.Errorf("unknown operation %q", fields[1]) - } - - if len(fields) < 5 { - return result, nil - } - - switch fields[3] { - case "||": - if err != nil { - result = fields[4] - break - } - default: - return "", fmt.Errorf( - "unsupported after-operation %q, only || is supported for chaining", - fields[3], - ) - } - - return result, nil -} - -func (ctx *Context) queryPath(fieldPath string) (result string, err error) { - field := strings.TrimSuffix(fieldPath, "?") - segments := strings.Split(field, ".")[1:] - path := make([]string, 0, len(segments)) - operations := make([]string, 0, len(segments)) - - for _, segment := range segments { - if strings.HasSuffix(segment, "()") { - operations = append(operations, segment[:len(segment)-2]) - continue - } - path = append(path, segment) - } - - if strings.HasPrefix(field, "this.") { - result, err = ctx.Query(path) - } - if strings.HasPrefix(field, "root.") { - result, err = GlobalContext.Query(path) - } - - if err != nil { - if strings.HasSuffix(fieldPath, "?") { - return "", nil - } - return "", fmt.Errorf("%s: %w", field, err) - } - - for _, operation := range operations { - switch operation { - case "to_lower": - result = strings.ToLower(result) - case "to_upper": - result = strings.ToUpper(result) - case "to_pascal_case": - result = strcase.ToCamel(result) - case "to_camel_case": - result = strcase.ToLowerCamel(result) - case "to_snake_case": - result = strcase.ToSnake(result) - case "to_kebab_case": - result = strcase.ToKebab(result) - case "capitalize": - result = strings.ToUpper(result[:1]) + result[1:] - case "length": - result = fmt.Sprintf("%d", len(result)) - case "filename": - result = filepath.Base(result) - case "dir": - result = filepath.Dir(result) - case "extension": - result = filepath.Ext(result) - case "trim_extension": - result = strings.TrimSuffix(result, filepath.Ext(result)) - default: - return "", fmt.Errorf("%s: unknown operation %q on %q", field, operation+"()", result) - } - } - - return result, nil -} - -func getFields(text string) (out []string) { - var ( - current []rune - inQuote bool - quoteChar rune - ) - - flush := func() { - if len(current) > 0 { - out = append(out, string(current)) - current = current[:0] - } - } - - for _, char := range text { - switch { - case (char == '"' || char == '\''): - if inQuote { - // closing quote? - if char == quoteChar { - inQuote = false - quoteChar = 0 - } else { - // different quote inside a quote: treat literally - current = append(current, char) - } - } else { - inQuote = true - quoteChar = char - } - - case unicode.IsSpace(char): - if inQuote { - current = append(current, char) - } else { - flush() - } - - default: - current = append(current, char) - } - } - - flush() - return -} diff --git a/lang/context/context_compute.go b/lang/context/context_compute.go new file mode 100644 index 0000000..7dfa4f0 --- /dev/null +++ b/lang/context/context_compute.go @@ -0,0 +1,178 @@ +package context + +import ( + "fmt" + "path/filepath" + "slices" + "strings" + "unicode" + + "github.com/iancoleman/strcase" +) + +func (ctx *Context) Compute(expression string) (string, error) { + fields := getFields(expression) + switch len(fields) { + case 0: + return "", nil + case 1: + return ctx.queryPath(fields[0]) + } + + variable, err := ctx.queryPath(fields[0]) + if err != nil && !slices.Contains(fields, "||") { + return "", err + } + + if len(fields) < 3 { + return "", fmt.Errorf("expression requires a value to the right of %q", fields[1]) + } + + var result string + switch fields[1] { + case "==": + result = fmt.Sprintf("%v", variable == fields[2]) + case "!=": + result = fmt.Sprintf("%v", variable != fields[2]) + case "has": + result = fmt.Sprintf("%v", strings.Contains(variable, fields[2])) + case "lacks": + result = fmt.Sprintf("%v", !strings.Contains(variable, fields[2])) + case "||": + if err != nil { + result = fields[2] + break + } + result = variable + default: + return "", fmt.Errorf("unknown operation %q", fields[1]) + } + + if len(fields) < 5 { + return result, nil + } + + switch fields[3] { + case "||": + if err != nil { + result = fields[4] + break + } + default: + return "", fmt.Errorf( + "unsupported after-operation %q, only || is supported for chaining", + fields[3], + ) + } + + return result, nil +} + +func (ctx *Context) queryPath(fieldPath string) (result string, err error) { + field := strings.TrimSuffix(fieldPath, "?") + segments := strings.Split(field, ".")[1:] + path := make([]string, 0, len(segments)) + operations := make([]string, 0, len(segments)) + + for _, segment := range segments { + if strings.HasSuffix(segment, "()") { + operations = append(operations, segment[:len(segment)-2]) + continue + } + path = append(path, segment) + } + + if strings.HasPrefix(field, "this.") { + result, err = ctx.Query(path) + } + if strings.HasPrefix(field, "root.") { + result, err = GlobalContext.Query(path) + } + + if err != nil { + if strings.HasSuffix(fieldPath, "?") { + return "", nil + } + return "", fmt.Errorf("%s: %w", field, err) + } + + for _, operation := range operations { + switch operation { + case "to_lower": + result = strings.ToLower(result) + case "to_upper": + result = strings.ToUpper(result) + case "to_pascal_case": + result = strcase.ToCamel(result) + case "to_camel_case": + result = strcase.ToLowerCamel(result) + case "to_snake_case": + result = strcase.ToSnake(result) + case "to_kebab_case": + result = strcase.ToKebab(result) + case "capitalize": + result = strings.ToUpper(result[:1]) + result[1:] + case "length": + result = fmt.Sprintf("%d", len(result)) + case "filename": + result = filepath.Base(result) + case "dir": + result = filepath.Dir(result) + case "extension": + result = filepath.Ext(result) + case "trim_extension": + result = strings.TrimSuffix(result, filepath.Ext(result)) + default: + return "", fmt.Errorf("%s: unknown operation %q on %q", field, operation+"()", result) + } + } + + return result, nil +} + +func getFields(text string) (out []string) { + var ( + current []rune + inQuote bool + quoteChar rune + ) + + flush := func() { + if len(current) > 0 { + out = append(out, string(current)) + current = current[:0] + } + } + + for _, char := range text { + switch { + case (char == '"' || char == '\''): + if inQuote { + // closing quote? + if char == quoteChar { + inQuote = false + quoteChar = 0 + } else { + // different quote inside a quote: treat literally + current = append(current, char) + } + } else { + inQuote = true + quoteChar = char + } + + case unicode.IsSpace(char): + if inQuote { + current = append(current, char) + } else { + flush() + } + + default: + current = append(current, char) + } + } + + flush() + return +} From 3e05908169f16a32a0cb22b4daf8addc959c86c5 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 18:06:04 +0300 Subject: [PATCH 16/18] docs: Add docs to attrs --- lang/attrs/attrs.go | 3 +++ lang/attrs/attrs_sort.go | 1 + 2 files changed, 4 insertions(+) diff --git a/lang/attrs/attrs.go b/lang/attrs/attrs.go index 371e809..eba1e8e 100644 --- a/lang/attrs/attrs.go +++ b/lang/attrs/attrs.go @@ -7,6 +7,7 @@ import ( "golang.org/x/net/html" ) +// XML attributes in a sorted manner type Attributes struct { order []string Values map[string]string @@ -26,6 +27,7 @@ func New(sourceAttrs []html.Attribute) *Attributes { return attrs.Sort() } +// NOTE: It prepends " " (space) to the output func (attrs *Attributes) Render(out printer.Writer) { for _, key := range attrs.order { out.WriteString(" ") @@ -33,6 +35,7 @@ func (attrs *Attributes) Render(out printer.Writer) { } } +// Overrides or saves new attribute. Use %s to format the original attribute func (attrs *Attributes) OverrideAttr(key string, value string) { original, ok := attrs.Values[key] if !ok { diff --git a/lang/attrs/attrs_sort.go b/lang/attrs/attrs_sort.go index 53b006a..d31b7e4 100644 --- a/lang/attrs/attrs_sort.go +++ b/lang/attrs/attrs_sort.go @@ -2,6 +2,7 @@ package attrs import "sort" +// Sorts all attributes based on [AttrSortOrder] func (attrs *Attributes) Sort() *Attributes { order := make(map[string]int) for i, s := range AttrSortOrder { From 857554cf118c18a60139182c03e9bb6799c0cf62 Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 18:22:02 +0300 Subject: [PATCH 17/18] feat: Add --output option --- cli/cli.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cli/cli.go b/cli/cli.go index 7c1715a..dd0ec18 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -16,6 +16,7 @@ var Options struct { Indent int `default:"4" desc:"The amount of spaces to be used for indentation (overwriten by --tabs)"` StripComments bool `desc:"Strips away HTML comments from the output"` Input string `desc:"Provide input to the provided files in the following format: 'attr1=value1,attr2.a.b.c=value2,...'"` + Output string `desc:"(Optional) output path. Use '.' to substitute the same filename (e.g. './out/.' -> './out/input.html')"` } func Main(args []string) error { @@ -55,7 +56,23 @@ func Main(args []string) error { return err } - template.Root().Render(os.Stdout, -1) + out := os.Stdout + if Options.Output != "" { + if strings.HasSuffix(Options.Output, ".") { + Options.Output = strings.TrimSuffix(Options.Output, ".") + base + } + + if err := os.MkdirAll(filepath.Dir(Options.Output), os.ModePerm); err != nil { + return err + } + + out, err = os.OpenFile(Options.Output, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + } + + template.Root().Render(out, -1) } return nil From acce4fd2aff4864dfdc97594b4e3db6d84d8309a Mon Sep 17 00:00:00 2001 From: BubbleFish Date: Fri, 25 Apr 2025 18:25:17 +0300 Subject: [PATCH 18/18] docs: Update the README to match new version --- README.md | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index ac98170..1e8a02a 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,10 @@ Mend is a simple HTML template processor designed to, but not limited to be used The produced HTML is always consistently formatted and sorted. -> [!IMPORTANT] -> Mend writes into **stdout** instead of a file. This is **not** a limitation, it's an [important advantage](https://github.com/bbfh-dev/mend/wiki#taking-advantage-of-stdout). - * [📥 Installation](#-installation) * [⚙️ Usage](#-usage) -* [📌 Developer notes](#-developer-notes) @@ -22,35 +18,36 @@ The produced HTML is always consistently formatted and sorted. Download the [latest release](https://github.com/bbfh-dev/mend/releases/latest) or install via the command line with: ```bash -go install github.com/bbfh-dev/mend +go install github.com/bbfh-dev/mend@latest ``` # ⚙️ Usage -Run `mend --help` to display usage information. +Check out the [Wiki](https://github.com/bbfh-dev/mend/wiki) for detailed documentation. + +Run `mend --help` to display CLI usage information. ```bash -Usage: - mend [html files...] +mend v1.0.0-alpha + +HTML template processor designed to, but not limited to be used to generate static websites -Commands: +Usage: + mend [options] Options: - --help, -h Print help and exit - --version, -V Print version and exit - --input, -i Set global input parameters - --indent Set amount of spaces to indent with. Gets ignored if --tabs is used - --tabs, -t Use tabs instead of spaces - --decomment Strips away any comments + --help + # Print this help message + --version + # Print the program version + --tabs, -t + # Use tabs for indentation + --indent (default: 4) + # The amount of spaces to be used for indentation (overwriten by --tabs) + --strip-comments + # Strips away HTML comments from the output + --input + # Provide input to the provided files in the following format: 'attr1=value1,attr2.a.b.c=value2,...' + --output + # (Optional) output path. Use '.' to substitute the same filename (e.g. './out/.' -> './out/input.html') ``` - -# 📌 Developer notes - -These are some important development notes, informing about parts of the project that need to be polished out. - -> **Expressions are very clunky.** -> -> 1. In code they require every node to implement its own processing while referencing a global function that handles them. Basically, it's just a big bowl of spaghetti. There's gotta be a better way of doing them. -> 1. The way expressions are parsed is very primitive, it could cause unexpected errors/behavior when using bad syntax. -> -> — [@bbfh-dev](https://github.com/bbfh-dev/)