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"
2019-08-31 15:05:43 +00:00
"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
2019-08-31 15:05:43 +00:00
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
}
2019-03-25 02:46:38 +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.
2019-03-25 02:46:38 +00:00
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 ( "&" ) ,
'<' : [ ] 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
}
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.
2019-03-25 02:46:38 +00:00
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
}