diff --git a/cli/htmgo/tasks/css/css.go b/cli/htmgo/tasks/css/css.go index e31423d..13b91b9 100644 --- a/cli/htmgo/tasks/css/css.go +++ b/cli/htmgo/tasks/css/css.go @@ -69,6 +69,11 @@ func downloadTailwindCli() { distro = "linux-arm64" case os == "linux" && arch == "amd64": distro = "linux-x64" + case os == "windows" && arch == "amd64": + distro = "windows-x64" + case os == "windows" && arch == "arm64": + distro = "windows-arm64" + default: log.Fatal(fmt.Sprintf("Unsupported OS/ARCH: %s/%s", os, arch)) } diff --git a/cli/htmgo/tasks/process/process.go b/cli/htmgo/tasks/process/process.go index 9191d65..c476c38 100644 --- a/cli/htmgo/tasks/process/process.go +++ b/cli/htmgo/tasks/process/process.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "slices" "strings" "syscall" @@ -178,7 +179,10 @@ func RunMany(commands []string, flags ...RunFlag) error { func Run(command string, flags ...RunFlag) error { parts := strings.Fields(command) cmd := exec.Command(parts[0], parts[1:]...) - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + if runtime.GOOS != "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + } if slices.Contains(flags, Silent) { cmd.Stdout = nil diff --git a/framework/h/tag.go b/framework/h/tag.go index 0f21ae0..f63a237 100644 --- a/framework/h/tag.go +++ b/framework/h/tag.go @@ -90,6 +90,10 @@ func Raw(text string) *RawContent { return NewRawContent(text) } +func Style(text string) Ren { + return Tag("style", Text(text)) +} + func MultiLineQuotes(text string) string { return "`" + text + "`" } @@ -140,6 +144,19 @@ func Input(inputType string, children ...Ren) Ren { } } +func IterMap[T any](m map[string]T, mapper func(key string, value T) *Element) *Element { + node := &Element{ + tag: "", + children: make([]Ren, len(m)), + } + index := 0 + for key, value := range m { + node.children[index] = mapper(key, value) + index++ + } + return node +} + func List[T any](items []T, mapper func(item T, index int) *Element) *Element { node := &Element{ tag: "", diff --git a/htmgo-site/internal/datastructures/map.go b/htmgo-site/internal/datastructures/map.go new file mode 100644 index 0000000..533ca4f --- /dev/null +++ b/htmgo-site/internal/datastructures/map.go @@ -0,0 +1,78 @@ +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 +} + +type Entry[K comparable, V any] struct { + Key K + Value V +} + +// Entries returns the key-value pairs in the order they were added. +func (om *OrderedMap[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]{ + 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/internal/dirwalk/walk.go b/htmgo-site/internal/dirwalk/walk.go index 56fc529..59df766 100644 --- a/htmgo-site/internal/dirwalk/walk.go +++ b/htmgo-site/internal/dirwalk/walk.go @@ -4,6 +4,8 @@ import ( "github.com/maddalax/htmgo/framework/h" "io/fs" "os" + "slices" + "strconv" "strings" ) @@ -13,8 +15,8 @@ type Page struct { Parts []string } -func WalkPages(dir string, system fs.FS) []Page { - pages := make([]Page, 0) +func WalkPages(dir string, system fs.FS) []*Page { + pages := make([]*Page, 0) fs.WalkDir(system, dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -23,7 +25,7 @@ func WalkPages(dir string, system fs.FS) []Page { if !d.IsDir() && (strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".go")) { fullPath := strings.Replace(path, dir, "", 1) fullPath = strings.TrimSuffix(fullPath, ".md") - pages = append(pages, Page{ + pages = append(pages, &Page{ RoutePath: fullPath, FilePath: path, Parts: h.Filter(strings.Split(fullPath, string(os.PathSeparator)), func(item string) bool { @@ -33,5 +35,23 @@ func WalkPages(dir string, system fs.FS) []Page { } return nil }) + + var getRouteOrder = func(page *Page) int { + fileName := page.Parts[len(page.Parts)-1] + if len(fileName) > 1 && fileName[1] == '_' { + num, err := strconv.ParseInt(fileName[0:1], 10, 64) + if err != nil { + return 0 + } + page.Parts[len(page.Parts)-1] = fileName[2:] + return int(num) + } + return 0 + } + + slices.SortFunc(pages, func(a *Page, b *Page) int { + return getRouteOrder(a) - getRouteOrder(b) + }) + return pages } diff --git a/htmgo-site/main.go b/htmgo-site/main.go index 4cf1a82..7f1eb24 100644 --- a/htmgo-site/main.go +++ b/htmgo-site/main.go @@ -46,7 +46,7 @@ func main() { __htmgo.RegisterPages(e) pages.RegisterMarkdown(e, "md", MarkdownAssets, func(ctx echo.Context, path string) error { - return pages.MarkdownHandler(ctx.(*h.RequestContext), path) + return pages.MarkdownHandler(ctx.(*h.RequestContext), path, "") }) }, }) diff --git a/htmgo-site/md/docs/1_quick-start/1_introduction.md b/htmgo-site/md/docs/1_quick-start/1_introduction.md new file mode 100644 index 0000000..43d612f --- /dev/null +++ b/htmgo-site/md/docs/1_quick-start/1_introduction.md @@ -0,0 +1,3 @@ +## **Introduction** + +htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx. diff --git a/htmgo-site/md/docs/quick-start/installation.md b/htmgo-site/md/docs/1_quick-start/2_installation.md similarity index 91% rename from htmgo-site/md/docs/quick-start/installation.md rename to htmgo-site/md/docs/1_quick-start/2_installation.md index 8d5e0ce..ba3b287 100644 --- a/htmgo-site/md/docs/quick-start/installation.md +++ b/htmgo-site/md/docs/1_quick-start/2_installation.md @@ -1,11 +1,14 @@ ## **Getting Started** +##### **Prerequisites:** +Go: https://go.dev/doc/install +
##### 1. **Install htmgo** ```bash -GONOPROXY=github.com/maddalax go install github.com/maddalax/htmgo/cli/htmgo@latest +go install github.com/maddalax/htmgo/cli/htmgo@latest ``` tip: GONOPROXY helps because the default proxy server for how go resolves modules appears to have fairly long caching on it, so without this env variable, an old version may get installed. diff --git a/htmgo-site/md/docs/2_core-concepts/pages.md b/htmgo-site/md/docs/2_core-concepts/pages.md new file mode 100644 index 0000000..7d0fd96 --- /dev/null +++ b/htmgo-site/md/docs/2_core-concepts/pages.md @@ -0,0 +1,3 @@ +## Pages ## + +Coming soon \ No newline at end of file diff --git a/htmgo-site/md/docs/2_core-concepts/partials.md b/htmgo-site/md/docs/2_core-concepts/partials.md new file mode 100644 index 0000000..17ce2b4 --- /dev/null +++ b/htmgo-site/md/docs/2_core-concepts/partials.md @@ -0,0 +1,3 @@ +## Partials ## + +Coming soon \ No newline at end of file diff --git a/htmgo-site/md/docs/example.md b/htmgo-site/md/docs/example.md deleted file mode 100644 index e69de29..0000000 diff --git a/htmgo-site/md/examples.md b/htmgo-site/md/examples.md index fe4d5d3..1aa745f 100644 --- a/htmgo-site/md/examples.md +++ b/htmgo-site/md/examples.md @@ -1 +1,4 @@ -Coming soon \ No newline at end of file +**Todo List MVC** +[Github](https://github.com/maddalax/htmgo/tree/master/examples/todo-list) +[Live Demo](https://todo-example.htmgo.dev) + diff --git a/htmgo-site/pages/base/root.go b/htmgo-site/pages/base/root.go index 5e68ff4..7f274d0 100644 --- a/htmgo-site/pages/base/root.go +++ b/htmgo-site/pages/base/root.go @@ -15,6 +15,11 @@ func RootPage(children ...h.Ren) *h.Element { h.Raw(` `), + h.Style(` + html { + scroll-behavior: smooth; + } + `), ), h.Body( h.Class("bg-neutral-50 min-h-screen overflow-x-hidden"), diff --git a/htmgo-site/pages/docs.go b/htmgo-site/pages/docs.go index d6898f2..21f83cb 100644 --- a/htmgo-site/pages/docs.go +++ b/htmgo-site/pages/docs.go @@ -5,6 +5,8 @@ import ( "github.com/maddalax/htmgo/framework/h" "htmgo-site/internal/dirwalk" "htmgo-site/pages/base" + "htmgo-site/partials" + "strings" ) func DocsPage(ctx *h.RequestContext) *h.Page { @@ -13,14 +15,17 @@ func DocsPage(ctx *h.RequestContext) *h.Page { return h.NewPage(base.RootPage( h.Div( - h.Class("flex flex-col justify-center items-center"), - h.List(pages, func(page dirwalk.Page, index int) *h.Element { - return MarkdownContent(ctx, page.FilePath) - }), + h.Class("flex gap-4 justify-center mb-12"), + partials.DocSidebar(pages), + h.Div( + h.Class("flex flex-col justify-center items-center mt-6 gap-12"), + h.List(pages, func(page *dirwalk.Page, index int) *h.Element { + return MarkdownContent(ctx, page.FilePath, strings.Join(page.Parts, "-")) + }), + ), + h.Div( + h.Class("min-h-12"), + ), ), - h.Div( - h.Class("min-h-12"), - ), - ), - ) + )) } diff --git a/htmgo-site/pages/index.go b/htmgo-site/pages/index.go index 7edb861..9b7a1af 100644 --- a/htmgo-site/pages/index.go +++ b/htmgo-site/pages/index.go @@ -5,5 +5,5 @@ import ( ) func IndexPage(ctx *h.RequestContext) *h.Page { - return h.NewPage(MarkdownPage(ctx, "md/index.md")) + return h.NewPage(MarkdownPage(ctx, "md/index.md", "")) } diff --git a/htmgo-site/pages/markdown.go b/htmgo-site/pages/markdown.go index 79a5b6c..04f6284 100644 --- a/htmgo-site/pages/markdown.go +++ b/htmgo-site/pages/markdown.go @@ -8,15 +8,15 @@ import ( "htmgo-site/pages/base" ) -func MarkdownHandler(ctx *h.RequestContext, path string) error { - return h.HtmlView(ctx, h.NewPage(MarkdownPage(ctx, path))) +func MarkdownHandler(ctx *h.RequestContext, path string, id string) error { + return h.HtmlView(ctx, h.NewPage(MarkdownPage(ctx, path, id))) } -func MarkdownPage(ctx *h.RequestContext, path string) *h.Element { +func MarkdownPage(ctx *h.RequestContext, path string, id string) *h.Element { return base.RootPage( h.Div( h.Class("w-full p-4 flex flex-col justify-center items-center"), - MarkdownContent(ctx, path), + MarkdownContent(ctx, path, id), h.Div( h.Class("min-h-12"), ), @@ -24,10 +24,11 @@ func MarkdownPage(ctx *h.RequestContext, path string) *h.Element { ) } -func MarkdownContent(ctx *h.RequestContext, path string) *h.Element { +func MarkdownContent(ctx *h.RequestContext, path string, id string) *h.Element { embeddedMd := ctx.Get("embeddedMarkdown").(*embed.FS) renderer := service.Get[markdown.Renderer](ctx.ServiceLocator()) return h.Div( + h.If(id != "", h.Id(id)), h.Article( h.Class("prose max-w-[95vw] md:max-w-2xl px-4 prose-code:text-black"), h.Raw(renderer.RenderFile(path, embeddedMd)), diff --git a/htmgo-site/partials/navbar.go b/htmgo-site/partials/navbar.go index ca42aef..35b1837 100644 --- a/htmgo-site/partials/navbar.go +++ b/htmgo-site/partials/navbar.go @@ -74,7 +74,6 @@ func NavBar(expanded bool) *h.Element { ) return h.Div( - h.Class("mb-4"), h.Id("navbar"), prelease, MobileNav(expanded), diff --git a/htmgo-site/partials/sidebar.go b/htmgo-site/partials/sidebar.go index 6230e8e..db52235 100644 --- a/htmgo-site/partials/sidebar.go +++ b/htmgo-site/partials/sidebar.go @@ -1,22 +1,77 @@ package partials -import "github.com/maddalax/htmgo/framework/h" +import ( + "github.com/maddalax/htmgo/framework/h" + "htmgo-site/internal/datastructures" + "htmgo-site/internal/dirwalk" + "strings" +) + +func formatPart(part string) string { + if part[1] == '_' { + part = part[2:] + } + part = strings.ReplaceAll(part, "-", " ") + part = strings.ReplaceAll(part, "_", " ") + part = strings.Title(part) + return part +} + +func partsToName(parts []string) string { + builder := strings.Builder{} + for i, part := range parts { + if i == 0 { + continue + } + part = formatPart(part) + builder.WriteString(part) + builder.WriteString(" ") + } + + return builder.String() +} + +func groupByFirstPart(pages []*dirwalk.Page) *datastructures.OrderedMap[string, []*dirwalk.Page] { + grouped := datastructures.NewOrderedMap[string, []*dirwalk.Page]() + for _, page := range pages { + if len(page.Parts) > 0 { + section := page.Parts[0] + existing, has := grouped.Get(section) + if !has { + existing = []*dirwalk.Page{} + grouped.Set(section, existing) + } + grouped.Set(section, append(existing, page)) + } + } + return grouped +} + +func DocSidebar(pages []*dirwalk.Page) *h.Element { + grouped := groupByFirstPart(pages) -func SideBar() *h.Element { return h.Div( - h.Class("w-40 top-[57px] absolute min-h-screen bg-neutral-50 border border-r-slate-300 p-3"), + h.Class("px-3 py-2 pr-6 min-h-[(calc(100%))] min-h-screen bg-neutral-50 border-r border-r-slate-300"), h.Div( - h.Class("max-w-prose mx-auto"), + h.H4(h.Text("Contents"), h.Class("mt-4 text-slate-900 font-bold mb-3")), h.Div( h.Class("flex flex-col gap-4"), - h.A( - h.Href("/docs"), - h.Text("Docs"), - ), - h.A( - h.Href("/examples"), - h.Text("Examples"), - ), + h.List(grouped.Entries(), func(entry datastructures.Entry[string, []*dirwalk.Page], index int) *h.Element { + return h.Div( + h.P(h.Text(formatPart(entry.Key)), h.Class("text-slate-800 font-bold")), + h.Div( + h.Class("pl-4 flex flex-col"), + h.List(entry.Value, func(page *dirwalk.Page, index int) *h.Element { + anchor := strings.Join(page.Parts, "-") + return h.A( + h.Href("#"+anchor), + h.Text(partsToName(page.Parts)), + h.ClassX("text-slate-900", map[string]bool{}), + ) + }), + ), + ) + }), ), ), )