package htmlformat import ( "bufio" "fmt" "io" "strings" "unicode" "unicode/utf8" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) // Document formats a HTML document. func Document(w io.Writer, r io.Reader) (err error) { node, err := html.Parse(r) if err != nil { return err } return Nodes(w, []*html.Node{node}) } // Fragment formats a fragment of a HTML document. func Fragment(w io.Writer, r io.Reader) (err error) { context := &html.Node{ Type: html.ElementNode, } nodes, err := html.ParseFragment(r, context) if err != nil { return err } return Nodes(w, nodes) } // Nodes formats a slice of HTML nodes. func Nodes(w io.Writer, nodes []*html.Node) (err error) { for _, node := range nodes { if err = printNode(w, node, 0); err != nil { return } } return } // The
tag indicates that the text within it should always be formatted
// as is. See https://github.com/ericchiang/pup/issues/33
func printPre(w io.Writer, n *html.Node) (err error) {
switch n.Type {
case html.TextNode:
s := n.Data
if _, err = fmt.Fprint(w, s); err != nil {
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err = printPre(w, c); err != nil {
return
}
}
case html.ElementNode:
if _, err = fmt.Fprintf(w, "<%s", n.Data); err != nil {
return
}
for _, a := range n.Attr {
val := html.EscapeString(a.Val)
if _, err = fmt.Fprintf(w, ` %s="%s"`, a.Key, val); err != nil {
return
}
}
if _, err = fmt.Fprint(w, ">"); err != nil {
return
}
if !isVoidElement(n) {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err = printPre(w, c); err != nil {
return
}
}
if _, err = fmt.Fprintf(w, "%s>", n.Data); err != nil {
return
}
}
case html.CommentNode:
data := n.Data
if _, err = fmt.Fprintf(w, "\n", data); err != nil {
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err = printPre(w, c); err != nil {
return
}
}
case html.DoctypeNode, html.DocumentNode:
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err = printPre(w, c); err != nil {
return
}
}
}
return
}
// Is this node a tag with no end tag such as or
?
// http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
func isVoidElement(n *html.Node) bool {
switch n.DataAtom {
case atom.Area, atom.Base, atom.Br, atom.Col, atom.Command, atom.Embed,
atom.Hr, atom.Img, atom.Input, atom.Keygen, atom.Link,
atom.Meta, atom.Param, atom.Source, atom.Track, atom.Wbr:
return true
}
return false
}
func isSpecialContentElement(n *html.Node) bool {
if n != nil {
switch n.DataAtom {
case atom.Style,
atom.Script:
return true
}
}
return false
}
func isEmptyTextNode(n *html.Node) bool {
return n.Type == html.TextNode && strings.TrimSpace(n.Data) == ""
}
func getFirstRune(s string) rune {
r, _ := utf8.DecodeRuneInString(s)
return r
}
func hasSingleTextChild(n *html.Node) bool {
return n != nil && n.FirstChild != nil && n.FirstChild == n.LastChild && n.FirstChild.Type == html.TextNode
}
func printNode(w io.Writer, n *html.Node, level int) (err error) {
switch n.Type {
case html.TextNode:
s := n.Data
s = strings.TrimSpace(s)
if s != "" {
if !isSpecialContentElement(n.Parent) && !hasSingleTextChild(n.Parent) &&
(n.PrevSibling == nil || !unicode.IsPunct(getFirstRune(s))) {
if err = printIndent(w, level); err != nil {
return
}
}
if isSpecialContentElement(n.Parent) {
scanner := bufio.NewScanner(strings.NewReader(s))
for scanner.Scan() {
t := scanner.Text()
if _, err = fmt.Fprintln(w); err != nil {
return
}
if err = printIndent(w, level+1); err != nil {
return
}
if _, err = fmt.Fprint(w, t); err != nil {
return
}
}
if err = scanner.Err(); err != nil {
return
}
if _, err = fmt.Fprintln(w); err != nil {
return
}
} else {
if _, err = fmt.Fprint(w, s); err != nil {
return
}
if !hasSingleTextChild(n.Parent) {
if _, err = fmt.Fprint(w, "\n"); err != nil {
return
}
}
}
}
case html.ElementNode:
if err = printIndent(w, level); err != nil {
return
}
if _, err = fmt.Fprintf(w, "<%s", n.Data); err != nil {
return
}
for _, a := range n.Attr {
val := html.EscapeString(a.Val)
if _, err = fmt.Fprintf(w, ` %s="%s"`, a.Key, val); err != nil {
return
}
}
if _, err = fmt.Fprint(w, ">"); err != nil {
return
}
if !hasSingleTextChild(n) {
if _, err = fmt.Fprint(w, "\n"); err != nil {
return
}
}
if !isVoidElement(n) {
if err = printChildren(w, n, level+1); err != nil {
return
}
if isSpecialContentElement(n) || !hasSingleTextChild(n) {
if err = printIndent(w, level); err != nil {
return
}
}
if _, err = fmt.Fprintf(w, "%s>", n.Data); err != nil {
return
}
if n.NextSibling == nil ||
(!unicode.IsPunct(getFirstRune(n.NextSibling.Data)) || n.NextSibling.Type == html.ElementNode) {
if _, err = fmt.Fprint(w, "\n"); err != nil {
return
}
}
}
case html.CommentNode:
if err = printIndent(w, level); err != nil {
return
}
if _, err = fmt.Fprintf(w, "\n", n.Data); err != nil {
return
}
if err = printChildren(w, n, level); err != nil {
return
}
case html.DoctypeNode, html.DocumentNode:
if err = printChildren(w, n, level); err != nil {
return
}
}
return
}
func printChildren(w io.Writer, n *html.Node, level int) (err error) {
child := n.FirstChild
for child != nil {
if err = printNode(w, child, level); err != nil {
return
}
child = child.NextSibling
}
return
}
func printIndent(w io.Writer, level int) (err error) {
_, err = fmt.Fprint(w, strings.Repeat(" ", level))
return err
}