diff --git a/cli/htmgo/tasks/astgen/map.go b/cli/htmgo/tasks/astgen/map.go deleted file mode 100644 index 201ccea..0000000 --- a/cli/htmgo/tasks/astgen/map.go +++ /dev/null @@ -1,82 +0,0 @@ -package astgen - -// 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 -} - -// Entries returns the key-value pairs in the order they were added. -func (om *OrderedMap[K, V]) Entries() []struct { - Key K - Value V -} { - entries := make([]struct { - Key K - Value V - }, len(om.keys)) - for i, key := range om.keys { - entries[i] = struct { - Key K - Value 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 - } - } - } -} diff --git a/cli/htmgo/tasks/astgen/util.go b/cli/htmgo/tasks/astgen/util.go index e55c5a9..af084fc 100644 --- a/cli/htmgo/tasks/astgen/util.go +++ b/cli/htmgo/tasks/astgen/util.go @@ -7,16 +7,3 @@ import ( func PanicF(format string, args ...interface{}) { panic(fmt.Sprintf(format, args...)) } - -func Unique[T any](slice []T, key func(item T) string) []T { - var result []T - seen := make(map[string]bool) - for _, v := range slice { - k := key(v) - if _, ok := seen[k]; !ok { - seen[k] = true - result = append(result, v) - } - } - return result -} diff --git a/htmgo-site/internal/datastructures/map.go b/framework/datastructure/orderedmap/orderedmap.go similarity index 63% rename from htmgo-site/internal/datastructures/map.go rename to framework/datastructure/orderedmap/orderedmap.go index 533ca4f..c356b68 100644 --- a/htmgo-site/internal/datastructures/map.go +++ b/framework/datastructure/orderedmap/orderedmap.go @@ -1,18 +1,24 @@ -package datastructures - -// 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 -} +package orderedmap type Entry[K comparable, V any] struct { Key K Value V } +// Map is a generic data structure that maintains the order of keys. +type Map[K comparable, V any] struct { + keys []K + values map[K]V +} + +func (om *Map[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() []Entry[K, V] { +func (om *Map[K, V]) Entries() []Entry[K, V] { entries := make([]Entry[K, V], len(om.keys)) for i, key := range om.keys { entries[i] = Entry[K, V]{ @@ -23,16 +29,16 @@ func (om *OrderedMap[K, V]) Entries() []Entry[K, V] { return entries } -// NewOrderedMap creates a new OrderedMap. -func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { - return &OrderedMap[K, V]{ +// New creates a new Map. +func New[K comparable, V any]() *Map[K, V] { + return &Map[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) { +// Set adds or updates a key-value pair in the Map. +func (om *Map[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 @@ -41,18 +47,18 @@ func (om *OrderedMap[K, V]) Set(key K, value V) { } // Get retrieves a value by key. -func (om *OrderedMap[K, V]) Get(key K) (V, bool) { +func (om *Map[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 { +func (om *Map[K, V]) Keys() []K { return om.keys } // Values returns the values in the order of their keys. -func (om *OrderedMap[K, V]) Values() []V { +func (om *Map[K, V]) Values() []V { values := make([]V, len(om.keys)) for i, key := range om.keys { values[i] = om.values[key] @@ -61,8 +67,8 @@ func (om *OrderedMap[K, V]) Values() []V { return values } -// Delete removes a key-value pair from the OrderedMap. -func (om *OrderedMap[K, V]) Delete(key K) { +// Delete removes a key-value pair from the Map. +func (om *Map[K, V]) Delete(key K) { if _, exists := om.values[key]; exists { // Remove the key from the map delete(om.values, key) diff --git a/framework/datastructure/orderedmap/orderedmap_test.go b/framework/datastructure/orderedmap/orderedmap_test.go new file mode 100644 index 0000000..e30bc9a --- /dev/null +++ b/framework/datastructure/orderedmap/orderedmap_test.go @@ -0,0 +1,33 @@ +package orderedmap + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestOrderedMap(t *testing.T) { + t.Parallel() + om := New[string, int]() + + alphabet := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} + for index, letter := range alphabet { + om.Set(letter, index) + } + + assert.Equal(t, alphabet, om.Keys()) + + c, ok := om.Get("c") + assert.True(t, ok) + assert.Equal(t, 2, c) + + for i, entry := range om.Entries() { + if i == 5 { + assert.Equal(t, "f", entry.Key) + } + } + + om.Delete("c") + value, ok := om.Get("c") + assert.False(t, ok) + assert.Equal(t, 0, value) +} diff --git a/framework/h/array.go b/framework/h/array.go index d1f8c48..7cf5529 100644 --- a/framework/h/array.go +++ b/framework/h/array.go @@ -1,5 +1,9 @@ package h +import ( + "github.com/maddalax/htmgo/framework/datastructure/orderedmap" +) + // Unique returns a new slice with only unique items. func Unique[T any](slice []T, key func(item T) string) []T { var result []T @@ -14,6 +18,7 @@ func Unique[T any](slice []T, key func(item T) string) []T { return result } +// Find returns the first item in the slice that matches the predicate. func Find[T any](slice []T, predicate func(item *T) bool) *T { for _, v := range slice { if predicate(&v) { @@ -23,6 +28,34 @@ func Find[T any](slice []T, predicate func(item *T) bool) *T { return nil } +// GroupBy groups the items in the slice by the key returned by the key function. +func GroupBy[T any, K comparable](slice []T, key func(item T) K) map[K][]T { + grouped := make(map[K][]T) + for _, item := range slice { + k := key(item) + items, ok := grouped[k] + if !ok { + items = []T{} + } + grouped[k] = append(items, item) + } + return grouped +} + +// GroupByOrdered groups the items in the slice by the key returned by the key function, and returns an Map. +func GroupByOrdered[T any, K comparable](slice []T, key func(item T) K) *orderedmap.Map[K, []T] { + grouped := orderedmap.New[K, []T]() + for _, item := range slice { + k := key(item) + items, ok := grouped.Get(k) + if !ok { + items = []T{} + } + grouped.Set(k, append(items, item)) + } + return grouped +} + // Filter returns a new slice with only items that match the predicate. func Filter[T any](slice []T, predicate func(item T) bool) []T { var result []T diff --git a/framework/h/array_test.go b/framework/h/array_test.go new file mode 100644 index 0000000..42d5132 --- /dev/null +++ b/framework/h/array_test.go @@ -0,0 +1,102 @@ +package h + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestUnique(t *testing.T) { + t.Parallel() + slice := []string{"a", "b", "b", "c", "d", "d", "x"} + unique := Unique(slice, func(item string) string { + return item + }) + assert.Equal(t, []string{"a", "b", "c", "d", "x"}, unique) +} + +func TestFilter(t *testing.T) { + t.Parallel() + slice := []string{"a", "b", "b", "c", "d", "d", "x"} + filtered := Filter(slice, func(item string) bool { + return item == "b" + }) + assert.Equal(t, []string{"b", "b"}, filtered) +} + +func TestMap(t *testing.T) { + t.Parallel() + slice := []string{"a", "b", "c"} + mapped := Map(slice, func(item string) string { + return strings.ToUpper(item) + }) + assert.Equal(t, []string{"A", "B", "C"}, mapped) +} + +func TestGroupBy(t *testing.T) { + t.Parallel() + + type Item struct { + Name string + Job string + } + + items := []Item{ + {Name: "Alice", Job: "Developer"}, + {Name: "Bob", Job: "Designer"}, + {Name: "Charlie", Job: "Developer"}, + {Name: "David", Job: "Designer"}, + {Name: "Eve", Job: "Developer"}, + {Name: "Frank", Job: "Product Manager"}, + } + + grouped := GroupBy(items, func(item Item) string { + return item.Job + }) + + assert.Equal(t, 3, len(grouped)) + assert.Equal(t, 3, len(grouped["Developer"])) + assert.Equal(t, 2, len(grouped["Designer"])) + assert.Equal(t, 1, len(grouped["Product Manager"])) +} + +func TestGroupByOrdered(t *testing.T) { + t.Parallel() + + type Item struct { + Name string + Job string + } + + items := []Item{ + {Name: "Alice", Job: "Developer"}, + {Name: "Bob", Job: "Designer"}, + {Name: "Charlie", Job: "Developer"}, + {Name: "David", Job: "Designer"}, + {Name: "Eve", Job: "Developer"}, + {Name: "Frank", Job: "Product Manager"}, + } + + grouped := GroupByOrdered(items, func(item Item) string { + return item.Job + }) + + keys := []string{"Developer", "Designer", "Product Manager"} + assert.Equal(t, keys, grouped.Keys()) + + devs, ok := grouped.Get("Developer") + assert.True(t, ok) + assert.Equal(t, 3, len(devs)) + assert.Equal(t, "Alice", devs[0].Name) + assert.Equal(t, "Charlie", devs[1].Name) + assert.Equal(t, "Eve", devs[2].Name) +} + +func TestFind(t *testing.T) { + t.Parallel() + slice := []string{"a", "b", "c"} + found := Find(slice, func(item *string) bool { + return *item == "b" + }) + assert.Equal(t, "b", *found) +} diff --git a/framework/h/attribute.go b/framework/h/attribute.go index 48efcb2..0a4fa89 100644 --- a/framework/h/attribute.go +++ b/framework/h/attribute.go @@ -2,16 +2,15 @@ package h import ( "fmt" - "strings" - + "github.com/maddalax/htmgo/framework/datastructure/orderedmap" "github.com/maddalax/htmgo/framework/hx" - "github.com/maddalax/htmgo/framework/internal/datastructure" + "strings" ) type AttributeMap = map[string]any type AttributeMapOrdered struct { - data *datastructure.OrderedMap[string, string] + data *orderedmap.Map[string, string] } func (m *AttributeMapOrdered) Set(key string, value any) { @@ -39,12 +38,12 @@ func (m *AttributeMapOrdered) Each(cb func(key string, value string)) { }) } -func (m *AttributeMapOrdered) Entries() []datastructure.MapEntry[string, string] { +func (m *AttributeMapOrdered) Entries() []orderedmap.Entry[string, string] { return m.data.Entries() } func NewAttributeMap(pairs ...string) *AttributeMapOrdered { - m := datastructure.NewOrderedMap[string, string]() + m := orderedmap.New[string, string]() if len(pairs)%2 == 0 { for i := 0; i < len(pairs); i++ { m.Set(pairs[i], pairs[i+1]) diff --git a/framework/internal/datastructure/map.go b/framework/internal/datastructure/map.go deleted file mode 100644 index e4741d2..0000000 --- a/framework/internal/datastructure/map.go +++ /dev/null @@ -1,84 +0,0 @@ -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 - } - } - } -} diff --git a/htmgo-site/pages/examples/data.go b/htmgo-site/pages/examples/data.go index f331353..d00c870 100644 --- a/htmgo-site/pages/examples/data.go +++ b/htmgo-site/pages/examples/data.go @@ -10,6 +10,7 @@ type Snippet struct { partial h.PartialFunc externalRoute string sourceCodePath string + category string } func SetSnippet(ctx *h.RequestContext, snippet *Snippet) { diff --git a/htmgo-site/pages/examples/examples.go b/htmgo-site/pages/examples/examples.go index 317dd66..8d0ba6c 100644 --- a/htmgo-site/pages/examples/examples.go +++ b/htmgo-site/pages/examples/examples.go @@ -3,6 +3,7 @@ package examples import "htmgo-site/partials/snippets" var FormWithLoadingStateSnippet = Snippet{ + category: "Forms", name: "Form", description: "A simple form submission example with a loading state", sidebarName: "Form With Loading State", @@ -11,6 +12,7 @@ var FormWithLoadingStateSnippet = Snippet{ } var UserAuthSnippet = Snippet{ + category: "Projects", name: "User Authentication", description: "An example showing basic user registration and login with htmgo", sidebarName: "User Authentication", @@ -20,6 +22,7 @@ var UserAuthSnippet = Snippet{ } var ChatSnippet = Snippet{ + category: "Projects", name: "Chat App", description: "A simple chat application built with htmgo using SSE for real-time updates", sidebarName: "Chat App Using SSE", @@ -29,6 +32,7 @@ var ChatSnippet = Snippet{ } var HackerNewsSnippet = Snippet{ + category: "Projects", name: "HackerNews Clone", description: "A hacker news reader clone built with htmgo", sidebarName: "HackerNews Clone", @@ -38,6 +42,7 @@ var HackerNewsSnippet = Snippet{ } var HtmgoSiteSnippet = Snippet{ + category: "Projects", name: "Htmgo Doc Site", description: "The htmgo site built with htmgo, recursion am I right?", sidebarName: "Htmgo Doc Site", @@ -47,6 +52,7 @@ var HtmgoSiteSnippet = Snippet{ } var TodoListSnippet = Snippet{ + category: "Projects", name: "Todo List", description: "A todo list built with htmgo", sidebarName: "Todo List", @@ -56,6 +62,7 @@ var TodoListSnippet = Snippet{ } var ClickToEditSnippet = Snippet{ + category: "Forms", name: "Inline Click To Edit", description: "List view of items with a click to edit button and persistence", sidebarName: "Inline Click To Edit", diff --git a/htmgo-site/pages/examples/sidebar.go b/htmgo-site/pages/examples/sidebar.go index 86315e4..57c6421 100644 --- a/htmgo-site/pages/examples/sidebar.go +++ b/htmgo-site/pages/examples/sidebar.go @@ -1,10 +1,16 @@ package examples import ( + "github.com/maddalax/htmgo/framework/datastructure/orderedmap" "github.com/maddalax/htmgo/framework/h" ) func SnippetSidebar() *h.Element { + + grouped := h.GroupByOrdered(examples, func(example Snippet) string { + return example.category + }) + return h.Div( h.Class("px-3 py-2 pr-6 md:min-h-screen pb-4 mb:pb-0 bg-neutral-50 border-r border-r-slate-300 overflow-y-auto"), h.Div( @@ -18,11 +24,22 @@ func SnippetSidebar() *h.Element { ), h.Div( h.Class("flex flex-col gap-2"), - h.List(examples, func(entry Snippet, index int) *h.Element { - return h.A( - h.Href(entry.path), - h.Text(entry.sidebarName), - h.Class("text-slate-900 hover:text-rose-400"), + h.List(grouped.Entries(), func(entry orderedmap.Entry[string, []Snippet], index int) *h.Element { + return h.Div( + h.P( + h.Text(entry.Key), + h.Class("text-slate-800 font-bold"), + ), + h.Div( + h.Class("pl-4 flex flex-col"), + h.List(entry.Value, func(entry Snippet, index int) *h.Element { + return h.A( + h.Href(entry.path), + h.Text(entry.sidebarName), + h.Class("text-slate-900 hover:text-rose-400"), + ) + }), + ), ) }), ), diff --git a/htmgo-site/partials/doc-sidebar.go b/htmgo-site/partials/doc-sidebar.go index 02d5320..a2d84bf 100644 --- a/htmgo-site/partials/doc-sidebar.go +++ b/htmgo-site/partials/doc-sidebar.go @@ -1,8 +1,8 @@ package partials import ( + "github.com/maddalax/htmgo/framework/datastructure/orderedmap" "github.com/maddalax/htmgo/framework/h" - "htmgo-site/internal/datastructures" "htmgo-site/internal/dirwalk" "strings" ) @@ -37,8 +37,8 @@ func partsToName(parts []string) string { return builder.String() } -func groupByFirstPart(pages []*dirwalk.Page) *datastructures.OrderedMap[string, []*dirwalk.Page] { - grouped := datastructures.NewOrderedMap[string, []*dirwalk.Page]() +func groupByFirstPart(pages []*dirwalk.Page) *orderedmap.Map[string, []*dirwalk.Page] { + grouped := orderedmap.New[string, []*dirwalk.Page]() for _, page := range pages { if len(page.Parts) > 0 { section := page.Parts[0] @@ -69,7 +69,7 @@ func DocSidebar(pages []*dirwalk.Page) *h.Element { ), h.Div( h.Class("flex flex-col gap-4"), - h.List(grouped.Entries(), func(entry datastructures.Entry[string, []*dirwalk.Page], index int) *h.Element { + h.List(grouped.Entries(), func(entry orderedmap.Entry[string, []*dirwalk.Page], index int) *h.Element { return h.Div( h.P( h.Text(formatPart(entry.Key)),