【问题标题】:Go: How would you "Pretty Print"/"Prettify" HTML?Go:你会如何“Pretty Print”/“Prettify” HTML?
【发布时间】:2014-02-02 17:25:29
【问题描述】:

在 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)
}

但是,这仅适用于将结构/接口转换为 [] 字节。我想要的是转换一串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>

如何仅使用字符串来完成?

【问题讨论】:

    标签: html xml xml-parsing go pretty-print


    【解决方案1】:

    我在尝试弄清楚如何在 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
            }
        }
    }
    

    【讨论】:

    • 我喜欢这个解决方案,但我仍在寻找不会重写文档(除了格式化空格)的 Golang XML 格式化程序/漂亮打印机。编组或使用编码器将更改命名空间声明。例如,像“”这样的元素将被翻译成像“”这样的东西,这似乎是无害的,除非是为了不改变格式而不是改变xml。跨度>
    • @JamesMcGill,对于不重写文档的 Golang XML 格式化程序/prettyprinter,请查看 github.com/go-xmlfmt/xmlfmt。我和你有同样的痛苦。 :-)
    【解决方案2】:

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

    这里是:

    GoHTML - HTML formatter for Go

    请检查这个包。

    谢谢,

    庆次

    【讨论】:

    • 非常感谢您。我自己仍在开发中的实现不如你的那么好,所以我将使用你的。
    • @KeijiYoshida 将漂亮的程序嵌入到从标准输入读取并写入标准输出的独立二进制文件中的最佳方法是什么?
    【解决方案3】:

    您可以使用 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)
        }
    }
    

    【讨论】:

      【解决方案4】:

      简答

      使用HTML prettyprint library for Go(我写的,*uhum*)。它对基本输入进行了一些测试和工作,并有望随着时间的推移变得更加健壮,尽管它现在还不是很健壮。请注意自述文件中的已知问题部分。

      长答案

      使用 code.google.com/p/go.net/html 包(上面的包就是这样做的),为简单的情况滚动你自己的 HTML 美化器相当容易。下面是一个非常简单的 Prettify 函数,以这种方式实现:

      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 cmets,也不能处理不符合 XHTML 的完全有效的自闭合 HTML5 标签,例如&lt;br&gt;,不能保证在应该保留空白时保留,以及我还没有想到的一系列其他边缘情况。仅将其用作参考、玩具或起点:)

      【讨论】:

        【解决方案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 模块支持非严格、自动关闭、草率解析。
        • 这似乎是正确的轨道 - 一个通用的 xml/html 解组器。但是,我敢打赌,如果我不能让属性工作,我将不得不制作自己的 Pretty-Print 解析器。
        • @GingerBill 我试图让属性工作但无法使用此方案。我什至编写了一个自定义解析器,在上面的Node 结构中捕获它们,但是序列化程序没有正确序列化。但我没有探索的是xml 模块公开的用于序列化属性的接口。
        • @GingerBill 我尝试过的另一种方法确实有效,但我认为它不可扩展,是添加我想要支持的所有已知属性(src、href、id、类等......)到 Node 结构,并将 xml:",attr,omitempty" 添加到字段中。如果它们在结构中不存在并且一切正常,这会导致它们被隐藏,但是它不再是通用的并且不支持未知属性。
        • @Not_a_Golfer 我打算做后一种解决方案,但查看 html 属性的数量,这可能会很耗时(大约 100 个)。
        猜你喜欢
        • 1970-01-01
        • 2013-08-20
        • 1970-01-01
        • 1970-01-01
        • 2014-11-25
        • 1970-01-01
        • 2016-03-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多