all repos — htmlformat @ bdff660c6c36e8df4fcf982c1ace4c35a8aa1c49

Go package and CLI tool used to format HTML

feat: first commit

Adrian Hesketh
commit

bdff660c6c36e8df4fcf982c1ace4c35a8aa1c49

7 files changed, 341 insertions(+), 0 deletions(-)

changed files
A LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT) + +Copyright (c) 2014 Eric Chiang +Copyright (c) 2021 Adrian Hesketh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.
A README.md
@@ -0,0 +1,45 @@
+# htmlformat + +htmlformat is a Go package and CLI tool used to format HTML. + +It is forked and simplified from the https://github.com/ericchiang/pup package. + +It does not aim to: + +* Colorize output. +* Modify the input HTML except for formatting (i.e. no HTML escaping will be applied). +* Provide any facilities to query the content. + +## Installation + +To use the CLI, you can install with Go > 1.20. + +``` +go install github.com/a-h/htmlformat/cmd/htmlformat@latest +``` + +## Usage + +### CLI + +```bash +echo '<ol><li style="&">A</li><li>B</li></ol>' | htmlformat +<ol> + <li style="&"> + A + </li> + <li> + B + </li> +</ol> +``` + +### Package + +```go +r := strings.NewReader(`<ol><li style="&">A</li><li>B</li></ol>`) +w := os.Stdout +if err := Fragment(w, r); err != nil { + log.Fatalf("failed to format: %v", err) +} +```
A cmd/htmlformat/main.go
@@ -0,0 +1,25 @@
+package main + +import ( + "flag" + "log" + "os" + + "github.com/a-h/htmlformat" +) + +var parseDocumentFlag = flag.Bool("document", false, "Set to true to parse a whole document") + +func main() { + flag.Parse() + + var err error + if *parseDocumentFlag { + err = htmlformat.Document(os.Stdout, os.Stdin) + } else { + err = htmlformat.Fragment(os.Stdout, os.Stdin) + } + if err != nil { + log.Fatalf("failed to format: %v", err) + } +}
A format.go
@@ -0,0 +1,187 @@
+package htmlformat + +import ( + "fmt" + "io" + "strings" + + "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 <pre> 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 := 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, "<!--%s-->\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 <meta> or <br>? +// 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 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 err = printIndent(w, level); err != nil { + return + } + if _, err = fmt.Fprintln(w, s); 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 { + if _, err = fmt.Fprintf(w, ` %s="%s"`, a.Key, a.Val); err != nil { + return + } + } + if _, err = fmt.Fprintln(w, ">"); err != nil { + return + } + if !isVoidElement(n) { + if err = printChildren(w, n, level+1); err != nil { + return + } + if err = printIndent(w, level); err != nil { + return + } + if _, err = fmt.Fprintf(w, "</%s>\n", n.Data); err != nil { + return + } + } + case html.CommentNode: + if err = printIndent(w, level); err != nil { + return + } + if _, err = fmt.Fprintf(w, "<!--%s-->\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) { + for ; level > 0; level-- { + if _, err = fmt.Fprint(w, " "); err != nil { + return + } + } + return +}
A format_test.go
@@ -0,0 +1,51 @@
+package htmlformat + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestFormat(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "html elements are indented", + input: `<ol> <li style="&"> A </li> <li> B </li> </ol> `, + expected: `<ol> + <li style="&"> + A + </li> + <li> + B + </li> +</ol> +`, + }, + { + name: "text fragments are supported", + input: `test 123`, + expected: `test 123` + "\n", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + r := strings.NewReader(test.input) + w := new(strings.Builder) + if err := Fragment(w, r); err != nil { + t.Fatalf("failed to format: %v", err) + } + if diff := cmp.Diff(test.expected, w.String()); diff != "" { + t.Error(diff) + } + }) + } +}
A go.mod
@@ -0,0 +1,7 @@
+module github.com/a-h/htmlformat + +go 1.20 + +require golang.org/x/net v0.9.0 + +require github.com/google/go-cmp v0.5.9
A go.sum
@@ -0,0 +1,4 @@
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=