Go:如何“漂亮打印”/“美化”HTML?

10
在Python、PHP和许多其他语言中,可以将HTML文档转换并“美化”。在Go中,可以使用MarshIndent函数非常轻松地对JSON和XML(从结构/接口)进行此操作。
Go中XML的示例:

http://play.golang.org/p/aBNfNxTEG1

package main

import (
    "encoding/xml"
    "fmt"
    "os"
)

func main() {
    type Address struct {
        City, State string
    }
    type Person struct {
        XMLName   xml.Name `xml:"person"`
        Id        int      `xml:"id,attr"`
        FirstName string   `xml:"name>first"`
        LastName  string   `xml:"name>last"`
        Age       int      `xml:"age"`
        Height    float32  `xml:"height,omitempty"`
        Married   bool
        Address
        Comment string `xml:",comment"`
    }

    v := &Person{Id: 13, FirstName: "John", LastName: "Doe", Age: 42}
    v.Comment = " Need more details. "
    v.Address = Address{"Hanga Roa", "Easter Island"}

    output, err := xml.MarshalIndent(v, "  ", "    ")
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }

    os.Stdout.Write(output)
}

然而,这只适用于将struct/interface转换为[]byte。我想要的是将一段HTML代码字符串自动缩进。例如:
原始HTML
<!doctype html><html><head>
<title>Website Title</title>
</head><body>
<div class="random-class">
<h1>I like pie</h1><p>It's true!</p></div>
</body></html>

美化后的HTML
<!doctype html>
<html>
    <head>
        <title>Website Title</title>
    </head>
    <body>
        <div class="random-class">
            <h1>I like pie</h1>
            <p>It's true!</p>
        </div>
    </body>
</html>

如何仅使用字符串实现此操作?

5个回答

16

我曾遇到同样的问题,我通过自己在Go中创建HTML格式化包来解决了它。

这就是它:

GoHTML - Go的HTML格式化程序

请查看这个包。

谢谢,

Keiji


非常感谢您。我自己的实现还在进行中,不如您的好,所以我将使用您的实现。 - Xplane
@KeijiYoshida,将您的代码美化器嵌入到一个独立的二进制文件中,以从标准输入读取并写入标准输出,哪种方式最好? - Sebastián Grignoli

8
我在尝试找到如何在Go中美化打印xml时,发现了这个问题。由于我没有在任何地方找到答案,这是我的解决方案:
import (
    "bytes"
    "encoding/xml"
    "io"
)

func formatXML(data []byte) ([]byte, error) {
    b := &bytes.Buffer{}
    decoder := xml.NewDecoder(bytes.NewReader(data))
    encoder := xml.NewEncoder(b)
    encoder.Indent("", "  ")
    for {
        token, err := decoder.Token()
        if err == io.EOF {
            encoder.Flush()
            return b.Bytes(), nil
        }
        if err != nil {
            return nil, err
        }
        err = encoder.EncodeToken(token)
        if err != nil {
            return nil, err
        }
    }
}

3
我喜欢这个解决方案,但仍在寻找一个不会重写文档(除了格式化空格之外)的Golang XML格式化程序/漂亮打印机。编组或使用编码器将更改命名空间声明。例如,像“<ns1:Element/>”这样的元素将被翻译为类似“<Element xmlns =“ ns1” />”的内容,这似乎是无害的,除非意图是不修改XML,只进行格式化。 - James McGill
@JamesMcGill,如果你想使用不会重写文档的 Golang XML 格式化程序/美化工具,请看 https://github.com/go-xmlfmt/xmlfmt/。我和你有着同样的烦恼 :-)。 - xpt

5

编辑:使用XML解析器找到了一个非常好的方法:

package main

import (
    "encoding/xml"
    "fmt"
)

func main() {
    html := "<html><head><title>Website Title</title></head><body><div class=\"random-class\"><h1>I like pie</h1><p>It's true!</p></div></body></html>"
    type node struct {
        Attr     []xml.Attr
        XMLName  xml.Name
        Children []node `xml:",any"`
        Text     string `xml:",chardata"`
    }
    x := node{}
    _ = xml.Unmarshal([]byte(html), &x)
    buf, _ := xml.MarshalIndent(x, "", "\t")
    fmt.Println(string(buf))
}

将输出以下内容:
<html>
    <head>
        <title>Website Title</title>
    </head>
    <body>
        <div>
            <h1>I like pie</h1>
            <p>It&#39;s true!</p>
        </div>
    </body>
</html>

这里几乎不是问题。Go的XML模块支持非严格、自动关闭、松散解析。 - Not_a_Golfer
这似乎是正确的方向 - 一个通用的xml / html解组器。然而,我敢打赌,如果我无法使属性起作用,我将不得不制作自己的漂亮解析器。 - Xplane
@GingerBill 我尝试过另一种方法,它确实有效,但我认为不可扩展。那就是将我想要支持的所有已知属性(src、href、id、class等)添加到Node结构中,并在字段上添加xml:",attr,omitempty"。如果它们不存在于结构中,则会被隐藏,所有东西都运作良好,但这样就不再是通用的,也不支持未知属性。 - Not_a_Golfer
@Not_a_Golfer 这正是问题所在。我知道对于我的大部分代码,我可能不会使用所有自定义属性,但如果它能做任何事情那就太好了。如果不可能的话,我将把它制作成一个开源库供人们使用,这样人们就可以在Go中漂亮地打印他们的HTML。 - Xplane
@Not_a_Golfer 我可能真的会这样做。谢谢你的想法。这样的东西应该很简单,也应该是xml库的一部分。我敢打赌,许多人都想要正确地解析HTML代码,出于某种原因。 - Xplane
显示剩余4条评论

2
您可以使用code.google.com/p/go.net/html解析HTML,并编写自己版本的Render函数(其中包括缩进跟踪)。但是请注意:在HTML中添加和删除空格时需要小心。虽然空格通常不重要,但是如果处理不当,渲染文本中可能会出现空格的添加和消失。以下是我最近编写的漂亮打印函数,它处理了一些特殊情况,但并非全部。
func prettyPrint(b *bytes.Buffer, n *html.Node, depth int) {
    switch n.Type {
    case html.DocumentNode:
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            prettyPrint(b, c, depth)
        }

    case html.ElementNode:
        justRender := false
        switch {
        case n.FirstChild == nil:
            justRender = true
        case n.Data == "pre" || n.Data == "textarea":
            justRender = true
        case n.Data == "script" || n.Data == "style":
            break
        case n.FirstChild == n.LastChild && n.FirstChild.Type == html.TextNode:
            if !isInline(n) {
                c := n.FirstChild
                c.Data = strings.Trim(c.Data, " \t\n\r")
            }
            justRender = true
        case isInline(n) && contentIsInline(n):
            justRender = true
        }
        if justRender {
            indent(b, depth)
            html.Render(b, n)
            b.WriteByte('\n')
            return
        }
        indent(b, depth)
        fmt.Fprintln(b, html.Token{
            Type: html.StartTagToken,
            Data: n.Data,
            Attr: n.Attr,
        })
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            if n.Data == "script" || n.Data == "style" && c.Type == html.TextNode {
                prettyPrintScript(b, c.Data, depth+1)
            } else {
                prettyPrint(b, c, depth+1)
            }
        }
        indent(b, depth)
        fmt.Fprintln(b, html.Token{
            Type: html.EndTagToken,
            Data: n.Data,
        })

    case html.TextNode:
        n.Data = strings.Trim(n.Data, " \t\n\r")
        if n.Data == "" {
            return
        }
        indent(b, depth)
        html.Render(b, n)
        b.WriteByte('\n')

    default:
        indent(b, depth)
        html.Render(b, n)
        b.WriteByte('\n')
    }
}

func isInline(n *html.Node) bool {
    switch n.Type {
    case html.TextNode, html.CommentNode:
        return true
    case html.ElementNode:
        switch n.Data {
        case "b", "big", "i", "small", "tt", "abbr", "acronym", "cite", "dfn", "em", "kbd", "strong", "samp", "var", "a", "bdo", "img", "map", "object", "q", "span", "sub", "sup", "button", "input", "label", "select", "textarea":
            return true
        default:
            return false
        }
    default:
        return false
    }
}

func contentIsInline(n *html.Node) bool {
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        if !isInline(c) || !contentIsInline(c) {
            return false
        }
    }
    return true
}

func indent(b *bytes.Buffer, depth int) {
    depth *= 2
    for i := 0; i < depth; i++ {
        b.WriteByte(' ')
    }
}

func prettyPrintScript(b *bytes.Buffer, s string, depth int) {
    for _, line := range strings.Split(s, "\n") {
        line = strings.TrimSpace(line)
        if line == "" {
            continue
        }
        depthChange := 0
        for _, c := range line {
            switch c {
            case '(', '[', '{':
                depthChange++
            case ')', ']', '}':
                depthChange--
            }
        }
        switch line[0] {
        case '.':
            indent(b, depth+1)
        case ')', ']', '}':
            indent(b, depth-1)
        default:
            indent(b, depth)
        }
        depth += depthChange
        fmt.Fprintln(b, line)
    }
}

2

简短回答

使用我编写的Go语言HTML美化库。它已经有一些测试,并且适用于基本输入,希望随着时间的推移变得更加强大,尽管现在并不是非常强大。请注意自述文件中的已知问题部分。

长回答

对于简单情况,使用code.google.com/p/go.net/html包(这就是上述包所做的)来制作自己的HTML美化程序相当容易。以下是一个非常简单的实现方式:

func Prettify(raw string, indent string) (pretty string, e error) {
    r := strings.NewReader(raw)
    z := html.NewTokenizer(r)
    pretty = ""
    depth := 0
    prevToken := html.CommentToken
    for {
        tt := z.Next()
        tokenString := string(z.Raw())

        // strip away newlines
        if tt == html.TextToken {
            stripped := strings.Trim(tokenString, "\n")
            if len(stripped) == 0 {
                continue
            }
        }

        if tt == html.EndTagToken {
            depth -= 1
        }

        if tt != html.TextToken {
            if prevToken != html.TextToken {
                pretty += "\n"
                for i := 0; i < depth; i++ {
                    pretty += indent
                }
            }
        }

        pretty += tokenString

        // last token
        if tt == html.ErrorToken {
            break
        } else if tt == html.StartTagToken {
            depth += 1
        }
        prevToken = tt
    }
    return strings.Trim(pretty, "\n"), nil
}

它可以处理像你提供的那个简单示例一样的内容。例如:

html := `<!DOCTYPE html><html><head>
<title>Website Title</title>
</head><body>
<div class="random-class">
<h1>I like pie</h1><p>It's true!</p></div>
</body></html>`
pretty, _ := Prettify(html, "    ")
fmt.Println(pretty)

将会打印出以下内容:
<!DOCTYPE html>
<html>
    <head>
        <title>Website Title</title>
    </head>
    <body>
        <div class="random-class">
            <h1>I like pie</h1>
            <p>It's true!</p>
        </div>
    </body>
</html>

请注意,这种简单的方法尚无法处理HTML注释,也无法处理非XHTML兼容的完全有效的自闭合HTML5标签(例如<br>),在应该保留空格时,空格不能保证被保留,还有许多其他我尚未考虑到的边缘情况。仅将其用作参考、玩具或起点 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接