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
}
func CreateHtmgoDir() {
if !HasFileFromRoot("__htmgo") {
CreateDirFromRoot("__htmgo")
}
}
func CreateDirFromRoot(dir string) error {
cwd := process.GetWorkingDir()
path := filepath.Join(cwd, dir)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,8 @@
package h
import (
"bytes"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"golang.org/x/net/html"
"sort"
"strconv"
"strings"
"sync"
@ -13,43 +10,12 @@ import (
"time"
)
// Sort attributes of a node by attribute name
func sortAttributes(node *html.Node) {
if node.Type == html.ElementNode && len(node.Attr) > 1 {
sort.SliceStable(node.Attr, func(i, j int) bool {
return node.Attr[i].Key < node.Attr[j].Key
})
}
}
// 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 TestSimpleRender(t *testing.T) {
t.Parallel()
result := Render(
Div(Attribute("id", "my-div"), Attribute("class", "my-class")),
)
assert.Equal(t, `<div id="my-div" class="my-class"></div>`, result)
}
func TestRender(t *testing.T) {
@ -57,10 +23,10 @@ func TestRender(t *testing.T) {
div := Div(
Id("my-div"),
Attribute("data-attr-2", "value"),
Attributes(&AttributeMap{
"data-attr-3": "value",
"data-attr-4": "value",
}),
AttributePairs(
"data-attr-3", "value",
"data-attr-4", "value",
),
HxBeforeRequest(
SetText("before request"),
),
@ -73,17 +39,31 @@ func TestRender(t *testing.T) {
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 := sortHtmlAttributes(expectedRaw)
result := sortHtmlAttributes(Render(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>`
result := Render(div)
assert.Equal(t,
expected,
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) {
t.Parallel()
str := "<div>hello, world</div>"
@ -134,12 +114,12 @@ func TestCached(t *testing.T) {
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page()))
secondRender := sortHtmlAttributes(Render(page()))
firstRender := Render(page())
secondRender := Render(page())
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
assert.Equal(t, firstRender, Render(ComplexPage()))
}
func TestCachedT(t *testing.T) {
@ -150,12 +130,12 @@ func TestCachedT(t *testing.T) {
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a")))
secondRender := sortHtmlAttributes(Render(page("a")))
firstRender := Render(page("a"))
secondRender := Render(page("a"))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
assert.Equal(t, firstRender, Render(ComplexPage()))
}
func TestCachedT2(t *testing.T) {
@ -166,12 +146,12 @@ func TestCachedT2(t *testing.T) {
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a", "b")))
secondRender := sortHtmlAttributes(Render(page("a", "b")))
firstRender := Render(page("a", "b"))
secondRender := Render(page("a", "b"))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
assert.Equal(t, firstRender, Render(ComplexPage()))
}
func TestCachedT3(t *testing.T) {
@ -182,12 +162,12 @@ func TestCachedT3(t *testing.T) {
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a", "b", "c")))
secondRender := sortHtmlAttributes(Render(page("a", "b", "c")))
firstRender := Render(page("a", "b", "c"))
secondRender := Render(page("a", "b", "c"))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
assert.Equal(t, firstRender, Render(ComplexPage()))
}
func TestCachedT4(t *testing.T) {
@ -198,12 +178,12 @@ func TestCachedT4(t *testing.T) {
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a", "b", "c", "d")))
secondRender := sortHtmlAttributes(Render(page("a", "b", "c", "d")))
firstRender := Render(page("a", "b", "c", "d"))
secondRender := Render(page("a", "b", "c", "d"))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
assert.Equal(t, firstRender, Render(ComplexPage()))
}
func TestCachedExpired(t *testing.T) {
@ -214,9 +194,9 @@ func TestCachedExpired(t *testing.T) {
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page()))
firstRender := Render(page())
time.Sleep(time.Millisecond * 5)
secondRender := sortHtmlAttributes(Render(page()))
secondRender := Render(page())
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 2, count)

View file

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

View file

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

View file

@ -11,7 +11,7 @@ type PartialFunc = func(ctx *RequestContext) *Partial
type Element struct {
tag string
attributes map[string]string
attributes *AttributeMapOrdered
meta any
children []Ren
}
@ -50,7 +50,7 @@ func Tag(tag string, children ...Ren) *Element {
return &Element{
tag: tag,
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 {
return &Element{
tag: "meta",
attributes: map[string]string{
"name": name,
"content": content,
},
attributes: AttributePairs(
"name", name,
"content", content,
),
children: make([]Ren, 0),
}
}
func LinkWithVersion(href string, rel string, version string) *Element {
attributeMap := AttributeMap{
"href": href + "?v=" + version,
"rel": rel,
}
return &Element{
tag: "link",
attributes: attributeMap.ToMap(),
attributes: AttributePairs(
"href", href+"?v="+version,
"rel", rel,
),
children: make([]Ren, 0),
}
}
func Link(href string, rel string) *Element {
attributeMap := AttributeMap{
"href": href,
"rel": rel,
}
return &Element{
tag: "link",
attributes: attributeMap.ToMap(),
attributes: AttributePairs(
"href", href,
"rel", rel,
),
children: make([]Ren, 0),
}
}
func ScriptWithVersion(url string, version string) *Element {
attributeMap := AttributeMap{
"src": url + "?v=" + version,
}
return &Element{
tag: "script",
attributes: attributeMap.ToMap(),
attributes: AttributePairs(
"src", url+"?v="+version,
),
children: make([]Ren, 0),
}
}
func Script(url string) *Element {
attributeMap := AttributeMap{
"src": url,
}
return &Element{
tag: "script",
attributes: attributeMap.ToMap(),
attributes: AttributePairs(
"src", url,
),
children: make([]Ren, 0),
}
}
@ -177,12 +173,11 @@ func Value(value any) *AttributeR {
}
func Input(inputType string, children ...Ren) *Element {
attributeMap := AttributeMap{
"type": inputType,
}
return &Element{
tag: "input",
attributes: attributeMap.ToMap(),
attributes: AttributePairs(
"type", inputType,
),
children: children,
}
}
@ -257,7 +252,7 @@ func TagF(tag string, format string, args ...interface{}) *Element {
An invocation can look like
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
*/
children := make([]Ren, 0)
@ -266,7 +261,7 @@ func TagF(tag string, format string, args ...interface{}) *Element {
switch d := arg.(type) {
case *Element:
children = append(children, d)
case *AttributeMap:
case *AttributeMapOrdered:
children = append(children, d)
case *ChildList:
for _, child := range d.Children {

View file

@ -2,46 +2,46 @@ package h
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...))
}
func GetPartial(partial PartialFunc, trigger ...string) *AttributeMap {
func GetPartial(partial PartialFunc, trigger ...string) *AttributeMapOrdered {
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)
}
func GetWithQs(path string, qs *Qs, trigger string) *AttributeMap {
func GetWithQs(path string, qs *Qs, trigger string) *AttributeMapOrdered {
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...)
}
func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMap {
func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMapOrdered {
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...))
}
func PostWithQs(url string, qs *Qs, trigger string) *AttributeMap {
func PostWithQs(url string, qs *Qs, trigger string) *AttributeMapOrdered {
return Post(SetQueryParams(url, qs), trigger)
}
func PostOnClick(url string) *AttributeMap {
func PostOnClick(url string) *AttributeMapOrdered {
return Post(url, hx.ClickEvent)
}
func PostPartialOnClick(partial PartialFunc) *AttributeMap {
func PostPartialOnClick(partial PartialFunc) *AttributeMapOrdered {
return PostOnClick(GetPartialPath(partial))
}
func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMap {
func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMapOrdered {
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
}
}
}
}