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:
		
							parent
							
								
									d3fc9c08c8
								
							
						
					
					
						commit
						812cfd0ad9
					
				
					 10 changed files with 509 additions and 16 deletions
				
			
		
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							|  | @ -124,6 +124,7 @@ require ( | |||
| 	gopkg.in/ini.v1 v1.52.0 | ||||
| 	gopkg.in/ldap.v3 v3.0.2 | ||||
| 	gopkg.in/testfixtures.v2 v2.5.0 | ||||
| 	gopkg.in/yaml.v2 v2.2.8 | ||||
| 	mvdan.cc/xurls/v2 v2.1.0 | ||||
| 	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 | ||||
| 	xorm.io/builder v0.3.7 | ||||
|  |  | |||
|  | @ -351,6 +351,27 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) { | |||
| 			visitText = false | ||||
| 		} else if node.Data == "code" || node.Data == "pre" { | ||||
| 			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 { | ||||
| 			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 ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/common" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	giteautil "code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	meta "github.com/yuin/goldmark-meta" | ||||
| 	"github.com/yuin/goldmark/ast" | ||||
| 	east "github.com/yuin/goldmark/extension/ast" | ||||
| 	"github.com/yuin/goldmark/parser" | ||||
|  | @ -24,17 +28,56 @@ import ( | |||
| 
 | ||||
| var byteMailto = []byte("mailto:") | ||||
| 
 | ||||
| // GiteaASTTransformer is a default transformer of the goldmark tree.
 | ||||
| type GiteaASTTransformer struct{} | ||||
| // 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{} | ||||
| 
 | ||||
| // 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) { | ||||
| 		if !entering { | ||||
| 			return ast.WalkContinue, nil | ||||
| 		} | ||||
| 
 | ||||
| 		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: | ||||
| 			// Images need two things:
 | ||||
| 			//
 | ||||
|  | @ -91,6 +134,21 @@ func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader, | |||
| 		} | ||||
| 		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 { | ||||
|  | @ -139,10 +197,10 @@ func newPrefixedIDs() *prefixedIDs { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewTaskCheckBoxHTMLRenderer creates a TaskCheckBoxHTMLRenderer to render tasklists
 | ||||
| // NewHTMLRenderer creates a HTMLRenderer to render
 | ||||
| // in the gitea form.
 | ||||
| func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | ||||
| 	r := &TaskCheckBoxHTMLRenderer{ | ||||
| func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | ||||
| 	r := &HTMLRenderer{ | ||||
| 		Config: html.NewConfig(), | ||||
| 	} | ||||
| 	for _, opt := range opts { | ||||
|  | @ -151,19 +209,109 @@ func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||
| 	return r | ||||
| } | ||||
| 
 | ||||
| // TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
 | ||||
| // renders checkboxes in list items.
 | ||||
| // Overrides the default goldmark one to present the gitea format
 | ||||
| type TaskCheckBoxHTMLRenderer struct { | ||||
| // HTMLRenderer is a renderer.NodeRenderer implementation that
 | ||||
| // renders gitea specific features.
 | ||||
| type HTMLRenderer struct { | ||||
| 	html.Config | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 		return ast.WalkContinue, nil | ||||
| 	} | ||||
|  |  | |||
|  | @ -54,13 +54,13 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | |||
| 						extension.Ellipsis: nil, | ||||
| 					}), | ||||
| 				), | ||||
| 				meta.New(meta.WithTable()), | ||||
| 				meta.Meta, | ||||
| 			), | ||||
| 			goldmark.WithParserOptions( | ||||
| 				parser.WithAttribute(), | ||||
| 				parser.WithAutoHeadingID(), | ||||
| 				parser.WithASTTransformers( | ||||
| 					util.Prioritized(&GiteaASTTransformer{}, 10000), | ||||
| 					util.Prioritized(&ASTTransformer{}, 10000), | ||||
| 				), | ||||
| 			), | ||||
| 			goldmark.WithRendererOptions( | ||||
|  | @ -71,7 +71,7 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | |||
| 		// Override the original Tasklist renderer!
 | ||||
| 		converter.Renderer().AddOptions( | ||||
| 			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 { | ||||
| 		log.Error("Unable to render: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	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
 | ||||
| 	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
 | ||||
| 	generalSafeAttrs := []string{"abbr", "accept", "accept-charset", | ||||
| 		"accesskey", "action", "align", "alt", | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ create_new = Create… | |||
| user_profile_and_more = Profile and Settings… | ||||
| signed_in_as = Signed in as | ||||
| enable_javascript = This website works better with JavaScript. | ||||
| toc = Table of Contents | ||||
| 
 | ||||
| username = Username | ||||
| 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 | ||||
| # gopkg.in/yaml.v2 v2.2.8 | ||||
| ## explicit | ||||
| gopkg.in/yaml.v2 | ||||
| # mvdan.cc/xurls/v2 v2.1.0 | ||||
| ## explicit | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue