SEANK.H.LIAO

go safer html

html generation from code

safer html generation

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.

gomponents

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}

htmlgo

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}