diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index efbc174b2e..8dbfe0c22e 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -6,6 +6,7 @@ package htmlutil import ( "fmt" "html/template" + "io" "slices" "strings" ) @@ -31,7 +32,7 @@ func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int return size, class } -func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { +func htmlFormatArgs(s template.HTML, rawArgs []any) []any { if !strings.Contains(string(s), "%") || len(rawArgs) == 0 { panic("HTMLFormat requires one or more arguments") } @@ -50,5 +51,35 @@ func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } - return template.HTML(fmt.Sprintf(string(s), args...)) + return args +} + +func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { + return template.HTML(fmt.Sprintf(string(s), htmlFormatArgs(s, rawArgs)...)) +} + +func HTMLPrintf(w io.Writer, s template.HTML, rawArgs ...any) (int, error) { + return fmt.Fprintf(w, string(s), htmlFormatArgs(s, rawArgs)...) +} + +func HTMLPrint(w io.Writer, s template.HTML) (int, error) { + return io.WriteString(w, string(s)) +} + +func HTMLPrintTag(w io.Writer, tag template.HTML, attrs map[string]string) (written int, err error) { + n, err := io.WriteString(w, "<"+string(tag)) + written += n + if err != nil { + return written, err + } + for k, v := range attrs { + n, err = fmt.Fprintf(w, ` %s="%s"`, template.HTMLEscapeString(k), template.HTMLEscapeString(v)) + written += n + if err != nil { + return written, err + } + } + n, err = io.WriteString(w, ">") + written += n + return written, err } diff --git a/modules/markup/common/footnote.go b/modules/markup/common/footnote.go index 1ece436c66..e552a28237 100644 --- a/modules/markup/common/footnote.go +++ b/modules/markup/common/footnote.go @@ -405,9 +405,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt if entering { n := node.(*FootnoteLink) is := strconv.Itoa(n.Index) - _, _ = w.WriteString(``) // FIXME: here and below, need to keep the classes _, _ = w.WriteString(is) @@ -419,7 +419,7 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { n := node.(*FootnoteBackLink) - _, _ = w.WriteString(` `) _, _ = w.WriteString("↩︎") @@ -431,7 +431,7 @@ func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source [ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*Footnote) if entering { - _, _ = w.WriteString(`
  • %s\n", locale.TrString("toc")) + + baseLevel := 6 + for _, header := range ctx.TocHeadingItems { + if header.HeadingLevel < baseLevel { + baseLevel = header.HeadingLevel + } + } + + currentLevel := baseLevel + indent := []byte{' ', ' '} + _, _ = htmlutil.HTMLPrint(out, "\n") + currentLevel-- + } + _, _ = htmlutil.HTMLPrint(out, "\n\n") +} + func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { if !ctx.usedByRender && ctx.RenderHelper != nil { defer ctx.RenderHelper.CleanUp() @@ -284,6 +329,9 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output } // Render everything to buf. + if ctx.TocShowInSection == TocShowInMain && len(ctx.TocHeadingItems) > 0 { + RenderTocHeadingItems(ctx, nil, output) + } for _, node := range newNodes { if err := html.Render(output, node); err != nil { return fmt.Errorf("markup.postProcess: html.Render: %w", err) @@ -314,7 +362,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod return node.NextSibling } - processNodeAttrID(ctx, node) + processNodeHeadingAndID(ctx, node) processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly if isEmojiNode(node) { diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go index 066ee9711d..f98e7429a2 100644 --- a/modules/markup/html_node.go +++ b/modules/markup/html_node.go @@ -14,7 +14,7 @@ import ( func isAnchorIDUserContent(s string) bool { // blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote // old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) - return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") + return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") || isAnchorIDFootnote(s) } func isAnchorIDFootnote(s string) bool { @@ -34,7 +34,10 @@ func isHeadingTag(node *html.Node) bool { } // getNodeText extracts the text content from a node and its children -func getNodeText(node *html.Node) string { +func getNodeText(node *html.Node, cached **string) string { + if *cached != nil { + return **cached + } var text strings.Builder var extractText func(*html.Node) extractText = func(n *html.Node) { @@ -46,36 +49,56 @@ func getNodeText(node *html.Node) string { } } extractText(node) - return text.String() + textStr := text.String() + *cached = &textStr + return textStr } -func processNodeAttrID(ctx *RenderContext, node *html.Node) { +func processNodeHeadingAndID(ctx *RenderContext, node *html.Node) { + // TODO: handle duplicate IDs, need to track existing IDs in the document // Add user-content- to IDs and "#" links if they don't already have them, // and convert the link href to a relative link to the host root - hasID := false + attrIDVal := "" for idx, attr := range node.Attr { if attr.Key == "id" { - hasID = true - if !isAnchorIDUserContent(attr.Val) { - node.Attr[idx].Val = "user-content-" + attr.Val + attrIDVal = attr.Val + if !isAnchorIDUserContent(attrIDVal) { + attrIDVal = "user-content-" + attrIDVal + node.Attr[idx].Val = attrIDVal } } } + if !isHeadingTag(node) || !ctx.RenderOptions.EnableHeadingIDGeneration { + return + } + // For heading tags (h1-h6) without an id attribute, generate one from the text content. // This ensures HTML headings like

    Title

    get proper permalink anchors // matching the behavior of Markdown headings. // Only enabled for repository files and wiki pages via EnableHeadingIDGeneration option. - if !hasID && isHeadingTag(node) && ctx.RenderOptions.EnableHeadingIDGeneration { - text := getNodeText(node) - if text != "" { + var nodeTextCached *string + if attrIDVal == "" { + nodeText := getNodeText(node, &nodeTextCached) + if nodeText != "" { // Use the same CleanValue function used by Markdown heading ID generation - cleanedID := string(common.CleanValue([]byte(text))) - if cleanedID != "" { - node.Attr = append(node.Attr, html.Attribute{Key: "id", Val: "user-content-" + cleanedID}) + attrIDVal = string(common.CleanValue([]byte(nodeText))) + if attrIDVal != "" { + attrIDVal = "user-content-" + attrIDVal + node.Attr = append(node.Attr, html.Attribute{Key: "id", Val: attrIDVal}) } } } + if ctx.TocShowInSection != "" { + nodeText := getNodeText(node, &nodeTextCached) + if nodeText != "" && attrIDVal != "" { + ctx.TocHeadingItems = append(ctx.TocHeadingItems, &TocHeadingItem{ + HeadingLevel: int(node.Data[1] - '0'), + AnchorID: attrIDVal, + InnerText: nodeText, + }) + } + } } func processFootnoteNode(ctx *RenderContext, node *html.Node) { diff --git a/modules/markup/html_toc_test.go b/modules/markup/html_toc_test.go new file mode 100644 index 0000000000..e93cfc9346 --- /dev/null +++ b/modules/markup/html_toc_test.go @@ -0,0 +1,60 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup_test + +import ( + "regexp" + "testing" + + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestToCWithHTML(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + + t1 := `tag link and Bold` + t2 := "code block ``" + t3 := "markdown **bold**" + input := `--- +include_toc: true +--- + +# ` + t1 + ` +## ` + t2 + ` +#### ` + t3 + ` +## last +` + + renderCtx := markup.NewTestRenderContext().WithEnableHeadingIDGeneration(true) + resultHTML, err := markdown.RenderString(renderCtx, input) + assert.NoError(t, err) + result := string(resultHTML) + re := regexp.MustCompile(`(?s)
    .*?
    `) + result = re.ReplaceAllString(result, "\n") + expected := `
    toc + +
    + +

    tag link and Bold

    +

    code block <a>

    +

    markdown bold

    +

    last

    +` + assert.Equal(t, expected, result) +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index b28fa9824e..555a171685 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -41,11 +41,10 @@ func (g *ASTTransformer) applyElementDir(n ast.Node) { // Transform transforms the given AST tree. func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { firstChild := node.FirstChild() - tocMode := "" ctx := pc.Get(renderContextKey).(*markup.RenderContext) rc := pc.Get(renderConfigKey).(*RenderConfig) - tocList := make([]Header, 0, 20) + tocMode := "" if rc.yamlNode != nil { metaNode := rc.toMetaNode(g) if metaNode != nil { @@ -60,8 +59,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } switch v := n.(type) { - case *ast.Heading: - g.transformHeading(ctx, v, reader, &tocList) case *ast.Paragraph: g.applyElementDir(v) case *ast.List: @@ -79,19 +76,18 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa return ast.WalkContinue, nil }) - showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main" - showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar - if len(tocList) > 0 && (showTocInMain || showTocInSidebar) { - if showTocInMain { - 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 + if ctx.RenderOptions.EnableHeadingIDGeneration { + showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main" + showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar + switch { + case showTocInMain: + ctx.TocShowInSection = markup.TocShowInMain + case showTocInSidebar: + ctx.TocShowInSection = markup.TocShowInSidebar } } - if len(rc.Lang) > 0 { + if rc.Lang != "" { node.SetAttributeString("lang", []byte(rc.Lang)) } } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 3b788432ba..792fb1f6d5 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -5,6 +5,7 @@ package markdown import ( + "bytes" "errors" "html/template" "io" @@ -21,10 +22,12 @@ import ( "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) @@ -57,7 +60,7 @@ func (l *limitWriter) Write(data []byte) (int, error) { // newParserContext creates a parser.Context with the render context set func newParserContext(ctx *markup.RenderContext) parser.Context { - pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) + pc := parser.NewContext() pc.Set(renderContextKey, ctx) return pc } @@ -101,12 +104,48 @@ func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.C } } +type goldmarkEmphasisParser struct { + parser.InlineParser +} + +func goldmarkNewEmphasisParser() parser.InlineParser { + return &goldmarkEmphasisParser{parser.NewEmphasisParser()} +} + +func (s *goldmarkEmphasisParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + line, _ := block.PeekLine() + if len(line) > 1 && line[0] == '_' { + // a special trick to avoid parsing emphasis in filenames like "module/__init__.py" + end := bytes.IndexByte(line[1:], '_') + mark := bytes.Index(line, []byte("_.py")) + // check whether the "end" matches "_.py" or "__.py" + if mark != -1 && (end == mark || end == mark-1) { + return nil + } + } + return s.InlineParser.Parse(parent, block, pc) +} + +func goldmarkDefaultParser() parser.Parser { + return parser.NewParser(parser.WithBlockParsers(parser.DefaultBlockParsers()...), + parser.WithInlineParsers([]util.PrioritizedValue{ + util.Prioritized(parser.NewCodeSpanParser(), 100), + util.Prioritized(parser.NewLinkParser(), 200), + util.Prioritized(parser.NewAutoLinkParser(), 300), + util.Prioritized(parser.NewRawHTMLParser(), 400), + util.Prioritized(goldmarkNewEmphasisParser(), 500), + }...), + parser.WithParagraphTransformers(parser.DefaultParagraphTransformers()...), + ) +} + // SpecializedMarkdown sets up the Gitea specific markdown extensions func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { // TODO: it could use a pool to cache the renderers to reuse them with different contexts // at the moment it is fast enough (see the benchmarks) r := &GlodmarkRender{ctx: ctx} r.goldmarkMarkdown = goldmark.New( + goldmark.WithParser(goldmarkDefaultParser()), goldmark.WithExtensions( extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)), extension.Strikethrough, @@ -131,7 +170,6 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { ), goldmark.WithParserOptions( parser.WithAttribute(), - parser.WithAutoHeadingID(), parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)), ), goldmark.WithRendererOptions(html.WithUnsafe()), diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 4eb01bcc2d..47b293e1e9 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -88,6 +88,7 @@ func TestRender_Images(t *testing.T) { } func TestTotal_RenderString(t *testing.T) { + setting.AppURL = AppURL defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() // Test cases without ambiguous links (It is not right to copy a whole file here, instead it should clearly test what is being tested) @@ -258,7 +259,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno }, }) for i := range sameCases { - line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), sameCases[i]) + line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas).WithEnableHeadingIDGeneration(true), sameCases[i]) assert.NoError(t, err) assert.Equal(t, testAnswers[i], string(line)) } @@ -545,5 +546,11 @@ func TestMarkdownLink(t *testing.T) { assert.Equal(t, `

    link1 link2 link3

    +`, string(result)) + + input = "https://example.com/__init__.py" + result, err = markdown.RenderString(markup.NewTestRenderContext("/base", localMetas), input) + assert.NoError(t, err) + assert.Equal(t, `

    https://example.com/__init__.py

    `, string(result)) } diff --git a/modules/markup/markdown/prefixed_id.go b/modules/markup/markdown/prefixed_id.go deleted file mode 100644 index 63d7fadc0a..0000000000 --- a/modules/markup/markdown/prefixed_id.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markdown - -import ( - "bytes" - "fmt" - - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/markup/common" - "code.gitea.io/gitea/modules/util" - - "github.com/yuin/goldmark/ast" -) - -type prefixedIDs struct { - values container.Set[string] -} - -// Generate generates a new element id. -func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte { - dft := []byte("id") - if kind == ast.KindHeading { - dft = []byte("heading") - } - return p.GenerateWithDefault(value, dft) -} - -// GenerateWithDefault generates a new element id. -func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { - result := common.CleanValue(value) - if len(result) == 0 { - result = dft - } - if !bytes.HasPrefix(result, []byte("user-content-")) { - result = append([]byte("user-content-"), result...) - } - if p.values.Add(util.UnsafeBytesToString(result)) { - return result - } - for i := 1; ; i++ { - newResult := fmt.Sprintf("%s-%d", result, i) - if p.values.Add(newResult) { - return []byte(newResult) - } - } -} - -// Put puts a given element id to the used ids table. -func (p *prefixedIDs) Put(value []byte) { - p.values.Add(util.UnsafeBytesToString(value)) -} - -func newPrefixedIDs() *prefixedIDs { - return &prefixedIDs{ - values: make(container.Set[string]), - } -} diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go deleted file mode 100644 index a11b9d0390..0000000000 --- a/modules/markup/markdown/toc.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markdown - -import ( - "net/url" - - "code.gitea.io/gitea/modules/translation" - - "github.com/yuin/goldmark/ast" -) - -// Header holds the data about a header. -type Header struct { - Level int - Text string - ID string -} - -func createTOCNode(toc []Header, lang string, detailsAttrs map[string]string) ast.Node { - details := NewDetails() - summary := NewSummary() - - for k, v := range detailsAttrs { - details.SetAttributeString(k, []byte(v)) - } - - summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc")))) - details.AppendChild(details, summary) - ul := ast.NewList('-') - details.AppendChild(details, ul) - currentLevel := 6 - for _, header := range toc { - if header.Level < currentLevel { - currentLevel = header.Level - } - } - for _, header := range toc { - for currentLevel > header.Level { - ul = ul.Parent().(*ast.List) - currentLevel-- - } - for currentLevel < header.Level { - newL := ast.NewList('-') - ul.AppendChild(ul, newL) - currentLevel++ - ul = newL - } - li := ast.NewListItem(currentLevel * 2) - a := ast.NewLink() - a.Destination = []byte("#" + url.QueryEscape(header.ID)) - a.AppendChild(a, ast.NewString([]byte(header.Text))) - li.AppendChild(li, a) - ul.AppendChild(ul, li) - } - - return details -} diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go deleted file mode 100644 index a229a7b1a4..0000000000 --- a/modules/markup/markdown/transform_heading.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markdown - -import ( - "fmt" - - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/util" - - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/text" -) - -func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]Header) { - for _, attr := range v.Attributes() { - if _, ok := attr.Value.([]byte); !ok { - v.SetAttribute(attr.Name, fmt.Appendf(nil, "%v", attr.Value)) - } - } - txt := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated - header := Header{ - Text: util.UnsafeBytesToString(txt), - Level: v.Level, - } - if id, found := v.AttributeString("id"); found { - header.ID = util.UnsafeBytesToString(id.([]byte)) - } - *tocList = append(*tocList, header) - g.applyElementDir(v) -} diff --git a/modules/markup/mdstripper/mdstripper.go b/modules/markup/mdstripper/mdstripper.go index 19b852a3ee..bf69051e87 100644 --- a/modules/markup/mdstripper/mdstripper.go +++ b/modules/markup/mdstripper/mdstripper.go @@ -165,7 +165,6 @@ func StripMarkdownBytes(rawBytes []byte) ([]byte, []string) { ), goldmark.WithParserOptions( parser.WithAttribute(), - parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( html.WithUnsafe(), diff --git a/modules/markup/render.go b/modules/markup/render.go index 12f002b0c6..96c9c5f0df 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" ) @@ -60,6 +59,19 @@ type RenderOptions struct { EnableHeadingIDGeneration bool } +type TocShowInSectionType string + +const ( + TocShowInSidebar TocShowInSectionType = "sidebar" + TocShowInMain TocShowInSectionType = "main" +) + +type TocHeadingItem struct { + HeadingLevel int + AnchorID string + InnerText string +} + // RenderContext represents a render context type RenderContext struct { ctx context.Context @@ -67,7 +79,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 + TocShowInSection TocShowInSectionType + TocHeadingItems []*TocHeadingItem RenderHelper RenderHelper RenderOptions RenderOptions diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 921e17fb6a..90d95bd250 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -277,12 +277,10 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return nil, nil } - if rctx.SidebarTocNode != nil { + if rctx.TocShowInSection == markup.TocShowInSidebar && len(rctx.TocHeadingItems) > 0 { 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) - } - ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String()) + markup.RenderTocHeadingItems(rctx, map[string]string{"open": ""}, &sb) + ctx.Data["WikiSidebarTocHTML"] = template.HTML(sb.String()) } if !isSideBar { diff --git a/web_src/js/markup/anchors.ts b/web_src/js/markup/anchors.ts index b5d90e565c..9b25821909 100644 --- a/web_src/js/markup/anchors.ts +++ b/web_src/js/markup/anchors.ts @@ -1,5 +1,6 @@ import {svg} from '../svg.ts'; +// FIXME: don't see why these tricks make sense. If these prefixes are not needed, they should be removed entirely by backend. const addPrefix = (str: string): string => `user-content-${str}`; const removePrefix = (str: string): string => str.replace(/^user-content-/, ''); const hasPrefix = (str: string): boolean => str.startsWith('user-content-');