Skip to content

Commit 812cfd0

Browse files
authored
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 [email protected]
1 parent d3fc9c0 commit 812cfd0

File tree

10 files changed

+509
-16
lines changed

10 files changed

+509
-16
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ require (
124124
gopkg.in/ini.v1 v1.52.0
125125
gopkg.in/ldap.v3 v3.0.2
126126
gopkg.in/testfixtures.v2 v2.5.0
127+
gopkg.in/yaml.v2 v2.2.8
127128
mvdan.cc/xurls/v2 v2.1.0
128129
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
129130
xorm.io/builder v0.3.7

modules/markup/html.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,27 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) {
351351
visitText = false
352352
} else if node.Data == "code" || node.Data == "pre" {
353353
return
354+
} else if node.Data == "i" {
355+
for _, attr := range node.Attr {
356+
if attr.Key != "class" {
357+
continue
358+
}
359+
classes := strings.Split(attr.Val, " ")
360+
for i, class := range classes {
361+
if class == "icon" {
362+
classes[0], classes[i] = classes[i], classes[0]
363+
attr.Val = strings.Join(classes, " ")
364+
365+
// Remove all children of icons
366+
child := node.FirstChild
367+
for child != nil {
368+
node.RemoveChild(child)
369+
child = node.FirstChild
370+
}
371+
break
372+
}
373+
}
374+
}
354375
}
355376
for n := node.FirstChild; n != nil; n = n.NextSibling {
356377
ctx.visitNode(n, visitText)

modules/markup/markdown/ast.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package markdown
6+
7+
import "github.com/yuin/goldmark/ast"
8+
9+
// Details is a block that contains Summary and details
10+
type Details struct {
11+
ast.BaseBlock
12+
}
13+
14+
// Dump implements Node.Dump .
15+
func (n *Details) Dump(source []byte, level int) {
16+
ast.DumpHelper(n, source, level, nil, nil)
17+
}
18+
19+
// KindDetails is the NodeKind for Details
20+
var KindDetails = ast.NewNodeKind("Details")
21+
22+
// Kind implements Node.Kind.
23+
func (n *Details) Kind() ast.NodeKind {
24+
return KindDetails
25+
}
26+
27+
// NewDetails returns a new Paragraph node.
28+
func NewDetails() *Details {
29+
return &Details{
30+
BaseBlock: ast.BaseBlock{},
31+
}
32+
}
33+
34+
// IsDetails returns true if the given node implements the Details interface,
35+
// otherwise false.
36+
func IsDetails(node ast.Node) bool {
37+
_, ok := node.(*Details)
38+
return ok
39+
}
40+
41+
// Summary is a block that contains the summary of details block
42+
type Summary struct {
43+
ast.BaseBlock
44+
}
45+
46+
// Dump implements Node.Dump .
47+
func (n *Summary) Dump(source []byte, level int) {
48+
ast.DumpHelper(n, source, level, nil, nil)
49+
}
50+
51+
// KindSummary is the NodeKind for Summary
52+
var KindSummary = ast.NewNodeKind("Summary")
53+
54+
// Kind implements Node.Kind.
55+
func (n *Summary) Kind() ast.NodeKind {
56+
return KindSummary
57+
}
58+
59+
// NewSummary returns a new Summary node.
60+
func NewSummary() *Summary {
61+
return &Summary{
62+
BaseBlock: ast.BaseBlock{},
63+
}
64+
}
65+
66+
// IsSummary returns true if the given node implements the Summary interface,
67+
// otherwise false.
68+
func IsSummary(node ast.Node) bool {
69+
_, ok := node.(*Summary)
70+
return ok
71+
}
72+
73+
// Icon is an inline for a fomantic icon
74+
type Icon struct {
75+
ast.BaseInline
76+
Name []byte
77+
}
78+
79+
// Dump implements Node.Dump .
80+
func (n *Icon) Dump(source []byte, level int) {
81+
m := map[string]string{}
82+
m["Name"] = string(n.Name)
83+
ast.DumpHelper(n, source, level, m, nil)
84+
}
85+
86+
// KindIcon is the NodeKind for Icon
87+
var KindIcon = ast.NewNodeKind("Icon")
88+
89+
// Kind implements Node.Kind.
90+
func (n *Icon) Kind() ast.NodeKind {
91+
return KindIcon
92+
}
93+
94+
// NewIcon returns a new Paragraph node.
95+
func NewIcon(name string) *Icon {
96+
return &Icon{
97+
BaseInline: ast.BaseInline{},
98+
Name: []byte(name),
99+
}
100+
}
101+
102+
// IsIcon returns true if the given node implements the Icon interface,
103+
// otherwise false.
104+
func IsIcon(node ast.Node) bool {
105+
_, ok := node.(*Icon)
106+
return ok
107+
}

modules/markup/markdown/goldmark.go

Lines changed: 160 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ package markdown
77
import (
88
"bytes"
99
"fmt"
10+
"regexp"
1011
"strings"
1112

13+
"code.gitea.io/gitea/modules/log"
1214
"code.gitea.io/gitea/modules/markup"
1315
"code.gitea.io/gitea/modules/markup/common"
16+
"code.gitea.io/gitea/modules/setting"
1417
giteautil "code.gitea.io/gitea/modules/util"
1518

19+
meta "github.com/yuin/goldmark-meta"
1620
"github.com/yuin/goldmark/ast"
1721
east "github.com/yuin/goldmark/extension/ast"
1822
"github.com/yuin/goldmark/parser"
@@ -24,17 +28,56 @@ import (
2428

2529
var byteMailto = []byte("mailto:")
2630

27-
// GiteaASTTransformer is a default transformer of the goldmark tree.
28-
type GiteaASTTransformer struct{}
31+
// Header holds the data about a header.
32+
type Header struct {
33+
Level int
34+
Text string
35+
ID string
36+
}
37+
38+
// ASTTransformer is a default transformer of the goldmark tree.
39+
type ASTTransformer struct{}
2940

3041
// Transform transforms the given AST tree.
31-
func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
42+
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
43+
metaData := meta.GetItems(pc)
44+
firstChild := node.FirstChild()
45+
createTOC := false
46+
var toc = []Header{}
47+
rc := &RenderConfig{
48+
Meta: "table",
49+
Icon: "table",
50+
Lang: "",
51+
}
52+
if metaData != nil {
53+
rc.ToRenderConfig(metaData)
54+
55+
metaNode := rc.toMetaNode(metaData)
56+
if metaNode != nil {
57+
node.InsertBefore(node, firstChild, metaNode)
58+
}
59+
createTOC = rc.TOC
60+
toc = make([]Header, 0, 100)
61+
}
62+
3263
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
3364
if !entering {
3465
return ast.WalkContinue, nil
3566
}
3667

3768
switch v := n.(type) {
69+
case *ast.Heading:
70+
if createTOC {
71+
text := n.Text(reader.Source())
72+
header := Header{
73+
Text: util.BytesToReadOnlyString(text),
74+
Level: v.Level,
75+
}
76+
if id, found := v.AttributeString("id"); found {
77+
header.ID = util.BytesToReadOnlyString(id.([]byte))
78+
}
79+
toc = append(toc, header)
80+
}
3881
case *ast.Image:
3982
// Images need two things:
4083
//
@@ -91,6 +134,21 @@ func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader,
91134
}
92135
return ast.WalkContinue, nil
93136
})
137+
138+
if createTOC && len(toc) > 0 {
139+
lang := rc.Lang
140+
if len(lang) == 0 {
141+
lang = setting.Langs[0]
142+
}
143+
tocNode := createTOCNode(toc, lang)
144+
if tocNode != nil {
145+
node.InsertBefore(node, firstChild, tocNode)
146+
}
147+
}
148+
149+
if len(rc.Lang) > 0 {
150+
node.SetAttributeString("lang", []byte(rc.Lang))
151+
}
94152
}
95153

96154
type prefixedIDs struct {
@@ -139,10 +197,10 @@ func newPrefixedIDs() *prefixedIDs {
139197
}
140198
}
141199

142-
// NewTaskCheckBoxHTMLRenderer creates a TaskCheckBoxHTMLRenderer to render tasklists
200+
// NewHTMLRenderer creates a HTMLRenderer to render
143201
// in the gitea form.
144-
func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
145-
r := &TaskCheckBoxHTMLRenderer{
202+
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
203+
r := &HTMLRenderer{
146204
Config: html.NewConfig(),
147205
}
148206
for _, opt := range opts {
@@ -151,19 +209,109 @@ func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
151209
return r
152210
}
153211

154-
// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
155-
// renders checkboxes in list items.
156-
// Overrides the default goldmark one to present the gitea format
157-
type TaskCheckBoxHTMLRenderer struct {
212+
// HTMLRenderer is a renderer.NodeRenderer implementation that
213+
// renders gitea specific features.
214+
type HTMLRenderer struct {
158215
html.Config
159216
}
160217

161218
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
162-
func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
219+
func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
220+
reg.Register(ast.KindDocument, r.renderDocument)
221+
reg.Register(KindDetails, r.renderDetails)
222+
reg.Register(KindSummary, r.renderSummary)
223+
reg.Register(KindIcon, r.renderIcon)
163224
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
164225
}
165226

166-
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
227+
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
228+
log.Info("renderDocument %v", node)
229+
n := node.(*ast.Document)
230+
231+
if val, has := n.AttributeString("lang"); has {
232+
var err error
233+
if entering {
234+
_, err = w.WriteString("<div")
235+
if err == nil {
236+
_, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
237+
}
238+
if err == nil {
239+
_, err = w.WriteRune('>')
240+
}
241+
} else {
242+
_, err = w.WriteString("</div>")
243+
}
244+
245+
if err != nil {
246+
return ast.WalkStop, err
247+
}
248+
}
249+
250+
return ast.WalkContinue, nil
251+
}
252+
253+
func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
254+
var err error
255+
if entering {
256+
_, err = w.WriteString("<details>")
257+
} else {
258+
_, err = w.WriteString("</details>")
259+
}
260+
261+
if err != nil {
262+
return ast.WalkStop, err
263+
}
264+
265+
return ast.WalkContinue, nil
266+
}
267+
268+
func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
269+
var err error
270+
if entering {
271+
_, err = w.WriteString("<summary>")
272+
} else {
273+
_, err = w.WriteString("</summary>")
274+
}
275+
276+
if err != nil {
277+
return ast.WalkStop, err
278+
}
279+
280+
return ast.WalkContinue, nil
281+
}
282+
283+
var validNameRE = regexp.MustCompile("^[a-z ]+$")
284+
285+
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
286+
if !entering {
287+
return ast.WalkContinue, nil
288+
}
289+
290+
n := node.(*Icon)
291+
292+
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
293+
294+
if len(name) == 0 {
295+
// skip this
296+
return ast.WalkContinue, nil
297+
}
298+
299+
if !validNameRE.MatchString(name) {
300+
// skip this
301+
return ast.WalkContinue, nil
302+
}
303+
304+
var err error
305+
_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
306+
307+
if err != nil {
308+
return ast.WalkStop, err
309+
}
310+
311+
return ast.WalkContinue, nil
312+
}
313+
314+
func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
167315
if !entering {
168316
return ast.WalkContinue, nil
169317
}

0 commit comments

Comments
 (0)