diff --git a/README.md b/README.md index 8b57007..7414eeb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ Composable HTML in pure Go. +The name is Danish for "hops", and also sounds a lot like a drunken person saying "html". I wasn't drunk when I made this, but I might as well have been. + +## Warning + +This is very much experimental and there's not much in form of escaping behavior yet. So use at your own risk. + ### But, why? I've been experimenting with [D\*](https://data-star.dev) recently and I was missing some of the ergonomics available to me when writing React/TypeScript. diff --git a/attribute.go b/attribute.go index 51d78dc..713a58b 100644 --- a/attribute.go +++ b/attribute.go @@ -32,7 +32,7 @@ func (a *Attribute) String() string { if a.noEscape { return fmt.Sprintf(`%s="%s"`, a.name, a.value) } - // TODO: Enable and test this + // TODO: Add escaping for HTML entities if needed return fmt.Sprintf(`%s="%s"`, a.name, a.value) //return fmt.Sprintf(`%s="%s"`, a.name, template.JSEscapeString(a.value)) } @@ -49,9 +49,9 @@ func WithMergeStrategy(a *Attribute, mfn func(v1, v2 string) string) *Attribute return a } -func (a *Attribute) Merge(v string) string { +func (a *Attribute) Merge(v1, v2 string) string { if a.mergeFn == nil { - return v + return v2 } - return a.mergeFn(a.value, v) + return a.mergeFn(v1, v2) } diff --git a/attributes.go b/attributes.go index 9194b30..7372aa2 100644 --- a/attributes.go +++ b/attributes.go @@ -5,24 +5,27 @@ import ( "strings" ) -type Attributes map[string]Attribute +type Attributes map[string]*Attribute // Sets or merges (according to merge strategy) an attribute -func (a *Attributes) Set(k string, v Attribute) error { - - if val, ok := (*a)[k]; ok { - val.value = val.Merge(v.value) - return nil +func (a Attributes) Set(k string, newAttribute *Attribute) { + + // If attribute already exists, we need to merge it + if existing, ok := (a)[k]; ok { + // Using the overriding attribute's merge function we merge with the existing value + // - This has the advantage of merging with classes that don't use merge + newValue := newAttribute.Merge(existing.value, newAttribute.value) + existing.value = newValue + (a)[k] = existing + return } - (*a)[k] = v - - return nil + (a)[k] = newAttribute } -func (a *Attributes) String() string { - var attributes = make([]Attribute, len(*a)) - for _, attr := range *a { - attributes = append(attributes, attr) +func (a Attributes) String() string { + var attributes = make([]Attribute, len(a)) + for _, attr := range a { + attributes = append(attributes, *attr) } slices.SortFunc(attributes, func(a, b Attribute) int { if a.name == "id" { diff --git a/go.mod b/go.mod index 00501ed..20ad5b8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/andrioid/humle go 1.22.3 + +require github.com/Oudwins/tailwind-merge-go v0.2.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c9354a7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/Oudwins/tailwind-merge-go v0.2.1 h1:jxRaEqGtwwwF48UuFIQ8g8XT7YSualNuGzCvQ89nPFE= +github.com/Oudwins/tailwind-merge-go v0.2.1/go.mod h1:kkZodgOPvZQ8f7SIrlWkG/w1g9JTbtnptnePIh3V72U= diff --git a/group.go b/group.go index 8436fd6..5a5611c 100644 --- a/group.go +++ b/group.go @@ -1,7 +1,41 @@ package humle +import ( + "io" + "strings" +) + type Group []Node -func (g *Group) Type() NodeType { +func (g Group) Type() NodeType { return NodeGroup } + +func (g Group) WriteTo(w io.Writer) (int64, error) { + var total int64 + for _, node := range g { + if group, ok := node.(Group); ok { + wb, err := group.WriteTo(w) + total += wb + if err != nil { + return total, err + } + } + if writable, ok := node.(NodeWriter); ok { + wb, err := writable.WriteTo(w) + total += wb + if err != nil { + return total, err + } + } + } + + return total, nil + +} + +func (g Group) String() string { + var b strings.Builder + g.WriteTo(&b) + return b.String() +} diff --git a/html.go b/html.go index f433a4a..d661c4f 100644 --- a/html.go +++ b/html.go @@ -1,5 +1,53 @@ package humle +import ( + "fmt" + "strings" + + "github.com/Oudwins/tailwind-merge-go/pkg/twmerge" +) + func Data(k, v string) *Attribute { return NewAttribute("data-"+k, v) } + +// Use this if you don't like default Class behavior of merging classes +func AttrClass(v ...string) *Attribute { + values := strings.Join(v, " ") + return WithMergeStrategy( + NewAttribute("class", values), + func(v1, v2 string) string { + if v1 == "" { + return v2 + } + if v2 == "" { + return v1 + } + return fmt.Sprintf("%s %s", v1, v2) + }, + ) + +} + +// Class attribute with tailwind-merge functionality +func Class(v ...string) *Attribute { + values := strings.Join(v, " ") + a := NewAttribute("class", twmerge.Merge(values)) + WithMergeStrategy(a, func(s1, s2 string) string { + return twmerge.Merge(s1, s2) + }) + return a +} + +func DocType() Node { + return RawHTML("") +} + +// Prints DocType and treats any nodes as siblings +func Document(nodes ...Node) Group { + g := []Node{ + DocType(), + Group(nodes), + } + return g +} diff --git a/html/element.go.tmpl b/html/element.go.tmpl deleted file mode 100644 index d560851..0000000 --- a/html/element.go.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -func {{ . | CasingFunc }}(nodes ...Node) *Element { - return NewElement("{{. | CasingElement}}", nodes...) -} diff --git a/html/element_void.go.tmpl b/html/element_void.go.tmpl deleted file mode 100644 index 8568692..0000000 --- a/html/element_void.go.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -func {{ . | CasingFunc }}(nodes ...Node) *Element { - el := NewElement("{{. | CasingElement}}", nodes...) - el = WithVoidElement(el) - return el -} diff --git a/html/generator.go b/html/generator.go index 979d0ac..96994b8 100644 --- a/html/generator.go +++ b/html/generator.go @@ -13,17 +13,26 @@ import ( // - Use `NewElement()`or `NewAttribute()“ if you're missing something or send a PR // Reference: https://developer.mozilla.org/en-US/docs/Glossary/Empty_element var input = TemplateInput{ - elements: []string{ + tags: []string{ + "html", "head", "title", "meta", "link", "style", + "script", "noscript", "template", + "article", "aside", "footer", "header", "nav", + "figure", "figcaption", "address", + "dl", "dt", "dd", "ol", "ul", "li", + "table", "caption", "thead", "tbody", "tfoot", + "tr", "th", "td", "col", "colgroup", + "form", "fieldset", "legend", "label", + "input", "select", "option", "optgroup", "div", "section", "main", "button", "a", "h1", "h2", "h3", "p", "span", - "input", "label", "field", - "pre", "script", "link", "head", "body", "meta", "svg", + "field", + "pre", "body", "svg", }, - voidElements: []string{"br", "hr"}, + voidTags: []string{"br", "hr"}, attributes: []string{ "id", "href", "alt", "placeholder", "src", "rel", "name", - "content", "charset", "lang", "type", "value", "class", + "content", "charset", "lang", "type", "value", }, } @@ -34,15 +43,18 @@ var funcs = template.FuncMap{ } type TemplateInput struct { - elements []string - voidElements []string - attributes []string + tags []string + voidTags []string + attributes []string } func FuncName(str string) string { str = strings.ToLower(str) switch str { + case "html": + return "HTML" case "id": + return "ID" default: letters := strings.Split(str, "") @@ -56,7 +68,7 @@ func main() { template.Must(templates.ParseGlob("html/*.go.tmpl")) // Create output file - elF, err := os.Create("html_elements_gen.go") + elF, err := os.Create("html_tags_gen.go") if err != nil { fmt.Fprintf(os.Stderr, "Error creating file: %v\n", err) os.Exit(1) @@ -80,15 +92,15 @@ func main() { // Process Elements fmt.Fprintf(elF, "\n// Elements\n") - for _, elem := range input.elements { - if err := templates.ExecuteTemplate(elF, "element.go.tmpl", elem); err != nil { + for _, elem := range input.tags { + if err := templates.ExecuteTemplate(elF, "tag.go.tmpl", elem); err != nil { log.Fatal(err) } } fmt.Fprintf(elF, "\n// Void Elements\n") - for _, elem := range input.voidElements { - if err := templates.ExecuteTemplate(elF, "element_void.go.tmpl", elem); err != nil { + for _, elem := range input.voidTags { + if err := templates.ExecuteTemplate(elF, "tag_void.go.tmpl", elem); err != nil { log.Fatal(err) } } diff --git a/html/tag.go.tmpl b/html/tag.go.tmpl new file mode 100644 index 0000000..dec1049 --- /dev/null +++ b/html/tag.go.tmpl @@ -0,0 +1,3 @@ +func {{ . | CasingFunc }}(nodes ...Node) *Tag { + return NewTag("{{. | CasingElement}}", nodes...) +} diff --git a/html/tag_void.go.tmpl b/html/tag_void.go.tmpl new file mode 100644 index 0000000..2ae37c9 --- /dev/null +++ b/html/tag_void.go.tmpl @@ -0,0 +1,5 @@ +func {{ . | CasingFunc }}(nodes ...Node) *Tag { + el := NewTag("{{. | CasingElement}}", nodes...) + el = WithVoidElement(el) + return el +} diff --git a/html_attributes_gen.go b/html_attributes_gen.go index 60dbfbe..9e85e42 100644 --- a/html_attributes_gen.go +++ b/html_attributes_gen.go @@ -39,6 +39,3 @@ func Type(v string) *Attribute { func Value(v string) *Attribute { return NewAttribute("value", v) } -func Class(v string) *Attribute { - return NewAttribute("class", v) -} diff --git a/html_elements_gen.go b/html_elements_gen.go index 15c268f..2d2fa84 100644 --- a/html_elements_gen.go +++ b/html_elements_gen.go @@ -3,75 +3,3 @@ package humle // Elements -func Div(nodes ...Node) *Element { - return NewElement("div", nodes...) -} -func Section(nodes ...Node) *Element { - return NewElement("section", nodes...) -} -func Main(nodes ...Node) *Element { - return NewElement("main", nodes...) -} -func Button(nodes ...Node) *Element { - return NewElement("button", nodes...) -} -func A(nodes ...Node) *Element { - return NewElement("a", nodes...) -} -func H1(nodes ...Node) *Element { - return NewElement("h1", nodes...) -} -func H2(nodes ...Node) *Element { - return NewElement("h2", nodes...) -} -func H3(nodes ...Node) *Element { - return NewElement("h3", nodes...) -} -func P(nodes ...Node) *Element { - return NewElement("p", nodes...) -} -func Span(nodes ...Node) *Element { - return NewElement("span", nodes...) -} -func Input(nodes ...Node) *Element { - return NewElement("input", nodes...) -} -func Label(nodes ...Node) *Element { - return NewElement("label", nodes...) -} -func Field(nodes ...Node) *Element { - return NewElement("field", nodes...) -} -func Pre(nodes ...Node) *Element { - return NewElement("pre", nodes...) -} -func Script(nodes ...Node) *Element { - return NewElement("script", nodes...) -} -func Link(nodes ...Node) *Element { - return NewElement("link", nodes...) -} -func Head(nodes ...Node) *Element { - return NewElement("head", nodes...) -} -func Body(nodes ...Node) *Element { - return NewElement("body", nodes...) -} -func Meta(nodes ...Node) *Element { - return NewElement("meta", nodes...) -} -func Svg(nodes ...Node) *Element { - return NewElement("svg", nodes...) -} - -// Void Elements -func Br(nodes ...Node) *Element { - el := NewElement("br", nodes...) - el = WithVoidElement(el) - return el -} -func Hr(nodes ...Node) *Element { - el := NewElement("hr", nodes...) - el = WithVoidElement(el) - return el -} diff --git a/html_tags_gen.go b/html_tags_gen.go new file mode 100644 index 0000000..62ba95f --- /dev/null +++ b/html_tags_gen.go @@ -0,0 +1,182 @@ +// Generated from humle/html package +// **DO NOT EDIT** +package humle + +// Elements +func HTML(nodes ...Node) *Tag { + return NewTag("html", nodes...) +} +func Head(nodes ...Node) *Tag { + return NewTag("head", nodes...) +} +func Title(nodes ...Node) *Tag { + return NewTag("title", nodes...) +} +func Meta(nodes ...Node) *Tag { + return NewTag("meta", nodes...) +} +func Link(nodes ...Node) *Tag { + return NewTag("link", nodes...) +} +func Style(nodes ...Node) *Tag { + return NewTag("style", nodes...) +} +func Script(nodes ...Node) *Tag { + return NewTag("script", nodes...) +} +func Noscript(nodes ...Node) *Tag { + return NewTag("noscript", nodes...) +} +func Template(nodes ...Node) *Tag { + return NewTag("template", nodes...) +} +func Article(nodes ...Node) *Tag { + return NewTag("article", nodes...) +} +func Aside(nodes ...Node) *Tag { + return NewTag("aside", nodes...) +} +func Footer(nodes ...Node) *Tag { + return NewTag("footer", nodes...) +} +func Header(nodes ...Node) *Tag { + return NewTag("header", nodes...) +} +func Nav(nodes ...Node) *Tag { + return NewTag("nav", nodes...) +} +func Figure(nodes ...Node) *Tag { + return NewTag("figure", nodes...) +} +func Figcaption(nodes ...Node) *Tag { + return NewTag("figcaption", nodes...) +} +func Address(nodes ...Node) *Tag { + return NewTag("address", nodes...) +} +func Dl(nodes ...Node) *Tag { + return NewTag("dl", nodes...) +} +func Dt(nodes ...Node) *Tag { + return NewTag("dt", nodes...) +} +func Dd(nodes ...Node) *Tag { + return NewTag("dd", nodes...) +} +func Ol(nodes ...Node) *Tag { + return NewTag("ol", nodes...) +} +func Ul(nodes ...Node) *Tag { + return NewTag("ul", nodes...) +} +func Li(nodes ...Node) *Tag { + return NewTag("li", nodes...) +} +func Table(nodes ...Node) *Tag { + return NewTag("table", nodes...) +} +func Caption(nodes ...Node) *Tag { + return NewTag("caption", nodes...) +} +func Thead(nodes ...Node) *Tag { + return NewTag("thead", nodes...) +} +func Tbody(nodes ...Node) *Tag { + return NewTag("tbody", nodes...) +} +func Tfoot(nodes ...Node) *Tag { + return NewTag("tfoot", nodes...) +} +func Tr(nodes ...Node) *Tag { + return NewTag("tr", nodes...) +} +func Th(nodes ...Node) *Tag { + return NewTag("th", nodes...) +} +func Td(nodes ...Node) *Tag { + return NewTag("td", nodes...) +} +func Col(nodes ...Node) *Tag { + return NewTag("col", nodes...) +} +func Colgroup(nodes ...Node) *Tag { + return NewTag("colgroup", nodes...) +} +func Form(nodes ...Node) *Tag { + return NewTag("form", nodes...) +} +func Fieldset(nodes ...Node) *Tag { + return NewTag("fieldset", nodes...) +} +func Legend(nodes ...Node) *Tag { + return NewTag("legend", nodes...) +} +func Label(nodes ...Node) *Tag { + return NewTag("label", nodes...) +} +func Input(nodes ...Node) *Tag { + return NewTag("input", nodes...) +} +func Select(nodes ...Node) *Tag { + return NewTag("select", nodes...) +} +func Option(nodes ...Node) *Tag { + return NewTag("option", nodes...) +} +func Optgroup(nodes ...Node) *Tag { + return NewTag("optgroup", nodes...) +} +func Div(nodes ...Node) *Tag { + return NewTag("div", nodes...) +} +func Section(nodes ...Node) *Tag { + return NewTag("section", nodes...) +} +func Main(nodes ...Node) *Tag { + return NewTag("main", nodes...) +} +func Button(nodes ...Node) *Tag { + return NewTag("button", nodes...) +} +func A(nodes ...Node) *Tag { + return NewTag("a", nodes...) +} +func H1(nodes ...Node) *Tag { + return NewTag("h1", nodes...) +} +func H2(nodes ...Node) *Tag { + return NewTag("h2", nodes...) +} +func H3(nodes ...Node) *Tag { + return NewTag("h3", nodes...) +} +func P(nodes ...Node) *Tag { + return NewTag("p", nodes...) +} +func Span(nodes ...Node) *Tag { + return NewTag("span", nodes...) +} +func Field(nodes ...Node) *Tag { + return NewTag("field", nodes...) +} +func Pre(nodes ...Node) *Tag { + return NewTag("pre", nodes...) +} +func Body(nodes ...Node) *Tag { + return NewTag("body", nodes...) +} +func Svg(nodes ...Node) *Tag { + return NewTag("svg", nodes...) +} + +// Void Elements +func Br(nodes ...Node) *Tag { + el := NewTag("br", nodes...) + el = WithVoidElement(el) + return el +} +func Hr(nodes ...Node) *Tag { + el := NewTag("hr", nodes...) + el = WithVoidElement(el) + return el +} diff --git a/humle.go b/humle.go index e60fa1a..2ef3b33 100644 --- a/humle.go +++ b/humle.go @@ -2,27 +2,6 @@ package humle import "io" -type NodeType int - -const ( - NodeElement = iota - NodeAttribute - NodeText - NodeGroup - NodeReader // TODO: Allow us to pass io.Reader like files to it -) - -type Node interface { - Type() NodeType -} - -// A node capable of writing HTML. Can be used as children -// E.g. Element, Text -type NodeWriter interface { - Node - io.WriterTo -} - -func WriteTo(n NodeWriter, w io.Writer) (int64, error) { +func WriteHTML(w io.Writer, n NodeWriter) (int64, error) { return n.WriteTo(w) } diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..c68c08b --- /dev/null +++ b/mise.toml @@ -0,0 +1,19 @@ +[tools] +go = "latest" +watchexec = "latest" + +[tasks.generate] +run = "go generate ./..." +sources = ["**/generator.go"] +outputs = ["**/*_gen.go"] + +[tasks.build] +depends = ["generate"] +run = "go build -race ." +sources = ["**/*.go"] + +# mise watch -r test +[tasks.test] +depends = ["build"] +sources = ["**/*.go"] +run = "go test ./..." \ No newline at end of file diff --git a/node_test.go b/node_test.go new file mode 100644 index 0000000..ba396c8 --- /dev/null +++ b/node_test.go @@ -0,0 +1,58 @@ +package humle_test + +import ( + "testing" + + . "github.com/andrioid/humle" +) + +var testMatrix2 = TestCases{ + "with value": TestCase{ + Input: Div(Text("test")), + Expected: `
test
`, + }, + "empty non-void": TestCase{ + Input: Div(), + Expected: `
`, + }, + "with class": TestCase{ + Input: Div(Class("bg-pink-500")), + Expected: `
`, + }, + "br": TestCase{ + Input: Br(), + Expected: `
`, + }, + "with text": TestCase{ + Input: Div(Text("hello")), + Expected: `
hello
`, + }, + "with id and data": TestCase{ + Input: Div(ID("divid"), Data("show", `$tab === 'logs'`)), + Expected: `
`, + }, + "raw": TestCase{ + Input: RawHTML("
"), + Expected: `
`, + }, + "raw inside div": TestCase{ + Input: Div(RawHTML("
")), + Expected: `
`, + }, + "document empty": TestCase{ + Input: Document(), + Expected: ``, + }, + "document with nodes": TestCase{ + Input: Document( + HTML( + Head(), + Body(), + )), + Expected: ``, + }, +} + +func TestRender(t *testing.T) { + testMatrix2.Test(t) +} diff --git a/nodes.go b/nodes.go new file mode 100644 index 0000000..92ded5f --- /dev/null +++ b/nodes.go @@ -0,0 +1,27 @@ +package humle + +import "io" + +type NodeType int + +const ( + NodeTag = iota + NodeAttribute + NodeTextValue + NodeGroup + NodeRawHTML + NodeSlot + NodeSignal + //NodeReader // TODO: Allow us to pass io.Reader like files to it +) + +type Node interface { + Type() NodeType +} + +// A node capable of writing HTML. Can be used as children +// E.g. Element, Text +type NodeWriter interface { + Node + io.WriterTo +} diff --git a/raw_tag.go b/raw_tag.go new file mode 100644 index 0000000..d876186 --- /dev/null +++ b/raw_tag.go @@ -0,0 +1,19 @@ +package humle + +import ( + "io" +) + +// Satisfies NodeWriter interface +// RawHTML is a node that represents raw HTML content. +// It does not escape the content, so it should be used with caution. +type RawHTML string + +func (n RawHTML) Type() NodeType { + return NodeRawHTML +} + +func (n RawHTML) WriteTo(w io.Writer) (int64, error) { + len, err := w.Write([]byte(n)) + return int64(len), err +} diff --git a/render_test.go b/render_test.go index 4590055..5752a0e 100644 --- a/render_test.go +++ b/render_test.go @@ -1,25 +1,10 @@ package humle_test -import ( - "fmt" - "testing" +import "testing" - . "github.com/andrioid/humle" -) - -var testMatrix = map[Node]string{ - Div(Text("test")): `
test
`, - Div(): `
`, - Div(Class("bg-pink-500")): `
`, - Br(): `
`, - Div(Text("hello")): `
hello
`, - Div(ID("divid"), Data("show", `$tab === 'logs'`)): `
`, -} - -func TestRender(t *testing.T) { - for el, expected := range testMatrix { - if fmt.Sprintf("%s", el) != expected { - t.Errorf("DOM mismatch\n\033[32m%s\033[0m\n\033[31m%s\033[0m", expected, el) - } - } +func TestRendering(t *testing.T) { + // TODO: Use string builder to emulate a HTTP response writer. + // This is a placeholder for the rendering test. + // The actual rendering tests should be implemented in the respective test files. + t.Skip("Rendering tests are not implemented yet.") } diff --git a/slots_signals.go b/slots_signals.go new file mode 100644 index 0000000..256b4da --- /dev/null +++ b/slots_signals.go @@ -0,0 +1,38 @@ +package humle + +import "io" + +// Allows delayed rendering at a named slot +type SignalNode struct { + name string + node NodeWriter +} + +func Signal(name string, node NodeWriter) *SignalNode { + return &SignalNode{ + name: name, + node: node, + } +} + +func (s *SignalNode) Type() NodeType { + return NodeSignal +} + +type SlotNode struct { + name string +} + +func Slot(name string) *SlotNode { + return &SlotNode{ + name: name, + } +} + +func (s *SlotNode) Type() NodeType { + return NodeSlot +} + +func (s *SlotNode) WriteTo(w io.Writer) (int64, error) { + return 0, nil +} diff --git a/slots_signals_test.go b/slots_signals_test.go new file mode 100644 index 0000000..161ee87 --- /dev/null +++ b/slots_signals_test.go @@ -0,0 +1,30 @@ +package humle_test + +import ( + "testing" + + . "github.com/andrioid/humle" +) + +func DocExample(nodes ...Node) Group { + return Document( + HTML( + Head( + //Slot("head"), + ), + ), + ) +} + +var testNameSignals = TestCases{ + "head": TestCase{ + Input: DocExample( + Signal("head", Title(Text("signal"))), + ), + Expected: ``, + }, +} + +func TestNamedSignals(t *testing.T) { + testNameSignals.Test(t) +} diff --git a/element.go b/tag.go similarity index 57% rename from element.go rename to tag.go index e82ec98..45dca5d 100644 --- a/element.go +++ b/tag.go @@ -2,58 +2,66 @@ package humle import ( "io" + "log" "strings" ) -type Element struct { +type Tag struct { name string children []NodeWriter - attributes *Attributes + attributes Attributes isVoidElement bool + // Experimental + namedSignals map[string][]Node } -func (e *Element) Type() NodeType { - return NodeElement +func (e *Tag) Type() NodeType { + return NodeTag } // Identifies nodes and assigns them properly for later rendering -func NewElement(name string, nodes ...Node) *Element { +func NewTag(name string, nodes ...Node) *Tag { el := parseNodes(name, nodes) - return &el + return el } -func parseNodes(name string, nodes []Node) Element { - el := Element{ +func parseNodes(name string, nodes []Node) *Tag { + el := Tag{ name: name, children: []NodeWriter{}, - attributes: &Attributes{}, + attributes: Attributes{}, } for _, node := range nodes { switch n := node.(type) { case *Attribute: // Store attributes for later rendering - el.attributes.Set(n.name, *n) - case *Element: + el.attributes.Set(n.name, n) + case *Tag: // Children for recursive rendering el.children = append(el.children, n) - case *Group: + case Group: // Flattened into attributes or children - g := parseNodes(name, *n) - for _, gattr := range *g.attributes { + g := parseNodes(name, n) + for _, gattr := range g.attributes { el.attributes.Set(gattr.name, gattr) } - for _, gel := range g.children { - el.children = append(el.children, gel) - } + el.children = append(el.children, g.children...) + case RawHTML: + el.children = append(el.children, n) case Text: // Simple child node el.children = append(el.children, n) + case *SlotNode: + el.children = append(el.children, n) + + default: + log.Fatalf("Unknown node type in tag parsing: %T\n", node) } } - return el + return &el // We want this to be a fresh allocation } -func (el *Element) WriteTo(w io.Writer) (int64, error) { +func (el *Tag) WriteTo(w io.Writer) (int64, error) { attrs := el.attributes.String() w.Write([]byte("<" + el.name)) if attrs != "" { @@ -77,14 +85,14 @@ func (el *Element) WriteTo(w io.Writer) (int64, error) { return 0, nil } -func (el *Element) String() string { +func (el *Tag) String() string { var b strings.Builder el.WriteTo(&b) return b.String() } -func WithVoidElement(el *Element) *Element { +func WithVoidElement(el *Tag) *Tag { el.isVoidElement = true return el } diff --git a/tailwind_test.go b/tailwind_test.go new file mode 100644 index 0000000..4eb2d6d --- /dev/null +++ b/tailwind_test.go @@ -0,0 +1,46 @@ +package humle_test + +import ( + "testing" + + . "github.com/andrioid/humle" +) + +func BaseButton(nodes ...Node) *Tag { + return Button( + Class("bg-blue-500"), + Group(nodes), + ) +} + +func PinkButton(nodes ...Node) *Tag { + return BaseButton( + Class("bg-pink-800"), + Group(nodes), + ) +} + +var testMatrixTailwind = TestCases{ + "base button": TestCase{ + Input: BaseButton(), + Expected: ``, + }, + "base button with tailwind merge": TestCase{ + Input: BaseButton(Class("bg-purple-500")), + Expected: ``, + }, + "pink button with tailwind merge": TestCase{ + Input: PinkButton( + Class("text-pink-100"), + ), + Expected: ``, + }, + "basic template merge test": TestCase{ + Input: Div(Class("bg-blue-500 bg-pink-500 bg-purple-500", "bg-orange-500")), + Expected: `
`, + }, +} + +func TestTailwind(t *testing.T) { + testMatrixTailwind.Test(t) +} diff --git a/test_utils.go b/test_utils.go new file mode 100644 index 0000000..fa3c0ea --- /dev/null +++ b/test_utils.go @@ -0,0 +1,26 @@ +package humle + +import ( + "strings" + "testing" +) + +type TestCase struct { + Input NodeWriter + Expected string +} + +type TestCases map[string]TestCase + +func (testcases TestCases) Test(t *testing.T) { + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + b := strings.Builder{} + WriteHTML(&b, tc.Input) + if b.String() != tc.Expected { + t.Errorf("💩 %v\n\033[32m%#v\033[0m\n\033[31m%#v\033[0m", name, tc.Expected, b.String()) + } + }) + } +} diff --git a/text.go b/text_value.go similarity index 53% rename from text.go rename to text_value.go index 67c83dd..b931080 100644 --- a/text.go +++ b/text_value.go @@ -1,14 +1,17 @@ package humle -import "io" +import ( + "html/template" + "io" +) type Text string func (n Text) Type() NodeType { - return NodeText + return NodeTextValue } func (n Text) WriteTo(w io.Writer) (int64, error) { - len, err := w.Write([]byte(n)) - return int64(len), err + template.HTMLEscape(w, []byte(n)) + return 0, nil }