mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Use markdown frontmatter to provide Table of contents, language and frontmatter rendering (#11047)
* Add control for the rendering of the frontmatter * Add control to include a TOC * Add control to set language - allows control of ToC header and CJK glyph choice. Signed-off-by: Andrew Thornton art27@cantab.net
This commit is contained in:
		
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							@@ -124,6 +124,7 @@ require (
 | 
				
			|||||||
	gopkg.in/ini.v1 v1.52.0
 | 
						gopkg.in/ini.v1 v1.52.0
 | 
				
			||||||
	gopkg.in/ldap.v3 v3.0.2
 | 
						gopkg.in/ldap.v3 v3.0.2
 | 
				
			||||||
	gopkg.in/testfixtures.v2 v2.5.0
 | 
						gopkg.in/testfixtures.v2 v2.5.0
 | 
				
			||||||
 | 
						gopkg.in/yaml.v2 v2.2.8
 | 
				
			||||||
	mvdan.cc/xurls/v2 v2.1.0
 | 
						mvdan.cc/xurls/v2 v2.1.0
 | 
				
			||||||
	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
 | 
						strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
 | 
				
			||||||
	xorm.io/builder v0.3.7
 | 
						xorm.io/builder v0.3.7
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -351,6 +351,27 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) {
 | 
				
			|||||||
			visitText = false
 | 
								visitText = false
 | 
				
			||||||
		} else if node.Data == "code" || node.Data == "pre" {
 | 
							} else if node.Data == "code" || node.Data == "pre" {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
 | 
							} else if node.Data == "i" {
 | 
				
			||||||
 | 
								for _, attr := range node.Attr {
 | 
				
			||||||
 | 
									if attr.Key != "class" {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									classes := strings.Split(attr.Val, " ")
 | 
				
			||||||
 | 
									for i, class := range classes {
 | 
				
			||||||
 | 
										if class == "icon" {
 | 
				
			||||||
 | 
											classes[0], classes[i] = classes[i], classes[0]
 | 
				
			||||||
 | 
											attr.Val = strings.Join(classes, " ")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											// Remove all children of icons
 | 
				
			||||||
 | 
											child := node.FirstChild
 | 
				
			||||||
 | 
											for child != nil {
 | 
				
			||||||
 | 
												node.RemoveChild(child)
 | 
				
			||||||
 | 
												child = node.FirstChild
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		for n := node.FirstChild; n != nil; n = n.NextSibling {
 | 
							for n := node.FirstChild; n != nil; n = n.NextSibling {
 | 
				
			||||||
			ctx.visitNode(n, visitText)
 | 
								ctx.visitNode(n, visitText)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										107
									
								
								modules/markup/markdown/ast.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								modules/markup/markdown/ast.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package markdown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "github.com/yuin/goldmark/ast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Details is a block that contains Summary and details
 | 
				
			||||||
 | 
					type Details struct {
 | 
				
			||||||
 | 
						ast.BaseBlock
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Dump implements Node.Dump .
 | 
				
			||||||
 | 
					func (n *Details) Dump(source []byte, level int) {
 | 
				
			||||||
 | 
						ast.DumpHelper(n, source, level, nil, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KindDetails is the NodeKind for Details
 | 
				
			||||||
 | 
					var KindDetails = ast.NewNodeKind("Details")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Kind implements Node.Kind.
 | 
				
			||||||
 | 
					func (n *Details) Kind() ast.NodeKind {
 | 
				
			||||||
 | 
						return KindDetails
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewDetails returns a new Paragraph node.
 | 
				
			||||||
 | 
					func NewDetails() *Details {
 | 
				
			||||||
 | 
						return &Details{
 | 
				
			||||||
 | 
							BaseBlock: ast.BaseBlock{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsDetails returns true if the given node implements the Details interface,
 | 
				
			||||||
 | 
					// otherwise false.
 | 
				
			||||||
 | 
					func IsDetails(node ast.Node) bool {
 | 
				
			||||||
 | 
						_, ok := node.(*Details)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Summary is a block that contains the summary of details block
 | 
				
			||||||
 | 
					type Summary struct {
 | 
				
			||||||
 | 
						ast.BaseBlock
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Dump implements Node.Dump .
 | 
				
			||||||
 | 
					func (n *Summary) Dump(source []byte, level int) {
 | 
				
			||||||
 | 
						ast.DumpHelper(n, source, level, nil, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KindSummary is the NodeKind for Summary
 | 
				
			||||||
 | 
					var KindSummary = ast.NewNodeKind("Summary")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Kind implements Node.Kind.
 | 
				
			||||||
 | 
					func (n *Summary) Kind() ast.NodeKind {
 | 
				
			||||||
 | 
						return KindSummary
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewSummary returns a new Summary node.
 | 
				
			||||||
 | 
					func NewSummary() *Summary {
 | 
				
			||||||
 | 
						return &Summary{
 | 
				
			||||||
 | 
							BaseBlock: ast.BaseBlock{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsSummary returns true if the given node implements the Summary interface,
 | 
				
			||||||
 | 
					// otherwise false.
 | 
				
			||||||
 | 
					func IsSummary(node ast.Node) bool {
 | 
				
			||||||
 | 
						_, ok := node.(*Summary)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Icon is an inline for a fomantic icon
 | 
				
			||||||
 | 
					type Icon struct {
 | 
				
			||||||
 | 
						ast.BaseInline
 | 
				
			||||||
 | 
						Name []byte
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Dump implements Node.Dump .
 | 
				
			||||||
 | 
					func (n *Icon) Dump(source []byte, level int) {
 | 
				
			||||||
 | 
						m := map[string]string{}
 | 
				
			||||||
 | 
						m["Name"] = string(n.Name)
 | 
				
			||||||
 | 
						ast.DumpHelper(n, source, level, m, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// KindIcon is the NodeKind for Icon
 | 
				
			||||||
 | 
					var KindIcon = ast.NewNodeKind("Icon")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Kind implements Node.Kind.
 | 
				
			||||||
 | 
					func (n *Icon) Kind() ast.NodeKind {
 | 
				
			||||||
 | 
						return KindIcon
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewIcon returns a new Paragraph node.
 | 
				
			||||||
 | 
					func NewIcon(name string) *Icon {
 | 
				
			||||||
 | 
						return &Icon{
 | 
				
			||||||
 | 
							BaseInline: ast.BaseInline{},
 | 
				
			||||||
 | 
							Name:       []byte(name),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsIcon returns true if the given node implements the Icon interface,
 | 
				
			||||||
 | 
					// otherwise false.
 | 
				
			||||||
 | 
					func IsIcon(node ast.Node) bool {
 | 
				
			||||||
 | 
						_, ok := node.(*Icon)
 | 
				
			||||||
 | 
						return ok
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,12 +7,16 @@ package markdown
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup"
 | 
						"code.gitea.io/gitea/modules/markup"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup/common"
 | 
						"code.gitea.io/gitea/modules/markup/common"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	giteautil "code.gitea.io/gitea/modules/util"
 | 
						giteautil "code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						meta "github.com/yuin/goldmark-meta"
 | 
				
			||||||
	"github.com/yuin/goldmark/ast"
 | 
						"github.com/yuin/goldmark/ast"
 | 
				
			||||||
	east "github.com/yuin/goldmark/extension/ast"
 | 
						east "github.com/yuin/goldmark/extension/ast"
 | 
				
			||||||
	"github.com/yuin/goldmark/parser"
 | 
						"github.com/yuin/goldmark/parser"
 | 
				
			||||||
@@ -24,17 +28,56 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var byteMailto = []byte("mailto:")
 | 
					var byteMailto = []byte("mailto:")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GiteaASTTransformer is a default transformer of the goldmark tree.
 | 
					// Header holds the data about a header.
 | 
				
			||||||
type GiteaASTTransformer struct{}
 | 
					type Header struct {
 | 
				
			||||||
 | 
						Level int
 | 
				
			||||||
 | 
						Text  string
 | 
				
			||||||
 | 
						ID    string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ASTTransformer is a default transformer of the goldmark tree.
 | 
				
			||||||
 | 
					type ASTTransformer struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Transform transforms the given AST tree.
 | 
					// Transform transforms the given AST tree.
 | 
				
			||||||
func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
 | 
					func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
 | 
				
			||||||
 | 
						metaData := meta.GetItems(pc)
 | 
				
			||||||
 | 
						firstChild := node.FirstChild()
 | 
				
			||||||
 | 
						createTOC := false
 | 
				
			||||||
 | 
						var toc = []Header{}
 | 
				
			||||||
 | 
						rc := &RenderConfig{
 | 
				
			||||||
 | 
							Meta: "table",
 | 
				
			||||||
 | 
							Icon: "table",
 | 
				
			||||||
 | 
							Lang: "",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if metaData != nil {
 | 
				
			||||||
 | 
							rc.ToRenderConfig(metaData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							metaNode := rc.toMetaNode(metaData)
 | 
				
			||||||
 | 
							if metaNode != nil {
 | 
				
			||||||
 | 
								node.InsertBefore(node, firstChild, metaNode)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							createTOC = rc.TOC
 | 
				
			||||||
 | 
							toc = make([]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) {
 | 
				
			||||||
		if !entering {
 | 
							if !entering {
 | 
				
			||||||
			return ast.WalkContinue, nil
 | 
								return ast.WalkContinue, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		switch v := n.(type) {
 | 
							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)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		case *ast.Image:
 | 
							case *ast.Image:
 | 
				
			||||||
			// Images need two things:
 | 
								// Images need two things:
 | 
				
			||||||
			//
 | 
								//
 | 
				
			||||||
@@ -91,6 +134,21 @@ func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader,
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		return ast.WalkContinue, nil
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if createTOC && len(toc) > 0 {
 | 
				
			||||||
 | 
							lang := rc.Lang
 | 
				
			||||||
 | 
							if len(lang) == 0 {
 | 
				
			||||||
 | 
								lang = setting.Langs[0]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tocNode := createTOCNode(toc, lang)
 | 
				
			||||||
 | 
							if tocNode != nil {
 | 
				
			||||||
 | 
								node.InsertBefore(node, firstChild, tocNode)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(rc.Lang) > 0 {
 | 
				
			||||||
 | 
							node.SetAttributeString("lang", []byte(rc.Lang))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type prefixedIDs struct {
 | 
					type prefixedIDs struct {
 | 
				
			||||||
@@ -139,10 +197,10 @@ func newPrefixedIDs() *prefixedIDs {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewTaskCheckBoxHTMLRenderer creates a TaskCheckBoxHTMLRenderer to render tasklists
 | 
					// NewHTMLRenderer creates a HTMLRenderer to render
 | 
				
			||||||
// in the gitea form.
 | 
					// in the gitea form.
 | 
				
			||||||
func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 | 
					func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 | 
				
			||||||
	r := &TaskCheckBoxHTMLRenderer{
 | 
						r := &HTMLRenderer{
 | 
				
			||||||
		Config: html.NewConfig(),
 | 
							Config: html.NewConfig(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	for _, opt := range opts {
 | 
						for _, opt := range opts {
 | 
				
			||||||
@@ -151,19 +209,109 @@ func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
 | 
				
			|||||||
	return r
 | 
						return r
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
 | 
					// HTMLRenderer is a renderer.NodeRenderer implementation that
 | 
				
			||||||
// renders checkboxes in list items.
 | 
					// renders gitea specific features.
 | 
				
			||||||
// Overrides the default goldmark one to present the gitea format
 | 
					type HTMLRenderer struct {
 | 
				
			||||||
type TaskCheckBoxHTMLRenderer struct {
 | 
					 | 
				
			||||||
	html.Config
 | 
						html.Config
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
 | 
					// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
 | 
				
			||||||
func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 | 
					func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 | 
				
			||||||
 | 
						reg.Register(ast.KindDocument, r.renderDocument)
 | 
				
			||||||
 | 
						reg.Register(KindDetails, r.renderDetails)
 | 
				
			||||||
 | 
						reg.Register(KindSummary, r.renderSummary)
 | 
				
			||||||
 | 
						reg.Register(KindIcon, r.renderIcon)
 | 
				
			||||||
	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 | 
						reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
					func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
				
			||||||
 | 
						log.Info("renderDocument %v", node)
 | 
				
			||||||
 | 
						n := node.(*ast.Document)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if val, has := n.AttributeString("lang"); has {
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							if entering {
 | 
				
			||||||
 | 
								_, err = w.WriteString("<div")
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									_, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									_, err = w.WriteRune('>')
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								_, err = w.WriteString("</div>")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return ast.WalkStop, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ast.WalkContinue, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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>")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							_, err = w.WriteString("</details>")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ast.WalkStop, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ast.WalkContinue, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						if entering {
 | 
				
			||||||
 | 
							_, err = w.WriteString("<summary>")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							_, err = w.WriteString("</summary>")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ast.WalkStop, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ast.WalkContinue, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var validNameRE = regexp.MustCompile("^[a-z ]+$")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
				
			||||||
 | 
						if !entering {
 | 
				
			||||||
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						n := node.(*Icon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						name := strings.TrimSpace(strings.ToLower(string(n.Name)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(name) == 0 {
 | 
				
			||||||
 | 
							// skip this
 | 
				
			||||||
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !validNameRE.MatchString(name) {
 | 
				
			||||||
 | 
							// skip this
 | 
				
			||||||
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ast.WalkStop, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ast.WalkContinue, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
				
			||||||
	if !entering {
 | 
						if !entering {
 | 
				
			||||||
		return ast.WalkContinue, nil
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,13 +54,13 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
 | 
				
			|||||||
						extension.Ellipsis: nil,
 | 
											extension.Ellipsis: nil,
 | 
				
			||||||
					}),
 | 
										}),
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				meta.New(meta.WithTable()),
 | 
									meta.Meta,
 | 
				
			||||||
			),
 | 
								),
 | 
				
			||||||
			goldmark.WithParserOptions(
 | 
								goldmark.WithParserOptions(
 | 
				
			||||||
				parser.WithAttribute(),
 | 
									parser.WithAttribute(),
 | 
				
			||||||
				parser.WithAutoHeadingID(),
 | 
									parser.WithAutoHeadingID(),
 | 
				
			||||||
				parser.WithASTTransformers(
 | 
									parser.WithASTTransformers(
 | 
				
			||||||
					util.Prioritized(&GiteaASTTransformer{}, 10000),
 | 
										util.Prioritized(&ASTTransformer{}, 10000),
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
			),
 | 
								),
 | 
				
			||||||
			goldmark.WithRendererOptions(
 | 
								goldmark.WithRendererOptions(
 | 
				
			||||||
@@ -71,7 +71,7 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
 | 
				
			|||||||
		// Override the original Tasklist renderer!
 | 
							// Override the original Tasklist renderer!
 | 
				
			||||||
		converter.Renderer().AddOptions(
 | 
							converter.Renderer().AddOptions(
 | 
				
			||||||
			renderer.WithNodeRenderers(
 | 
								renderer.WithNodeRenderers(
 | 
				
			||||||
				util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 1000),
 | 
									util.Prioritized(NewHTMLRenderer(), 10),
 | 
				
			||||||
			),
 | 
								),
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -85,7 +85,6 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
 | 
				
			|||||||
	if err := converter.Convert(giteautil.NormalizeEOL(body), &buf, parser.WithContext(pc)); err != nil {
 | 
						if err := converter.Convert(giteautil.NormalizeEOL(body), &buf, parser.WithContext(pc)); err != nil {
 | 
				
			||||||
		log.Error("Unable to render: %v", err)
 | 
							log.Error("Unable to render: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return markup.SanitizeReader(&buf).Bytes()
 | 
						return markup.SanitizeReader(&buf).Bytes()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										163
									
								
								modules/markup/markdown/renderconfig.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								modules/markup/markdown/renderconfig.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package markdown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/yuin/goldmark/ast"
 | 
				
			||||||
 | 
						east "github.com/yuin/goldmark/extension/ast"
 | 
				
			||||||
 | 
						"gopkg.in/yaml.v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RenderConfig represents rendering configuration for this file
 | 
				
			||||||
 | 
					type RenderConfig struct {
 | 
				
			||||||
 | 
						Meta string
 | 
				
			||||||
 | 
						Icon string
 | 
				
			||||||
 | 
						TOC  bool
 | 
				
			||||||
 | 
						Lang string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToRenderConfig converts a yaml.MapSlice to a RenderConfig
 | 
				
			||||||
 | 
					func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) {
 | 
				
			||||||
 | 
						if meta == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						found := false
 | 
				
			||||||
 | 
						var giteaMetaControl yaml.MapItem
 | 
				
			||||||
 | 
						for _, item := range meta {
 | 
				
			||||||
 | 
							strKey, ok := item.Key.(string)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							strKey = strings.TrimSpace(strings.ToLower(strKey))
 | 
				
			||||||
 | 
							switch strKey {
 | 
				
			||||||
 | 
							case "gitea":
 | 
				
			||||||
 | 
								giteaMetaControl = item
 | 
				
			||||||
 | 
								found = true
 | 
				
			||||||
 | 
							case "include_toc":
 | 
				
			||||||
 | 
								val, ok := item.Value.(bool)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rc.TOC = val
 | 
				
			||||||
 | 
							case "lang":
 | 
				
			||||||
 | 
								val, ok := item.Value.(string)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								val = strings.TrimSpace(val)
 | 
				
			||||||
 | 
								if len(val) == 0 {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								rc.Lang = val
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if found {
 | 
				
			||||||
 | 
							switch v := giteaMetaControl.Value.(type) {
 | 
				
			||||||
 | 
							case string:
 | 
				
			||||||
 | 
								switch v {
 | 
				
			||||||
 | 
								case "none":
 | 
				
			||||||
 | 
									rc.Meta = "none"
 | 
				
			||||||
 | 
								case "table":
 | 
				
			||||||
 | 
									rc.Meta = "table"
 | 
				
			||||||
 | 
								default: // "details"
 | 
				
			||||||
 | 
									rc.Meta = "details"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case yaml.MapSlice:
 | 
				
			||||||
 | 
								for _, item := range v {
 | 
				
			||||||
 | 
									strKey, ok := item.Key.(string)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									strKey = strings.TrimSpace(strings.ToLower(strKey))
 | 
				
			||||||
 | 
									switch strKey {
 | 
				
			||||||
 | 
									case "meta":
 | 
				
			||||||
 | 
										val, ok := item.Value.(string)
 | 
				
			||||||
 | 
										if !ok {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										switch strings.TrimSpace(strings.ToLower(val)) {
 | 
				
			||||||
 | 
										case "none":
 | 
				
			||||||
 | 
											rc.Meta = "none"
 | 
				
			||||||
 | 
										case "table":
 | 
				
			||||||
 | 
											rc.Meta = "table"
 | 
				
			||||||
 | 
										default: // "details"
 | 
				
			||||||
 | 
											rc.Meta = "details"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									case "details_icon":
 | 
				
			||||||
 | 
										val, ok := item.Value.(string)
 | 
				
			||||||
 | 
										if !ok {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										rc.Icon = strings.TrimSpace(strings.ToLower(val))
 | 
				
			||||||
 | 
									case "include_toc":
 | 
				
			||||||
 | 
										val, ok := item.Value.(bool)
 | 
				
			||||||
 | 
										if !ok {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										rc.TOC = val
 | 
				
			||||||
 | 
									case "lang":
 | 
				
			||||||
 | 
										val, ok := item.Value.(string)
 | 
				
			||||||
 | 
										if !ok {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										val = strings.TrimSpace(val)
 | 
				
			||||||
 | 
										if len(val) == 0 {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										rc.Lang = val
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (rc *RenderConfig) toMetaNode(meta yaml.MapSlice) ast.Node {
 | 
				
			||||||
 | 
						switch rc.Meta {
 | 
				
			||||||
 | 
						case "table":
 | 
				
			||||||
 | 
							return metaToTable(meta)
 | 
				
			||||||
 | 
						case "details":
 | 
				
			||||||
 | 
							return metaToDetails(meta, rc.Icon)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func metaToTable(meta yaml.MapSlice) ast.Node {
 | 
				
			||||||
 | 
						table := east.NewTable()
 | 
				
			||||||
 | 
						alignments := []east.Alignment{}
 | 
				
			||||||
 | 
						for range meta {
 | 
				
			||||||
 | 
							alignments = append(alignments, east.AlignNone)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						row := east.NewTableRow(alignments)
 | 
				
			||||||
 | 
						for _, item := range meta {
 | 
				
			||||||
 | 
							cell := east.NewTableCell()
 | 
				
			||||||
 | 
							cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Key))))
 | 
				
			||||||
 | 
							row.AppendChild(row, cell)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						table.AppendChild(table, east.NewTableHeader(row))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						row = east.NewTableRow(alignments)
 | 
				
			||||||
 | 
						for _, item := range meta {
 | 
				
			||||||
 | 
							cell := east.NewTableCell()
 | 
				
			||||||
 | 
							cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Value))))
 | 
				
			||||||
 | 
							row.AppendChild(row, cell)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						table.AppendChild(table, row)
 | 
				
			||||||
 | 
						return table
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func metaToDetails(meta yaml.MapSlice, icon string) ast.Node {
 | 
				
			||||||
 | 
						details := NewDetails()
 | 
				
			||||||
 | 
						summary := NewSummary()
 | 
				
			||||||
 | 
						summary.AppendChild(summary, NewIcon(icon))
 | 
				
			||||||
 | 
						details.AppendChild(details, summary)
 | 
				
			||||||
 | 
						details.AppendChild(details, metaToTable(meta))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return details
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										49
									
								
								modules/markup/markdown/toc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/markup/markdown/toc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package markdown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/unknwon/i18n"
 | 
				
			||||||
 | 
						"github.com/yuin/goldmark/ast"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createTOCNode(toc []Header, lang string) ast.Node {
 | 
				
			||||||
 | 
						details := NewDetails()
 | 
				
			||||||
 | 
						summary := NewSummary()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						summary.AppendChild(summary, ast.NewString([]byte(i18n.Tr(lang, "toc"))))
 | 
				
			||||||
 | 
						details.AppendChild(details, summary)
 | 
				
			||||||
 | 
						ul := ast.NewList('-')
 | 
				
			||||||
 | 
						details.AppendChild(details, ul)
 | 
				
			||||||
 | 
						currentLevel := 6
 | 
				
			||||||
 | 
						for _, header := range toc {
 | 
				
			||||||
 | 
							if header.Level < currentLevel {
 | 
				
			||||||
 | 
								currentLevel = header.Level
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, header := range toc {
 | 
				
			||||||
 | 
							for currentLevel > header.Level {
 | 
				
			||||||
 | 
								ul = ul.Parent().(*ast.List)
 | 
				
			||||||
 | 
								currentLevel--
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for currentLevel < header.Level {
 | 
				
			||||||
 | 
								newL := ast.NewList('-')
 | 
				
			||||||
 | 
								ul.AppendChild(ul, newL)
 | 
				
			||||||
 | 
								currentLevel++
 | 
				
			||||||
 | 
								ul = newL
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							li := ast.NewListItem(currentLevel * 2)
 | 
				
			||||||
 | 
							a := ast.NewLink()
 | 
				
			||||||
 | 
							a.Destination = []byte(fmt.Sprintf("#%s", url.PathEscape(header.ID)))
 | 
				
			||||||
 | 
							a.AppendChild(a, ast.NewString([]byte(header.Text)))
 | 
				
			||||||
 | 
							li.AppendChild(li, a)
 | 
				
			||||||
 | 
							ul.AppendChild(ul, li)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return details
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -56,6 +56,9 @@ func ReplaceSanitizer() {
 | 
				
			|||||||
	// Allow classes for task lists
 | 
						// Allow classes for task lists
 | 
				
			||||||
	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list`)).OnElements("ul")
 | 
						sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list`)).OnElements("ul")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Allow icons
 | 
				
			||||||
 | 
						sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i", "span")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Allow generally safe attributes
 | 
						// Allow generally safe attributes
 | 
				
			||||||
	generalSafeAttrs := []string{"abbr", "accept", "accept-charset",
 | 
						generalSafeAttrs := []string{"abbr", "accept", "accept-charset",
 | 
				
			||||||
		"accesskey", "action", "align", "alt",
 | 
							"accesskey", "action", "align", "alt",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ create_new = Create…
 | 
				
			|||||||
user_profile_and_more = Profile and Settings…
 | 
					user_profile_and_more = Profile and Settings…
 | 
				
			||||||
signed_in_as = Signed in as
 | 
					signed_in_as = Signed in as
 | 
				
			||||||
enable_javascript = This website works better with JavaScript.
 | 
					enable_javascript = This website works better with JavaScript.
 | 
				
			||||||
 | 
					toc = Table of Contents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
username = Username
 | 
					username = Username
 | 
				
			||||||
email = Email Address
 | 
					email = Email Address
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							@@ -844,6 +844,7 @@ gopkg.in/toqueteos/substring.v1
 | 
				
			|||||||
# gopkg.in/warnings.v0 v0.1.2
 | 
					# gopkg.in/warnings.v0 v0.1.2
 | 
				
			||||||
gopkg.in/warnings.v0
 | 
					gopkg.in/warnings.v0
 | 
				
			||||||
# gopkg.in/yaml.v2 v2.2.8
 | 
					# gopkg.in/yaml.v2 v2.2.8
 | 
				
			||||||
 | 
					## explicit
 | 
				
			||||||
gopkg.in/yaml.v2
 | 
					gopkg.in/yaml.v2
 | 
				
			||||||
# mvdan.cc/xurls/v2 v2.1.0
 | 
					# mvdan.cc/xurls/v2 v2.1.0
 | 
				
			||||||
## explicit
 | 
					## explicit
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user