Skip to content

Commit ba921fd

Browse files
authored
Fix markdown frontmatter rendering (#34102)
Fix #34101
1 parent f94ee4f commit ba921fd

File tree

8 files changed

+136
-106
lines changed

8 files changed

+136
-106
lines changed

modules/markup/markdown/ast.go

+24-29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package markdown
55

66
import (
7+
"html/template"
78
"strconv"
89

910
"github.com/yuin/goldmark/ast"
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
2930

3031
// NewDetails returns a new Paragraph node.
3132
func NewDetails() *Details {
32-
return &Details{
33-
BaseBlock: ast.BaseBlock{},
34-
}
33+
return &Details{}
3534
}
3635

3736
// Summary is a block that contains the summary of details block
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
5453

5554
// NewSummary returns a new Summary node.
5655
func NewSummary() *Summary {
57-
return &Summary{
58-
BaseBlock: ast.BaseBlock{},
59-
}
56+
return &Summary{}
6057
}
6158

6259
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
@@ -95,29 +92,6 @@ type Icon struct {
9592
Name []byte
9693
}
9794

98-
// Dump implements Node.Dump .
99-
func (n *Icon) Dump(source []byte, level int) {
100-
m := map[string]string{}
101-
m["Name"] = string(n.Name)
102-
ast.DumpHelper(n, source, level, m, nil)
103-
}
104-
105-
// KindIcon is the NodeKind for Icon
106-
var KindIcon = ast.NewNodeKind("Icon")
107-
108-
// Kind implements Node.Kind.
109-
func (n *Icon) Kind() ast.NodeKind {
110-
return KindIcon
111-
}
112-
113-
// NewIcon returns a new Paragraph node.
114-
func NewIcon(name string) *Icon {
115-
return &Icon{
116-
BaseInline: ast.BaseInline{},
117-
Name: []byte(name),
118-
}
119-
}
120-
12195
// ColorPreview is an inline for a color preview
12296
type ColorPreview struct {
12397
ast.BaseInline
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
175149
AttentionType: attentionType,
176150
}
177151
}
152+
153+
var KindRawHTML = ast.NewNodeKind("RawHTML")
154+
155+
type RawHTML struct {
156+
ast.BaseBlock
157+
rawHTML template.HTML
158+
}
159+
160+
func (n *RawHTML) Dump(source []byte, level int) {
161+
m := map[string]string{}
162+
m["RawHTML"] = string(n.rawHTML)
163+
ast.DumpHelper(n, source, level, m, nil)
164+
}
165+
166+
func (n *RawHTML) Kind() ast.NodeKind {
167+
return KindRawHTML
168+
}
169+
170+
func NewRawHTML(rawHTML template.HTML) *RawHTML {
171+
return &RawHTML{rawHTML: rawHTML}
172+
}

modules/markup/markdown/convertyaml.go

+29-14
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,22 @@
44
package markdown
55

66
import (
7+
"strings"
8+
9+
"code.gitea.io/gitea/modules/htmlutil"
10+
"code.gitea.io/gitea/modules/svg"
11+
712
"github.com/yuin/goldmark/ast"
813
east "github.com/yuin/goldmark/extension/ast"
914
"gopkg.in/yaml.v3"
1015
)
1116

1217
func nodeToTable(meta *yaml.Node) ast.Node {
13-
for {
14-
if meta == nil {
15-
return nil
16-
}
17-
switch meta.Kind {
18-
case yaml.DocumentNode:
19-
meta = meta.Content[0]
20-
continue
21-
default:
22-
}
23-
break
18+
for meta != nil && meta.Kind == yaml.DocumentNode {
19+
meta = meta.Content[0]
20+
}
21+
if meta == nil {
22+
return nil
2423
}
2524
switch meta.Kind {
2625
case yaml.MappingNode:
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
7271
return table
7372
}
7473

75-
func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
74+
func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
75+
for meta != nil && meta.Kind == yaml.DocumentNode {
76+
meta = meta.Content[0]
77+
}
78+
if meta == nil {
79+
return nil
80+
}
81+
if meta.Kind != yaml.MappingNode {
82+
return nil
83+
}
84+
var keys []string
85+
for i := 0; i < len(meta.Content); i += 2 {
86+
if meta.Content[i].Kind == yaml.ScalarNode {
87+
keys = append(keys, meta.Content[i].Value)
88+
}
89+
}
7690
details := NewDetails()
91+
details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
7792
summary := NewSummary()
78-
summary.AppendChild(summary, NewIcon(icon))
93+
summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
94+
summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
7995
details.AppendChild(details, summary)
8096
details.AppendChild(details, nodeToTable(meta))
81-
8297
return details
8398
}

modules/markup/markdown/goldmark.go

+5-29
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ package markdown
55

66
import (
77
"fmt"
8-
"regexp"
9-
"strings"
10-
"sync"
118

129
"code.gitea.io/gitea/modules/container"
1310
"code.gitea.io/gitea/modules/markup"
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
5148

5249
tocList := make([]Header, 0, 20)
5350
if rc.yamlNode != nil {
54-
metaNode := rc.toMetaNode()
51+
metaNode := rc.toMetaNode(g)
5552
if metaNode != nil {
5653
node.InsertBefore(node, firstChild, metaNode)
5754
}
@@ -112,11 +109,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
112109
}
113110
}
114111

115-
// it is copied from old code, which is quite doubtful whether it is correct
116-
var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
117-
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
118-
})
119-
120112
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
121113
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
122114
r := &HTMLRenderer{
@@ -141,11 +133,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
141133
reg.Register(ast.KindDocument, r.renderDocument)
142134
reg.Register(KindDetails, r.renderDetails)
143135
reg.Register(KindSummary, r.renderSummary)
144-
reg.Register(KindIcon, r.renderIcon)
145136
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
146137
reg.Register(KindAttention, r.renderAttention)
147138
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
148139
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
140+
reg.Register(KindRawHTML, r.renderRawHTML)
149141
}
150142

151143
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -207,30 +199,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
207199
return ast.WalkContinue, nil
208200
}
209201

210-
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
202+
func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
211203
if !entering {
212204
return ast.WalkContinue, nil
213205
}
214-
215-
n := node.(*Icon)
216-
217-
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
218-
219-
if len(name) == 0 {
220-
// skip this
221-
return ast.WalkContinue, nil
222-
}
223-
224-
if !reValidIconName().MatchString(name) {
225-
// skip this
226-
return ast.WalkContinue, nil
227-
}
228-
229-
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
230-
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
206+
n := node.(*RawHTML)
207+
_, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
231208
if err != nil {
232209
return ast.WalkStop, err
233210
}
234-
235211
return ast.WalkContinue, nil
236212
}

modules/markup/markdown/markdown.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
184184
// Preserve original length.
185185
bufWithMetadataLength := len(buf)
186186

187-
rc := &RenderConfig{
188-
Meta: markup.RenderMetaAsDetails,
189-
Icon: "table",
190-
Lang: "",
191-
}
187+
rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
192188
buf, _ = ExtractMetadataBytes(buf, rc)
193189

194190
metaLength := bufWithMetadataLength - len(buf)

modules/markup/markdown/markdown_test.go

+62-6
Original file line numberDiff line numberDiff line change
@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
383383
}
384384
}
385385

386-
func TestTaskList(t *testing.T) {
386+
func TestMarkdownFrontmatter(t *testing.T) {
387387
testcases := []struct {
388-
testcase string
388+
name string
389+
input string
389390
expected string
390391
}{
392+
{
393+
"MapInFrontmatter",
394+
`---
395+
key1: val1
396+
key2: val2
397+
---
398+
test
399+
`,
400+
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
401+
<thead>
402+
<tr>
403+
<th>key1</th>
404+
<th>key2</th>
405+
</tr>
406+
</thead>
407+
<tbody>
408+
<tr>
409+
<td>val1</td>
410+
<td>val2</td>
411+
</tr>
412+
</tbody>
413+
</table>
414+
</details><p>test</p>
415+
`,
416+
},
417+
418+
{
419+
"ListInFrontmatter",
420+
`---
421+
- item1
422+
- item2
423+
---
424+
test
425+
`,
426+
`- item1
427+
- item2
428+
429+
<p>test</p>
430+
`,
431+
},
432+
433+
{
434+
"StringInFrontmatter",
435+
`---
436+
anything
437+
---
438+
test
439+
`,
440+
`anything
441+
442+
<p>test</p>
443+
`,
444+
},
445+
391446
{
392447
// data-source-position should take into account YAML frontmatter.
448+
"ListAfterFrontmatter",
393449
`---
394450
foo: bar
395451
---
396452
- [ ] task 1`,
397-
`<details><summary><i class="icon table"></i></summary><table>
453+
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
398454
<thead>
399455
<tr>
400456
<th>foo</th>
@@ -414,9 +470,9 @@ foo: bar
414470
}
415471

416472
for _, test := range testcases {
417-
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
418-
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
419-
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
473+
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
474+
assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
475+
assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
420476
}
421477
}
422478

modules/markup/markdown/renderconfig.go

+3-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
// RenderConfig represents rendering configuration for this file
1717
type RenderConfig struct {
1818
Meta markup.RenderMetaMode
19-
Icon string
2019
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
2120
Lang string
2221
yamlNode *yaml.Node
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
7473

7574
type yamlRenderConfig struct {
7675
Meta *string `yaml:"meta"`
77-
Icon *string `yaml:"details_icon"`
76+
Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
7877
TOC *string `yaml:"include_toc"`
7978
Lang *string `yaml:"lang"`
8079
}
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
9695
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
9796
}
9897

99-
if cfg.Gitea.Icon != nil {
100-
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
101-
}
102-
10398
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
10499
rc.Lang = *cfg.Gitea.Lang
105100
}
@@ -111,15 +106,15 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
111106
return nil
112107
}
113108

114-
func (rc *RenderConfig) toMetaNode() ast.Node {
109+
func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
115110
if rc.yamlNode == nil {
116111
return nil
117112
}
118113
switch rc.Meta {
119114
case markup.RenderMetaAsTable:
120115
return nodeToTable(rc.yamlNode)
121116
case markup.RenderMetaAsDetails:
122-
return nodeToDetails(rc.yamlNode, rc.Icon)
117+
return nodeToDetails(g, rc.yamlNode)
123118
default:
124119
return nil
125120
}

0 commit comments

Comments
 (0)