0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-01-24 06:25:54 +01:00

feat: introduce SidebarTocHeaders for improved sidebar TOC generation

- Add a new Header type to encapsulate header data for generating the sidebar TOC.
- Update the rendering logic to utilize SidebarTocHeaders, providing a more flexible structure for TOC generation.
- Implement extraction of headers from orgmode documents to populate SidebarTocHeaders.
- Ensure backward compatibility by maintaining the legacy SidebarTocNode for existing functionality.
This commit is contained in:
hamki 2026-01-01 09:10:15 +08:00
parent 694d5510bf
commit ed9eb57fc7
8 changed files with 145 additions and 6 deletions

View File

@ -88,6 +88,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
} else {
tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"})
ctx.SidebarTocNode = tocNode
// Also set the generic SidebarTocHeaders for the new abstraction
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}
}
}
}

View File

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

View File

@ -36,6 +36,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
@ -63,7 +70,8 @@ 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
SidebarTocNode ast.Node // Deprecated: use SidebarTocHeaders instead, keep for compatibility
SidebarTocHeaders []Header // Headers for generating sidebar TOC
RenderHelper RenderHelper
RenderOptions RenderOptions

View File

@ -0,0 +1,78 @@
// 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 <details> element with nested <ul> lists representing the header hierarchy.
func RenderSidebarTocHTML(headers []Header, lang string) template.HTML {
if len(headers) == 0 {
return ""
}
var sb strings.Builder
// Start with <details open>
sb.WriteString(`<details open>`)
sb.WriteString(`<summary>`)
sb.WriteString(html.EscapeString(translation.NewLocale(lang).TrString("toc")))
sb.WriteString(`</summary>`)
// Find the minimum level to start with
minLevel := 6
for _, header := range headers {
if header.Level < minLevel {
minLevel = header.Level
}
}
// Build nested list structure
currentLevel := minLevel
sb.WriteString(`<ul>`)
openLists := 1
for _, header := range headers {
// Close lists if we need to go up levels
for currentLevel > header.Level {
sb.WriteString(`</ul>`)
openLists--
currentLevel--
}
// Open new lists if we need to go down levels
for currentLevel < header.Level {
sb.WriteString(`<ul>`)
openLists++
currentLevel++
}
// Write the list item with link
sb.WriteString(`<li>`)
sb.WriteString(`<a href="#`)
sb.WriteString(url.QueryEscape(header.ID))
sb.WriteString(`">`)
sb.WriteString(html.EscapeString(header.Text))
sb.WriteString(`</a>`)
sb.WriteString(`</li>`)
}
// Close all remaining open lists
for openLists > 0 {
sb.WriteString(`</ul>`)
openLists--
}
sb.WriteString(`</details>`)
return template.HTML(sb.String())
}

View File

@ -180,7 +180,12 @@ func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input i
return escaped, output, err
}
func renderSidebarTocHTML(rctx *markup.RenderContext) template.HTML {
func renderSidebarTocHTML(rctx *markup.RenderContext, lang string) template.HTML {
// Prefer the new generic SidebarTocHeaders
if len(rctx.SidebarTocHeaders) > 0 {
return markup.RenderSidebarTocHTML(rctx.SidebarTocHeaders, lang)
}
// Fallback to legacy SidebarTocNode for backward compatibility
if rctx.SidebarTocNode == nil {
return ""
}

View File

@ -93,7 +93,7 @@ func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedTy
return true
}
ctx.Data["FileSidebarHTML"] = renderSidebarTocHTML(rctx)
ctx.Data["FileSidebarHTML"] = renderSidebarTocHTML(rctx, ctx.Locale.Language())
return true
}

View File

@ -207,7 +207,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
delete(ctx.Data, "IsMarkup")
}
ctx.Data["FileSidebarHTML"] = renderSidebarTocHTML(rctx)
ctx.Data["FileSidebarHTML"] = renderSidebarTocHTML(rctx, ctx.Locale.Language())
}
if ctx.Data["IsMarkup"] != true {

View File

@ -277,7 +277,10 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return nil, nil
}
if rctx.SidebarTocNode != nil {
// Render sidebar TOC - prefer generic headers, fallback to legacy node
if len(rctx.SidebarTocHeaders) > 0 {
ctx.Data["WikiSidebarTocHTML"] = markup.RenderSidebarTocHTML(rctx.SidebarTocHeaders, ctx.Locale.Language())
} else if rctx.SidebarTocNode != nil {
sb := strings.Builder{}
if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil {
log.Error("Failed to render wiki sidebar TOC: %v", err)