diff options
Diffstat (limited to 'markup/asciidocext')
-rw-r--r-- | markup/asciidocext/convert.go | 129 | ||||
-rw-r--r-- | markup/asciidocext/convert_test.go | 35 |
2 files changed, 162 insertions, 2 deletions
diff --git a/markup/asciidocext/convert.go b/markup/asciidocext/convert.go index 7dd3e3533..73ed85daa 100644 --- a/markup/asciidocext/convert.go +++ b/markup/asciidocext/convert.go @@ -17,6 +17,7 @@ package asciidocext import ( + "bytes" "os/exec" "path/filepath" @@ -24,6 +25,8 @@ import ( "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config" "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/internal" + "github.com/gohugoio/hugo/markup/tableofcontents" + "golang.org/x/net/html" ) /* ToDo: RelPermalink patch for svg posts not working*/ @@ -45,16 +48,32 @@ func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) }), nil } +type asciidocResult struct { + converter.Result + toc tableofcontents.Root +} + +func (r asciidocResult) TableOfContents() tableofcontents.Root { + return r.toc +} + type asciidocConverter struct { ctx converter.DocumentContext cfg converter.ProviderConfig } func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) { - return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil + content, toc, err := extractTOC(a.getAsciidocContent(ctx.Src, a.ctx)) + if err != nil { + return nil, err + } + return asciidocResult{ + Result: converter.Bytes(content), + toc: toc, + }, nil } -func (c *asciidocConverter) Supports(feature identity.Identity) bool { +func (a *asciidocConverter) Supports(_ identity.Identity) bool { return false } @@ -182,6 +201,112 @@ func getAsciidoctorExecPath() string { return path } +// extractTOC extracts the toc from the given src html. +// It returns the html without the TOC, and the TOC data +func extractTOC(src []byte) ([]byte, tableofcontents.Root, error) { + var buf bytes.Buffer + buf.Write(src) + node, err := html.Parse(&buf) + if err != nil { + return nil, tableofcontents.Root{}, err + } + var ( + f func(*html.Node) bool + toc tableofcontents.Root + 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.FirstChild != nil { + toVisit = append(toVisit, n.FirstChild) + } + if n.NextSibling != nil { + if ok := f(n.NextSibling); ok { + return true + } + } + for len(toVisit) > 0 { + nv := toVisit[0] + toVisit = toVisit[1:] + if ok := f(nv); ok { + return true + } + } + return false + } + f(node) + if err != nil { + return nil, tableofcontents.Root{}, err + } + buf.Reset() + err = html.Render(&buf, node) + if err != nil { + return nil, tableofcontents.Root{}, err + } + // ltrim <html><head></head><body> and rtrim </body></html> which are added by html.Render + res := buf.Bytes()[25:] + res = res[:len(res)-14] + return res, toc, nil +} + +// parseTOC returns a TOC root from the given toc Node +func parseTOC(doc *html.Node) (tableofcontents.Root, error) { + var ( + toc tableofcontents.Root + f func(*html.Node, int, int) + ) + f = func(n *html.Node, parent, level int) { + if n.Type == html.ElementNode { + switch n.Data { + case "ul": + if level == 0 { + parent += 1 + } + level += 1 + f(n.FirstChild, parent, 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) + } + } + } + f(n.FirstChild, parent, level) + } + } + if n.NextSibling != nil { + f(n.NextSibling, parent, level) + } + } + f(doc.FirstChild, 0, 0) + return toc, nil +} + // Supports returns whether Asciidoctor is installed on this computer. func Supports() bool { return getAsciidoctorExecPath() != "" diff --git a/markup/asciidocext/convert_test.go b/markup/asciidocext/convert_test.go index 22ac8e557..1c4b77bb9 100644 --- a/markup/asciidocext/convert_test.go +++ b/markup/asciidocext/convert_test.go @@ -270,3 +270,38 @@ func TestConvert(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(string(b.Bytes()), qt.Equals, "<div class=\"paragraph\">\n<p>testContent</p>\n</div>\n") } + +func TestTableOfContents(t *testing.T) { + if !Supports() { + t.Skip("asciidoc/asciidoctor not installed") + } + c := qt.New(t) + p, err := Provider.New(converter.ProviderConfig{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: macro +:toclevels: 4 +toc::[] + +=== Introduction + +== Section 1 + +=== Section 1.1 + +==== Section 1.1.1 + +=== Section 1.2 + +testContent + +== Section 2 +`)}) + c.Assert(err, qt.IsNil) + toc, ok := b.(converter.TableOfContentsProvider) + c.Assert(ok, qt.Equals, true) + root := toc.TableOfContents() + 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>") +} |