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

@@ -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"
@@ -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.

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

View 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{})
}