markup/asciidocext: Fix AsciiDoc TOC with code
authorHelder Pereira <helfper@gmail.com>
Wed, 9 Sep 2020 21:41:53 +0000 (22:41 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 10 Sep 2020 19:53:13 +0000 (21:53 +0200)
Fixes #7649

docs/content/en/content-management/formats.md
docs/content/en/content-management/toc.md
markup/asciidocext/convert.go
markup/asciidocext/convert_test.go
markup/tableofcontents/tableofcontents.go

index 94d7e0f17312415853f8d9794b47feb8310f08f0..f8ccffefd9feb495c102754106a55bcf487827a0 100644 (file)
@@ -49,7 +49,7 @@ tool on your machine to be able to use these formats.
 
 Hugo passes reasonable default arguments to these external helpers by default:
 
-- `asciidoctor`: `--no-header-footer --trace -`
+- `asciidoctor`: `--no-header-footer -`
 - `rst2html`: `--leave-comments --initial-header-level=2`
 - `pandoc`: `--mathjax`
 
@@ -81,7 +81,7 @@ noheaderorfooter | true | Output an embeddable document, which excludes the head
 safemode | `unsafe` | Safe mode level `unsafe`, `safe`, `server` or `secure`. Don't change this unless you know what you are doing.
 sectionnumbers | `false` | Auto-number section titles.
 verbose | `false` | Verbosely print processing information and configuration file checks to stderr.
-trace | `true` | Include backtrace information on errors.
+trace | `false` | Include backtrace information on errors.
 failurelevel | `fatal` | The minimum logging level that triggers a non-zero exit code (failure).
 workingfoldercurrent | `false` | Set the working folder to the rendered `adoc` file, so [include](https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files) will work with relative paths. This setting uses the `asciidoctor` cli parameter `--base-dir` and attribute `outdir=`. For rendering [asciidoctor-diagram](https://asciidoctor.org/docs/asciidoctor-diagram/)  `workingfoldercurrent` must be set to `true`.
 
index efc47b4b8b723458dba1bd362cb311c4f4dde71c..bee5a587bad23a3f31b42ece255b1213acb1ee16 100644 (file)
@@ -92,11 +92,11 @@ The following is a [partial template][partials] that adds slightly more logic fo
 With the preceding example, even pages with > 400 words *and* `toc` not set to `false` will not render a table of contents if there are no headings in the page for the `{{.TableOfContents}}` variable to pull from.
 {{% /note %}}
 
-## Usage with asciidoc
+## Usage with AsciiDoc
 
-Hugo supports table of contents with Asciidoc content format.
+Hugo supports table of contents with AsciiDoc content format.
 
-In the header of your content file, specify the Asciidoc TOC directives, by using the macro style:
+In the header of your content file, specify the AsciiDoc TOC directives, by using the macro or auto style:
 
 ```asciidoc
 // <!-- Your front matter up here -->
@@ -117,7 +117,7 @@ He lay on his armour-like back, and if he lifted his head a little he could see
 
 A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame. It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops
 ```
-Hugo will take this Asciddoc and create a table of contents store it in the page variable `.TableOfContents`, in the same as described for Markdown.
+Hugo will take this AsciiDoc and create a table of contents store it in the page variable `.TableOfContents`, in the same as described for Markdown.
 
 [conditionals]: /templates/introduction/#conditionals
 [front matter]: /content-management/front-matter/
index 73ed85daafefbacace452b50fa2b897630fd5aed..c337131d6153ca463e5f4f5d05dc89e1cc7ec097 100644 (file)
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package asciidocext converts Asciidoc to HTML using Asciidoc or Asciidoctor
-// external binaries. The `asciidoc` module is reserved for a future golang
+// Package asciidocext converts AsciiDoc to HTML using Asciidoctor
+// external binary. The `asciidoc` module is reserved for a future golang
 // implementation.
 package asciidocext
 
 import (
        "bytes"
+       "io"
        "os/exec"
        "path/filepath"
 
@@ -77,12 +78,12 @@ func (a *asciidocConverter) Supports(_ identity.Identity) bool {
        return false
 }
 
-// getAsciidocContent calls asciidoctor or asciidoc as an external helper
+// getAsciidocContent calls asciidoctor as an external helper
 // to convert AsciiDoc content to HTML.
 func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
        path := getAsciidoctorExecPath()
        if path == "" {
-               a.cfg.Logger.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n",
+               a.cfg.Logger.ERROR.Println("asciidoctor not found in $PATH: Please install.\n",
                        "                 Leaving AsciiDoc content unrendered.")
                return src
        }
@@ -216,30 +217,21 @@ func extractTOC(src []byte) ([]byte, tableofcontents.Root, error) {
                toVisit []*html.Node
        )
        f = func(n *html.Node) bool {
-               if n.Type == html.ElementNode && n.Data == "div" {
-                       for _, a := range n.Attr {
-                               if a.Key == "id" && a.Val == "toc" {
-                                       toc, err = parseTOC(n)
-                                       if err != nil {
-                                               return false
-                                       }
-                                       n.Parent.RemoveChild(n)
-                                       return true
-                               }
-                       }
+               if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" {
+                       toc = parseTOC(n)
+                       n.Parent.RemoveChild(n)
+                       return true
                }
                if n.FirstChild != nil {
                        toVisit = append(toVisit, n.FirstChild)
                }
-               if n.NextSibling != nil {
-                       if ok := f(n.NextSibling); ok {
-                               return true
-                       }
+               if n.NextSibling != nil && f(n.NextSibling) {
+                       return true
                }
                for len(toVisit) > 0 {
                        nv := toVisit[0]
                        toVisit = toVisit[1:]
-                       if ok := f(nv); ok {
+                       if f(nv) {
                                return true
                        }
                }
@@ -261,50 +253,58 @@ func extractTOC(src []byte) ([]byte, tableofcontents.Root, error) {
 }
 
 // parseTOC returns a TOC root from the given toc Node
-func parseTOC(doc *html.Node) (tableofcontents.Root, error) {
+func parseTOC(doc *html.Node) tableofcontents.Root {
        var (
                toc tableofcontents.Root
                f   func(*html.Node, int, int)
        )
-       f = func(n *html.Node, parent, level int) {
+       f = func(n *html.Node, row, level int) {
                if n.Type == html.ElementNode {
                        switch n.Data {
                        case "ul":
                                if level == 0 {
-                                       parent += 1
+                                       row++
                                }
-                               level += 1
-                               f(n.FirstChild, parent, level)
+                               level++
+                               f(n.FirstChild, row, level)
                        case "li":
                                for c := n.FirstChild; c != nil; c = c.NextSibling {
                                        if c.Type != html.ElementNode || c.Data != "a" {
                                                continue
                                        }
-                                       var href string
-                                       for _, a := range c.Attr {
-                                               if a.Key == "href" {
-                                                       href = a.Val[1:]
-                                                       break
-                                               }
-                                       }
-                                       for d := c.FirstChild; d != nil; d = d.NextSibling {
-                                               if d.Type == html.TextNode {
-                                                       toc.AddAt(tableofcontents.Header{
-                                                               Text: d.Data,
-                                                               ID:   href,
-                                                       }, parent, level)
-                                               }
-                                       }
+                                       href := attr(c, "href")[1:]
+                                       toc.AddAt(tableofcontents.Header{
+                                               Text: nodeContent(c),
+                                               ID:   href,
+                                       }, row, level)
                                }
-                               f(n.FirstChild, parent, level)
+                               f(n.FirstChild, row, level)
                        }
                }
                if n.NextSibling != nil {
-                       f(n.NextSibling, parent, level)
+                       f(n.NextSibling, row, level)
                }
        }
        f(doc.FirstChild, 0, 0)
-       return toc, nil
+       return toc
+}
+
+func attr(node *html.Node, key string) string {
+       for _, a := range node.Attr {
+               if a.Key == key {
+                       return a.Val
+               }
+       }
+       return ""
+}
+
+func nodeContent(node *html.Node) string {
+       var buf bytes.Buffer
+       w := io.Writer(&buf)
+       for c := node.FirstChild; c != nil; c = c.NextSibling {
+               html.Render(w, c)
+       }
+       return buf.String()
 }
 
 // Supports returns whether Asciidoctor is installed on this computer.
index c9c8ee4fed342b7440c454173128d992cc455bea..eb38d2d7bffa7f159b15c1d0b3c146e328577e54 100644 (file)
@@ -11,8 +11,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package asciidocext converts Asciidoc to HTML using Asciidoc or Asciidoctor
-// external binaries. The `asciidoc` module is reserved for a future golang
+// Package asciidocext converts AsciiDoc to HTML using Asciidoctor
+// external binary. The `asciidoc` module is reserved for a future golang
 // implementation.
 
 package asciidocext
@@ -24,6 +24,7 @@ import (
        "github.com/gohugoio/hugo/common/loggers"
        "github.com/gohugoio/hugo/markup/converter"
        "github.com/gohugoio/hugo/markup/markup_config"
+       "github.com/gohugoio/hugo/markup/tableofcontents"
        "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
@@ -250,7 +251,7 @@ func TestAsciidoctorAttributes(t *testing.T) {
 
 func TestConvert(t *testing.T) {
        if !Supports() {
-               t.Skip("asciidoc/asciidoctor not installed")
+               t.Skip("asciidoctor not installed")
        }
        c := qt.New(t)
 
@@ -273,7 +274,7 @@ func TestConvert(t *testing.T) {
 
 func TestTableOfContents(t *testing.T) {
        if !Supports() {
-               t.Skip("asciidoc/asciidoctor not installed")
+               t.Skip("asciidoctor not installed")
        }
        c := qt.New(t)
        p, err := Provider.New(converter.ProviderConfig{Logger: loggers.NewErrorLogger()})
@@ -305,3 +306,42 @@ testContent
        c.Assert(root.ToHTML(2, 4, false), qt.Equals, "<nav id=\"TableOfContents\">\n  <ul>\n    <li><a href=\"#_introduction\">Introduction</a></li>\n    <li><a href=\"#_section_1\">Section 1</a>\n      <ul>\n        <li><a href=\"#_section_1_1\">Section 1.1</a>\n          <ul>\n            <li><a href=\"#_section_1_1_1\">Section 1.1.1</a></li>\n          </ul>\n        </li>\n        <li><a href=\"#_section_1_2\">Section 1.2</a></li>\n      </ul>\n    </li>\n    <li><a href=\"#_section_2\">Section 2</a></li>\n  </ul>\n</nav>")
        c.Assert(root.ToHTML(2, 3, false), qt.Equals, "<nav id=\"TableOfContents\">\n  <ul>\n    <li><a href=\"#_introduction\">Introduction</a></li>\n    <li><a href=\"#_section_1\">Section 1</a>\n      <ul>\n        <li><a href=\"#_section_1_1\">Section 1.1</a></li>\n        <li><a href=\"#_section_1_2\">Section 1.2</a></li>\n      </ul>\n    </li>\n    <li><a href=\"#_section_2\">Section 2</a></li>\n  </ul>\n</nav>")
 }
+
+func TestTableOfContentsWithCode(t *testing.T) {
+       if !Supports() {
+               t.Skip("asciidoctor not installed")
+       }
+       c := qt.New(t)
+       mconf := markup_config.Default
+       p, err := Provider.New(
+               converter.ProviderConfig{
+                       MarkupConfig: mconf,
+                       Logger:       loggers.NewErrorLogger(),
+               },
+       )
+       c.Assert(err, qt.IsNil)
+       conv, err := p.New(converter.DocumentContext{})
+       c.Assert(err, qt.IsNil)
+       b, err := conv.Convert(converter.RenderContext{Src: []byte(`:toc: auto
+
+== Some ` + "`code`" + ` in the title
+`)})
+       c.Assert(err, qt.IsNil)
+       toc, ok := b.(converter.TableOfContentsProvider)
+       c.Assert(ok, qt.Equals, true)
+       expected := tableofcontents.Headers{
+               {},
+               {
+                       ID:   "",
+                       Text: "",
+                       Headers: tableofcontents.Headers{
+                               {
+                                       ID:      "_some_code_in_the_title",
+                                       Text:    "Some <code>code</code> in the title",
+                                       Headers: nil,
+                               },
+                       },
+               },
+       }
+       c.Assert(toc.TableOfContents().Headers, qt.DeepEquals, expected)
+}
index 7803100836fb1b30391e175cafc4db0c21189ec0..9f11242334f4933437a7b14c6048940b93516d36 100644 (file)
@@ -40,19 +40,19 @@ type Root struct {
 }
 
 // AddAt adds the header into the given location.
-func (toc *Root) AddAt(h Header, y, x int) {
-       for i := len(toc.Headers); i <= y; i++ {
+func (toc *Root) AddAt(h Header, row, level int) {
+       for i := len(toc.Headers); i <= row; i++ {
                toc.Headers = append(toc.Headers, Header{})
        }
 
-       if x == 0 {
-               toc.Headers[y] = h
+       if level == 0 {
+               toc.Headers[row] = h
                return
        }
 
-       header := &toc.Headers[y]
+       header := &toc.Headers[row]
 
-       for i := 1; i < x; i++ {
+       for i := 1; i < level; i++ {
                if len(header.Headers) == 0 {
                        header.Headers = append(header.Headers, Header{})
                }