diff --git a/modules/markup/html.go b/modules/markup/html.go
index c2339838e5..acd2f18471 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -254,7 +254,7 @@ func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]strin
currentLevel := baseLevel
indent := []byte{' ', ' '}
_, _ = htmlutil.HTMLPrint(out, "
\n")
- for i, header := range ctx.TocHeadingItems {
+ for _, header := range ctx.TocHeadingItems {
// Go deeper: open nested elements (wrapped in - 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, "
- %s", header.AnchorID, header.InnerText)
- // Check if next item is at a deeper level - if so, don't close the
- yet
- nextIsDeeper := i+1 < len(ctx.TocHeadingItems) && ctx.TocHeadingItems[i+1].HeadingLevel > header.HeadingLevel
- if !nextIsDeeper {
- _, _ = htmlutil.HTMLPrint(out, "
")
- }
- _, _ = htmlutil.HTMLPrint(out, "\n")
+ _, _ = htmlutil.HTMLPrintf(out, "- %s
\n", header.AnchorID, header.InnerText)
}
// Close any remaining nested levels
for currentLevel > baseLevel {
diff --git a/modules/markup/html_toc_test.go b/modules/markup/html_toc_test.go
index ab363878a4..1493d50e86 100644
--- a/modules/markup/html_toc_test.go
+++ b/modules/markup/html_toc_test.go
@@ -38,9 +38,9 @@ include_toc: true
result = re.ReplaceAllString(result, "\n")
expected := `toc
- - tag link and Bold
+
- tag link and Bold
- - code block <a>
+
- code block <a>
- markdown bold
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index d84c0fb861..2873045595 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -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
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 1b66ac4d24..e97804fdf2 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -105,69 +105,3 @@ int a;
`)
}
-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 & "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 & "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)
- })
-}
diff --git a/web_src/js/features/file-view.ts b/web_src/js/features/file-view.ts
index 0747bc429d..9e41da791a 100644
--- a/web_src/js/features/file-view.ts
+++ b/web_src/js/features/file-view.ts
@@ -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);
}
});
}