gomarkdown / markdown

markdown parser and HTML renderer for Go
Other
1.41k stars 175 forks source link

limit toc level #164

Closed maltegrosse closed 3 years ago

maltegrosse commented 4 years ago

Hello, is there any possibility to limit the toc level? e.g. max 2 levels -> # headline and ## sub-headline

kjk commented 4 years ago

For HTML generation, you can use RenderNodeHook (https://github.com/gomarkdown/markdown/blob/master/html/renderer.go#L105) and over-write generation of ast.Heading (https://github.com/gomarkdown/markdown/blob/master/html/renderer.go#L977).

You can also traverse ast you get from the parser and change node.Level in ast.Heading nodes.

maltegrosse commented 4 years ago

Thank you, can u give me some pseudo code example for it?

maltegrosse commented 4 years ago

so, it was quite tricky but thanks to @Josua-SR I could manage it. it would be great if u could somehow add the toc level as an input parameter. for others:

/*
 * Custom Renderer Interface for special TOC handling
 */

type CustomHTMLRenderer struct {
    r *html.Renderer
}

func (c *CustomHTMLRenderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus {
    return c.r.RenderNode(w, node, entering)
}

func (c *CustomHTMLRenderer) RenderHeader(w io.Writer, ast ast.Node) {
    // render as usual
    c.r.RenderHeader(w, ast)
    // generate custom toc
    c.writeTOC(w, ast)

}

func (c *CustomHTMLRenderer) RenderFooter(w io.Writer, ast ast.Node) {
    c.r.RenderFooter(w, ast)
}

func newCustomHTMLRenderer(r *html.Renderer) *CustomHTMLRenderer {
    return &CustomHTMLRenderer{
        r: r,
    }
}

/*
 * forked writeTOC function
 */
func (c *CustomHTMLRenderer) writeTOC(w io.Writer, doc ast.Node) {
    buf := bytes.Buffer{}

    inHeading := false
    tocLevel := 0
    headingCount := 0

    ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
        if nodeData, ok := node.(*ast.Heading); ok && !nodeData.IsTitleblock {
            if (nodeData.Level >= 3) {
                return ast.GoToNext
            }

            inHeading = entering
            if !entering {
                buf.WriteString("</a>")
                return ast.GoToNext
            }
            if nodeData.HeadingID == "" {
                nodeData.HeadingID = fmt.Sprintf("toc_%d", headingCount)
            }
            if nodeData.Level == tocLevel {
                buf.WriteString("</li>\n\n<li>")
            } else if nodeData.Level < tocLevel {
                for nodeData.Level < tocLevel {
                    tocLevel--
                    buf.WriteString("</li>\n</ul>")
                }
                buf.WriteString("</li>\n\n<li>")
            } else {
                for nodeData.Level > tocLevel {
                    tocLevel++
                    buf.WriteString("\n<ul>\n<li>")
                }
            }

            fmt.Fprintf(&buf, `<a href="#%s">`, nodeData.HeadingID)
            headingCount++
            return ast.GoToNext
        }

        if inHeading {
            return c.r.RenderNode(&buf, node, entering)
        }

        return ast.GoToNext
    })

    for ; tocLevel > 0; tocLevel-- {
        buf.WriteString("</li>\n</ul>")
    }

    if buf.Len() > 0 {
        io.WriteString(w, "<nav>\n")
        w.Write(buf.Bytes())
        io.WriteString(w, "\n\n</nav>\n")
    }
    //c.r.lastOutputLen = buf.Len() // TODO: this is about internal state :(
}
htmlFlags := html.CommonFlags // make sure to remove the | html.TOC

opts := html.RendererOptions{Flags: htmlFlags}
renderer := newCustomHTMLRenderer(html.NewRenderer(opts))
// continue as always....

hint: html import can clash with other imports...

    "github.com/gomarkdown/markdown"
    "github.com/gomarkdown/markdown/ast"
    "github.com/gomarkdown/markdown/html"
    "github.com/gomarkdown/markdown/parser"