Skip to content

Commit

Permalink
html: Rewrite (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
sunshineplan committed Dec 13, 2023
1 parent 30e9d66 commit 89d9c8f
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 75 deletions.
120 changes: 120 additions & 0 deletions html/element.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package html

import (
"fmt"
"slices"
"strings"
)

var _ HTMLer = new(Element)

type Element struct {
tag string
attrs map[string]string
content HTML
}

func (e *Element) Attribute(name, value string) *Element {
e.attrs[name] = value
return e
}

func (e *Element) Class(class ...string) *Element {
return e.Attribute("class", strings.Join(class, " "))
}

func (e *Element) Href(href string) *Element {
return e.Attribute("href", href)
}

func (e *Element) Src(src string) *Element {
return e.Attribute("src", src)
}

func (e *Element) Style(style string) *Element {
return e.Attribute("style", style)
}

func content(v any) HTML {
switch v := v.(type) {
case nil:
return ""
case HTML:
return v
case HTMLer:
return v.HTML()
case string:
return HTML(EscapeString(v))
default:
return HTML(EscapeString(fmt.Sprint(v)))
}
}

func (e *Element) Content(v any) *Element {
e.content = content(v)
return e
}

func (e *Element) AppendContent(v any) *Element {
e.content += content(v)
return e
}

func (e *Element) AppendChild(child *Element) *Element {
return e.AppendContent(child)
}

// https://developer.mozilla.org/en-US/docs/Glossary/Void_element
func (e Element) isVoidElement() bool {
return slices.Contains([]string{
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
}, strings.ToLower(e.tag))
}

// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
func (e Element) printAttrs() string {
var s []string
for k, v := range e.attrs {
if v == "" || v == "true" {
s = append(s, k)
} else if v == "false" {
continue
} else {
s = append(s, fmt.Sprintf("%s=%q", k, v))
}
}
slices.Sort(s)
return strings.Join(s, " ")
}

func (e *Element) HTML() HTML {
var b strings.Builder
fmt.Fprint(&b, "<", e.tag)
if attrs := e.printAttrs(); attrs != "" {
fmt.Fprint(&b, " ", attrs)
}
if e.isVoidElement() {
fmt.Fprint(&b, ">")
} else {
fmt.Fprint(&b, ">", e.content)
fmt.Fprintf(&b, "</%s>", e.tag)
}
return HTML(b.String())
}

func NewElement(tag string) *Element {
return &Element{tag, make(map[string]string), ""}
}
44 changes: 44 additions & 0 deletions html/element_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package html

import "testing"

func TestElement(t *testing.T) {
for _, tc := range []struct {
tag string
attrs [][2]string
content string
html HTML
}{
{"a", [][2]string{{"href", "/"}, {"style", "display:none"}}, "test", `<a href="/" style="display:none">test</a>`},
{"p", nil, "test", "<p>test</p>"},
{"p", [][2]string{{"hidden", "true"}}, "test", "<p hidden>test</p>"},
{"p", [][2]string{{"hidden", "false"}}, "test", "<p>test</p>"},
{"div", nil, "<test>", "<div>&lt;test&gt;</div>"},
} {
e := NewElement(tc.tag).Content(tc.content)
for _, i := range tc.attrs {
e.Attribute(i[0], i[1])
}
if res := e.HTML(); tc.html != res {
t.Errorf("expected %q; got %q", tc.html, res)
}
}
if div, expect := Div().Content(Br()).HTML(), "<div><br></div>"; expect != string(div) {
t.Errorf("expected %q; got %q", expect, div)
}
}

func TestAppend(t *testing.T) {
e := Div()
if expect := "<div></div>"; expect != string(e.HTML()) {
t.Errorf("expected %q; got %q", expect, e.HTML())
}
e.AppendContent("test")
if expect := "<div>test</div>"; expect != string(e.HTML()) {
t.Errorf("expected %q; got %q", expect, e.HTML())
}
e.AppendChild(Img().Src("test"))
if expect := `<div>test<img src="test"></div>`; expect != string(e.HTML()) {
t.Errorf("expected %q; got %q", expect, e.HTML())
}
}
67 changes: 25 additions & 42 deletions html/html.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,36 @@
package html

import (
"fmt"
"html"
"strings"
)
import "html"

var (
EscapeString = html.EscapeString
UnescapeString = html.UnescapeString
)

type Attribute struct {
Name string
Value string
}

func Attributes(pairs ...string) (attributes []Attribute) {
if len(pairs)%2 != 0 {
panic("pairs must have even number of elements")
}
for i := 0; i < len(pairs); i = i + 2 {
attributes = append(attributes, Attribute{pairs[i], pairs[i+1]})
}
return
}

func (attr Attribute) String() string {
if attr.Value == "" || attr.Value == "true" {
return attr.Name
}
return fmt.Sprintf("%s=%q", attr.Name, attr.Value)
}

type HTML string

func Element[T HTML | string](tag string, attributes []Attribute, content T) HTML {
var b strings.Builder
fmt.Fprint(&b, "<", tag)
for _, i := range attributes {
fmt.Fprint(&b, " ", i)
}
fmt.Fprint(&b, ">")
switch any(content).(type) {
case HTML:
fmt.Fprint(&b, content)
default:
fmt.Fprint(&b, EscapeString(string(content)))
}
fmt.Fprintf(&b, "</%s>", tag)
return HTML(b.String())
type HTMLer interface {
HTML() HTML
}

func A() *Element { return NewElement("a") }
func B() *Element { return NewElement("b") }
func Br() *Element { return NewElement("br") }
func Div() *Element { return NewElement("div") }
func Em() *Element { return NewElement("em") }
func Form() *Element { return NewElement("form") }
func H1() *Element { return NewElement("h1") }
func H2() *Element { return NewElement("h2") }
func I() *Element { return NewElement("i") }
func Img() *Element { return NewElement("img") }
func Input() *Element { return NewElement("input") }
func Label() *Element { return NewElement("label") }
func Li() *Element { return NewElement("li") }
func P() *Element { return NewElement("p") }
func Span() *Element { return NewElement("span") }
func Svg() *Element { return NewElement("svg") }
func Table() *Element { return NewElement("table") }
func Tbody() *Element { return NewElement("tbody") }
func Title() *Element { return NewElement("title") }
func Thead() *Element { return NewElement("thead") }
func Ul() *Element { return NewElement("ul") }
33 changes: 0 additions & 33 deletions html/html_test.go

This file was deleted.

69 changes: 69 additions & 0 deletions html/table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package html

import "strconv"

func Tr[T *TableHeader | *TableData](element ...T) *Element {
tr := NewElement("tr")
for _, i := range element {
tr.AppendContent(i)
}
return tr
}

var (
_ HTMLer = new(TableHeader)
_ HTMLer = new(TableData)
)

type (
TableHeader struct{ *Element }
TableData struct{ *Element }
)

func Th(content any) *TableHeader {
return &TableHeader{NewElement("th").Content(content)}
}

func (th *TableHeader) Abbr(abbr string) *TableHeader {
th.Element.Attribute("abbr", abbr)
return th
}

func (th *TableHeader) Colspan(n uint) *TableHeader {
th.Element.Attribute("colspan", strconv.FormatUint(uint64(n), 10))
return th
}

func (th *TableHeader) Headers(headers string) *TableHeader {
th.Element.Attribute("headers", headers)
return th
}

func (th *TableHeader) Rowspan(n uint) *TableHeader {
th.Element.Attribute("rowspan", strconv.FormatUint(uint64(n), 10))
return th
}

func (th *TableHeader) Scope(scope string) *TableHeader {
th.Element.Attribute("scope", scope)
return th
}

func Td(content any) *TableData {
return &TableData{NewElement("td").Content(content)}
}

func (td *TableData) Colspan(n uint) *TableData {
td.Element.Attribute("colspan", strconv.FormatUint(uint64(n), 10))
return td
}

func (td *TableData) Headers(headers string) *TableData {
td.Element.Attribute("headers", headers)
return td
}

func (td *TableData) Rowspan(n uint) *TableData {
td.Element.Attribute("rowspan", strconv.FormatUint(uint64(n), 10))
return td
}
12 changes: 12 additions & 0 deletions html/table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package html

import "testing"

func TestTable(t *testing.T) {
table := `<table><thead><tr><th colspan="2">H1</th><th>H2</th></tr></thead><tbody><tr><td>B1</td><td>B2</td></tr></tbody></table>`
if e := Table().
AppendChild(Thead().AppendChild(Tr(Th("H1").Colspan(2), Th("H2")))).
AppendChild(Tbody().AppendChild(Tr(Td("B1"), Td("B2")))); string(e.HTML()) != table {
t.Errorf("expected %q; got %q", table, e.HTML())
}
}

0 comments on commit 89d9c8f

Please sign in to comment.