aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod1
-rw-r--r--modules/markup/html.go21
-rw-r--r--modules/markup/markdown/ast.go107
-rw-r--r--modules/markup/markdown/goldmark.go172
-rw-r--r--modules/markup/markdown/markdown.go7
-rw-r--r--modules/markup/markdown/renderconfig.go163
-rw-r--r--modules/markup/markdown/toc.go49
-rw-r--r--modules/markup/sanitizer.go3
-rw-r--r--options/locale/locale_en-US.ini1
-rw-r--r--vendor/modules.txt1
10 files changed, 509 insertions, 16 deletions
diff --git a/go.mod b/go.mod
index e391da4921..8ed0fe9289 100644
--- a/go.mod
+++ b/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
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 51d161ecca..294b870d8c 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -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)
diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
new file mode 100644
index 0000000000..f79d12435b
--- /dev/null
+++ b/modules/markup/markdown/ast.go
@@ -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
+}
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 70f47e289e..6edb3e6971 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -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
}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index c48bbab301..e50301ffe4 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -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()
}
diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go
new file mode 100644
index 0000000000..bef67e9e59
--- /dev/null
+++ b/modules/markup/markdown/renderconfig.go
@@ -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
+}
diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go
new file mode 100644
index 0000000000..189821c341
--- /dev/null
+++ b/modules/markup/markdown/toc.go
@@ -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
+}
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index b5c6dc25f4..95c6eb0dc4 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -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",
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 848cb05a86..2a4789e22e 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -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
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 545b100d6f..d6d4d6c8a8 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -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