mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 12:53:52 +01: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:
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/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
|
||||||
|
|
Loading…
Reference in a new issue