Skip to content

Commit

Permalink
markup/asciidocext: Fix AsciiDoc TOC with code
Browse files Browse the repository at this point in the history
Fixes #7649
  • Loading branch information
helfper authored and bep committed Sep 10, 2020
1 parent 746ba80 commit 6a848cb
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 59 deletions.
4 changes: 2 additions & 2 deletions docs/content/en/content-management/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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`.

Expand Down
8 changes: 4 additions & 4 deletions docs/content/en/content-management/toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand All @@ -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/
Expand Down
86 changes: 43 additions & 43 deletions markup/asciidocext/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
// 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"

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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.
Expand Down
48 changes: 44 additions & 4 deletions markup/asciidocext/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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()})
Expand Down Expand Up @@ -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)
}
12 changes: 6 additions & 6 deletions markup/tableofcontents/tableofcontents.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
}
Expand Down

0 comments on commit 6a848cb

Please sign in to comment.