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>tags/v1.18.0-dev
@@ -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) | |||
} |
@@ -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 | |||
} | |||
@@ -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() | |||
@@ -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 |
@@ -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 | |||
}, | |||
}} | |||
} | |||
@@ -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 |
@@ -64,20 +64,39 @@ | |||
<p>{{.FormatWarning}}</p> | |||
</div> | |||
{{end}} | |||
<div class="ui {{if .sidebarPresent}}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}}grid equal width{{end}}" style="margin-top: 1rem;"> | |||
<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" $}} | |||
{{.content | Safe}} | |||
</div> | |||
{{if .sidebarPresent}} | |||
{{if or .sidebarPresent .toc}} | |||
<div class="column" style="padding-top: 0;"> | |||
<div class="ui segment wiki-content-sidebar"> | |||
{{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> | |||
{{end}} | |||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}} | |||
{{.sidebarContent | Safe}} | |||
</div> | |||
{{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"> | |||
{{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> | |||
{{end}} | |||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}} | |||
{{.sidebarContent | Safe}} | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
</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 */ | |||
.ui.buttons .unescape-button { | |||
border-top-right-radius: .28571429rem; |