From ac88f21ecc5612befe51f7ab6ffcb76c681daba5 Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 8 Jun 2022 09:59:16 +0100 Subject: [PATCH] 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 --- modules/markup/markdown/goldmark.go | 43 ++++++++------------ modules/markup/markdown/markdown.go | 8 ++-- modules/markup/markdown/toc.go | 3 +- modules/markup/renderer.go | 28 ++++++++----- modules/templates/helper.go | 61 +++++++++++++++++++++++++++++ routers/web/repo/wiki.go | 2 + templates/repo/wiki/view.tmpl | 39 +++++++++++++----- web_src/less/_repository.less | 12 ++++++ 8 files changed, 146 insertions(+), 50 deletions(-) diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 9b6cd3aae..1750128de 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -27,13 +27,6 @@ import ( var byteMailto = []byte("mailto:") -// Header holds the data about a header. -type Header struct { - Level int - Text string - ID string -} - // ASTTransformer is a default transformer of the goldmark tree. type ASTTransformer struct{} @@ -42,12 +35,13 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa metaData := meta.GetItems(pc) firstChild := node.FirstChild() createTOC := false - toc := []Header{} + ctx := pc.Get(renderContextKey).(*markup.RenderContext) rc := &RenderConfig{ Meta: "table", Icon: "table", Lang: "", } + if metaData != nil { rc.ToRenderConfig(metaData) @@ -56,7 +50,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa node.InsertBefore(node, firstChild, metaNode) } createTOC = rc.TOC - toc = make([]Header, 0, 100) + ctx.TableOfContents = make([]markup.Header, 0, 100) } _ = 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 switch v := n.(type) { case *ast.Heading: - if createTOC { - text := n.Text(reader.Source()) - header := Header{ - Text: util.BytesToReadOnlyString(text), - Level: v.Level, - } - if id, found := v.AttributeString("id"); found { - header.ID = util.BytesToReadOnlyString(id.([]byte)) - } - toc = append(toc, header) - } else { - for _, attr := range v.Attributes() { - if _, ok := attr.Value.([]byte); !ok { - v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) - } + for _, attr := range v.Attributes() { + if _, ok := attr.Value.([]byte); !ok { + v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) } } + text := n.Text(reader.Source()) + header := markup.Header{ + Text: util.BytesToReadOnlyString(text), + Level: v.Level, + } + if id, found := v.AttributeString("id"); found { + header.ID = util.BytesToReadOnlyString(id.([]byte)) + } + ctx.TableOfContents = append(ctx.TableOfContents, header) case *ast.Image: // Images need two things: // @@ -199,12 +190,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa return ast.WalkContinue, nil }) - if createTOC && len(toc) > 0 { + if createTOC && len(ctx.TableOfContents) > 0 { lang := rc.Lang if len(lang) == 0 { lang = setting.Langs[0] } - tocNode := createTOCNode(toc, lang) + tocNode := createTOCNode(ctx.TableOfContents, lang) if tocNode != nil { node.InsertBefore(node, firstChild, tocNode) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 320c2f7f8..7ebdfea6c 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -34,9 +34,10 @@ var ( ) var ( - urlPrefixKey = parser.NewContextKey() - isWikiKey = parser.NewContextKey() - renderMetasKey = parser.NewContextKey() + urlPrefixKey = parser.NewContextKey() + isWikiKey = parser.NewContextKey() + renderMetasKey = parser.NewContextKey() + renderContextKey = parser.NewContextKey() ) type limitWriter struct { @@ -67,6 +68,7 @@ func newParserContext(ctx *markup.RenderContext) parser.Context { pc.Set(urlPrefixKey, ctx.URLPrefix) pc.Set(isWikiKey, ctx.IsWiki) pc.Set(renderMetasKey, ctx.Metas) + pc.Set(renderContextKey, ctx) return pc } diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go index 9d11b771f..fec45103e 100644 --- a/modules/markup/markdown/toc.go +++ b/modules/markup/markdown/toc.go @@ -8,12 +8,13 @@ import ( "fmt" "net/url" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/translation/i18n" "github.com/yuin/goldmark/ast" ) -func createTOCNode(toc []Header, lang string) ast.Node { +func createTOCNode(toc []markup.Header, lang string) ast.Node { details := NewDetails() summary := NewSummary() diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index cf8b9bace..53ecbfce2 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -33,18 +33,26 @@ func Init() { } } +// Header holds the data about a header. +type Header struct { + Level int + Text string + ID string +} + // RenderContext represents a render context type RenderContext struct { - Ctx context.Context - Filename string - Type string - IsWiki bool - URLPrefix string - Metas map[string]string - DefaultLink string - GitRepo *git.Repository - ShaExistCache map[string]bool - cancelFn func() + Ctx context.Context + Filename string + Type string + IsWiki bool + URLPrefix string + Metas map[string]string + DefaultLink string + GitRepo *git.Repository + ShaExistCache map[string]bool + cancelFn func() + TableOfContents []Header } // Cancel runs any cleanup functions that have been registered for this Ctx diff --git a/modules/templates/helper.go b/modules/templates/helper.go index cc0fed744..ef7b70c09 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -18,6 +18,7 @@ import ( "reflect" "regexp" "runtime" + "strconv" "strings" texttmpl "text/template" "time" @@ -390,6 +391,66 @@ func NewFuncMap() []template.FuncMap { "Join": strings.Join, "QueryEscape": url.QueryEscape, "DotEscape": DotEscape, + "Iterate": func(arg interface{}) (items []uint64) { + count := uint64(0) + switch val := arg.(type) { + case uint64: + count = val + case *uint64: + count = *val + case int64: + if val < 0 { + val = 0 + } + count = uint64(val) + case *int64: + if *val < 0 { + *val = 0 + } + count = uint64(*val) + case int: + if val < 0 { + val = 0 + } + count = uint64(val) + case *int: + if *val < 0 { + *val = 0 + } + count = uint64(*val) + case uint: + count = uint64(val) + case *uint: + count = uint64(*val) + case int32: + if val < 0 { + val = 0 + } + count = uint64(val) + case *int32: + if *val < 0 { + *val = 0 + } + count = uint64(*val) + case uint32: + count = uint64(val) + case *uint32: + count = uint64(*val) + case string: + cnt, _ := strconv.ParseInt(val, 10, 64) + if cnt < 0 { + cnt = 0 + } + count = uint64(cnt) + } + if count <= 0 { + return items + } + for i := uint64(0); i < count; i++ { + items = append(items, i) + } + return items + }, }} } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 77f60a1df..f4aabbf48 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -280,6 +280,8 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Data["footerPresent"] = false } + ctx.Data["toc"] = rctx.TableOfContents + // get commit count - wiki revisions commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) ctx.Data["CommitCount"] = commitsCount diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 04faa90b9..3189ed64c 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -64,20 +64,39 @@

{{.FormatWarning}}

{{end}} -
-
+
+
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} {{.content | Safe}}
- {{if .sidebarPresent}} + {{if or .sidebarPresent .toc}}
- + {{if .toc}} +
+
+ +
{{.i18n.Tr "toc"}}
+
+ {{$level := 0}} + {{range .toc}} + {{if lt $level .Level}}{{range Iterate (Subtract .Level $level)}}
    {{end}}{{end}} + {{if gt $level .Level}}{{range Iterate (Subtract $level .Level)}}
{{end}}{{end}} + {{$level = .Level}} +
  • {{.Text}}
  • + {{end}} + {{range Iterate $level}}{{end}} +
    +
    + {{end}} + {{if .sidebarPresent}} + + {{end}}
    {{end}}
    diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 0cae04d11..d73cb9033 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3088,6 +3088,18 @@ td.blob-excerpt { } } +.wiki-content-toc { + > ul > li { + margin-bottom: 4px; + } + + ul { + margin: 0; + list-style: none; + padding-left: 1em; + } +} + /* fomantic's last-child selector does not work with hidden last child */ .ui.buttons .unescape-button { border-top-right-radius: .28571429rem;