~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.



file:

gsx/gsx.go



package gsx
import (
"fmt"
"io"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
_ "github.com/alecthomas/repr"
"github.com/rotisserie/eris"
"github.com/samber/lo"
)
var (
htmlElements = []string{"a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bb", "bdo", "big", "blockquote", "body", "br /", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "command", "datagrid", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "eventsource", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1 to <h6>", "head", "header", "hgroup", "hr /", "html", "i", "iframe", "img", "input", "ins", "isindex", "kbd", "keygen", "label", "legend", "li", "link", "map", "mark", "menu", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"}
voidElements = []string{"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"}
compMap = map[string]ComponentFunc{}
funcMap = map[string]interface{}{}
refRegex = regexp.MustCompile(`{(.*?)}`)
)
type (
M map[string]interface{}
MS map[string]string
Arr []interface{}
ComponentFunc struct {
Name string
Func interface{}
Args []string
Styles M
}
link struct {
Rel string
Href string
Type string
As string
}
)
func RegisterComponent(f interface{}, styles M, args ...string) {
name := getFunctionName(f)
compMap[name] = ComponentFunc{
Name: name,
Func: f,
Args: args,
Styles: styles,
}
}
func RegisterFunc(f interface{}) {
name := getFunctionName(f)
funcMap[name] = f
}
func getFunctionName(temp interface{}) string {
strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
return strs[len(strs)-1]
}
func (comp ComponentFunc) Render(c *Context, tag *Tag) []*Tag {
args := []reflect.Value{reflect.ValueOf(c)}
funcType := reflect.TypeOf(comp.Func)
for i, arg := range comp.Args {
if v, ok := c.data[arg]; ok {
args = append(args, reflect.ValueOf(v))
} else {
t := funcType.In(i + 1)
v, _ := lo.Find(tag.Attributes, func(a *Attribute) bool {
return a.Key == arg
})
var data interface{}
if v.Value.Ref != nil {
data = getRefValue(c, *v.Value.Ref)
} else if v.Value.Str != nil {
data = *v.Value.Str
}
switch t.Kind() {
case reflect.Int:
var value int
if v, ok := data.(int); ok {
value = v
} else {
s, ok := data.(string)
if !ok {
panic(eris.Errorf("expected component %s: prop %s to be of type string but got %+v ", comp.Name, arg, data))
}
value, _ = strconv.Atoi(s)
}
c.Set(arg, value)
args = append(args, reflect.ValueOf(value))
case reflect.Bool:
var value bool
if v, ok := data.(bool); ok {
value = v
} else {
s, ok := data.(string)
if !ok {
panic(eris.Errorf("expected component %s: prop %s to be of type string but got %+v ", comp.Name, arg, data))
}
value, _ = strconv.ParseBool(s)
}
c.Set(arg, value)
args = append(args, reflect.ValueOf(value))
default:
c.Set(arg, data)
args = append(args, reflect.ValueOf(data))
}
}
}
result := reflect.ValueOf(comp.Func).Call(args)
tags := result[0].Interface().([]*Tag)
return populate(c, tags)
}
func Write(c *Context, w io.Writer, tags []*Tag) {
if c.hx == nil {
w.Write([]byte("<!DOCTYPE html>\n<html lang='en'>\n<head>\n<meta charset='UTF-8'>\n"))
w.Write([]byte(" <meta http-equiv='Content-Type' content='text/html;charset=utf-8'><meta content='utf-8' http-equiv='encoding'>\n"))
w.Write([]byte(" <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover'>\n"))
for k, v := range c.meta {
w.Write([]byte(fmt.Sprintf(" <meta name='%s' content='%s'>\n", k, v)))
}
for k, v := range c.meta {
if k == "title" {
w.Write([]byte(fmt.Sprintf(" <title>%s</title>\n", v)))
}
}
for _, v := range c.links {
if v.Type != "" || v.As != "" {
w.Write([]byte(fmt.Sprintf(" <link rel='%s' href='%s' type='%s' as='%s'>\n", v.Rel, v.Href, v.Type, v.As)))
} else {
w.Write([]byte(fmt.Sprintf(" <link rel='%s' href='%s'>\n", v.Rel, v.Href)))
}
}
funcName := c.Get("funcName").(string)
styles := computeCss(c.styles, funcName)
w.Write([]byte(fmt.Sprintf(" <style>%s</style>\n", styles)))
for src, sdefer := range c.scripts {
if sdefer {
w.Write([]byte(fmt.Sprintf(" <script src='%s' defer='true'></script>\n", src)))
} else {
w.Write([]byte(fmt.Sprintf(" <script src='%s'></script>\n", src)))
}
}
w.Write([]byte("</head>\n <body _='on htmx:error(errorInfo) put errorInfo.xhr.response into #error'>\n"))
}
out := RenderString(tags)
w.Write([]byte(out))
if c.hx == nil {
w.Write([]byte(" </body>\n</html>"))
}
}
func GetComponentStyles() string {
css := ""
for k, v := range compMap {
if v.Styles != nil {
css += computeCss(v.Styles, k)
}
}
return css
}
func convert(ref string, i interface{}) interface{} {
switch iv := i.(type) {
case bool:
if strings.Contains(ref, "!") {
return !iv
} else {
return iv
}
case int:
return iv
case string:
return iv
case []*Tag:
return iv
default:
return iv
}
}
func getRefValue(c *Context, ref string) interface{} {
if ref == "true" {
return true
} else if ref == "false" {
return false
} else if f, ok := funcMap[ref]; ok {
return f.(func() string)()
} else {
parts := strings.Split(strings.ReplaceAll(ref, "!", ""), ".")
if len(parts) == 2 {
if v, ok := c.data[parts[0]]; ok {
a := reflect.ValueOf(v)
if a.Kind() == reflect.Ptr {
i := a.Elem().FieldByName(parts[1]).Interface()
return convert(ref, i)
} else {
i := a.FieldByName(parts[1]).Interface()
return convert(ref, i)
}
}
}
return convert(ref, c.data[ref])
}
}
func removeBrackets(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, "{", ""), "}", "")
}
func removeQuotes(s string) string {
return strings.ReplaceAll(s, `"`, "")
}
func substituteString(c *Context, v string) string {
found := refRegex.FindString(v)
if found != "" {
varValue := fmt.Sprintf("%v", getRefValue(c, removeBrackets(found)))
return strings.ReplaceAll(v, found, varValue)
}
return v
}
func populate(c *Context, tags []*Tag) []*Tag {
for _, t := range tags {
populateTag(c, t)
}
return tags
}
func populateTag(c *Context, tag *Tag) {
if tag.Name == "" {
if tag.Text.Str == nil && tag.Text.Ref != nil {
value := getRefValue(c, *tag.Text.Ref)
children, ok := value.([]*Tag)
if ok {
tag.Name = "fragment"
tag.Children = children
} else {
sValue := fmt.Sprintf("%+v", value)
tag.Text.Str = &sValue
}
} else if loop := tag.Text.For; loop != nil {
tag.Name = "fragment"
data := getRefValue(c, loop.Reference)
statement := loop.Statements[0].ReturnStatement
switch reflect.TypeOf(data).Kind() {
case reflect.Slice:
v := reflect.ValueOf(data)
for i := 0; i < v.Len(); i++ {
compContext := c.Clone(tag.Name)
compContext.data[loop.Index] = i
compContext.data[loop.Key] = v.Index(i).Interface()
newTags := populate(compContext, cloneTags(statement.Tags))
tag.Children = append(tag.Children, newTags...)
}
}
}
} else {
if comp, ok := compMap[tag.Name]; ok {
tag.Name = "fragment"
if tag.SelfClosing {
tag.SelfClosing = false
}
compContext := c.Clone(comp.Name)
nodes := comp.Render(compContext, tag)
populate(compContext, tag.Children)
compContext.Set("children", tag.Children)
populate(compContext, nodes)
tag.Children = nodes
} else {
for _, a := range tag.Attributes {
if a.Value.Str != nil {
if strings.Contains(*a.Value.Str, "{") {
subs := substituteString(c, removeQuotes(*a.Value.Str))
a.Value = &Literal{Str: &subs}
} else {
*a.Value.Str = removeQuotes(*a.Value.Str)
}
} else if a.Value.Ref != nil {
value := getRefValue(c, *a.Value.Ref)
subs := fmt.Sprintf("%v", value)
a.Value = &Literal{Str: &subs}
} else if a.Key == "class" && a.Value.KV != nil {
classes := []string{}
for _, a := range a.Value.KV {
varValue := getRefValue(c, a.Value)
if varValue.(bool) {
classes = append(classes, removeQuotes(a.Key))
}
}
result := strings.Join(classes, " ")
a.Value.Str = &result
}
}
populate(c, tag.Children)
}
}
}