diff --git a/html/element.go b/html/element.go new file mode 100644 index 0000000..c4037b1 --- /dev/null +++ b/html/element.go @@ -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, "", e.tag) + } + return HTML(b.String()) +} + +func NewElement(tag string) *Element { + return &Element{tag, make(map[string]string), ""} +} diff --git a/html/element_test.go b/html/element_test.go new file mode 100644 index 0000000..459800a --- /dev/null +++ b/html/element_test.go @@ -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", `test`}, + {"p", nil, "test", "

test

"}, + {"p", [][2]string{{"hidden", "true"}}, "test", ""}, + {"p", [][2]string{{"hidden", "false"}}, "test", "

test

"}, + {"div", nil, "", "
<test>
"}, + } { + 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(), "

"; expect != string(div) { + t.Errorf("expected %q; got %q", expect, div) + } +} + +func TestAppend(t *testing.T) { + e := Div() + if expect := "
"; expect != string(e.HTML()) { + t.Errorf("expected %q; got %q", expect, e.HTML()) + } + e.AppendContent("test") + if expect := "
test
"; expect != string(e.HTML()) { + t.Errorf("expected %q; got %q", expect, e.HTML()) + } + e.AppendChild(Img().Src("test")) + if expect := `
test
`; expect != string(e.HTML()) { + t.Errorf("expected %q; got %q", expect, e.HTML()) + } +} diff --git a/html/html.go b/html/html.go index 98f33cd..b00869a 100644 --- a/html/html.go +++ b/html/html.go @@ -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, "", 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") } diff --git a/html/html_test.go b/html/html_test.go deleted file mode 100644 index 837b01a..0000000 --- a/html/html_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package html - -import "testing" - -func TestAttributes(t *testing.T) { - defer func() { - if e := recover(); e == nil { - t.Error("expected panic") - } - }() - Attributes("test") -} - -func TestElement(t *testing.T) { - for _, tc := range []struct { - tag string - attrs []string - content string - html HTML - }{ - {"a", []string{"href", "/", "style", "display:none"}, "test", `test`}, - {"p", nil, "test", "

test

"}, - {"p", []string{"hidden", ""}, "test", ""}, - {"div", nil, "", "
<test>
"}, - } { - if res := Element(tc.tag, Attributes(tc.attrs...), tc.content); tc.html != res { - t.Errorf("expected %q; got %q", tc.html, res) - } - } - if div, expect := Element("div", nil, HTML("
")), "

"; expect != string(div) { - t.Errorf("expected %q; got %q", expect, div) - } -} diff --git a/html/table.go b/html/table.go new file mode 100644 index 0000000..4e6c1af --- /dev/null +++ b/html/table.go @@ -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 +} diff --git a/html/table_test.go b/html/table_test.go new file mode 100644 index 0000000..ccb1f2a --- /dev/null +++ b/html/table_test.go @@ -0,0 +1,12 @@ +package html + +import "testing" + +func TestTable(t *testing.T) { + table := `
H1H2
B1B2
` + 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()) + } +}