Skip to content

Commit 1ab16e4

Browse files
wxiaoguangdelvh
andauthoredApr 17, 2023
Improve Wiki TOC (#24137)
The old code has a lot of technical debts, eg: `repo/wiki/view.tmpl` / `Iterate` This PR improves the Wiki TOC display and improves the code. --------- Co-authored-by: delvh <dev.lh@web.de>
1 parent f045e58 commit 1ab16e4

File tree

12 files changed

+128
-117
lines changed

12 files changed

+128
-117
lines changed
 

‎modules/markup/markdown/convertyaml.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func nodeToTable(meta *yaml.Node) ast.Node {
3434

3535
func mappingNodeToTable(meta *yaml.Node) ast.Node {
3636
table := east.NewTable()
37-
alignments := []east.Alignment{}
37+
alignments := make([]east.Alignment, 0, len(meta.Content)/2)
3838
for i := 0; i < len(meta.Content); i += 2 {
3939
alignments = append(alignments, east.AlignNone)
4040
}

‎modules/markup/markdown/goldmark.go

+20-14
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@ type ASTTransformer struct{}
3434
// Transform transforms the given AST tree.
3535
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
3636
firstChild := node.FirstChild()
37-
createTOC := false
37+
tocMode := ""
3838
ctx := pc.Get(renderContextKey).(*markup.RenderContext)
3939
rc := pc.Get(renderConfigKey).(*RenderConfig)
40+
41+
tocList := make([]markup.Header, 0, 20)
4042
if rc.yamlNode != nil {
4143
metaNode := rc.toMetaNode()
4244
if metaNode != nil {
4345
node.InsertBefore(node, firstChild, metaNode)
4446
}
45-
createTOC = rc.TOC
46-
ctx.TableOfContents = make([]markup.Header, 0, 100)
47+
tocMode = rc.TOC
4748
}
4849

4950
attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
@@ -59,15 +60,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
5960
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
6061
}
6162
}
62-
text := n.Text(reader.Source())
63+
txt := n.Text(reader.Source())
6364
header := markup.Header{
64-
Text: util.BytesToReadOnlyString(text),
65+
Text: util.BytesToReadOnlyString(txt),
6566
Level: v.Level,
6667
}
6768
if id, found := v.AttributeString("id"); found {
6869
header.ID = util.BytesToReadOnlyString(id.([]byte))
6970
}
70-
ctx.TableOfContents = append(ctx.TableOfContents, header)
71+
tocList = append(tocList, header)
7172
case *ast.Image:
7273
// Images need two things:
7374
//
@@ -201,14 +202,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
201202
return ast.WalkContinue, nil
202203
})
203204

204-
if createTOC && len(ctx.TableOfContents) > 0 {
205-
lang := rc.Lang
206-
if len(lang) == 0 {
207-
lang = setting.Langs[0]
208-
}
209-
tocNode := createTOCNode(ctx.TableOfContents, lang)
210-
if tocNode != nil {
205+
showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main"
206+
showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar
207+
if len(tocList) > 0 && (showTocInMain || showTocInSidebar) {
208+
if showTocInMain {
209+
tocNode := createTOCNode(tocList, rc.Lang, nil)
211210
node.InsertBefore(node, firstChild, tocNode)
211+
} else {
212+
tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"})
213+
ctx.SidebarTocNode = tocNode
212214
}
213215
}
214216

@@ -373,7 +375,11 @@ func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.
373375
func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
374376
var err error
375377
if entering {
376-
_, err = w.WriteString("<details>")
378+
if _, err = w.WriteString("<details"); err != nil {
379+
return ast.WalkStop, err
380+
}
381+
html.RenderAttributes(w, node, nil)
382+
_, err = w.WriteString(">")
377383
} else {
378384
_, err = w.WriteString("</details>")
379385
}

‎modules/markup/markdown/markdown.go

+14-9
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import (
2929
)
3030

3131
var (
32-
converter goldmark.Markdown
33-
once = sync.Once{}
32+
specMarkdown goldmark.Markdown
33+
specMarkdownOnce sync.Once
3434
)
3535

3636
var (
@@ -56,7 +56,7 @@ func (l *limitWriter) Write(data []byte) (int, error) {
5656
if err != nil {
5757
return n, err
5858
}
59-
return n, fmt.Errorf("Rendered content too large - truncating render")
59+
return n, fmt.Errorf("rendered content too large - truncating render")
6060
}
6161
n, err := l.w.Write(data)
6262
l.sum += int64(n)
@@ -73,10 +73,10 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
7373
return pc
7474
}
7575

76-
// actualRender renders Markdown to HTML without handling special links.
77-
func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
78-
once.Do(func() {
79-
converter = goldmark.New(
76+
// SpecializedMarkdown sets up the Gitea specific markdown extensions
77+
func SpecializedMarkdown() goldmark.Markdown {
78+
specMarkdownOnce.Do(func() {
79+
specMarkdown = goldmark.New(
8080
goldmark.WithExtensions(
8181
extension.NewTable(
8282
extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
@@ -139,13 +139,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
139139
)
140140

141141
// Override the original Tasklist renderer!
142-
converter.Renderer().AddOptions(
142+
specMarkdown.Renderer().AddOptions(
143143
renderer.WithNodeRenderers(
144144
util.Prioritized(NewHTMLRenderer(), 10),
145145
),
146146
)
147147
})
148+
return specMarkdown
149+
}
148150

151+
// actualRender renders Markdown to HTML without handling special links.
152+
func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
153+
converter := SpecializedMarkdown()
149154
lw := &limitWriter{
150155
w: output,
151156
limit: setting.UI.MaxDisplayFileSize * 3,
@@ -174,7 +179,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
174179
buf = giteautil.NormalizeEOL(buf)
175180

176181
rc := &RenderConfig{
177-
Meta: "table",
182+
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
178183
Icon: "table",
179184
Lang: "",
180185
}

‎modules/markup/markdown/renderconfig.go

+38-41
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,42 @@ import (
77
"fmt"
88
"strings"
99

10+
"code.gitea.io/gitea/modules/markup"
11+
1012
"github.com/yuin/goldmark/ast"
1113
"gopkg.in/yaml.v3"
1214
)
1315

1416
// RenderConfig represents rendering configuration for this file
1517
type RenderConfig struct {
16-
Meta string
18+
Meta markup.RenderMetaMode
1719
Icon string
18-
TOC bool
20+
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
1921
Lang string
2022
yamlNode *yaml.Node
2123
}
2224

25+
func renderMetaModeFromString(s string) markup.RenderMetaMode {
26+
switch strings.TrimSpace(strings.ToLower(s)) {
27+
case "none":
28+
return markup.RenderMetaAsNone
29+
case "table":
30+
return markup.RenderMetaAsTable
31+
default: // "details"
32+
return markup.RenderMetaAsDetails
33+
}
34+
}
35+
2336
// UnmarshalYAML implement yaml.v3 UnmarshalYAML
2437
func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
2538
if rc == nil {
26-
rc = &RenderConfig{
27-
Meta: "table",
28-
Icon: "table",
29-
Lang: "",
30-
}
39+
return nil
3140
}
41+
3242
rc.yamlNode = value
3343

3444
type commonRenderConfig struct {
35-
TOC bool `yaml:"include_toc"`
45+
TOC string `yaml:"include_toc"`
3646
Lang string `yaml:"lang"`
3747
}
3848
var basic commonRenderConfig
@@ -54,58 +64,45 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
5464

5565
if err := value.Decode(&stringBasic); err == nil {
5666
if stringBasic.Gitea != "" {
57-
switch strings.TrimSpace(strings.ToLower(stringBasic.Gitea)) {
58-
case "none":
59-
rc.Meta = "none"
60-
case "table":
61-
rc.Meta = "table"
62-
default: // "details"
63-
rc.Meta = "details"
64-
}
67+
rc.Meta = renderMetaModeFromString(stringBasic.Gitea)
6568
}
6669
return nil
6770
}
6871

69-
type giteaControl struct {
72+
type yamlRenderConfig struct {
7073
Meta *string `yaml:"meta"`
7174
Icon *string `yaml:"details_icon"`
72-
TOC *bool `yaml:"include_toc"`
75+
TOC *string `yaml:"include_toc"`
7376
Lang *string `yaml:"lang"`
7477
}
7578

76-
type complexGiteaConfig struct {
77-
Gitea *giteaControl `yaml:"gitea"`
79+
type yamlRenderConfigWrapper struct {
80+
Gitea *yamlRenderConfig `yaml:"gitea"`
7881
}
79-
var complex complexGiteaConfig
80-
if err := value.Decode(&complex); err != nil {
81-
return fmt.Errorf("unable to decode into complexRenderConfig %w", err)
82+
83+
var cfg yamlRenderConfigWrapper
84+
if err := value.Decode(&cfg); err != nil {
85+
return fmt.Errorf("unable to decode into yamlRenderConfigWrapper %w", err)
8286
}
8387

84-
if complex.Gitea == nil {
88+
if cfg.Gitea == nil {
8589
return nil
8690
}
8791

88-
if complex.Gitea.Meta != nil {
89-
switch strings.TrimSpace(strings.ToLower(*complex.Gitea.Meta)) {
90-
case "none":
91-
rc.Meta = "none"
92-
case "table":
93-
rc.Meta = "table"
94-
default: // "details"
95-
rc.Meta = "details"
96-
}
92+
if cfg.Gitea.Meta != nil {
93+
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
9794
}
9895

99-
if complex.Gitea.Icon != nil {
100-
rc.Icon = strings.TrimSpace(strings.ToLower(*complex.Gitea.Icon))
96+
if cfg.Gitea.Icon != nil {
97+
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
10198
}
10299

103-
if complex.Gitea.Lang != nil && *complex.Gitea.Lang != "" {
104-
rc.Lang = *complex.Gitea.Lang
100+
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
101+
rc.Lang = *cfg.Gitea.Lang
105102
}
106103

107-
if complex.Gitea.TOC != nil {
108-
rc.TOC = *complex.Gitea.TOC
104+
if cfg.Gitea.TOC != nil {
105+
rc.TOC = *cfg.Gitea.TOC
109106
}
110107

111108
return nil
@@ -116,9 +113,9 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
116113
return nil
117114
}
118115
switch rc.Meta {
119-
case "table":
116+
case markup.RenderMetaAsTable:
120117
return nodeToTable(rc.yamlNode)
121-
case "details":
118+
case markup.RenderMetaAsDetails:
122119
return nodeToDetails(rc.yamlNode, rc.Icon)
123120
default:
124121
return nil

‎modules/markup/markdown/renderconfig_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
6060
},
6161
{
6262
"toc", &RenderConfig{
63-
TOC: true,
63+
TOC: "true",
6464
Meta: "table",
6565
Icon: "table",
6666
Lang: "",
6767
}, "include_toc: true",
6868
},
6969
{
7070
"tocfalse", &RenderConfig{
71-
TOC: false,
71+
TOC: "false",
7272
Meta: "table",
7373
Icon: "table",
7474
Lang: "",
@@ -78,7 +78,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
7878
"toclang", &RenderConfig{
7979
Meta: "table",
8080
Icon: "table",
81-
TOC: true,
81+
TOC: "true",
8282
Lang: "testlang",
8383
}, `
8484
include_toc: true
@@ -120,7 +120,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
120120
"complex2", &RenderConfig{
121121
Lang: "two",
122122
Meta: "table",
123-
TOC: true,
123+
TOC: "true",
124124
Icon: "smiley",
125125
}, `
126126
lang: one
@@ -155,7 +155,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
155155
t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
156156
}
157157
if got.TOC != tt.expected.TOC {
158-
t.Errorf("TOC Expected %t Got %t", tt.expected.TOC, got.TOC)
158+
t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
159159
}
160160
})
161161
}

‎modules/markup/markdown/toc.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ import (
1313
"github.com/yuin/goldmark/ast"
1414
)
1515

16-
func createTOCNode(toc []markup.Header, lang string) ast.Node {
16+
func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]string) ast.Node {
1717
details := NewDetails()
1818
summary := NewSummary()
1919

20+
for k, v := range detailsAttrs {
21+
details.SetAttributeString(k, []byte(v))
22+
}
23+
2024
summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
2125
details.AppendChild(details, summary)
2226
ul := ast.NewList('-')
@@ -40,7 +44,7 @@ func createTOCNode(toc []markup.Header, lang string) ast.Node {
4044
}
4145
li := ast.NewListItem(currentLevel * 2)
4246
a := ast.NewLink()
43-
a.Destination = []byte(fmt.Sprintf("#%s", url.PathEscape(header.ID)))
47+
a.Destination = []byte(fmt.Sprintf("#%s", url.QueryEscape(header.ID)))
4448
a.AppendChild(a, ast.NewString([]byte(header.Text)))
4549
li.AppendChild(li, a)
4650
ul.AppendChild(ul, li)

‎modules/markup/renderer.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ import (
1616

1717
"code.gitea.io/gitea/modules/git"
1818
"code.gitea.io/gitea/modules/setting"
19+
20+
"github.com/yuin/goldmark/ast"
21+
)
22+
23+
type RenderMetaMode string
24+
25+
const (
26+
RenderMetaAsDetails RenderMetaMode = "details" // default
27+
RenderMetaAsNone RenderMetaMode = "none"
28+
RenderMetaAsTable RenderMetaMode = "table"
1929
)
2030

2131
type ProcessorHelper struct {
@@ -63,7 +73,8 @@ type RenderContext struct {
6373
GitRepo *git.Repository
6474
ShaExistCache map[string]bool
6575
cancelFn func()
66-
TableOfContents []Header
76+
SidebarTocNode ast.Node
77+
RenderMetaAs RenderMetaMode
6778
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
6879
}
6980

‎modules/templates/helper.go

-7
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,6 @@ func NewFuncMap() []template.FuncMap {
171171
}
172172
return false
173173
},
174-
"Iterate": func(arg interface{}) (items []int64) {
175-
count, _ := util.ToInt64(arg)
176-
for i := int64(0); i < count; i++ {
177-
items = append(items, i)
178-
}
179-
return items
180-
},
181174

182175
// -----------------------------------------------------------------
183176
// setting

‎routers/web/base.go

+11-13
Original file line numberDiff line numberDiff line change
@@ -143,31 +143,29 @@ func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler {
143143
"locale": lc,
144144
}
145145

146-
user := context.GetContextUser(req)
146+
// TODO: this recovery handler is usually called without Gitea's web context, so we shouldn't touch that context too much
147+
// Otherwise, the 500 page may cause new panics, eg: cache.GetContextWithData, it makes the developer&users couldn't find the original panic
148+
user := context.GetContextUser(req) // almost always nil
147149
if user == nil {
148150
// Get user from session if logged in - do not attempt to sign-in
149151
user = auth.SessionUser(sessionStore)
150152
}
151-
if user != nil {
152-
store["IsSigned"] = true
153-
store["SignedUser"] = user
154-
store["SignedUserID"] = user.ID
155-
store["SignedUserName"] = user.Name
156-
store["IsAdmin"] = user.IsAdmin
157-
} else {
158-
store["SignedUserID"] = int64(0)
159-
store["SignedUserName"] = ""
160-
}
161153

162154
httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
163155
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
164156

165-
if !setting.IsProd {
157+
if !setting.IsProd || (user != nil && user.IsAdmin) {
166158
store["ErrorMsg"] = combinedErr
167159
}
160+
161+
defer func() {
162+
if err := recover(); err != nil {
163+
log.Error("HTML render in Recovery handler panics again: %v", err)
164+
}
165+
}()
168166
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
169167
if err != nil {
170-
log.Error("%v", err)
168+
log.Error("HTML render in Recovery handler fails again: %v", err)
171169
}
172170
}
173171
}()

‎routers/web/repo/wiki.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,15 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
298298
ctx.Data["footerPresent"] = false
299299
}
300300

301-
ctx.Data["toc"] = rctx.TableOfContents
301+
if rctx.SidebarTocNode != nil {
302+
sb := &strings.Builder{}
303+
err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode)
304+
if err != nil {
305+
log.Error("Failed to render wiki sidebar TOC: %v", err)
306+
} else {
307+
ctx.Data["sidebarTocContent"] = sb.String()
308+
}
309+
}
302310

303311
// get commit count - wiki revisions
304312
commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)

‎templates/repo/wiki/view.tmpl

+6-18
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,16 @@
6565
<p>{{.FormatWarning}}</p>
6666
</div>
6767
{{end}}
68-
<div class="ui gt-mt-0 {{if or .sidebarPresent .toc}}grid equal width{{end}}">
69-
<div class="ui {{if or .sidebarPresent .toc}}eleven wide column{{else}}gt-ml-0{{end}} segment markup wiki-content-main">
68+
<div class="ui gt-mt-0 {{if or .sidebarPresent .sidebarTocContent}}grid equal width{{end}}">
69+
<div class="ui {{if or .sidebarPresent .sidebarTocContent}}eleven wide column{{else}}gt-ml-0{{end}} segment markup wiki-content-main">
7070
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
7171
{{.content | Safe}}
7272
</div>
73-
{{if or .sidebarPresent .toc}}
74-
<div class="column" style="padding-top: 0;">
75-
{{if .toc}}
73+
{{if or .sidebarPresent .sidebarTocContent}}
74+
<div class="column gt-pt-0">
75+
{{if .sidebarTocContent}}
7676
<div class="ui segment wiki-content-toc">
77-
<details open>
78-
<summary>
79-
<div class="ui header">{{.locale.Tr "toc"}}</div>
80-
</summary>
81-
{{$level := 0}}
82-
{{range .toc}}
83-
{{if lt $level .Level}}{{range Iterate (Eval .Level "-" $level)}}<ul>{{end}}{{end}}
84-
{{if gt $level .Level}}{{range Iterate (Eval $level "-" .Level)}}</ul>{{end}}{{end}}
85-
{{$level = .Level}}
86-
<li><a href="#{{.ID}}">{{.Text}}</a></li>
87-
{{end}}
88-
{{range Iterate $level}}</ul>{{end}}
89-
</details>
77+
{{.sidebarTocContent | Safe}}
9078
</div>
9179
{{end}}
9280
{{if .sidebarPresent}}

‎web_src/css/repository.css

+6-5
Original file line numberDiff line numberDiff line change
@@ -3261,14 +3261,15 @@ td.blob-excerpt {
32613261
display: none;
32623262
}
32633263

3264-
.wiki-content-toc > ul > li {
3265-
margin-bottom: 4px;
3266-
}
3267-
32683264
.wiki-content-toc ul {
32693265
margin: 0;
32703266
list-style: none;
3271-
padding-left: 1em;
3267+
padding: 5px 0 5px 1em;
3268+
}
3269+
3270+
.wiki-content-toc ul ul {
3271+
border-left: 1px var(--color-secondary);
3272+
border-left-style: dashed;
32723273
}
32733274

32743275
/* fomantic's last-child selector does not work with hidden last child */

0 commit comments

Comments
 (0)
Please sign in to comment.