diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index b28fa9824e..35ecc77b5a 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -86,8 +86,10 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa tocNode := createTOCNode(tocList, rc.Lang, nil) node.InsertBefore(node, firstChild, tocNode) } else { - tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"}) - ctx.SidebarTocNode = tocNode + ctx.SidebarTocHeaders = make([]markup.Header, len(tocList)) + for i, h := range tocList { + ctx.SidebarTocHeaders[i] = markup.Header{Level: h.Level, Text: h.Text, ID: h.ID} + } } } diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 93c335d244..c015d52636 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -96,7 +96,16 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error w := &orgWriter{rctx: ctx, HTMLWriter: htmlWriter} htmlWriter.ExtendingWriter = w - res, err := org.New().Silent().Parse(input, "").Write(w) + // Parse the document first to extract outline for TOC + doc := org.New().Silent().Parse(input, "") + if doc.Error != nil { + return fmt.Errorf("orgmode.Parse failed: %w", doc.Error) + } + + // Extract headers from the document outline for sidebar TOC + ctx.SidebarTocHeaders = extractHeadersFromOutline(doc.Outline) + + res, err := doc.Write(w) if err != nil { return fmt.Errorf("orgmode.Render failed: %w", err) } @@ -104,6 +113,37 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error return err } +// extractHeadersFromOutline recursively extracts headers from org document outline +func extractHeadersFromOutline(outline org.Outline) []markup.Header { + var headers []markup.Header + collectHeaders(outline.Section, &headers) + return headers +} + +// collectHeaders recursively collects headers from sections +func collectHeaders(section *org.Section, headers *[]markup.Header) { + if section == nil { + return + } + + // Process current section's headline + if section.Headline != nil { + h := section.Headline + // Convert headline title nodes to plain text + titleText := org.String(h.Title...) + *headers = append(*headers, markup.Header{ + Level: h.Lvl, + Text: titleText, + ID: h.ID(), + }) + } + + // Process child sections + for _, child := range section.Children { + collectHeaders(child, headers) + } +} + // RenderString renders orgmode string to HTML string func RenderString(ctx *markup.RenderContext, content string) (string, error) { var buf strings.Builder diff --git a/modules/markup/render.go b/modules/markup/render.go index 12f002b0c6..f09aea8e0b 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - "github.com/yuin/goldmark/ast" "golang.org/x/sync/errgroup" ) @@ -36,6 +35,13 @@ var RenderBehaviorForTesting struct { DisableAdditionalAttributes bool } +// Header holds the data about a header for generating TOC +type Header struct { + Level int + Text string + ID string +} + type RenderOptions struct { UseAbsoluteLink bool @@ -67,7 +73,7 @@ type RenderContext struct { // the context might be used by the "render" function, but it might also be used by "postProcess" function usedByRender bool - SidebarTocNode ast.Node + SidebarTocHeaders []Header // Headers for generating sidebar TOC RenderHelper RenderHelper RenderOptions RenderOptions diff --git a/modules/markup/sidebar_toc.go b/modules/markup/sidebar_toc.go new file mode 100644 index 0000000000..441e540d6a --- /dev/null +++ b/modules/markup/sidebar_toc.go @@ -0,0 +1,77 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "html" + "html/template" + "net/url" + "strings" + + "code.gitea.io/gitea/modules/translation" +) + +// RenderSidebarTocHTML renders a list of headers into HTML for sidebar TOC display. +// It generates a
element with nested