diff options
author | Markus Wüstenberg | 2020-09-13 22:50:19 +0200 |
---|---|---|
committer | Markus Wüstenberg | 2020-09-13 22:50:19 +0200 |
commit | fc3cc0f0f3a59bf4fe27a4e32dc2ef420b3fa2f8 (patch) | |
tree | f3f933970dccd06a347613d3663035c023ac0017 | |
download | gomponents-fc3cc0f0f3a59bf4fe27a4e32dc2ef420b3fa2f8.tar.lz gomponents-fc3cc0f0f3a59bf4fe27a4e32dc2ef420b3fa2f8.tar.zst gomponents-fc3cc0f0f3a59bf4fe27a4e32dc2ef420b3fa2f8.zip |
Add first implementation of Node, El, Attr, Text
-rw-r--r-- | .editorconfig | 15 | ||||
-rw-r--r-- | .github/workflows/go.yml | 42 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | README.md | 8 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | gomponents.go | 100 | ||||
-rw-r--r-- | gomponents_test.go | 75 |
7 files changed, 264 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1fd14ef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[Makefile] +indent_style = tab + +[*.go] +indent_style = tab diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..e9ccec8 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,42 @@ +name: Go + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ^1.15 + id: go + + - name: Check out + uses: actions/checkout@v2 + + - name: Get dependencies + run: go get -v -t -d ./... + + - name: Build + run: go build -v . + + - name: Test + run: go test -v ./... + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v2 + + - name: Lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.30 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5af54ef --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Maragu ApS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1eaed1c --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# gomponents + +gomponents are components of DOM nodes for Go, that can render to an HTML page. +gomponents aims to make it easy to build HTML pages of reusable components, +without the use of a template language. Think server-side-rendered React, +but without the virtual DOM and diffing. + +The implementation is still incomplete, but usable. The API may change until version 1 is reached. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5a1df18 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module "github.com/maragudk/gomponents" + +go 1.15 diff --git a/gomponents.go b/gomponents.go new file mode 100644 index 0000000..bd7e09c --- /dev/null +++ b/gomponents.go @@ -0,0 +1,100 @@ +package gomponents + +import ( + "fmt" + "html/template" + "strings" +) + +// Node is a DOM node that can Render itself to a string representation. +type Node interface { + Render() string +} + +// NodeFunc is render function that is also a Node. +type NodeFunc func() string + +func (n NodeFunc) Render() string { + return n() +} + +// El creates an element DOM Node with a name and child Nodes. +// Use this if no convenience creator exists. +func El(name string, children ...Node) NodeFunc { + return func() string { + var b, attrString, childrenString strings.Builder + + b.WriteString("<") + b.WriteString(name) + + if len(children) == 0 { + b.WriteString("/>") + return b.String() + } + + for _, c := range children { + s := c.Render() + if _, ok := c.(attr); ok { + attrString.WriteString(s) + continue + } + childrenString.WriteString(c.Render()) + } + + b.WriteString(attrString.String()) + + if childrenString.Len() == 0 { + b.WriteString("/>") + return b.String() + } + + b.WriteString(">") + b.WriteString(childrenString.String()) + b.WriteString("</") + b.WriteString(name) + b.WriteString(">") + return b.String() + } +} + +// Attr creates an attr DOM Node. +// If one parameter is passed, it's a name-only attribute (like "required"). +// If two parameters are passed, it's a name-value attribute (like `class="header"`). +// More parameter counts make Attr panic. +// Use this if no convenience creator exists. +func Attr(name string, value ...string) Node { + switch len(value) { + case 0: + return attr{name: name} + case 1: + return attr{name: name, value: &value[0]} + default: + panic("attribute must be just name or name and value pair") + } +} + +type attr struct { + name string + value *string +} + +func (a attr) Render() string { + if a.value == nil { + return fmt.Sprintf(" %v", a.name) + } + return fmt.Sprintf(` %v="%v"`, a.name, *a.value) +} + +// Text creates a text DOM Node that Renders the escaped string t. +func Text(t string) NodeFunc { + return func() string { + return template.HTMLEscaper(t) + } +} + +// Raw creates a raw Node that just Renders the unescaped string t. +func Raw(t string) NodeFunc { + return func() string { + return t + } +} diff --git a/gomponents_test.go b/gomponents_test.go new file mode 100644 index 0000000..21ab30a --- /dev/null +++ b/gomponents_test.go @@ -0,0 +1,75 @@ +package gomponents_test + +import ( + "testing" + + g "github.com/maragudk/gomponents" +) + +func TestAttr(t *testing.T) { + t.Run("renders just the local name with one argument", func(t *testing.T) { + a := g.Attr("required") + equal(t, " required", a.Render()) + }) + + t.Run("renders the name and value when given two arguments", func(t *testing.T) { + a := g.Attr("id", "hat") + equal(t, ` id="hat"`, a.Render()) + }) + + t.Run("panics with more than two arguments", func(t *testing.T) { + called := false + defer func() { + if err := recover(); err != nil { + called = true + } + }() + g.Attr("name", "value", "what is this?") + if !called { + t.FailNow() + } + }) +} + +func TestEl(t *testing.T) { + t.Run("renders an empty element if no children given", func(t *testing.T) { + e := g.El("div") + equal(t, "<div/>", e.Render()) + }) + + t.Run("renders an empty element if only attributes given as children", func(t *testing.T) { + e := g.El("div", g.Attr("class", "hat")) + equal(t, `<div class="hat"/>`, e.Render()) + }) + + t.Run("renders an element, attributes, and element children", func(t *testing.T) { + e := g.El("div", g.Attr("class", "hat"), g.El("span")) + equal(t, `<div class="hat"><span/></div>`, e.Render()) + }) + + t.Run("renders attributes at the correct place regardless of placement in parameter list", func(t *testing.T) { + e := g.El("div", g.El("span"), g.Attr("class", "hat")) + equal(t, `<div class="hat"><span/></div>`, e.Render()) + }) +} + +func TestText(t *testing.T) { + t.Run("renders escaped text", func(t *testing.T) { + e := g.Text("<div/>") + equal(t, "<div/>", e.Render()) + }) +} + +func TestRaw(t *testing.T) { + t.Run("renders raw text", func(t *testing.T) { + e := g.Raw("<div/>") + equal(t, "<div/>", e.Render()) + }) +} + +func equal(t *testing.T, expected, actual string) { + if expected != actual { + t.Errorf("expected %v but got %v", expected, actual) + t.FailNow() + } +} |