212 lines
4.7 KiB
Go
212 lines
4.7 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"
|
|
"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 string
|
|
}
|
|
|
|
// 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) {
|
|
err = writeStringsTo(w, &n, a.Key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if a.Value != "" {
|
|
escaped := html.EscapeString(a.Value)
|
|
err = writeStringsTo(w, &n, "='", escaped, "'")
|
|
}
|
|
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 {
|
|
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("&"),
|
|
'<': []byte("<"),
|
|
'>': []byte(">"),
|
|
'\'': []byte("'"),
|
|
'"': []byte("""),
|
|
}
|
|
|
|
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
|
|
}
|