update attrs to use ordered map

This commit is contained in:
maddalax 2024-09-30 12:39:48 -05:00
parent 0fa096ea2f
commit 7b83e2fde7
12 changed files with 311 additions and 193 deletions

View file

@ -17,6 +17,12 @@ func HasFileFromRoot(file string) bool {
return err == nil return err == nil
} }
func CreateHtmgoDir() {
if !HasFileFromRoot("__htmgo") {
CreateDirFromRoot("__htmgo")
}
}
func CreateDirFromRoot(dir string) error { func CreateDirFromRoot(dir string) error {
cwd := process.GetWorkingDir() cwd := process.GetWorkingDir()
path := filepath.Join(cwd, dir) path := filepath.Join(cwd, dir)

View file

@ -5,7 +5,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"github.com/maddalax/htmgo/cli/htmgo/internal" "github.com/maddalax/htmgo/cli/htmgo/internal"
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen" "github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets" "github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css" "github.com/maddalax/htmgo/cli/htmgo/tasks/css"
@ -57,10 +56,6 @@ func main() {
slog.Debug("Running task:", slog.String("task", taskName)) slog.Debug("Running task:", slog.String("task", taskName))
slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir())) slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
if !dirutil.HasFileFromRoot("__htmgo") {
dirutil.CreateDirFromRoot("__htmgo")
}
if taskName == "watch" { if taskName == "watch" {
fmt.Printf("Running in watch mode\n") fmt.Printf("Running in watch mode\n")
os.Setenv("ENV", "development") os.Setenv("ENV", "development")

View file

@ -36,6 +36,8 @@ func getModuleVersion(modulePath string) (string, error) {
} }
func CopyAssets() { func CopyAssets() {
dirutil.CreateHtmgoDir()
moduleName := "github.com/maddalax/htmgo/framework" moduleName := "github.com/maddalax/htmgo/framework"
modulePath := module.GetDependencyPath(moduleName) modulePath := module.GetDependencyPath(moduleName)

View file

@ -3,27 +3,50 @@ package h
import ( import (
"fmt" "fmt"
"github.com/maddalax/htmgo/framework/hx" "github.com/maddalax/htmgo/framework/hx"
"github.com/maddalax/htmgo/framework/internal/datastructure"
"strings" "strings"
) )
type AttributeMap map[string]any type AttributeMap = map[string]any
func (m *AttributeMap) ToMap() map[string]string { type AttributeMapOrdered struct {
result := make(map[string]string) data *datastructure.OrderedMap[string, string]
for k, v := range *m { }
switch v.(type) {
case AttributeMap: func (m *AttributeMapOrdered) Set(key string, value any) {
m2 := v.(*AttributeMap).ToMap() switch v := value.(type) {
for _, a := range m2 {
result[k] = a
}
case string: case string:
result[k] = v.(string) m.data.Set(key, v)
case *AttributeMapOrdered:
v.Each(func(k string, v any) {
m.Set(k, v)
})
case *AttributeR:
m.data.Set(v.Name, v.Value)
default: default:
result[k] = fmt.Sprintf("%v", v) m.data.Set(key, fmt.Sprintf("%v", value))
}
}
func (m *AttributeMapOrdered) Each(cb func(key string, value any)) {
m.data.Each(func(key string, value string) {
cb(key, value)
})
}
func (m *AttributeMapOrdered) Entries() []datastructure.MapEntry[string, string] {
return m.data.Entries()
}
func NewAttributeMap(pairs ...string) *AttributeMapOrdered {
m := datastructure.NewOrderedMap[string, string]()
if len(pairs)%2 == 0 {
for i := 0; i < len(pairs); i++ {
m.Set(pairs[i], pairs[i+1])
i++
} }
} }
return result return &AttributeMapOrdered{data: m}
} }
func Attribute(key string, value string) *AttributeR { func Attribute(key string, value string) *AttributeR {
@ -33,28 +56,24 @@ func Attribute(key string, value string) *AttributeR {
} }
} }
func AttributeList(children ...*AttributeR) *AttributeMap { func AttributeList(children ...*AttributeR) *AttributeMapOrdered {
m := make(AttributeMap) m := NewAttributeMap()
for _, child := range children { for _, c := range children {
m[child.Name] = child.Value m.Set(c.Name, c.Value)
} }
return &m return m
} }
func Attributes(attrs *AttributeMap) *AttributeMap { func Attributes(attributes *AttributeMap) *AttributeMapOrdered {
return attrs m := NewAttributeMap()
for k, v := range *attributes {
m.Set(k, v)
}
return m
} }
func AttributePairs(pairs ...string) *AttributeMap { func AttributePairs(pairs ...string) *AttributeMapOrdered {
if len(pairs)%2 != 0 { return NewAttributeMap(pairs...)
return &AttributeMap{}
}
m := make(AttributeMap)
for i := 0; i < len(pairs); i++ {
m[pairs[i]] = pairs[i+1]
i++
}
return &m
} }
func Checked() Ren { func Checked() Ren {

View file

@ -100,12 +100,12 @@ func TestIncrement(t *testing.T) {
func TestSetInnerHtml(t *testing.T) { func TestSetInnerHtml(t *testing.T) {
htmlContent := Div(Span(UnsafeRaw("inner content"))) htmlContent := Div(Span(UnsafeRaw("inner content")))
compareIgnoreSpaces(t, renderJs(t, SetInnerHtml(htmlContent)), "this.innerHTML = `<div ><span >inner content</span></div>`;") compareIgnoreSpaces(t, renderJs(t, SetInnerHtml(htmlContent)), "this.innerHTML = `<div><span>inner content</span></div>`;")
} }
func TestSetOuterHtml(t *testing.T) { func TestSetOuterHtml(t *testing.T) {
htmlContent := Div(Span(UnsafeRaw("outer content"))) htmlContent := Div(Span(UnsafeRaw("outer content")))
compareIgnoreSpaces(t, renderJs(t, SetOuterHtml(htmlContent)), "this.outerHTML = `<div ><span >outer content</span></div>`;") compareIgnoreSpaces(t, renderJs(t, SetOuterHtml(htmlContent)), "this.outerHTML = `<div><span>outer content</span></div>`;")
} }
func TestAddAttribute(t *testing.T) { func TestAddAttribute(t *testing.T) {

View file

@ -24,7 +24,7 @@ func validateCommands(cmds []Command) {
break break
case ComplexJsCommand: case ComplexJsCommand:
break break
case *AttributeMap: case *AttributeMapOrdered:
break break
case *Element: case *Element:
panic(fmt.Sprintf("element is not allowed in lifecycle events. Got: %v", t)) panic(fmt.Sprintf("element is not allowed in lifecycle events. Got: %v", t))

View file

@ -1,11 +1,8 @@
package h package h
import ( import (
"bytes"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/html"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -13,43 +10,12 @@ import (
"time" "time"
) )
// Sort attributes of a node by attribute name func TestSimpleRender(t *testing.T) {
func sortAttributes(node *html.Node) { t.Parallel()
if node.Type == html.ElementNode && len(node.Attr) > 1 { result := Render(
sort.SliceStable(node.Attr, func(i, j int) bool { Div(Attribute("id", "my-div"), Attribute("class", "my-class")),
return node.Attr[i].Key < node.Attr[j].Key )
}) assert.Equal(t, `<div id="my-div" class="my-class"></div>`, result)
}
}
// Traverse and sort attributes in the entire HTML tree
func traverseAndSortAttributes(node *html.Node) {
sortAttributes(node)
for child := node.FirstChild; child != nil; child = child.NextSibling {
traverseAndSortAttributes(child)
}
}
// Parse HTML, sort attributes, and render back to a string
func sortHtmlAttributes(input string) string {
// Parse the HTML string into a node tree
doc, err := html.Parse(strings.NewReader(input))
if err != nil {
return ""
}
// Traverse and sort attributes for each node
traverseAndSortAttributes(doc)
// Use a buffer to capture the rendered HTML
var buf bytes.Buffer
err = html.Render(&buf, doc)
if err != nil {
return ""
}
// Return the rendered HTML as a string
return buf.String()
} }
func TestRender(t *testing.T) { func TestRender(t *testing.T) {
@ -57,10 +23,10 @@ func TestRender(t *testing.T) {
div := Div( div := Div(
Id("my-div"), Id("my-div"),
Attribute("data-attr-2", "value"), Attribute("data-attr-2", "value"),
Attributes(&AttributeMap{ AttributePairs(
"data-attr-3": "value", "data-attr-3", "value",
"data-attr-4": "value", "data-attr-4", "value",
}), ),
HxBeforeRequest( HxBeforeRequest(
SetText("before request"), SetText("before request"),
), ),
@ -73,17 +39,31 @@ func TestRender(t *testing.T) {
Text("hello, child"), Text("hello, child"),
) )
div.attributes["data-attr-1"] = "value" div.attributes.Set("data-attr-1", "value")
expectedRaw := `<div data-attr-1="value" id="my-div" data-attr-2="value" data-attr-3="value" data-attr-4="value" hx-on::before-request="this.innerText = 'before request';" hx-on::after-request="this.innerText = 'after request';"><div >hello, world</div>hello, child</div>` expected := `<div data-attr-1="value" id="my-div" data-attr-2="value" data-attr-3="value" data-attr-4="value" hx-on::before-request="this.innerText = &#39;before request&#39;;" hx-on::after-request="this.innerText = &#39;after request&#39;;"><div>hello, world</div>hello, child</div>`
expected := sortHtmlAttributes(expectedRaw) result := Render(div)
result := sortHtmlAttributes(Render(div))
assert.Equal(t, assert.Equal(t,
expected, expected,
result) result)
} }
func TestRenderAttributes_1(t *testing.T) {
t.Parallel()
div := Div(
AttributePairs("class", "bg-red-500"),
Attributes(&AttributeMap{
"id": Id("my-div"),
}),
Attribute("disabled", "true"),
)
assert.Equal(t,
`<div class="bg-red-500" id="my-div" disabled="true"></div>`,
Render(div),
)
}
func TestRawContent(t *testing.T) { func TestRawContent(t *testing.T) {
t.Parallel() t.Parallel()
str := "<div>hello, world</div>" str := "<div>hello, world</div>"
@ -98,14 +78,14 @@ func TestConditional(t *testing.T) {
Ternary(true, Text("true"), Text("false")), Ternary(true, Text("true"), Text("false")),
), ),
) )
assert.Equal(t, "<div >true</div>", result) assert.Equal(t, "<div>true</div>", result)
result = Render( result = Render(
Div( Div(
If(false, Text("true")), If(false, Text("true")),
), ),
) )
assert.Equal(t, "<div ></div>", result) assert.Equal(t, "<div></div>", result)
} }
func TestTagSelfClosing(t *testing.T) { func TestTagSelfClosing(t *testing.T) {
@ -121,7 +101,7 @@ func TestTagSelfClosing(t *testing.T) {
assert.Equal(t, `<div id="test"></div>`, Render( assert.Equal(t, `<div id="test"></div>`, Render(
Div(Id("test")), Div(Id("test")),
)) ))
assert.Equal(t, `<div id="test"><div ></div></div>`, Render( assert.Equal(t, `<div id="test"><div></div></div>`, Render(
Div(Id("test"), Div()), Div(Id("test"), Div()),
)) ))
} }
@ -134,12 +114,12 @@ func TestCached(t *testing.T) {
return ComplexPage() return ComplexPage()
}) })
firstRender := sortHtmlAttributes(Render(page())) firstRender := Render(page())
secondRender := sortHtmlAttributes(Render(page())) secondRender := Render(page())
assert.Equal(t, firstRender, secondRender) assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) assert.Equal(t, firstRender, Render(ComplexPage()))
} }
func TestCachedT(t *testing.T) { func TestCachedT(t *testing.T) {
@ -150,12 +130,12 @@ func TestCachedT(t *testing.T) {
return ComplexPage() return ComplexPage()
}) })
firstRender := sortHtmlAttributes(Render(page("a"))) firstRender := Render(page("a"))
secondRender := sortHtmlAttributes(Render(page("a"))) secondRender := Render(page("a"))
assert.Equal(t, firstRender, secondRender) assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) assert.Equal(t, firstRender, Render(ComplexPage()))
} }
func TestCachedT2(t *testing.T) { func TestCachedT2(t *testing.T) {
@ -166,12 +146,12 @@ func TestCachedT2(t *testing.T) {
return ComplexPage() return ComplexPage()
}) })
firstRender := sortHtmlAttributes(Render(page("a", "b"))) firstRender := Render(page("a", "b"))
secondRender := sortHtmlAttributes(Render(page("a", "b"))) secondRender := Render(page("a", "b"))
assert.Equal(t, firstRender, secondRender) assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) assert.Equal(t, firstRender, Render(ComplexPage()))
} }
func TestCachedT3(t *testing.T) { func TestCachedT3(t *testing.T) {
@ -182,12 +162,12 @@ func TestCachedT3(t *testing.T) {
return ComplexPage() return ComplexPage()
}) })
firstRender := sortHtmlAttributes(Render(page("a", "b", "c"))) firstRender := Render(page("a", "b", "c"))
secondRender := sortHtmlAttributes(Render(page("a", "b", "c"))) secondRender := Render(page("a", "b", "c"))
assert.Equal(t, firstRender, secondRender) assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) assert.Equal(t, firstRender, Render(ComplexPage()))
} }
func TestCachedT4(t *testing.T) { func TestCachedT4(t *testing.T) {
@ -198,12 +178,12 @@ func TestCachedT4(t *testing.T) {
return ComplexPage() return ComplexPage()
}) })
firstRender := sortHtmlAttributes(Render(page("a", "b", "c", "d"))) firstRender := Render(page("a", "b", "c", "d"))
secondRender := sortHtmlAttributes(Render(page("a", "b", "c", "d"))) secondRender := Render(page("a", "b", "c", "d"))
assert.Equal(t, firstRender, secondRender) assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) assert.Equal(t, firstRender, Render(ComplexPage()))
} }
func TestCachedExpired(t *testing.T) { func TestCachedExpired(t *testing.T) {
@ -214,9 +194,9 @@ func TestCachedExpired(t *testing.T) {
return ComplexPage() return ComplexPage()
}) })
firstRender := sortHtmlAttributes(Render(page())) firstRender := Render(page())
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
secondRender := sortHtmlAttributes(Render(page())) secondRender := Render(page())
assert.Equal(t, firstRender, secondRender) assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 2, count) assert.Equal(t, 2, count)
@ -409,9 +389,9 @@ func TestCacheByKeyT1_2(t *testing.T) {
} }
}) })
assert.Equal(t, "<p >one</p>", Render(cachedItem("one"))) assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
assert.Equal(t, 2, renderCount) assert.Equal(t, 2, renderCount)
} }
@ -425,10 +405,10 @@ func TestCacheByKeyT1Expired(t *testing.T) {
} }
}) })
assert.Equal(t, "<p >one</p>", Render(cachedItem("one"))) assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
time.Sleep(time.Millisecond * 2) time.Sleep(time.Millisecond * 2)
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
assert.Equal(t, 3, renderCount) assert.Equal(t, 3, renderCount)
} }
@ -442,14 +422,14 @@ func TestCacheByKeyT1Expired_2(t *testing.T) {
} }
}) })
assert.Equal(t, "<p >one</p>", Render(cachedItem("one"))) assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
time.Sleep(time.Millisecond * 3) time.Sleep(time.Millisecond * 3)
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
time.Sleep(time.Millisecond * 3) time.Sleep(time.Millisecond * 3)
assert.Equal(t, "<p >one</p>", Render(cachedItem("one"))) assert.Equal(t, "<p>one</p>", Render(cachedItem("one")))
assert.Equal(t, "<p >two</p>", Render(cachedItem("two"))) assert.Equal(t, "<p>two</p>", Render(cachedItem("two")))
assert.Equal(t, 3, renderCount) assert.Equal(t, 3, renderCount)
} }
@ -529,7 +509,7 @@ func TestBackgroundCleaner(t *testing.T) {
func TestEscapeHtml(t *testing.T) { func TestEscapeHtml(t *testing.T) {
t.Parallel() t.Parallel()
assert.Equal(t, "&lt;script&gt;alert(1)&lt;/script&gt;", Render(Text("<script>alert(1)</script>"))) assert.Equal(t, "&lt;script&gt;alert(1)&lt;/script&gt;", Render(Text("<script>alert(1)</script>")))
assert.Equal(t, "<p >&lt;script&gt;alert(1)&lt;/script&gt;</p>", Render(Pf("<script>alert(1)</script>"))) assert.Equal(t, "<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>", Render(Pf("<script>alert(1)</script>")))
} }

View file

@ -5,6 +5,11 @@ type AttributeR struct {
Value string Value string
} }
type KeyValue[T any] struct {
Key string
Value T
}
type TextContent struct { type TextContent struct {
Content string Content string
} }

View file

@ -38,6 +38,13 @@ var voidTags = map[string]bool{
type RenderContext struct { type RenderContext struct {
builder *strings.Builder builder *strings.Builder
scripts []string scripts []string
next any
prev any
}
func (ctx *RenderContext) PrevIsAttribute() bool {
_, ok := ctx.prev.(*AttributeR)
return ok
} }
func (ctx *RenderContext) AddScript(funcName string, body string) { func (ctx *RenderContext) AddScript(funcName string, body string) {
@ -50,6 +57,29 @@ func (ctx *RenderContext) AddScript(funcName string, body string) {
ctx.scripts = append(ctx.scripts, script) ctx.scripts = append(ctx.scripts, script)
} }
func each[T any](ctx *RenderContext, arr []T, cb func(T)) {
for i, r := range arr {
if i == len(arr)-1 {
ctx.next = nil
} else {
ctx.next = arr[i+1]
}
cb(r)
}
}
func eachAttrMap(ctx *RenderContext, m *AttributeMapOrdered, cb func(string, string)) {
entries := m.Entries()
for i, entry := range entries {
if i == len(entries)-1 {
ctx.next = nil
} else {
ctx.next = entries[i+1]
}
cb(entry.Key, entry.Value)
}
}
func (node *Element) Render(context *RenderContext) { func (node *Element) Render(context *RenderContext) {
// some elements may not have a tag, such as a Fragment // some elements may not have a tag, such as a Fragment
@ -68,11 +98,9 @@ func (node *Element) Render(context *RenderContext) {
if node.tag != "" { if node.tag != "" {
context.builder.WriteString("<") context.builder.WriteString("<")
context.builder.WriteString(node.tag) context.builder.WriteString(node.tag)
context.builder.WriteString(" ") eachAttrMap(context, node.attributes, func(key string, value string) {
NewAttribute(key, value).Render(context)
for name, value := range node.attributes { })
NewAttribute(name, value).Render(context)
}
} }
totalChildren := 0 totalChildren := 0
@ -110,7 +138,7 @@ func (node *Element) Render(context *RenderContext) {
// second pass, render any attributes within the tag // second pass, render any attributes within the tag
for _, child := range node.children { for _, child := range node.children {
switch child.(type) { switch child.(type) {
case *AttributeMap: case *AttributeMapOrdered:
child.Render(context) child.Render(context)
case *AttributeR: case *AttributeR:
child.Render(context) child.Render(context)
@ -132,7 +160,7 @@ func (node *Element) Render(context *RenderContext) {
// render the children elements that are not attributes // render the children elements that are not attributes
for _, child := range node.children { for _, child := range node.children {
switch child.(type) { switch child.(type) {
case *AttributeMap: case *AttributeMapOrdered:
continue continue
case *AttributeR: case *AttributeR:
continue continue
@ -157,55 +185,59 @@ func (node *Element) Render(context *RenderContext) {
func renderScripts(context *RenderContext) { func renderScripts(context *RenderContext) {
for _, script := range context.scripts { for _, script := range context.scripts {
context.builder.WriteString(script) context.builder.WriteString(script)
context.prev = script
} }
context.scripts = []string{} context.scripts = []string{}
} }
func (a *AttributeR) Render(context *RenderContext) { func (a *AttributeR) Render(context *RenderContext) {
context.builder.WriteString(" ")
context.builder.WriteString(a.Name) context.builder.WriteString(a.Name)
if a.Value != "" { if a.Value != "" {
context.builder.WriteString(`=`) context.builder.WriteString(`=`)
context.builder.WriteString(`"`) context.builder.WriteString(`"`)
context.builder.WriteString(html.EscapeString(a.Value)) context.builder.WriteString(html.EscapeString(a.Value))
context.builder.WriteString(`"`) context.builder.WriteString(`"`)
} else {
context.builder.WriteString(" ")
} }
context.prev = a
} }
func (t *TextContent) Render(context *RenderContext) { func (t *TextContent) Render(context *RenderContext) {
context.builder.WriteString(template.HTMLEscapeString(t.Content)) context.builder.WriteString(template.HTMLEscapeString(t.Content))
context.prev = t
} }
func (r *RawContent) Render(context *RenderContext) { func (r *RawContent) Render(context *RenderContext) {
context.builder.WriteString(r.Content) context.builder.WriteString(r.Content)
context.prev = r
} }
func (c *ChildList) Render(context *RenderContext) { func (c *ChildList) Render(context *RenderContext) {
for _, child := range c.Children { for _, child := range c.Children {
child.Render(context) child.Render(context)
context.prev = child
} }
} }
func (j SimpleJsCommand) Render(context *RenderContext) { func (j SimpleJsCommand) Render(context *RenderContext) {
context.builder.WriteString(j.Command) context.builder.WriteString(j.Command)
context.prev = j
} }
func (j ComplexJsCommand) Render(context *RenderContext) { func (j ComplexJsCommand) Render(context *RenderContext) {
context.builder.WriteString(j.Command) context.builder.WriteString(j.Command)
context.prev = j
} }
func (p *Partial) Render(context *RenderContext) { func (p *Partial) Render(context *RenderContext) {
p.Root.Render(context) p.Root.Render(context)
context.prev = p
} }
func (m *AttributeMap) Render(context *RenderContext) { func (m *AttributeMapOrdered) Render(context *RenderContext) {
m2 := m.ToMap() eachAttrMap(context, m, func(key string, value string) {
NewAttribute(key, value).Render(context)
for k, v := range m2 { })
context.builder.WriteString(" ")
NewAttribute(k, v).Render(context)
}
} }
func (l *LifeCycle) fromAttributeMap(event string, key string, value string, context *RenderContext) { func (l *LifeCycle) fromAttributeMap(event string, key string, value string, context *RenderContext) {
@ -222,21 +254,21 @@ func (l *LifeCycle) Render(context *RenderContext) {
for event, commands := range l.handlers { for event, commands := range l.handlers {
m[event] = "" m[event] = ""
for _, command := range commands { each(context, commands, func(command Command) {
switch c := command.(type) { switch c := command.(type) {
case SimpleJsCommand: case SimpleJsCommand:
m[event] += fmt.Sprintf("%s;", c.Command) m[event] += fmt.Sprintf("%s;", c.Command)
case ComplexJsCommand: case ComplexJsCommand:
context.AddScript(c.TempFuncName, c.Command) context.AddScript(c.TempFuncName, c.Command)
m[event] += fmt.Sprintf("%s(this);", c.TempFuncName) m[event] += fmt.Sprintf("%s(this);", c.TempFuncName)
case *AttributeMap: case *AttributeMapOrdered:
for k, v := range c.ToMap() { eachAttrMap(context, c, func(k string, v string) {
l.fromAttributeMap(event, k, v, context) l.fromAttributeMap(event, k, v, context)
} })
case *AttributeR: case *AttributeR:
l.fromAttributeMap(event, c.Name, c.Value, context) l.fromAttributeMap(event, c.Name, c.Value, context)
} }
} })
} }
children := make([]Ren, 0) children := make([]Ren, 0)

View file

@ -11,7 +11,7 @@ type PartialFunc = func(ctx *RequestContext) *Partial
type Element struct { type Element struct {
tag string tag string
attributes map[string]string attributes *AttributeMapOrdered
meta any meta any
children []Ren children []Ren
} }
@ -50,7 +50,7 @@ func Tag(tag string, children ...Ren) *Element {
return &Element{ return &Element{
tag: tag, tag: tag,
children: children, children: children,
attributes: make(map[string]string), attributes: NewAttributeMap(),
} }
} }
@ -69,56 +69,52 @@ func Body(children ...Ren) *Element {
func Meta(name string, content string) *Element { func Meta(name string, content string) *Element {
return &Element{ return &Element{
tag: "meta", tag: "meta",
attributes: map[string]string{ attributes: AttributePairs(
"name": name, "name", name,
"content": content, "content", content,
}, ),
children: make([]Ren, 0), children: make([]Ren, 0),
} }
} }
func LinkWithVersion(href string, rel string, version string) *Element { func LinkWithVersion(href string, rel string, version string) *Element {
attributeMap := AttributeMap{
"href": href + "?v=" + version,
"rel": rel,
}
return &Element{ return &Element{
tag: "link", tag: "link",
attributes: attributeMap.ToMap(), attributes: AttributePairs(
"href", href+"?v="+version,
"rel", rel,
),
children: make([]Ren, 0), children: make([]Ren, 0),
} }
} }
func Link(href string, rel string) *Element { func Link(href string, rel string) *Element {
attributeMap := AttributeMap{
"href": href,
"rel": rel,
}
return &Element{ return &Element{
tag: "link", tag: "link",
attributes: attributeMap.ToMap(), attributes: AttributePairs(
"href", href,
"rel", rel,
),
children: make([]Ren, 0), children: make([]Ren, 0),
} }
} }
func ScriptWithVersion(url string, version string) *Element { func ScriptWithVersion(url string, version string) *Element {
attributeMap := AttributeMap{
"src": url + "?v=" + version,
}
return &Element{ return &Element{
tag: "script", tag: "script",
attributes: attributeMap.ToMap(), attributes: AttributePairs(
"src", url+"?v="+version,
),
children: make([]Ren, 0), children: make([]Ren, 0),
} }
} }
func Script(url string) *Element { func Script(url string) *Element {
attributeMap := AttributeMap{
"src": url,
}
return &Element{ return &Element{
tag: "script", tag: "script",
attributes: attributeMap.ToMap(), attributes: AttributePairs(
"src", url,
),
children: make([]Ren, 0), children: make([]Ren, 0),
} }
} }
@ -177,12 +173,11 @@ func Value(value any) *AttributeR {
} }
func Input(inputType string, children ...Ren) *Element { func Input(inputType string, children ...Ren) *Element {
attributeMap := AttributeMap{
"type": inputType,
}
return &Element{ return &Element{
tag: "input", tag: "input",
attributes: attributeMap.ToMap(), attributes: AttributePairs(
"type", inputType,
),
children: children, children: children,
} }
} }
@ -257,7 +252,7 @@ func TagF(tag string, format string, args ...interface{}) *Element {
An invocation can look like An invocation can look like
h.H3F("build simple and scalable systems with %s", "go + htmx", h.Class("-mt-4")), h.H3F("build simple and scalable systems with %s", "go + htmx", h.Class("-mt-4")),
where the args may be a mix of strings, *Element, *AttributeMap, *ChildList, *AttributeR where the args may be a mix of strings, *Element, *AttributeMapOrdered, *ChildList, *AttributeR
We need to separate the children from the format arguments We need to separate the children from the format arguments
*/ */
children := make([]Ren, 0) children := make([]Ren, 0)
@ -266,7 +261,7 @@ func TagF(tag string, format string, args ...interface{}) *Element {
switch d := arg.(type) { switch d := arg.(type) {
case *Element: case *Element:
children = append(children, d) children = append(children, d)
case *AttributeMap: case *AttributeMapOrdered:
children = append(children, d) children = append(children, d)
case *ChildList: case *ChildList:
for _, child := range d.Children { for _, child := range d.Children {

View file

@ -2,46 +2,46 @@ package h
import "github.com/maddalax/htmgo/framework/hx" import "github.com/maddalax/htmgo/framework/hx"
func Get(path string, trigger ...string) *AttributeMap { func Get(path string, trigger ...string) *AttributeMapOrdered {
return AttributeList(Attribute(hx.GetAttr, path), HxTriggerString(trigger...)) return AttributeList(Attribute(hx.GetAttr, path), HxTriggerString(trigger...))
} }
func GetPartial(partial PartialFunc, trigger ...string) *AttributeMap { func GetPartial(partial PartialFunc, trigger ...string) *AttributeMapOrdered {
return Get(GetPartialPath(partial), trigger...) return Get(GetPartialPath(partial), trigger...)
} }
func GetPartialWithQs(partial PartialFunc, qs *Qs, trigger string) *AttributeMap { func GetPartialWithQs(partial PartialFunc, qs *Qs, trigger string) *AttributeMapOrdered {
return Get(GetPartialPathWithQs(partial, qs), trigger) return Get(GetPartialPathWithQs(partial, qs), trigger)
} }
func GetWithQs(path string, qs *Qs, trigger string) *AttributeMap { func GetWithQs(path string, qs *Qs, trigger string) *AttributeMapOrdered {
return Get(SetQueryParams(path, qs), trigger) return Get(SetQueryParams(path, qs), trigger)
} }
func PostPartial(partial PartialFunc, triggers ...string) *AttributeMap { func PostPartial(partial PartialFunc, triggers ...string) *AttributeMapOrdered {
return Post(GetPartialPath(partial), triggers...) return Post(GetPartialPath(partial), triggers...)
} }
func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMap { func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMapOrdered {
return Post(GetPartialPathWithQs(partial, qs), trigger...) return Post(GetPartialPathWithQs(partial, qs), trigger...)
} }
func Post(url string, trigger ...string) *AttributeMap { func Post(url string, trigger ...string) *AttributeMapOrdered {
return AttributeList(Attribute(hx.PostAttr, url), HxTriggerString(trigger...)) return AttributeList(Attribute(hx.PostAttr, url), HxTriggerString(trigger...))
} }
func PostWithQs(url string, qs *Qs, trigger string) *AttributeMap { func PostWithQs(url string, qs *Qs, trigger string) *AttributeMapOrdered {
return Post(SetQueryParams(url, qs), trigger) return Post(SetQueryParams(url, qs), trigger)
} }
func PostOnClick(url string) *AttributeMap { func PostOnClick(url string) *AttributeMapOrdered {
return Post(url, hx.ClickEvent) return Post(url, hx.ClickEvent)
} }
func PostPartialOnClick(partial PartialFunc) *AttributeMap { func PostPartialOnClick(partial PartialFunc) *AttributeMapOrdered {
return PostOnClick(GetPartialPath(partial)) return PostOnClick(GetPartialPath(partial))
} }
func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMap { func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMapOrdered {
return PostOnClick(GetPartialPathWithQs(partial, qs)) return PostOnClick(GetPartialPathWithQs(partial, qs))
} }

View file

@ -0,0 +1,84 @@
package datastructure
type MapEntry[K comparable, V any] struct {
Key K
Value V
}
// OrderedMap is a generic data structure that maintains the order of keys.
type OrderedMap[K comparable, V any] struct {
keys []K
values map[K]V
}
func (om *OrderedMap[K, V]) Each(cb func(key K, value V)) {
for _, key := range om.keys {
cb(key, om.values[key])
}
}
// Entries returns the key-value pairs in the order they were added.
func (om *OrderedMap[K, V]) Entries() []MapEntry[K, V] {
entries := make([]MapEntry[K, V], len(om.keys))
for i, key := range om.keys {
entries[i] = MapEntry[K, V]{
Key: key,
Value: om.values[key],
}
}
return entries
}
// NewOrderedMap creates a new OrderedMap.
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
return &OrderedMap[K, V]{
keys: []K{},
values: make(map[K]V),
}
}
// Set adds or updates a key-value pair in the OrderedMap.
func (om *OrderedMap[K, V]) Set(key K, value V) {
// Check if the key already exists
if _, exists := om.values[key]; !exists {
om.keys = append(om.keys, key) // Append key to the keys slice if it's a new key
}
om.values[key] = value
}
// Get retrieves a value by key.
func (om *OrderedMap[K, V]) Get(key K) (V, bool) {
value, exists := om.values[key]
return value, exists
}
// Keys returns the keys in the order they were added.
func (om *OrderedMap[K, V]) Keys() []K {
return om.keys
}
// Values returns the values in the order of their keys.
func (om *OrderedMap[K, V]) Values() []V {
values := make([]V, len(om.keys))
for i, key := range om.keys {
values[i] = om.values[key]
}
return values
}
// Delete removes a key-value pair from the OrderedMap.
func (om *OrderedMap[K, V]) Delete(key K) {
if _, exists := om.values[key]; exists {
// Remove the key from the map
delete(om.values, key)
// Remove the key from the keys slice
for i, k := range om.keys {
if k == key {
om.keys = append(om.keys[:i], om.keys[i+1:]...)
break
}
}
}
}