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, false, 0); 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 collapseWhitespace(in string) string {
leading := unicode.IsSpace(getFirstRune(in))
trailing := unicode.IsSpace(getLastRune(in))
out := strings.TrimSpace(in)
switch {
case leading && trailing:
return " " + out + " "
case leading:
return " " + out
case trailing:
return out + " "
default:
return out
}
}
func getFirstRune(s string) rune {
r, _ := utf8.DecodeRuneInString(s)
return r
}
func getLastRune(s string) rune {
r, _ := utf8.DecodeLastRuneInString(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, pre bool, level int) (err error) {
switch n.Type {
case html.TextNode:
if pre {
if _, err = fmt.Fprint(w, n.Data); err != nil {
return
}
return nil
}
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, collapseWhitespace(s)); err != nil {
return
}
if !hasSingleTextChild(n.Parent) &&
(n.NextSibling == nil || !unicode.IsPunct(getLastRune(strings.TrimSpace(s)))) {
if _, err = fmt.Fprint(w, "\n"); err != nil {
return
}
}
}
}
case html.ElementNode:
if n.PrevSibling == nil ||
(n.PrevSibling.Type != html.TextNode || !unicode.IsPunct(getLastRune(strings.TrimSpace(n.PrevSibling.Data)))) {
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, isPreFormatted(n.Data), 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, false, level); err != nil {
return
}
case html.DoctypeNode, html.DocumentNode:
if err = printChildren(w, n, false, level); err != nil {
return
}
}
return
}
func isPreFormatted(s string) bool {
return s == "pre" || s == "script" || s == "style"
}
func printChildren(w io.Writer, n *html.Node, pre bool, level int) (err error) {
child := n.FirstChild
for child != nil {
if err = printNode(w, child, pre, 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
}