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 } // 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 } 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 { 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, "") 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