Skip to content

Commit

Permalink
Use markdown frontmatter to provide Table of contents, language and f…
Browse files Browse the repository at this point in the history
…rontmatter rendering (go-gitea#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 [email protected]
  • Loading branch information
zeripath authored and Yohann Delafollye committed Jul 31, 2020
1 parent 025317e commit 6667d47
Show file tree
Hide file tree
Showing 10 changed files with 509 additions and 16 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions modules/markup/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
107 changes: 107 additions & 0 deletions modules/markup/markdown/ast.go
Original file line number Diff line number Diff line change
@@ -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
}
172 changes: 160 additions & 12 deletions modules/markup/markdown/goldmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
//
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
Loading

0 comments on commit 6667d47

Please sign in to comment.