Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Wiki TOC #24137

Merged
merged 5 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/markup/markdown/convertyaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func nodeToTable(meta *yaml.Node) ast.Node {

func mappingNodeToTable(meta *yaml.Node) ast.Node {
table := east.NewTable()
alignments := []east.Alignment{}
alignments := make([]east.Alignment, 0, len(meta.Content)/2)
for i := 0; i < len(meta.Content); i += 2 {
alignments = append(alignments, east.AlignNone)
}
Expand Down
34 changes: 20 additions & 14 deletions modules/markup/markdown/goldmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ type ASTTransformer struct{}
// Transform transforms the given AST tree.
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
firstChild := node.FirstChild()
createTOC := false
tocMode := ""
ctx := pc.Get(renderContextKey).(*markup.RenderContext)
rc := pc.Get(renderConfigKey).(*RenderConfig)

tocList := make([]markup.Header, 0, 20)
if rc.yamlNode != nil {
metaNode := rc.toMetaNode()
if metaNode != nil {
node.InsertBefore(node, firstChild, metaNode)
}
createTOC = rc.TOC
ctx.TableOfContents = make([]markup.Header, 0, 100)
tocMode = rc.TOC
}

attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
Expand All @@ -59,15 +60,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
}
}
text := n.Text(reader.Source())
txt := n.Text(reader.Source())
header := markup.Header{
Text: util.BytesToReadOnlyString(text),
Text: util.BytesToReadOnlyString(txt),
Level: v.Level,
}
if id, found := v.AttributeString("id"); found {
header.ID = util.BytesToReadOnlyString(id.([]byte))
}
ctx.TableOfContents = append(ctx.TableOfContents, header)
tocList = append(tocList, header)
case *ast.Image:
// Images need two things:
//
Expand Down Expand Up @@ -201,14 +202,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
return ast.WalkContinue, nil
})

if createTOC && len(ctx.TableOfContents) > 0 {
lang := rc.Lang
if len(lang) == 0 {
lang = setting.Langs[0]
}
tocNode := createTOCNode(ctx.TableOfContents, lang)
if tocNode != nil {
showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main"
showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar
if len(tocList) > 0 && (showTocInMain || showTocInSidebar) {
if showTocInMain {
tocNode := createTOCNode(tocList, rc.Lang, nil)
node.InsertBefore(node, firstChild, tocNode)
} else {
tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"})
ctx.SidebarTocNode = tocNode
}
}

Expand Down Expand Up @@ -373,7 +375,11 @@ func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.
func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
var err error
if entering {
_, err = w.WriteString("<details>")
if _, err = w.WriteString("<details"); err != nil {
return ast.WalkStop, err
}
html.RenderAttributes(w, node, nil)
_, err = w.WriteString(">")
} else {
_, err = w.WriteString("</details>")
}
Expand Down
23 changes: 14 additions & 9 deletions modules/markup/markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
)

var (
converter goldmark.Markdown
once = sync.Once{}
specMarkdown goldmark.Markdown
specMarkdownOnce sync.Once
)

var (
Expand All @@ -56,7 +56,7 @@ func (l *limitWriter) Write(data []byte) (int, error) {
if err != nil {
return n, err
}
return n, fmt.Errorf("Rendered content too large - truncating render")
return n, fmt.Errorf("rendered content too large - truncating render")
}
n, err := l.w.Write(data)
l.sum += int64(n)
Expand All @@ -73,10 +73,10 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
return pc
}

// actualRender renders Markdown to HTML without handling special links.
func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
once.Do(func() {
converter = goldmark.New(
// SpecializedMarkdown sets up the Gitea specific markdown extensions
func SpecializedMarkdown() goldmark.Markdown {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
specMarkdownOnce.Do(func() {
specMarkdown = goldmark.New(
goldmark.WithExtensions(
extension.NewTable(
extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
Expand Down Expand Up @@ -139,13 +139,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
)

// Override the original Tasklist renderer!
converter.Renderer().AddOptions(
specMarkdown.Renderer().AddOptions(
renderer.WithNodeRenderers(
util.Prioritized(NewHTMLRenderer(), 10),
),
)
})
return specMarkdown
}

// actualRender renders Markdown to HTML without handling special links.
func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
converter := SpecializedMarkdown()
lw := &limitWriter{
w: output,
limit: setting.UI.MaxDisplayFileSize * 3,
Expand Down Expand Up @@ -174,7 +179,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
buf = giteautil.NormalizeEOL(buf)

rc := &RenderConfig{
Meta: "table",
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
Icon: "table",
Lang: "",
}
Expand Down
79 changes: 38 additions & 41 deletions modules/markup/markdown/renderconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,42 @@ import (
"fmt"
"strings"

"code.gitea.io/gitea/modules/markup"

"github.com/yuin/goldmark/ast"
"gopkg.in/yaml.v3"
)

// RenderConfig represents rendering configuration for this file
type RenderConfig struct {
Meta string
Meta markup.RenderMetaMode
Icon string
TOC bool
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
Lang string
yamlNode *yaml.Node
}

func renderMetaModeFromString(s string) markup.RenderMetaMode {
switch strings.TrimSpace(strings.ToLower(s)) {
case "none":
return markup.RenderMetaAsNone
case "table":
return markup.RenderMetaAsTable
default: // "details"
return markup.RenderMetaAsDetails
}
}

// UnmarshalYAML implement yaml.v3 UnmarshalYAML
func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
if rc == nil {
rc = &RenderConfig{
Meta: "table",
Icon: "table",
Lang: "",
}
return nil
}

rc.yamlNode = value

type commonRenderConfig struct {
TOC bool `yaml:"include_toc"`
TOC string `yaml:"include_toc"`
Lang string `yaml:"lang"`
}
var basic commonRenderConfig
Expand All @@ -54,58 +64,45 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {

if err := value.Decode(&stringBasic); err == nil {
if stringBasic.Gitea != "" {
switch strings.TrimSpace(strings.ToLower(stringBasic.Gitea)) {
case "none":
rc.Meta = "none"
case "table":
rc.Meta = "table"
default: // "details"
rc.Meta = "details"
}
rc.Meta = renderMetaModeFromString(stringBasic.Gitea)
}
return nil
}

type giteaControl struct {
type yamlRenderConfig struct {
Meta *string `yaml:"meta"`
Icon *string `yaml:"details_icon"`
TOC *bool `yaml:"include_toc"`
TOC *string `yaml:"include_toc"`
Lang *string `yaml:"lang"`
}

type complexGiteaConfig struct {
Gitea *giteaControl `yaml:"gitea"`
type yamlRenderConfigWrapper struct {
Gitea *yamlRenderConfig `yaml:"gitea"`
}
var complex complexGiteaConfig
if err := value.Decode(&complex); err != nil {
return fmt.Errorf("unable to decode into complexRenderConfig %w", err)

var cfg yamlRenderConfigWrapper
if err := value.Decode(&cfg); err != nil {
return fmt.Errorf("unable to decode into yamlRenderConfigWrapper %w", err)
}

if complex.Gitea == nil {
if cfg.Gitea == nil {
return nil
}

if complex.Gitea.Meta != nil {
switch strings.TrimSpace(strings.ToLower(*complex.Gitea.Meta)) {
case "none":
rc.Meta = "none"
case "table":
rc.Meta = "table"
default: // "details"
rc.Meta = "details"
}
if cfg.Gitea.Meta != nil {
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
}

if complex.Gitea.Icon != nil {
rc.Icon = strings.TrimSpace(strings.ToLower(*complex.Gitea.Icon))
if cfg.Gitea.Icon != nil {
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
}

if complex.Gitea.Lang != nil && *complex.Gitea.Lang != "" {
rc.Lang = *complex.Gitea.Lang
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
rc.Lang = *cfg.Gitea.Lang
}

if complex.Gitea.TOC != nil {
rc.TOC = *complex.Gitea.TOC
if cfg.Gitea.TOC != nil {
rc.TOC = *cfg.Gitea.TOC
}

return nil
Expand All @@ -116,9 +113,9 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
return nil
}
switch rc.Meta {
case "table":
case markup.RenderMetaAsTable:
return nodeToTable(rc.yamlNode)
case "details":
case markup.RenderMetaAsDetails:
return nodeToDetails(rc.yamlNode, rc.Icon)
default:
return nil
Expand Down
10 changes: 5 additions & 5 deletions modules/markup/markdown/renderconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
},
{
"toc", &RenderConfig{
TOC: true,
TOC: "true",
Meta: "table",
Icon: "table",
Lang: "",
}, "include_toc: true",
},
{
"tocfalse", &RenderConfig{
TOC: false,
TOC: "false",
Meta: "table",
Icon: "table",
Lang: "",
Expand All @@ -78,7 +78,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
"toclang", &RenderConfig{
Meta: "table",
Icon: "table",
TOC: true,
TOC: "true",
Lang: "testlang",
}, `
include_toc: true
Expand Down Expand Up @@ -120,7 +120,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
"complex2", &RenderConfig{
Lang: "two",
Meta: "table",
TOC: true,
TOC: "true",
Icon: "smiley",
}, `
lang: one
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
}
if got.TOC != tt.expected.TOC {
t.Errorf("TOC Expected %t Got %t", tt.expected.TOC, got.TOC)
t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
}
})
}
Expand Down
8 changes: 6 additions & 2 deletions modules/markup/markdown/toc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import (
"github.com/yuin/goldmark/ast"
)

func createTOCNode(toc []markup.Header, lang string) ast.Node {
func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]string) ast.Node {
details := NewDetails()
summary := NewSummary()

for k, v := range detailsAttrs {
details.SetAttributeString(k, []byte(v))
}

summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
details.AppendChild(details, summary)
ul := ast.NewList('-')
Expand All @@ -40,7 +44,7 @@ func createTOCNode(toc []markup.Header, lang string) ast.Node {
}
li := ast.NewListItem(currentLevel * 2)
a := ast.NewLink()
a.Destination = []byte(fmt.Sprintf("#%s", url.PathEscape(header.ID)))
a.Destination = []byte(fmt.Sprintf("#%s", url.QueryEscape(header.ID)))
a.AppendChild(a, ast.NewString([]byte(header.Text)))
li.AppendChild(li, a)
ul.AppendChild(ul, li)
Expand Down
13 changes: 12 additions & 1 deletion modules/markup/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ import (

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"

"github.com/yuin/goldmark/ast"
)

type RenderMetaMode string

const (
RenderMetaAsDetails RenderMetaMode = "details" // default
RenderMetaAsNone RenderMetaMode = "none"
RenderMetaAsTable RenderMetaMode = "table"
)

type ProcessorHelper struct {
Expand Down Expand Up @@ -63,7 +73,8 @@ type RenderContext struct {
GitRepo *git.Repository
ShaExistCache map[string]bool
cancelFn func()
TableOfContents []Header
SidebarTocNode ast.Node
RenderMetaAs RenderMetaMode
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
}

Expand Down
7 changes: 0 additions & 7 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,6 @@ func NewFuncMap() []template.FuncMap {
}
return false
},
"Iterate": func(arg interface{}) (items []int64) {
count, _ := util.ToInt64(arg)
for i := int64(0); i < count; i++ {
items = append(items, i)
}
return items
},

// -----------------------------------------------------------------
// setting
Expand Down
Loading