hatmill/hatmill.go

228 lines
5.0 KiB
Go
Raw Normal View History

2019-03-21 18:40:54 +00:00
package hatmill
2019-03-21 17:26:40 +00:00
2019-04-03 03:42:16 +00:00
//go:generate go run internal/codegen/codegen.go -input defs.json -elemfile element/generated.go -elempkg element -attribfile attribute/generated.go -attribpkg attribute
2019-04-29 04:03:28 +00:00
import (
"bytes"
"fmt"
2019-04-29 04:03:28 +00:00
"html"
"io"
)
2019-03-21 17:26:40 +00:00
2019-04-28 21:28:57 +00:00
// Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text.
2019-03-22 03:31:24 +00:00
type Term interface {
2019-03-21 18:40:54 +00:00
io.WriterTo
2019-03-21 17:26:40 +00:00
2019-03-22 03:31:24 +00:00
// isHtml is a no-op method to prevent external Term implementations, because
2019-03-21 18:40:54 +00:00
// Go doesn't have sum types.
isHtml()
2019-03-21 17:26:40 +00:00
}
2019-03-21 18:40:54 +00:00
// writeStringsTo attempts to write the strings in ss to w, incrementing n by the
// number of bytes written.
func writeStringsTo(w io.Writer, n *int64, ss ...string) error {
for _, s := range ss {
nCurr, err := io.WriteString(w, s)
*n += int64(nCurr)
if err != nil {
return err
}
}
return nil
2019-03-21 17:26:40 +00:00
}
2019-03-25 02:54:09 +00:00
// Attrib represents an HTML attribute.
2019-03-21 17:26:40 +00:00
type Attrib struct {
2019-03-21 18:40:54 +00:00
Key string
Value fmt.Stringer
}
2020-05-25 21:18:17 +00:00
func (a Attrib) Empty() bool {
return a == Attrib{}
}
2019-03-25 02:54:09 +00:00
// WriteTo writes a to w as an HTML attribute in the form key="value", or
2019-04-29 04:03:28 +00:00
// simply key if value is empty. Special characters in value are replaced with
// HTML entities. It returns the number of bytes written and any
2019-03-25 02:54:09 +00:00
// error encountered.
2019-03-21 17:26:40 +00:00
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
2020-05-25 21:18:17 +00:00
if a.Empty() {
return 0, nil
}
2019-03-21 18:40:54 +00:00
err = writeStringsTo(w, &n, a.Key)
if err != nil {
return
}
2019-08-31 19:34:34 +00:00
var value string
if a.Value != nil {
value = html.EscapeString(a.Value.String())
}
if value != "" {
err = writeStringsTo(w, &n, "='", value, "'")
2019-03-21 18:40:54 +00:00
}
return
2019-03-21 17:26:40 +00:00
}
2019-03-29 02:18:25 +00:00
// VoidElement represents a void HTML element, that is one that cannot have
2019-03-25 02:54:09 +00:00
// children.
2019-03-29 02:18:25 +00:00
type VoidElement struct {
2019-03-21 18:40:54 +00:00
TagName string
Attribs []Attrib
}
2019-03-21 17:26:40 +00:00
2019-03-29 02:18:25 +00:00
func (VoidElement) isHtml() {}
2019-03-21 17:26:40 +00:00
2019-03-25 02:54:09 +00:00
// WriteTo writes the HTML markup represented by e to w, returning the number
// of bytes written and any error encountered.
2019-03-29 02:18:25 +00:00
func (e VoidElement) WriteTo(w io.Writer) (n int64, err error) {
2019-03-21 18:40:54 +00:00
err = writeStringsTo(w, &n, "<", e.TagName)
if err != nil {
return
}
2019-03-21 17:26:40 +00:00
2019-03-21 18:40:54 +00:00
for _, attrib := range e.Attribs {
2020-05-25 21:18:17 +00:00
if attrib.Empty() {
// attrib.WriteTo already does nothing in this case, but
// let's not clutter our tags with extra spaces.
continue
}
2019-03-21 18:40:54 +00:00
err = writeStringsTo(w, &n, " ")
if err != nil {
return
}
2019-03-21 17:26:40 +00:00
2019-03-21 18:40:54 +00:00
var nCurr int64
nCurr, err = attrib.WriteTo(w)
n += int64(nCurr)
if err != nil {
return
}
}
2019-03-21 17:26:40 +00:00
2019-03-21 18:40:54 +00:00
err = writeStringsTo(w, &n, ">")
return
2019-03-21 17:26:40 +00:00
}
2019-03-25 02:54:09 +00:00
// ParentElement represents an HTML element that can have children.
2019-03-21 18:40:54 +00:00
type ParentElement struct {
2019-03-29 02:18:25 +00:00
VoidElement
2019-04-28 21:27:46 +00:00
Children Terms
2019-03-21 17:26:40 +00:00
}
func (e ParentElement) isHtml() {}
2019-03-21 18:40:54 +00:00
2019-03-25 02:54:09 +00:00
// WriteTo writes the HTML markup represented by e to w, returning the number
// of bytes written and any error encountered.
func (e ParentElement) WriteTo(w io.Writer) (n int64, err error) {
2019-03-29 02:18:25 +00:00
n, err = e.VoidElement.WriteTo(w)
2019-03-21 18:40:54 +00:00
if err != nil {
return
}
2019-03-21 17:26:40 +00:00
2019-04-28 21:27:46 +00:00
var nChildren int64
nChildren, err = e.Children.WriteTo(w)
n += nChildren
if err != nil {
return
2019-03-21 18:40:54 +00:00
}
2019-03-21 17:26:40 +00:00
2019-03-21 18:40:54 +00:00
err = writeStringsTo(w, &n, "</", e.TagName, ">")
return
2019-03-21 17:26:40 +00:00
}
2019-03-25 02:54:09 +00:00
// Text represents an HTML text node.
2019-03-21 18:40:54 +00:00
type Text string
2019-03-21 17:26:40 +00:00
2019-03-21 18:40:54 +00:00
func (Text) isHtml() {}
2019-03-21 17:26:40 +00:00
2019-04-29 04:03:28 +00:00
var entities = map[byte][]byte{
'&': []byte("&amp;"),
'<': []byte("&lt;"),
'>': []byte("&gt;"),
'\'': []byte("&#39;"),
'"': []byte("&#34;"),
}
type escapedWriter struct {
io.Writer
}
func (w escapedWriter) Write(p []byte) (n int, err error) {
const specialChars = `&<>'"`
for len(p) > 0 {
var nCurr int
idx := bytes.IndexAny(p, specialChars)
if idx == -1 {
nCurr, err = w.Writer.Write(p)
n += nCurr
return
}
nCurr, err = w.Writer.Write(p[:idx])
n += nCurr
if err != nil {
return n, err
}
entity := entities[p[idx]]
nCurr, err = w.Writer.Write(entity)
n += nCurr
if err != nil {
return n, err
}
p = p[idx+1:]
}
return n, nil
}
2019-03-25 02:54:09 +00:00
// WriteTo writes the contents of t to w, returning the number of bytes written
2019-04-29 04:03:28 +00:00
// and any error encountered. It replaces special characters with HTML entities.
2019-03-21 18:40:54 +00:00
func (t Text) WriteTo(w io.Writer) (n int64, err error) {
2019-04-29 04:03:28 +00:00
err = writeStringsTo(escapedWriter{Writer: w}, &n, string(t))
2019-03-21 18:40:54 +00:00
return
2019-03-21 17:26:40 +00:00
}
2019-05-28 04:34:01 +00:00
// RawText represents an HTML text node. Unlike Text, its contents are not
// escaped when written, so it should be used with care. It is intended mainly
// for use in <style> and <script> elements.
type RawText string
func (RawText) isHtml() {}
// WriteTo writes the contents of t to w, returning the number of bytes written
// and any error encountered. It does not escape special characters.
func (t RawText) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(w, &n, string(t))
return
}
// WriteDocument writes an HTML5 DOCTYPE declaration, followed by root.
2019-03-21 18:40:54 +00:00
// root should probably be an <html> element.
func WriteDocument(w io.Writer, root ParentElement) (n int64, err error) {
2019-03-21 18:40:54 +00:00
err = writeStringsTo(w, &n, "<!DOCTYPE html>")
if err != nil {
return
}
var nroot int64
nroot, err = root.WriteTo(w)
n += nroot
return
}
2019-04-28 21:27:46 +00:00
// Terms is a list of HTML nodes.
type Terms []Term
func (Terms) isHtml() {}
func (ts Terms) WriteTo(w io.Writer) (n int64, err error) {
for _, t := range ts {
var nCurr int64
nCurr, err = t.WriteTo(w)
n += int64(nCurr)
if err != nil {
return
}
}
return
}