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:
parent
c3a465f7d9
commit
1004c2250f
@ -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 {
|
||||
|
||||
@ -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 <a></a>
|
||||
<li><a href="#user-content-code-block-a" rel="nofollow">code block <a></a></li>
|
||||
<li><ul>
|
||||
<li><ul>
|
||||
<li><a href="#user-content-markdown-bold" rel="nofollow">markdown bold</a></li>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user