Merge pull request #12 from maddalax/attr-ordered
Update attribute methods to use an ordered map so we can have deterministic output
This commit is contained in:
commit
e33ab7366d
12 changed files with 307 additions and 188 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,54 @@ 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 *AttributeMap:
|
||||||
|
for k, v2 := range *v {
|
||||||
|
m.Set(k, v2)
|
||||||
|
}
|
||||||
|
case *AttributeMapOrdered:
|
||||||
|
v.Each(func(k string, v2 string) {
|
||||||
|
m.Set(k, v2)
|
||||||
|
})
|
||||||
|
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 string)) {
|
||||||
|
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 +60,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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -73,17 +39,64 @@ 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 = 'before request';" hx-on::after-request="this.innerText = 'after request';"><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 TestRenderAttributes_2(t *testing.T) {
|
||||||
|
div := Div(
|
||||||
|
AttributePairs("class", "bg-red-500", "id", "my-div"),
|
||||||
|
Button(
|
||||||
|
AttributePairs("class", "bg-blue-500", "id", "my-button"),
|
||||||
|
Text("Click me"),
|
||||||
|
Attribute("disabled", "true"),
|
||||||
|
Attribute("data-attr", "value"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
`<div class="bg-red-500" id="my-div"><button class="bg-blue-500" id="my-button" disabled="true" data-attr="value">Click me</button></div>`,
|
||||||
|
Render(div))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderEmptyDiv(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.Equal(t,
|
||||||
|
`<div></div>`,
|
||||||
|
Render(Div()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderVoidElement(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.Equal(t,
|
||||||
|
`<input type="text"/>`,
|
||||||
|
Render(Input("text")),
|
||||||
|
)
|
||||||
|
assert.Equal(t, `<input/>`, Render(Tag("input")))
|
||||||
|
}
|
||||||
|
|
||||||
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 +111,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 +134,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 +147,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 +163,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 +179,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 +195,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 +211,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 +227,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 +422,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 +438,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 +455,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 +542,7 @@ func TestBackgroundCleaner(t *testing.T) {
|
||||||
func TestEscapeHtml(t *testing.T) {
|
func TestEscapeHtml(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
assert.Equal(t, "<script>alert(1)</script>", Render(Text("<script>alert(1)</script>")))
|
assert.Equal(t, "<script>alert(1)</script>", Render(Text("<script>alert(1)</script>")))
|
||||||
assert.Equal(t, "<p ><script>alert(1)</script></p>", Render(Pf("<script>alert(1)</script>")))
|
assert.Equal(t, "<p><script>alert(1)</script></p>", Render(Pf("<script>alert(1)</script>")))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,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(" ")
|
node.attributes.Each(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 +108,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 +130,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
|
||||||
|
|
@ -162,14 +160,13 @@ func renderScripts(context *RenderContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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(" ")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,13 +196,10 @@ func (p *Partial) Render(context *RenderContext) {
|
||||||
p.Root.Render(context)
|
p.Root.Render(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AttributeMap) Render(context *RenderContext) {
|
func (m *AttributeMapOrdered) Render(context *RenderContext) {
|
||||||
m2 := m.ToMap()
|
m.Each(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,6 +216,7 @@ 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 {
|
for _, command := range commands {
|
||||||
switch c := command.(type) {
|
switch c := command.(type) {
|
||||||
case SimpleJsCommand:
|
case SimpleJsCommand:
|
||||||
|
|
@ -229,14 +224,15 @@ func (l *LifeCycle) Render(context *RenderContext) {
|
||||||
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() {
|
c.Each(func(key string, value string) {
|
||||||
l.fromAttributeMap(event, k, v, context)
|
l.fromAttributeMap(event, key, value, 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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
framework/internal/datastructure/map.go
Normal file
84
framework/internal/datastructure/map.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue