├── .github
└── workflows
│ ├── go.yml
│ └── release.yaml
├── .gitignore
├── LICENSE
├── README.md
├── carbon.md
├── carbon.png
├── daz.go
├── daz.go.png
├── daz_test.go
├── doc.go
├── examples
├── README.md
├── server.go
└── server.go.png
└── go.mod
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build:
12 | name: Build
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Set up Go 1.x
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: ^1.15
20 |
21 | - name: Check out code into the Go module directory
22 | uses: actions/checkout@v2
23 |
24 | - name: Get dependencies
25 | run: |
26 | go get -v -t -d ./...
27 | if [ -f Gopkg.toml ]; then
28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
29 | dep ensure
30 | fi
31 |
32 | - name: Build
33 | run: go build -v ./...
34 |
35 | - name: Test
36 | run: go test -v ./...
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | # Sequence of patterns matched against refs/tags
4 | tags:
5 | - 'v*' # Push events matching v*
6 |
7 | name: Release
8 |
9 | jobs:
10 | build:
11 | name: Build and upload release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 | - name: Build project # This would actually build your project, using zip for an example artifact
17 | run: |
18 | GOOS=linux GOARCH=amd64 go build -o daz daz.go
19 | tar -czvf daz-linux-amd64.tar.gz daz
20 | - name: Create Release
21 | id: create_release
22 | uses: actions/create-release@v1
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | with:
26 | tag_name: ${{ github.ref }}
27 | release_name: Release ${{ github.ref }}
28 | draft: false
29 | prerelease: false
30 | - name: Upload Release Asset
31 | id: upload-release-asset
32 | uses: actions/upload-release-asset@v1
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 | with:
36 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
37 | asset_path: ./daz-linux-amd64.tar.gz
38 | asset_name: daz-linux-amd64.tar.gz
39 | asset_content_type: application/gzip
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | main
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Steve Lacy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # daz
2 | > Composable HTML components in Golang
3 |
4 |
5 |
6 |
7 |
8 | [](https://godoc.org/github.com/stevelacy/daz)
9 |
10 |
11 | 
12 |
13 | Daz is a "functional" alternative to using templates, and allows for nested components/lists
14 | Also enables template-free server-side rendered components with support for nested lists. It is inspired by [HyperScript](https://github.com/hyperhype/hyperscript).
15 |
16 |
17 | A component can be created and used with simple functions:
18 | ```golang
19 | // Example prop for a component
20 | type User struct {
21 | Name string
22 | // ...
23 | }
24 |
25 | func MyComponent(user User) HTML {
26 | return H(
27 | "div",
28 | Attr{"class": "bg-grey-50"},
29 | user.Name,
30 | )
31 | }
32 |
33 | func Root() HTML {
34 | user := User{Name: "Daz"}
35 | return H("html", MyComponent(user))
36 | }
37 |
38 | // And used in a handler:
39 |
40 | func Handler(w http.ResponseWriter, r *http.Request) {
41 | w.Write([]byte(Root()()))
42 | }
43 | ```
44 |
45 | Lists can be easily created without needing to embed a `range / end` in a template:
46 | ```golang
47 | items := []HTML{
48 | H("li", "item one"),
49 | H("li", "item two"),
50 | }
51 |
52 | element := H("ul", Attr{"class": "bg-grey-50"}, items)
53 |
54 | div := H("div", element)
55 | ```
56 |
57 |
58 | ### Install
59 |
60 | ```
61 | import (
62 | "github.com/stevelacy/daz"
63 | )
64 |
65 | ```
66 |
67 | ### Usage
68 |
69 | #### func `H`
70 |
71 | Create a HTML element:
72 | ```golang
73 | H("div", ...attrs)
74 |
75 | ```
76 |
77 | #### struct `Attr`
78 |
79 | HTML attributes:
80 | ```golang
81 | Attr{
82 | "class": "app",
83 | "onClick": "javascriptFunc()",
84 | }
85 | ```
86 |
87 | #### func `UnsafeContent`
88 |
89 | This will bypass HTML sanitization and allow for direct injecting
90 | ```golang
91 |
92 | injection := ""
93 | root := H("div", UnsafeContent(injection))
94 | //
95 | ```
96 |
--------------------------------------------------------------------------------
/carbon.md:
--------------------------------------------------------------------------------
1 | ### Carbon.now.sh
2 |
3 | https://carbon.now.sh/68eRaNf1Tvi1t0q8I0Dq
4 |
5 | ```go
6 |
7 | func Handler(w http.ResponseWriter, r *http.Request) {
8 | links := H("link", Attr{
9 | "href": "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",
10 | "rel": "stylesheet",
11 | })
12 | meta := []HTML{
13 | H("meta", Attr{"charset": "utf-8"}),
14 | H("meta", Attr{
15 | "name": "viewport",
16 | "content": "width=device-width, initial-scale=1.0",
17 | }),
18 | }
19 | head := H("head", H("title", "Example Server"), meta, links)
20 | style := Attr{"style": "background: #efefef;"}
21 | body := H("body", style)
22 | html := H("html", head, body)
23 | w.Write([]byte(html()))
24 | }
25 |
26 | ```
27 |
--------------------------------------------------------------------------------
/carbon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephenlacy/daz/d45417daa31463c284c3577fbc6d3fb77bd63e8d/carbon.png
--------------------------------------------------------------------------------
/daz.go:
--------------------------------------------------------------------------------
1 | package daz
2 |
3 | import (
4 | "fmt"
5 | "html"
6 | "strings"
7 | )
8 |
9 | var selfClosingTags = map[string]int{
10 | "area": 1,
11 | "br": 1,
12 | "hr": 1,
13 | "image": 1,
14 | "input": 1,
15 | "img": 1,
16 | "link": 1,
17 | "meta": 1,
18 | }
19 |
20 | // Attr is a HTML element attribute
21 | // => Attr{"href": "#"}
22 | type Attr map[string]string
23 |
24 | type HTML func() string
25 |
26 | // dangerous contents type
27 | type dangerousContents func() (string, bool)
28 |
29 | // UnsafeContent allows injection of JS or HTML from functions
30 | func UnsafeContent(str string) dangerousContents {
31 | return func() (string, bool) {
32 | return str, true
33 | }
34 | }
35 |
36 | // H is the base HTML func
37 | func H(el string, attrs ...interface{}) HTML {
38 | contents := []string{}
39 | attributes := ""
40 | for _, v := range attrs {
41 | switch v := v.(type) {
42 | case string:
43 | contents = append(contents, escape(v))
44 | case Attr:
45 | attributes = attributes + getAttributes(v)
46 | case []string:
47 | children := strings.Join(v, "")
48 | contents = append(contents, escape(children))
49 | case []HTML:
50 | children := subItems(v)
51 | contents = append(contents, children)
52 | case HTML:
53 | contents = append(contents, v())
54 | case dangerousContents:
55 | t, _ := v()
56 | contents = append(contents, t)
57 | case func() string:
58 | contents = append(contents, escape(v()))
59 | default:
60 | contents = append(contents, escape(fmt.Sprintf("%v", v)))
61 | }
62 | }
63 | return func() string {
64 | elc := escape(el)
65 | if _, ok := selfClosingTags[elc]; ok {
66 | return "<" + elc + attributes + " />"
67 | }
68 | return "<" + elc + attributes + ">" + strings.Join(contents, "") + "" + elc + ">"
69 | }
70 | }
71 |
72 | func escape(str string) string {
73 | return html.EscapeString(str)
74 | }
75 |
76 | func subItems(attrs []HTML) string {
77 | res := []string{}
78 | for _, v := range attrs {
79 | res = append(res, v())
80 | }
81 | return strings.Join(res, "")
82 | }
83 |
84 | func getAttributes(attributes Attr) string {
85 | res := []string{}
86 | for k, v := range attributes {
87 | pair := fmt.Sprintf("%v='%v'", escape(k), escape(v))
88 | res = append(res, pair)
89 | }
90 | prefix := ""
91 | if len(res) > 0 {
92 | prefix = " "
93 | }
94 | return prefix + strings.Join(res, " ")
95 | }
96 |
--------------------------------------------------------------------------------
/daz.go.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephenlacy/daz/d45417daa31463c284c3577fbc6d3fb77bd63e8d/daz.go.png
--------------------------------------------------------------------------------
/daz_test.go:
--------------------------------------------------------------------------------
1 | package daz
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | var fixture1 = ""
8 | var fixture2 = "onetwothree
"
9 | var fixture3 = ""
10 | var fixture4 = "content
"
11 | var fixture5 = "O'Brian
"
12 | var fixture6 = ""
13 | var fixture7 = "<script>alert('xss')</script>
"
14 | var fixture8 = "
"
15 |
16 | func TestBasicRender(t *testing.T) {
17 | attrs := Attr{"class": "app view"}
18 | nav := H("nav", "Welcome")
19 | header := H("header", "test 1", nav)
20 | escaped := H("div", "")
21 | root := H("div", attrs, header, escaped)
22 | res := root()
23 | if res != fixture1 {
24 | t.Errorf("got: %v wanted: %v", res, fixture1)
25 | }
26 | }
27 |
28 | func TestStringItems(t *testing.T) {
29 | items := []string{"one", "two", "three"}
30 | root := H("div", items)
31 | res := root()
32 | if res != fixture2 {
33 | t.Errorf("got: %v wanted: %v", res, fixture2)
34 | }
35 | }
36 |
37 | func TestItems1(t *testing.T) {
38 | one := H("div", "one")
39 | two := func() string { return "one" }
40 | three := H("", "text")
41 | items := []HTML{one, two, three}
42 |
43 | root := H("div", items)
44 | res := root()
45 | if res != fixture3 {
46 | t.Errorf("got: %v wanted: %v", res, fixture3)
47 | }
48 | }
49 | func TestItems2(t *testing.T) {
50 | one := H("div", "one")
51 | two := func() string { return "one" }
52 | three := H("", "text")
53 | items := []HTML{one, two, three}
54 |
55 | root := H("div", items)
56 | res := root()
57 | if res != fixture3 {
58 | t.Errorf("got: %v wanted: %v", res, fixture3)
59 | }
60 | }
61 |
62 | func TestAttrs(t *testing.T) {
63 | attr1 := Attr{"class": "bg-grey-50"}
64 | attr2 := Attr{"data-id": "div-1"}
65 | root := H("div", attr1, "content", attr2)
66 | res := root()
67 | if res != fixture4 {
68 | t.Errorf("got: %v wanted: %v", res, fixture4)
69 | }
70 | }
71 | func TestQuoted(t *testing.T) {
72 | value := "input value's"
73 | input := H("input", Attr{"type": "text", "value": value})
74 | root := H("div", "O'Brian", input)
75 | res := root()
76 | if res != fixture5 {
77 | t.Errorf("got: %v wanted: %v", res, fixture5)
78 | }
79 | }
80 |
81 | func TestSelfClosing(t *testing.T) {
82 | root := H("div", H("img", Attr{"src": "https://example.com/image.png"}), H("br"))
83 | res := root()
84 | if res != fixture6 {
85 | t.Errorf("got: %v wanted: %v", res, fixture6)
86 | }
87 | }
88 |
89 | func TestXSS1(t *testing.T) {
90 | root := H("div", "")
91 | res := root()
92 | if res != fixture7 {
93 | t.Errorf("got: %v wanted: %v", res, fixture7)
94 | }
95 | }
96 |
97 | func TestUnsafeContent(t *testing.T) {
98 | injection := ""
99 | root := H("div", UnsafeContent(injection))
100 | res := root()
101 | if res != fixture8 {
102 | t.Errorf("got: %v wanted: %v", res, fixture8)
103 | }
104 | }
105 |
106 | func BenchmarkBasicRender(b *testing.B) {
107 | attrs := Attr{"class": "app view"}
108 | nav := H("nav", "Welcome")
109 | header := H("header", "test 1", nav)
110 | root := H("div", attrs, header)
111 | root()
112 | }
113 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package daz is a library for building composable HTML components in Go. It is a functional alternative to using templates, and allows for nested components/lists.
3 |
4 | Daz is a "functional" alternative to using templates, and allows for nested components/lists
5 | Also enables template-free server-side rendered components with support for nested lists. It is inspired by https://github.com/hyperhype/hyperscript
6 |
7 |
8 | Basic usage:
9 |
10 |
11 | element := H("div", Attr{"class": "bg-grey-50"})
12 |
13 | html := H("html", element)
14 |
15 | w.Write([]byte(html()))
16 |
17 |
18 | Lists can be easily created without needing to embed a `range / end` in a template:
19 |
20 |
21 | items := []HTML{H("li", "item one"), H("li", "item two")}
22 |
23 | element := H("ul", Attr{"class": "bg-grey-50"})
24 |
25 | div := H("div", element)
26 |
27 | */
28 | package daz
29 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # daz examples
2 |
3 | [server.go](./server.go) is a simple single route server showcasing [Tailwind](https://tailwindcss.com/)
4 |
5 | 
6 |
--------------------------------------------------------------------------------
/examples/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | . "github.com/stevelacy/daz"
8 | )
9 |
10 | // User is an example prop for a component
11 | type User struct {
12 | Name string
13 | }
14 |
15 | func main() {
16 | http.HandleFunc("/", rootHandler)
17 | fmt.Println("listening on :3000")
18 | http.ListenAndServe(":3000", nil)
19 | }
20 |
21 | func rootHandler(w http.ResponseWriter, r *http.Request) {
22 | title := "Example Server"
23 | description := "Welcome to daz"
24 |
25 | user := User{Name: "Daz"}
26 |
27 | links := H("link", Attr{
28 | "href": "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",
29 | "rel": "stylesheet",
30 | })
31 |
32 | meta := []HTML{
33 | H("meta", Attr{"charset": "UTF-8"}),
34 | H("meta", Attr{
35 | "name": "viewport",
36 | "content": "width=device-width, initial-scale=1.0",
37 | }),
38 | }
39 |
40 | head := H("head", H("title", title), meta, links)
41 | style := Attr{"style": "background: #efefef"}
42 |
43 | body := H(
44 | "body",
45 | style,
46 | AppComponent(user, description),
47 | )
48 | html := H("html", head, body)
49 | w.Write([]byte(html()))
50 | }
51 |
52 | func navItems(user User) []HTML {
53 | // get itmes from somewhere such as a database
54 | items := []HTML{H("li", "item one"), H("li", "item two")}
55 |
56 | // example runtime modification
57 | lastElement := H("li", user.Name)
58 | items = append(items, lastElement)
59 | return items
60 | }
61 |
62 | // AppComponent is a daz component. It returns a daz.H func
63 | func AppComponent(user User, description string) HTML {
64 | nav := H("nav", navItems(user))
65 | return H(
66 | "div", Attr{"class": "bg-grey-50"},
67 | H("div", Attr{"class": "max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8 lg:flex lg:items-center lg:justify-between"},
68 | H("h2", Attr{"class": "text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl"},
69 | H("span", Attr{"class": "block"}, description),
70 | H("span", Attr{"class": "block text-indigo-600"}, "This example uses Tailwind CSS"),
71 | ),
72 | H("div", Attr{"class": "mt-8 lex lg:mt-0 lg:flex-shrink-0"},
73 | H("div", Attr{"class": "inline-flex rounded-md shadow"},
74 | H("a", Attr{"href": "#", "class": "inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"}, "Get Started")),
75 | ),
76 | H("div", Attr{"class": "ml-3 inline-flex rounded-md shadow"},
77 | H("a", Attr{"href": "#", "class": "inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50"}, "Learn More")),
78 | H("div",
79 | Attr{"class": "mt-1 flex rounded-md shadow-sm"},
80 | H("span", Attr{"class": "inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm"}, "http://"),
81 | H("input", Attr{"type": "text", "name": "test", "class": "focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300", "placeholder": "input's value"}),
82 | ),
83 | nav,
84 | ),
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/examples/server.go.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephenlacy/daz/d45417daa31463c284c3577fbc6d3fb77bd63e8d/examples/server.go.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/stevelacy/daz
2 |
3 | go 1.14
4 |
--------------------------------------------------------------------------------