hatmill/hatmill.go

228 lines
5.0 KiB
Go

package hatmill
//go:generate go run internal/codegen/codegen.go -input defs.json -elemfile element/generated.go -elempkg element -attribfile attribute/generated.go -attribpkg attribute
import (
"bytes"
"fmt"
"html"
"io"
)
// Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text.
type Term interface {
io.WriterTo
// isHtml is a no-op method to prevent external Term implementations, because
// Go doesn't have sum types.
isHtml()
}
// 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
}
// Attrib represents an HTML attribute.
type Attrib struct {
Key string
Value fmt.Stringer
}
func (a Attrib) Empty() bool {
return a == Attrib{}
}
// WriteTo writes a to w as an HTML attribute in the form key="value", or
// simply key if value is empty. Special characters in value are replaced with
// HTML entities. It returns the number of bytes written and any
// error encountered.
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
if a.Empty() {
return 0, nil
}
err = writeStringsTo(w, &n, a.Key)
if err != nil {
return
}
var value string
if a.Value != nil {
value = html.EscapeString(a.Value.String())
}
if value != "" {
err = writeStringsTo(w, &n, "='", value, "'")
}
return
}
// VoidElement represents a void HTML element, that is one that cannot have
// children.
type VoidElement struct {
TagName string
Attribs []Attrib
}
func (VoidElement) isHtml() {}
// WriteTo writes the HTML markup represented by e to w, returning the number
// of bytes written and any error encountered.
func (e VoidElement) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(w, &n, "<", e.TagName)
if err != nil {
return
}
for _, attrib := range e.Attribs {
if attrib.Empty() {
// attrib.WriteTo already does nothing in this case, but
// let's not clutter our tags with extra spaces.
continue
}
err = writeStringsTo(w, &n, " ")
if err != nil {
return
}
var nCurr int64
nCurr, err = attrib.WriteTo(w)
n += int64(nCurr)
if err != nil {
return
}
}
err = writeStringsTo(w, &n, ">")
return
}
// ParentElement represents an HTML element that can have children.
type ParentElement struct {
VoidElement
Children Terms
}
func (e ParentElement) isHtml() {}
// 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) {
n, err = e.VoidElement.WriteTo(w)
if err != nil {
return
}
var nChildren int64
nChildren, err = e.Children.WriteTo(w)
n += nChildren
if err != nil {
return
}
err = writeStringsTo(w, &n, "</", e.TagName, ">")
return
}
// Text represents an HTML text node.
type Text string
func (Text) isHtml() {}
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
}
// WriteTo writes the contents of t to w, returning the number of bytes written
// and any error encountered. It replaces special characters with HTML entities.
func (t Text) WriteTo(w io.Writer) (n int64, err error) {
err = writeStringsTo(escapedWriter{Writer: w}, &n, string(t))
return
}
// 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.
// root should probably be an <html> element.
func WriteDocument(w io.Writer, root ParentElement) (n int64, err error) {
err = writeStringsTo(w, &n, "<!DOCTYPE html>")
if err != nil {
return
}
var nroot int64
nroot, err = root.WriteTo(w)
n += nroot
return
}
// 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
}