mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	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 <art27@cantab.net>
This commit is contained in:
		@@ -27,13 +27,6 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var byteMailto = []byte("mailto:")
 | 
					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.
 | 
					// ASTTransformer is a default transformer of the goldmark tree.
 | 
				
			||||||
type ASTTransformer struct{}
 | 
					type ASTTransformer struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,12 +35,13 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
				
			|||||||
	metaData := meta.GetItems(pc)
 | 
						metaData := meta.GetItems(pc)
 | 
				
			||||||
	firstChild := node.FirstChild()
 | 
						firstChild := node.FirstChild()
 | 
				
			||||||
	createTOC := false
 | 
						createTOC := false
 | 
				
			||||||
	toc := []Header{}
 | 
						ctx := pc.Get(renderContextKey).(*markup.RenderContext)
 | 
				
			||||||
	rc := &RenderConfig{
 | 
						rc := &RenderConfig{
 | 
				
			||||||
		Meta: "table",
 | 
							Meta: "table",
 | 
				
			||||||
		Icon: "table",
 | 
							Icon: "table",
 | 
				
			||||||
		Lang: "",
 | 
							Lang: "",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if metaData != nil {
 | 
						if metaData != nil {
 | 
				
			||||||
		rc.ToRenderConfig(metaData)
 | 
							rc.ToRenderConfig(metaData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +50,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
				
			|||||||
			node.InsertBefore(node, firstChild, metaNode)
 | 
								node.InsertBefore(node, firstChild, metaNode)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		createTOC = rc.TOC
 | 
							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) {
 | 
						_ = 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) {
 | 
							switch v := n.(type) {
 | 
				
			||||||
		case *ast.Heading:
 | 
							case *ast.Heading:
 | 
				
			||||||
			if createTOC {
 | 
								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())
 | 
								text := n.Text(reader.Source())
 | 
				
			||||||
				header := Header{
 | 
								header := markup.Header{
 | 
				
			||||||
				Text:  util.BytesToReadOnlyString(text),
 | 
									Text:  util.BytesToReadOnlyString(text),
 | 
				
			||||||
				Level: v.Level,
 | 
									Level: v.Level,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if id, found := v.AttributeString("id"); found {
 | 
								if id, found := v.AttributeString("id"); found {
 | 
				
			||||||
				header.ID = util.BytesToReadOnlyString(id.([]byte))
 | 
									header.ID = util.BytesToReadOnlyString(id.([]byte))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
				toc = append(toc, header)
 | 
								ctx.TableOfContents = append(ctx.TableOfContents, header)
 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				for _, attr := range v.Attributes() {
 | 
					 | 
				
			||||||
					if _, ok := attr.Value.([]byte); !ok {
 | 
					 | 
				
			||||||
						v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		case *ast.Image:
 | 
							case *ast.Image:
 | 
				
			||||||
			// Images need two things:
 | 
								// Images need two things:
 | 
				
			||||||
			//
 | 
								//
 | 
				
			||||||
@@ -199,12 +190,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
				
			|||||||
		return ast.WalkContinue, nil
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if createTOC && len(toc) > 0 {
 | 
						if createTOC && len(ctx.TableOfContents) > 0 {
 | 
				
			||||||
		lang := rc.Lang
 | 
							lang := rc.Lang
 | 
				
			||||||
		if len(lang) == 0 {
 | 
							if len(lang) == 0 {
 | 
				
			||||||
			lang = setting.Langs[0]
 | 
								lang = setting.Langs[0]
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		tocNode := createTOCNode(toc, lang)
 | 
							tocNode := createTOCNode(ctx.TableOfContents, lang)
 | 
				
			||||||
		if tocNode != nil {
 | 
							if tocNode != nil {
 | 
				
			||||||
			node.InsertBefore(node, firstChild, tocNode)
 | 
								node.InsertBefore(node, firstChild, tocNode)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,6 +37,7 @@ var (
 | 
				
			|||||||
	urlPrefixKey     = parser.NewContextKey()
 | 
						urlPrefixKey     = parser.NewContextKey()
 | 
				
			||||||
	isWikiKey        = parser.NewContextKey()
 | 
						isWikiKey        = parser.NewContextKey()
 | 
				
			||||||
	renderMetasKey   = parser.NewContextKey()
 | 
						renderMetasKey   = parser.NewContextKey()
 | 
				
			||||||
 | 
						renderContextKey = parser.NewContextKey()
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type limitWriter struct {
 | 
					type limitWriter struct {
 | 
				
			||||||
@@ -67,6 +68,7 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
 | 
				
			|||||||
	pc.Set(urlPrefixKey, ctx.URLPrefix)
 | 
						pc.Set(urlPrefixKey, ctx.URLPrefix)
 | 
				
			||||||
	pc.Set(isWikiKey, ctx.IsWiki)
 | 
						pc.Set(isWikiKey, ctx.IsWiki)
 | 
				
			||||||
	pc.Set(renderMetasKey, ctx.Metas)
 | 
						pc.Set(renderMetasKey, ctx.Metas)
 | 
				
			||||||
 | 
						pc.Set(renderContextKey, ctx)
 | 
				
			||||||
	return pc
 | 
						return pc
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,12 +8,13 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/markup"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/translation/i18n"
 | 
						"code.gitea.io/gitea/modules/translation/i18n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/yuin/goldmark/ast"
 | 
						"github.com/yuin/goldmark/ast"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func createTOCNode(toc []Header, lang string) ast.Node {
 | 
					func createTOCNode(toc []markup.Header, lang string) ast.Node {
 | 
				
			||||||
	details := NewDetails()
 | 
						details := NewDetails()
 | 
				
			||||||
	summary := NewSummary()
 | 
						summary := NewSummary()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,13 @@ func Init() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Header holds the data about a header.
 | 
				
			||||||
 | 
					type Header struct {
 | 
				
			||||||
 | 
						Level int
 | 
				
			||||||
 | 
						Text  string
 | 
				
			||||||
 | 
						ID    string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RenderContext represents a render context
 | 
					// RenderContext represents a render context
 | 
				
			||||||
type RenderContext struct {
 | 
					type RenderContext struct {
 | 
				
			||||||
	Ctx             context.Context
 | 
						Ctx             context.Context
 | 
				
			||||||
@@ -45,6 +52,7 @@ type RenderContext struct {
 | 
				
			|||||||
	GitRepo         *git.Repository
 | 
						GitRepo         *git.Repository
 | 
				
			||||||
	ShaExistCache   map[string]bool
 | 
						ShaExistCache   map[string]bool
 | 
				
			||||||
	cancelFn        func()
 | 
						cancelFn        func()
 | 
				
			||||||
 | 
						TableOfContents []Header
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Cancel runs any cleanup functions that have been registered for this Ctx
 | 
					// Cancel runs any cleanup functions that have been registered for this Ctx
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ import (
 | 
				
			|||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"runtime"
 | 
						"runtime"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	texttmpl "text/template"
 | 
						texttmpl "text/template"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@@ -390,6 +391,66 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"Join":        strings.Join,
 | 
							"Join":        strings.Join,
 | 
				
			||||||
		"QueryEscape": url.QueryEscape,
 | 
							"QueryEscape": url.QueryEscape,
 | 
				
			||||||
		"DotEscape":   DotEscape,
 | 
							"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
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}}
 | 
						}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -280,6 +280,8 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 | 
				
			|||||||
		ctx.Data["footerPresent"] = false
 | 
							ctx.Data["footerPresent"] = false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["toc"] = rctx.TableOfContents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// get commit count - wiki revisions
 | 
						// get commit count - wiki revisions
 | 
				
			||||||
	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
 | 
						commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
 | 
				
			||||||
	ctx.Data["CommitCount"] = commitsCount
 | 
						ctx.Data["CommitCount"] = commitsCount
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,13 +64,31 @@
 | 
				
			|||||||
				<p>{{.FormatWarning}}</p>
 | 
									<p>{{.FormatWarning}}</p>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
		<div class="ui {{if .sidebarPresent}}grid equal width{{end}}" style="margin-top: 1rem;">
 | 
							<div class="ui {{if or .sidebarPresent .toc}}grid equal width{{end}}" style="margin-top: 1rem;">
 | 
				
			||||||
			<div class="ui {{if .sidebarPresent}}eleven wide column{{end}} segment markup wiki-content-main">
 | 
								<div class="ui {{if or .sidebarPresent .toc}}eleven wide column{{end}} segment markup wiki-content-main">
 | 
				
			||||||
				{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 | 
									{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
 | 
				
			||||||
				{{.content | Safe}}
 | 
									{{.content | Safe}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			{{if .sidebarPresent}}
 | 
								{{if or .sidebarPresent .toc}}
 | 
				
			||||||
			<div class="column" style="padding-top: 0;">
 | 
								<div class="column" style="padding-top: 0;">
 | 
				
			||||||
 | 
									{{if .toc}}
 | 
				
			||||||
 | 
										<div class="ui segment wiki-content-toc">
 | 
				
			||||||
 | 
											<details open>
 | 
				
			||||||
 | 
												<summary>
 | 
				
			||||||
 | 
													<div class="ui header">{{.i18n.Tr "toc"}}</div>
 | 
				
			||||||
 | 
												</summary>
 | 
				
			||||||
 | 
												{{$level := 0}}
 | 
				
			||||||
 | 
												{{range .toc}}
 | 
				
			||||||
 | 
													{{if lt $level .Level}}{{range Iterate (Subtract .Level $level)}}<ul>{{end}}{{end}}
 | 
				
			||||||
 | 
													{{if gt $level .Level}}{{range Iterate (Subtract $level .Level)}}</ul>{{end}}{{end}}
 | 
				
			||||||
 | 
													{{$level = .Level}}
 | 
				
			||||||
 | 
													<li><a href="#{{.ID}}">{{.Text}}</a></li>
 | 
				
			||||||
 | 
												{{end}}
 | 
				
			||||||
 | 
												{{range Iterate $level}}</ul>{{end}}
 | 
				
			||||||
 | 
											</details>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
									{{if .sidebarPresent}}
 | 
				
			||||||
					<div class="ui segment wiki-content-sidebar">
 | 
										<div class="ui segment wiki-content-sidebar">
 | 
				
			||||||
						{{if and .CanWriteWiki (not .Repository.IsMirror)}}
 | 
											{{if and .CanWriteWiki (not .Repository.IsMirror)}}
 | 
				
			||||||
							<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>
 | 
												<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>
 | 
				
			||||||
@@ -78,6 +96,7 @@
 | 
				
			|||||||
						{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
 | 
											{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
 | 
				
			||||||
						{{.sidebarContent | Safe}}
 | 
											{{.sidebarContent | Safe}}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			{{end}}
 | 
								{{end}}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 */
 | 
					/* fomantic's last-child selector does not work with hidden last child */
 | 
				
			||||||
.ui.buttons .unescape-button {
 | 
					.ui.buttons .unescape-button {
 | 
				
			||||||
  border-top-right-radius: .28571429rem;
 | 
					  border-top-right-radius: .28571429rem;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user