├── .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 | [![GoDoc](https://godoc.org/github.com/stevelacy/daz?status.svg)](https://godoc.org/github.com/stevelacy/daz)![Go](https://github.com/stevelacy/daz/workflows/Go/badge.svg) 9 | 10 | 11 | ![daz carbon example](./carbon.png) 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, "") + "" 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 = "
test 1
<escaped>
" 8 | var fixture2 = "
onetwothree
" 9 | var fixture3 = "
one
one<>text
" 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 | ![server.go](./server.go.png) 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 | --------------------------------------------------------------------------------