mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-20 20:07:03 +01: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:
parent
c1c07e533c
commit
ac88f21ecc
8 changed files with 146 additions and 50 deletions
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue