Recently, I was working on one of my internal sites,
and encountered a broken link in a table.
I scratched my head very hard until I realized it was because of the way I generated the table:
The link had a text like foo | bar
,
which i put into a markdown link (I was reusing my blog's markdown renderer): [foo | bar](https://example.com)
,
which is then put into a table and it breaks, because |
is read as the end of the table column.
:facepalm:
Fo the moment,
I bodged it with strings.NewReplacer("|", "¦").Replace(s)
,
but that left me thinking of what better ways I have to safely generate html.
Also, after having written a lot of helm templates for my day job,
I felt like I really wanted something that could leverage existing language tooling support,
so no DSLs.
That meant things like html/template,
github.com/google/safehtml/template,
and github.com/a-h/templ
weren't really up for consideration.
Looking around I saw 2 projects with similar goals: github.com/maragudk/gomponents and github.com/theplant/htmlgo.
Both look very similar in both how their api other than args vs fluid chaining for attrs.
Good: compact html output, more go style (args), project more active/responsive?
Bad: mixing of attributes and elements.
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "log"
7
8 "github.com/maragudk/gomponents"
9 "github.com/maragudk/gomponents/html"
10)
11
12func main() {
13 page := html.HTML(
14 html.Lang("en"),
15 html.Head(
16 html.TitleEl(gomponents.Text("Hello World")),
17 html.Meta(html.Name("description"), html.Content("This is a page")),
18 html.Meta(html.Charset("utf8")),
19 html.Meta(html.Name("viewport"), html.Content("width=device-width, initial-scale=1")),
20 html.Link(html.Rel("me"), html.Href("http://example.com")),
21 ),
22 html.Body(
23 html.Table(
24 html.THead(
25 html.Tr(
26 html.Td(gomponents.Text("head0")),
27 html.Td(gomponents.Text("head1")),
28 html.Td(gomponents.Text("head2")),
29 ),
30 ),
31 html.TBody(
32 html.H1(gomponents.Text("hello world")),
33 html.P(gomponents.Text("this is some text")),
34 html.P(gomponents.Text("<script>alert(‘XSS’)</script>")),
35 html.Tr(
36 html.Td(gomponents.Text("row1")),
37 html.Td(gomponents.Text("row1")),
38 html.Td(gomponents.Text("row1")),
39 ),
40 html.Tr(
41 html.Td(gomponents.Text("row2")),
42 html.Td(gomponents.Text("row2")),
43 html.Td(gomponents.Text("row2")),
44 ),
45 html.Script(gomponents.Raw("alert(‘script’)")),
46 ),
47 ),
48 ),
49 )
50 var buf bytes.Buffer
51 err := page.Render(&buf)
52 if err != nil {
53 log.Fatalln(err)
54 }
55 fmt.Println(buf.String())
56}
Good: separation of elements and attributes, better completion?
Bad: messy (lots of newlines) output, html element is obscured, not very active project.
1package main
2
3import (
4 "context"
5 "fmt"
6 "log"
7
8 "github.com/theplant/htmlgo"
9)
10
11func main() {
12 page := htmlgo.HTML(
13 htmlgo.Head(
14 htmlgo.Meta().Charset("utf8"),
15 htmlgo.Meta().Name("viewport").Content("width=device-width, initial-scale=1"),
16 htmlgo.Meta().Name("description").Content("This is a page"),
17 htmlgo.Title("Hello World"),
18 htmlgo.Link("http://example.com").Rel("me"),
19 ),
20 htmlgo.Body(
21 htmlgo.H1("hello world"),
22 htmlgo.P(htmlgo.Text("this is some text")),
23 htmlgo.P(htmlgo.Text("<script>alert(‘XSS’)</script>")),
24 htmlgo.Table(
25 htmlgo.Thead(
26 htmlgo.Tr(
27 htmlgo.Td(htmlgo.Text("head0")),
28 htmlgo.Td(htmlgo.Text("head1")),
29 htmlgo.Td(htmlgo.Text("head2")),
30 ),
31 ),
32 htmlgo.Tbody(
33 htmlgo.Tr(
34 htmlgo.Td(htmlgo.Text("row1")),
35 htmlgo.Td(htmlgo.Text("row1")),
36 htmlgo.Td(htmlgo.Text("row1")),
37 ),
38 htmlgo.Tr().AppendChildren(
39 htmlgo.Td(htmlgo.Text("row2")),
40 htmlgo.Td(htmlgo.Text("row2")),
41 htmlgo.Td(htmlgo.Text("row2")),
42 ),
43 htmlgo.Script("alert(‘script’)"),
44 ),
45 ),
46 ),
47 ).(htmlgo.HTMLComponents)[1].(*htmlgo.HTMLTagBuilder).Attr("lang", "en")
48
49 b, err := page.MarshalHTML(context.Background())
50 if err != nil {
51 log.Fatalln(err)
52 }
53 fmt.Println(string(b))
54}