markup/asciidocext: Fix AsciiDoc TOC with code

Fixes #7649
This commit is contained in:
Helder Pereira 2020-09-09 22:41:53 +01:00 committed by Bjørn Erik Pedersen
parent 746ba803af
commit 6a848cbc3a
5 changed files with 99 additions and 59 deletions

View 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: 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` - `rst2html`: `--leave-comments --initial-header-level=2`
- `pandoc`: `--mathjax` - `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. 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. sectionnumbers | `false` | Auto-number section titles.
verbose | `false` | Verbosely print processing information and configuration file checks to stderr. 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). 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`. 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`.

View 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. 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 %}} {{% /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 ```asciidoc
// <!-- Your front matter up here --> // <!-- 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 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 [conditionals]: /templates/introduction/#conditionals
[front matter]: /content-management/front-matter/ [front matter]: /content-management/front-matter/

View file

@ -11,13 +11,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package asciidocext converts Asciidoc to HTML using Asciidoc or Asciidoctor // Package asciidocext converts AsciiDoc to HTML using Asciidoctor
// external binaries. The `asciidoc` module is reserved for a future golang // external binary. The `asciidoc` module is reserved for a future golang
// implementation. // implementation.
package asciidocext package asciidocext
import ( import (
"bytes" "bytes"
"io"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -77,12 +78,12 @@ func (a *asciidocConverter) Supports(_ identity.Identity) bool {
return false return false
} }
// getAsciidocContent calls asciidoctor or asciidoc as an external helper // getAsciidocContent calls asciidoctor as an external helper
// to convert AsciiDoc content to HTML. // to convert AsciiDoc content to HTML.
func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte { func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
path := getAsciidoctorExecPath() path := getAsciidoctorExecPath()
if path == "" { 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.") " Leaving AsciiDoc content unrendered.")
return src return src
} }
@ -216,30 +217,21 @@ func extractTOC(src []byte) ([]byte, tableofcontents.Root, error) {
toVisit []*html.Node toVisit []*html.Node
) )
f = func(n *html.Node) bool { f = func(n *html.Node) bool {
if n.Type == html.ElementNode && n.Data == "div" { if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" {
for _, a := range n.Attr { toc = parseTOC(n)
if a.Key == "id" && a.Val == "toc" {
toc, err = parseTOC(n)
if err != nil {
return false
}
n.Parent.RemoveChild(n) n.Parent.RemoveChild(n)
return true return true
} }
}
}
if n.FirstChild != nil { if n.FirstChild != nil {
toVisit = append(toVisit, n.FirstChild) toVisit = append(toVisit, n.FirstChild)
} }
if n.NextSibling != nil { if n.NextSibling != nil && f(n.NextSibling) {
if ok := f(n.NextSibling); ok {
return true return true
} }
}
for len(toVisit) > 0 { for len(toVisit) > 0 {
nv := toVisit[0] nv := toVisit[0]
toVisit = toVisit[1:] toVisit = toVisit[1:]
if ok := f(nv); ok { if f(nv) {
return true return true
} }
} }
@ -261,50 +253,58 @@ func extractTOC(src []byte) ([]byte, tableofcontents.Root, error) {
} }
// parseTOC returns a TOC root from the given toc Node // 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 ( var (
toc tableofcontents.Root toc tableofcontents.Root
f func(*html.Node, int, int) 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 { if n.Type == html.ElementNode {
switch n.Data { switch n.Data {
case "ul": case "ul":
if level == 0 { if level == 0 {
parent += 1 row++
} }
level += 1 level++
f(n.FirstChild, parent, level) f(n.FirstChild, row, level)
case "li": case "li":
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type != html.ElementNode || c.Data != "a" { if c.Type != html.ElementNode || c.Data != "a" {
continue continue
} }
var href string href := attr(c, "href")[1:]
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{ toc.AddAt(tableofcontents.Header{
Text: d.Data, Text: nodeContent(c),
ID: href, ID: href,
}, parent, level) }, row, level)
} }
} f(n.FirstChild, row, level)
}
f(n.FirstChild, parent, level)
} }
} }
if n.NextSibling != nil { if n.NextSibling != nil {
f(n.NextSibling, parent, level) f(n.NextSibling, row, level)
} }
} }
f(doc.FirstChild, 0, 0) 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. // Supports returns whether Asciidoctor is installed on this computer.

View file

@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package asciidocext converts Asciidoc to HTML using Asciidoc or Asciidoctor // Package asciidocext converts AsciiDoc to HTML using Asciidoctor
// external binaries. The `asciidoc` module is reserved for a future golang // external binary. The `asciidoc` module is reserved for a future golang
// implementation. // implementation.
package asciidocext package asciidocext
@ -24,6 +24,7 @@ import (
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/spf13/viper" "github.com/spf13/viper"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
@ -250,7 +251,7 @@ func TestAsciidoctorAttributes(t *testing.T) {
func TestConvert(t *testing.T) { func TestConvert(t *testing.T) {
if !Supports() { if !Supports() {
t.Skip("asciidoc/asciidoctor not installed") t.Skip("asciidoctor not installed")
} }
c := qt.New(t) c := qt.New(t)
@ -273,7 +274,7 @@ func TestConvert(t *testing.T) {
func TestTableOfContents(t *testing.T) { func TestTableOfContents(t *testing.T) {
if !Supports() { if !Supports() {
t.Skip("asciidoc/asciidoctor not installed") t.Skip("asciidoctor not installed")
} }
c := qt.New(t) c := qt.New(t)
p, err := Provider.New(converter.ProviderConfig{Logger: loggers.NewErrorLogger()}) 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, 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>") 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)
}

View file

@ -40,19 +40,19 @@ type Root struct {
} }
// AddAt adds the header into the given location. // AddAt adds the header into the given location.
func (toc *Root) AddAt(h Header, y, x int) { func (toc *Root) AddAt(h Header, row, level int) {
for i := len(toc.Headers); i <= y; i++ { for i := len(toc.Headers); i <= row; i++ {
toc.Headers = append(toc.Headers, Header{}) toc.Headers = append(toc.Headers, Header{})
} }
if x == 0 { if level == 0 {
toc.Headers[y] = h toc.Headers[row] = h
return 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 { if len(header.Headers) == 0 {
header.Headers = append(header.Headers, Header{}) header.Headers = append(header.Headers, Header{})
} }