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