Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions renderers/markdown_toc.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ func processTOC(content []byte, opts *TOCOptions) ([]byte, error) {
return content, nil
}

// Generate the TOC HTML
toc, err := generateTOC(content, opts)
// Generate the TOC HTML from the same DOM tree
// This ensures that {:.no_toc} paragraphs removed by extractHeadings()
// are also removed from the final output
toc, err := generateTOCFromDoc(doc, opts)
if err != nil {
return nil, err
}
Expand All @@ -109,9 +111,8 @@ func processTOC(content []byte, opts *TOCOptions) ([]byte, error) {
// Extract the body content (html.Render wraps in <html><head></head><body>...</body></html>)
result := extractBodyContent(buf.Bytes())

// Note: We don't remove {:.no_toc} markers from the final output
// They remain as literal text when inside headings/paragraphs
// Only {:.no_toc} in sibling paragraphs (IAL markers) are removed during TOC generation
// Note: {:.no_toc} in sibling paragraphs (IAL markers) are removed by extractHeadings()
// during TOC generation. Literal {:.no_toc} text inside headings remains in the output.

return result, nil
}
Expand All @@ -137,15 +138,11 @@ func shouldProcessTOC(content []byte) bool {
return false
}

// generateTOC parses HTML content and creates a table of contents
func generateTOC(content []byte, opts *TOCOptions) (string, error) {
// Parse the HTML document
doc, err := html.Parse(bytes.NewReader(content))
if err != nil {
return "", err
}

// Extract headings
// generateTOCFromDoc creates a table of contents from an already-parsed HTML document
// This allows the caller to reuse the DOM and benefit from any modifications made
// during heading extraction (e.g., removal of {:.no_toc} paragraphs)
func generateTOCFromDoc(doc *html.Node, opts *TOCOptions) (string, error) {
// Extract headings (this also removes {:.no_toc} sibling paragraphs from the DOM)
headings := extractHeadings(doc)

// Filter headings by level if opts is provided
Expand Down
56 changes: 56 additions & 0 deletions renderers/markdown_toc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,62 @@ func TestNoTocInline(t *testing.T) {
}
}

// TestNoTocParagraphRemoval tests that {:.no_toc} sibling paragraphs are removed from output
// This is a regression test for issue #100
func TestNoTocParagraphRemoval(t *testing.T) {
markdown := `* TOC
{:toc}

## White/wheat loaf

## Should not appear
{:.no_toc}

## Focaccia

## Ciabatta`

html, err := renderMarkdown([]byte(markdown))
if err != nil {
t.Fatalf("Error rendering markdown: %v", err)
}

htmlStr := string(html)

// The {:.no_toc} paragraph should be completely removed from output
if containsString(htmlStr, "{:.no_toc}") {
t.Error("{:.no_toc} marker should be removed from output, but found it")
}

// The excluded heading should NOT appear in the TOC
tocStart := strings.Index(htmlStr, "<ul id=\"markdown-toc\">")
tocEnd := strings.Index(htmlStr, "</ul>")
if tocStart == -1 || tocEnd == -1 {
t.Fatal("Could not find TOC in output")
}
tocContent := htmlStr[tocStart : tocEnd+5]

if containsString(tocContent, "Should not appear") {
t.Error("Heading marked with {:.no_toc} should not appear in TOC")
}

// But the excluded heading should still appear in the document body
if !containsString(htmlStr, "<h2") || !containsString(htmlStr, "Should not appear") {
t.Error("Heading should still appear in document body, just not in TOC")
}

// Other headings should appear in TOC
if !containsString(tocContent, "White/wheat loaf") {
t.Error("White/wheat loaf should appear in TOC")
}
if !containsString(tocContent, "Focaccia") {
t.Error("Focaccia should appear in TOC")
}
if !containsString(tocContent, "Ciabatta") {
t.Error("Ciabatta should appear in TOC")
}
}

// Helper function to check if a string contains a substring
func containsString(s, substr string) bool {
return strings.Contains(s, substr)
Expand Down
2 changes: 1 addition & 1 deletion renderers/renderers.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (p *Manager) getTOCOptions() *TOCOptions {
opts := &TOCOptions{
MinLevel: 2,
MaxLevel: 6,
UseJekyllHTML: false,
UseJekyllHTML: true,
}

// Check for kramdown configuration
Expand Down