0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-08 09:03:58 +02:00

Refactor TOC rendering and improve HTML structure

- Updated the rendering of table of contents (TOC) items to ensure proper closing of list items in HTML.
- Removed redundant header extraction logic from orgmode, enabling TOC extraction during post-processing.
- Simplified sidebar visibility state management by utilizing user settings instead of localStorage.

This enhances the overall structure and maintainability of the markup rendering process.
This commit is contained in:
hamki 2026-02-03 06:24:21 +08:00
parent c3a465f7d9
commit 1004c2250f
5 changed files with 15 additions and 126 deletions

View File

@ -254,7 +254,7 @@ func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]strin
currentLevel := baseLevel
indent := []byte{' ', ' '}
_, _ = htmlutil.HTMLPrint(out, "<ul>\n")
for i, header := range ctx.TocHeadingItems {
for _, header := range ctx.TocHeadingItems {
// Go deeper: open nested <ul> elements (wrapped in <li> for valid HTML)
for currentLevel < header.HeadingLevel {
_, _ = out.Write(indent)
@ -270,13 +270,7 @@ func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]strin
currentLevel--
}
_, _ = out.Write(indent)
_, _ = htmlutil.HTMLPrintf(out, "<li><a href=\"#%s\">%s</a>", header.AnchorID, header.InnerText)
// Check if next item is at a deeper level - if so, don't close the <li> yet
nextIsDeeper := i+1 < len(ctx.TocHeadingItems) && ctx.TocHeadingItems[i+1].HeadingLevel > header.HeadingLevel
if !nextIsDeeper {
_, _ = htmlutil.HTMLPrint(out, "</li>")
}
_, _ = htmlutil.HTMLPrint(out, "\n")
_, _ = htmlutil.HTMLPrintf(out, "<li><a href=\"#%s\">%s</a></li>\n", header.AnchorID, header.InnerText)
}
// Close any remaining nested levels
for currentLevel > baseLevel {

View File

@ -38,9 +38,9 @@ include_toc: true
result = re.ReplaceAllString(result, "\n")
expected := `<details><summary>toc</summary>
<ul>
<li><a href="#user-content-tag-link-and-bold" rel="nofollow">tag link and Bold</a>
<li><a href="#user-content-tag-link-and-bold" rel="nofollow">tag link and Bold</a></li>
<li><ul>
<li><a href="#user-content-code-block-a" rel="nofollow">code block &lt;a&gt;</a>
<li><a href="#user-content-code-block-a" rel="nofollow">code block &lt;a&gt;</a></li>
<li><ul>
<li><ul>
<li><a href="#user-content-markdown-bold" rel="nofollow">markdown bold</a></li>

View File

@ -70,15 +70,14 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
w := &orgWriter{rctx: ctx, HTMLWriter: htmlWriter}
htmlWriter.ExtendingWriter = 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.TocHeadingItems = extractTocHeadingItems(doc.Outline)
if len(ctx.TocHeadingItems) > 0 {
// Enable TOC extraction in post-process step for orgmode files
// The actual TOC items will be extracted from HTML headings during post-processing
if ctx.RenderOptions.EnableHeadingIDGeneration {
ctx.TocShowInSection = markup.TocShowInSidebar
}
@ -90,37 +89,6 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
return err
}
// extractTocHeadingItems recursively extracts headers from org document outline
func extractTocHeadingItems(outline org.Outline) []*markup.TocHeadingItem {
var items []*markup.TocHeadingItem
collectTocHeadingItems(outline.Section, &items)
return items
}
// collectTocHeadingItems recursively collects headers from sections
func collectTocHeadingItems(section *org.Section, items *[]*markup.TocHeadingItem) {
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...)
*items = append(*items, &markup.TocHeadingItem{
HeadingLevel: h.Lvl,
InnerText: titleText,
AnchorID: h.ID(),
})
}
// Process child sections
for _, child := range section.Children {
collectTocHeadingItems(child, items)
}
}
// RenderString renders orgmode string to HTML string
func RenderString(ctx *markup.RenderContext, content string) (string, error) {
var buf strings.Builder

View File

@ -105,69 +105,3 @@ int a;
</div>`)
}
func TestRender_TocHeaderExtraction(t *testing.T) {
// Test single level headers
t.Run("SingleLevel", func(t *testing.T) {
input := `* Header 1
* Header 2
* Header 3
`
ctx := markup.NewTestRenderContext()
_, err := orgmode.RenderString(ctx, input)
assert.NoError(t, err)
assert.Len(t, ctx.TocHeadingItems, 3)
assert.Equal(t, "Header 1", ctx.TocHeadingItems[0].InnerText)
assert.Equal(t, "Header 2", ctx.TocHeadingItems[1].InnerText)
assert.Equal(t, "Header 3", ctx.TocHeadingItems[2].InnerText)
for _, item := range ctx.TocHeadingItems {
assert.Equal(t, 1, item.HeadingLevel)
}
})
// Test nested headers
t.Run("NestedHeaders", func(t *testing.T) {
input := `* Level 1
** Level 2
*** Level 3
** Another Level 2
`
ctx := markup.NewTestRenderContext()
_, err := orgmode.RenderString(ctx, input)
assert.NoError(t, err)
assert.Len(t, ctx.TocHeadingItems, 4)
assert.Equal(t, 1, ctx.TocHeadingItems[0].HeadingLevel)
assert.Equal(t, 2, ctx.TocHeadingItems[1].HeadingLevel)
assert.Equal(t, 3, ctx.TocHeadingItems[2].HeadingLevel)
assert.Equal(t, 2, ctx.TocHeadingItems[3].HeadingLevel)
})
// Test headers with special characters
t.Run("SpecialCharacters", func(t *testing.T) {
input := `* Header with <special> & "characters"
* Another header
`
ctx := markup.NewTestRenderContext()
_, err := orgmode.RenderString(ctx, input)
assert.NoError(t, err)
assert.Len(t, ctx.TocHeadingItems, 2)
assert.Equal(t, `Header with <special> & "characters"`, ctx.TocHeadingItems[0].InnerText)
})
// Test empty document
t.Run("EmptyDocument", func(t *testing.T) {
input := `Just some text without headers.`
ctx := markup.NewTestRenderContext()
_, err := orgmode.RenderString(ctx, input)
assert.NoError(t, err)
assert.Empty(t, ctx.TocHeadingItems)
})
// Test that TocShowInSection is set correctly
t.Run("TocShowInSection", func(t *testing.T) {
input := `* Header 1`
ctx := markup.NewTestRenderContext()
_, err := orgmode.RenderString(ctx, input)
assert.NoError(t, err)
assert.Equal(t, markup.TocShowInSidebar, ctx.TocShowInSection)
})
}

View File

@ -2,6 +2,7 @@ import type {FileRenderPlugin} from '../render/plugin.ts';
import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts';
import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
import {localUserSettings} from '../modules/user-settings.ts';
import {createElementFromHTML, showElem, toggleElemClass} from '../utils/dom.ts';
import {html} from '../utils/html.ts';
import {basename} from '../utils.ts';
@ -155,9 +156,8 @@ function initSidebarToggle(elFileView: HTMLElement): void {
}
};
// Restore saved state from localStorage (default to hidden)
const savedState = localStorage.getItem('file-view-sidebar-visible');
const isVisible = savedState === 'true'; // default to hidden
// Restore saved state (default to hidden)
const isVisible = localUserSettings.getBoolean('file-view-sidebar-visible');
// Apply initial state
if (isVisible) {
@ -168,6 +168,9 @@ function initSidebarToggle(elFileView: HTMLElement): void {
// Update sidebar position on resize to keep aligned with file content
// Only observe the segment element to avoid unnecessary updates from unrelated page changes
const fileHeader = elFileView.querySelector('.file-header');
const segment = elFileView.querySelector('.ui.bottom.segment');
const resizeObserver = new ResizeObserver(() => {
updatePosition();
});
@ -177,8 +180,6 @@ function initSidebarToggle(elFileView: HTMLElement): void {
// Update position using IntersectionObserver instead of scroll event
// This provides better performance and avoids scroll event issues
const fileHeader = elFileView.querySelector('.file-header');
const segment = elFileView.querySelector('.ui.bottom.segment');
if (fileHeader && segment) {
// Use a small set of thresholds to get periodic position updates during scroll
const thresholds = [0, 0.25, 0.5, 0.75, 1];
@ -193,18 +194,10 @@ function initSidebarToggle(elFileView: HTMLElement): void {
const isCurrentlyVisible = !sidebar.classList.contains('sidebar-panel-hidden');
if (isCurrentlyVisible) {
hideSidebar();
try {
localStorage.setItem('file-view-sidebar-visible', 'false');
} catch {
// Ignore storage errors (e.g., disabled or full localStorage)
}
localUserSettings.setBoolean('file-view-sidebar-visible', false);
} else {
showSidebar();
try {
localStorage.setItem('file-view-sidebar-visible', 'true');
} catch {
// Ignore storage errors (e.g., disabled or full localStorage)
}
localUserSettings.setBoolean('file-view-sidebar-visible', true);
}
});
}