Skip to content

Commit ac88f21

Browse files
authored
Automatically render wiki TOC (#19873)
Automatically add sidebar in the wiki view containing a TOC for the wiki page. Make the TOC collapsable Signed-off-by: Andrew Thornton <[email protected]>
1 parent c1c07e5 commit ac88f21

File tree

8 files changed

+146
-50
lines changed

8 files changed

+146
-50
lines changed

modules/markup/markdown/goldmark.go

+17-26
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,6 @@ import (
2727

2828
var byteMailto = []byte("mailto:")
2929

30-
// Header holds the data about a header.
31-
type Header struct {
32-
Level int
33-
Text string
34-
ID string
35-
}
36-
3730
// ASTTransformer is a default transformer of the goldmark tree.
3831
type ASTTransformer struct{}
3932

@@ -42,12 +35,13 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
4235
metaData := meta.GetItems(pc)
4336
firstChild := node.FirstChild()
4437
createTOC := false
45-
toc := []Header{}
38+
ctx := pc.Get(renderContextKey).(*markup.RenderContext)
4639
rc := &RenderConfig{
4740
Meta: "table",
4841
Icon: "table",
4942
Lang: "",
5043
}
44+
5145
if metaData != nil {
5246
rc.ToRenderConfig(metaData)
5347

@@ -56,7 +50,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
5650
node.InsertBefore(node, firstChild, metaNode)
5751
}
5852
createTOC = rc.TOC
59-
toc = make([]Header, 0, 100)
53+
ctx.TableOfContents = make([]markup.Header, 0, 100)
6054
}
6155

6256
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -66,23 +60,20 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
6660

6761
switch v := n.(type) {
6862
case *ast.Heading:
69-
if createTOC {
70-
text := n.Text(reader.Source())
71-
header := Header{
72-
Text: util.BytesToReadOnlyString(text),
73-
Level: v.Level,
74-
}
75-
if id, found := v.AttributeString("id"); found {
76-
header.ID = util.BytesToReadOnlyString(id.([]byte))
77-
}
78-
toc = append(toc, header)
79-
} else {
80-
for _, attr := range v.Attributes() {
81-
if _, ok := attr.Value.([]byte); !ok {
82-
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
83-
}
63+
for _, attr := range v.Attributes() {
64+
if _, ok := attr.Value.([]byte); !ok {
65+
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
8466
}
8567
}
68+
text := n.Text(reader.Source())
69+
header := markup.Header{
70+
Text: util.BytesToReadOnlyString(text),
71+
Level: v.Level,
72+
}
73+
if id, found := v.AttributeString("id"); found {
74+
header.ID = util.BytesToReadOnlyString(id.([]byte))
75+
}
76+
ctx.TableOfContents = append(ctx.TableOfContents, header)
8677
case *ast.Image:
8778
// Images need two things:
8879
//
@@ -199,12 +190,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
199190
return ast.WalkContinue, nil
200191
})
201192

202-
if createTOC && len(toc) > 0 {
193+
if createTOC && len(ctx.TableOfContents) > 0 {
203194
lang := rc.Lang
204195
if len(lang) == 0 {
205196
lang = setting.Langs[0]
206197
}
207-
tocNode := createTOCNode(toc, lang)
198+
tocNode := createTOCNode(ctx.TableOfContents, lang)
208199
if tocNode != nil {
209200
node.InsertBefore(node, firstChild, tocNode)
210201
}

modules/markup/markdown/markdown.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ var (
3434
)
3535

3636
var (
37-
urlPrefixKey = parser.NewContextKey()
38-
isWikiKey = parser.NewContextKey()
39-
renderMetasKey = parser.NewContextKey()
37+
urlPrefixKey = parser.NewContextKey()
38+
isWikiKey = parser.NewContextKey()
39+
renderMetasKey = parser.NewContextKey()
40+
renderContextKey = parser.NewContextKey()
4041
)
4142

4243
type limitWriter struct {
@@ -67,6 +68,7 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
6768
pc.Set(urlPrefixKey, ctx.URLPrefix)
6869
pc.Set(isWikiKey, ctx.IsWiki)
6970
pc.Set(renderMetasKey, ctx.Metas)
71+
pc.Set(renderContextKey, ctx)
7072
return pc
7173
}
7274

modules/markup/markdown/toc.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import (
88
"fmt"
99
"net/url"
1010

11+
"code.gitea.io/gitea/modules/markup"
1112
"code.gitea.io/gitea/modules/translation/i18n"
1213

1314
"github.com/yuin/goldmark/ast"
1415
)
1516

16-
func createTOCNode(toc []Header, lang string) ast.Node {
17+
func createTOCNode(toc []markup.Header, lang string) ast.Node {
1718
details := NewDetails()
1819
summary := NewSummary()
1920

modules/markup/renderer.go

+18-10
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,26 @@ func Init() {
3333
}
3434
}
3535

36+
// Header holds the data about a header.
37+
type Header struct {
38+
Level int
39+
Text string
40+
ID string
41+
}
42+
3643
// RenderContext represents a render context
3744
type RenderContext struct {
38-
Ctx context.Context
39-
Filename string
40-
Type string
41-
IsWiki bool
42-
URLPrefix string
43-
Metas map[string]string
44-
DefaultLink string
45-
GitRepo *git.Repository
46-
ShaExistCache map[string]bool
47-
cancelFn func()
45+
Ctx context.Context
46+
Filename string
47+
Type string
48+
IsWiki bool
49+
URLPrefix string
50+
Metas map[string]string
51+
DefaultLink string
52+
GitRepo *git.Repository
53+
ShaExistCache map[string]bool
54+
cancelFn func()
55+
TableOfContents []Header
4856
}
4957

5058
// Cancel runs any cleanup functions that have been registered for this Ctx

modules/templates/helper.go

+61
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"reflect"
1919
"regexp"
2020
"runtime"
21+
"strconv"
2122
"strings"
2223
texttmpl "text/template"
2324
"time"
@@ -390,6 +391,66 @@ func NewFuncMap() []template.FuncMap {
390391
"Join": strings.Join,
391392
"QueryEscape": url.QueryEscape,
392393
"DotEscape": DotEscape,
394+
"Iterate": func(arg interface{}) (items []uint64) {
395+
count := uint64(0)
396+
switch val := arg.(type) {
397+
case uint64:
398+
count = val
399+
case *uint64:
400+
count = *val
401+
case int64:
402+
if val < 0 {
403+
val = 0
404+
}
405+
count = uint64(val)
406+
case *int64:
407+
if *val < 0 {
408+
*val = 0
409+
}
410+
count = uint64(*val)
411+
case int:
412+
if val < 0 {
413+
val = 0
414+
}
415+
count = uint64(val)
416+
case *int:
417+
if *val < 0 {
418+
*val = 0
419+
}
420+
count = uint64(*val)
421+
case uint:
422+
count = uint64(val)
423+
case *uint:
424+
count = uint64(*val)
425+
case int32:
426+
if val < 0 {
427+
val = 0
428+
}
429+
count = uint64(val)
430+
case *int32:
431+
if *val < 0 {
432+
*val = 0
433+
}
434+
count = uint64(*val)
435+
case uint32:
436+
count = uint64(val)
437+
case *uint32:
438+
count = uint64(*val)
439+
case string:
440+
cnt, _ := strconv.ParseInt(val, 10, 64)
441+
if cnt < 0 {
442+
cnt = 0
443+
}
444+
count = uint64(cnt)
445+
}
446+
if count <= 0 {
447+
return items
448+
}
449+
for i := uint64(0); i < count; i++ {
450+
items = append(items, i)
451+
}
452+
return items
453+
},
393454
}}
394455
}
395456

routers/web/repo/wiki.go

+2
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
280280
ctx.Data["footerPresent"] = false
281281
}
282282

283+
ctx.Data["toc"] = rctx.TableOfContents
284+
283285
// get commit count - wiki revisions
284286
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
285287
ctx.Data["CommitCount"] = commitsCount

templates/repo/wiki/view.tmpl

+29-10
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,39 @@
6464
<p>{{.FormatWarning}}</p>
6565
</div>
6666
{{end}}
67-
<div class="ui {{if .sidebarPresent}}grid equal width{{end}}" style="margin-top: 1rem;">
68-
<div class="ui {{if .sidebarPresent}}eleven wide column{{end}} segment markup wiki-content-main">
67+
<div class="ui {{if or .sidebarPresent .toc}}grid equal width{{end}}" style="margin-top: 1rem;">
68+
<div class="ui {{if or .sidebarPresent .toc}}eleven wide column{{end}} segment markup wiki-content-main">
6969
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
7070
{{.content | Safe}}
7171
</div>
72-
{{if .sidebarPresent}}
72+
{{if or .sidebarPresent .toc}}
7373
<div class="column" style="padding-top: 0;">
74-
<div class="ui segment wiki-content-sidebar">
75-
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
76-
<a class="ui right floated muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{.i18n.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
77-
{{end}}
78-
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
79-
{{.sidebarContent | Safe}}
80-
</div>
74+
{{if .toc}}
75+
<div class="ui segment wiki-content-toc">
76+
<details open>
77+
<summary>
78+
<div class="ui header">{{.i18n.Tr "toc"}}</div>
79+
</summary>
80+
{{$level := 0}}
81+
{{range .toc}}
82+
{{if lt $level .Level}}{{range Iterate (Subtract .Level $level)}}<ul>{{end}}{{end}}
83+
{{if gt $level .Level}}{{range Iterate (Subtract $level .Level)}}</ul>{{end}}{{end}}
84+
{{$level = .Level}}
85+
<li><a href="#{{.ID}}">{{.Text}}</a></li>
86+
{{end}}
87+
{{range Iterate $level}}</ul>{{end}}
88+
</details>
89+
</div>
90+
{{end}}
91+
{{if .sidebarPresent}}
92+
<div class="ui segment wiki-content-sidebar">
93+
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
94+
<a class="ui right floated muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{.i18n.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
95+
{{end}}
96+
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
97+
{{.sidebarContent | Safe}}
98+
</div>
99+
{{end}}
81100
</div>
82101
{{end}}
83102
</div>

web_src/less/_repository.less

+12
Original file line numberDiff line numberDiff line change
@@ -3088,6 +3088,18 @@ td.blob-excerpt {
30883088
}
30893089
}
30903090

3091+
.wiki-content-toc {
3092+
> ul > li {
3093+
margin-bottom: 4px;
3094+
}
3095+
3096+
ul {
3097+
margin: 0;
3098+
list-style: none;
3099+
padding-left: 1em;
3100+
}
3101+
}
3102+
30913103
/* fomantic's last-child selector does not work with hidden last child */
30923104
.ui.buttons .unescape-button {
30933105
border-top-right-radius: .28571429rem;

0 commit comments

Comments
 (0)