Skip to content

Commit 6b686ee

Browse files
authored
Refactor mdplain package to use yuin/goldmark library for markdown processing (#332)
* Add test for `PlainMarkdown()` * Refactor `mdplain` package implementation to use `yuin/goldmark` library
1 parent 3b3c4c7 commit 6b686ee

File tree

7 files changed

+325
-172
lines changed

7 files changed

+325
-172
lines changed

go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ require (
1212
github.com/hashicorp/terraform-json v0.21.0
1313
github.com/mattn/go-colorable v0.1.13
1414
github.com/rogpeppe/go-internal v1.12.0
15-
github.com/russross/blackfriday v1.6.0
1615
github.com/yuin/goldmark v1.6.0
1716
github.com/yuin/goldmark-meta v1.1.0
1817
github.com/zclconf/go-cty v1.14.2

go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq
9090
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
9191
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
9292
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
93-
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
94-
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
9593
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
9694
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
9795
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=

internal/mdplain/mdplain.go

+19-7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@
33

44
package mdplain
55

6-
import "github.com/russross/blackfriday"
6+
import (
7+
"bytes"
78

8-
// Clean runs a VERY naive cleanup of markdown text to make it more palatable as plain text.
9-
func PlainMarkdown(md string) (string, error) {
10-
pt := &Text{}
11-
12-
html := blackfriday.MarkdownOptions([]byte(md), pt, blackfriday.Options{})
9+
"github.com/yuin/goldmark"
10+
"github.com/yuin/goldmark/extension"
11+
)
1312

14-
return string(html), nil
13+
// Clean runs a VERY naive cleanup of markdown text to make it more palatable as plain text.
14+
func PlainMarkdown(markdown string) (string, error) {
15+
var buf bytes.Buffer
16+
extensions := []goldmark.Extender{
17+
extension.Linkify,
18+
}
19+
md := goldmark.New(
20+
goldmark.WithExtensions(extensions...),
21+
goldmark.WithRenderer(NewTextRenderer()),
22+
)
23+
if err := md.Convert([]byte(markdown), &buf); err != nil {
24+
return "", err
25+
}
26+
return buf.String(), nil
1527
}

internal/mdplain/mdplain_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mdplain
5+
6+
import (
7+
"os"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
)
12+
13+
func TestPlainMarkdown(t *testing.T) {
14+
t.Parallel()
15+
16+
input, err := os.ReadFile("testdata/markdown.md")
17+
if err != nil {
18+
t.Errorf("Error opening file: %s", err.Error())
19+
return
20+
}
21+
22+
expectedFile, err := os.ReadFile("testdata/mdplain.txt")
23+
if err != nil {
24+
t.Errorf("Error opening file: %s", err.Error())
25+
return
26+
}
27+
28+
expected := string(expectedFile)
29+
actual, err := PlainMarkdown(string(input))
30+
if err != nil {
31+
t.Errorf("Error rendering markdown: %s", err.Error())
32+
return
33+
}
34+
if !cmp.Equal(expected, actual) {
35+
t.Errorf(cmp.Diff(expected, actual))
36+
}
37+
38+
}

internal/mdplain/renderer.go

+85-162
Original file line numberDiff line numberDiff line change
@@ -5,175 +5,98 @@ package mdplain
55

66
import (
77
"bytes"
8+
"io"
89

9-
"github.com/russross/blackfriday"
10+
"github.com/yuin/goldmark/ast"
11+
extAST "github.com/yuin/goldmark/extension/ast"
12+
"github.com/yuin/goldmark/renderer"
1013
)
1114

12-
type Text struct{}
13-
14-
func TextRenderer() blackfriday.Renderer {
15-
return &Text{}
16-
}
17-
18-
func (options *Text) GetFlags() int {
19-
return 0
20-
}
21-
22-
func (options *Text) TitleBlock(out *bytes.Buffer, text []byte) {
23-
text = bytes.TrimPrefix(text, []byte("% "))
24-
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
25-
out.Write(text)
26-
out.WriteString("\n")
27-
}
28-
29-
func (options *Text) Header(out *bytes.Buffer, text func() bool, level int, id string) {
30-
marker := out.Len()
31-
doubleSpace(out)
32-
33-
if !text() {
34-
out.Truncate(marker)
35-
return
15+
type TextRender struct{}
16+
17+
func NewTextRenderer() *TextRender {
18+
return &TextRender{}
19+
}
20+
21+
func (r *TextRender) Render(w io.Writer, source []byte, n ast.Node) error {
22+
out := bytes.NewBuffer([]byte{})
23+
err := ast.Walk(n, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
24+
if !entering || node.Type() == ast.TypeDocument {
25+
return ast.WalkContinue, nil
26+
}
27+
28+
switch node := node.(type) {
29+
case *ast.Blockquote, *ast.Heading:
30+
doubleSpace(out)
31+
out.Write(node.Text(source))
32+
return ast.WalkSkipChildren, nil
33+
case *ast.ThematicBreak:
34+
doubleSpace(out)
35+
return ast.WalkSkipChildren, nil
36+
case *ast.CodeBlock:
37+
doubleSpace(out)
38+
for i := 0; i < node.Lines().Len(); i++ {
39+
line := node.Lines().At(i)
40+
out.Write(line.Value(source))
41+
}
42+
return ast.WalkSkipChildren, nil
43+
case *ast.FencedCodeBlock:
44+
doubleSpace(out)
45+
doubleSpace(out)
46+
for i := 0; i < node.Lines().Len(); i++ {
47+
line := node.Lines().At(i)
48+
_, _ = out.Write(line.Value(source))
49+
}
50+
return ast.WalkSkipChildren, nil
51+
case *ast.List:
52+
doubleSpace(out)
53+
return ast.WalkContinue, nil
54+
case *ast.Paragraph:
55+
doubleSpace(out)
56+
if node.Text(source)[0] == '|' { // Write tables as-is.
57+
for i := 0; i < node.Lines().Len(); i++ {
58+
line := node.Lines().At(i)
59+
out.Write(line.Value(source))
60+
}
61+
return ast.WalkSkipChildren, nil
62+
}
63+
return ast.WalkContinue, nil
64+
case *ast.AutoLink, *extAST.Strikethrough:
65+
out.Write(node.Text(source))
66+
return ast.WalkContinue, nil
67+
case *ast.CodeSpan:
68+
out.Write(node.Text(source))
69+
return ast.WalkSkipChildren, nil
70+
case *ast.Link:
71+
_, err := out.Write(node.Text(source))
72+
if !isRelativeLink(node.Destination) {
73+
out.WriteString(" ")
74+
out.Write(node.Destination)
75+
}
76+
return ast.WalkSkipChildren, err
77+
case *ast.Text:
78+
out.Write(node.Text(source))
79+
if node.SoftLineBreak() {
80+
doubleSpace(out)
81+
}
82+
return ast.WalkContinue, nil
83+
case *ast.Image:
84+
return ast.WalkSkipChildren, nil
85+
86+
}
87+
return ast.WalkContinue, nil
88+
})
89+
if err != nil {
90+
return err
3691
}
37-
}
38-
39-
func (options *Text) BlockHtml(out *bytes.Buffer, text []byte) {
40-
doubleSpace(out)
41-
out.Write(text)
42-
out.WriteByte('\n')
43-
}
44-
45-
func (options *Text) HRule(out *bytes.Buffer) {
46-
doubleSpace(out)
47-
}
48-
49-
func (options *Text) BlockCode(out *bytes.Buffer, text []byte, lang string) {
50-
options.BlockCodeNormal(out, text, lang)
51-
}
52-
53-
func (options *Text) BlockCodeNormal(out *bytes.Buffer, text []byte, lang string) {
54-
doubleSpace(out)
55-
out.Write(text)
56-
}
57-
58-
func (options *Text) BlockQuote(out *bytes.Buffer, text []byte) {
59-
doubleSpace(out)
60-
out.Write(text)
61-
}
62-
63-
func (options *Text) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
64-
doubleSpace(out)
65-
out.Write(header)
66-
out.Write(body)
67-
}
68-
69-
func (options *Text) TableRow(out *bytes.Buffer, text []byte) {
70-
doubleSpace(out)
71-
out.Write(text)
72-
}
73-
74-
func (options *Text) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
75-
doubleSpace(out)
76-
out.Write(text)
77-
}
78-
79-
func (options *Text) TableCell(out *bytes.Buffer, text []byte, align int) {
80-
doubleSpace(out)
81-
out.Write(text)
82-
}
83-
84-
func (options *Text) Footnotes(out *bytes.Buffer, text func() bool) {
85-
options.HRule(out)
86-
options.List(out, text, 0)
87-
}
88-
89-
func (options *Text) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
90-
out.Write(text)
91-
}
92-
93-
func (options *Text) List(out *bytes.Buffer, text func() bool, flags int) {
94-
marker := out.Len()
95-
doubleSpace(out)
96-
97-
if !text() {
98-
out.Truncate(marker)
99-
return
92+
_, err = w.Write(out.Bytes())
93+
if err != nil {
94+
return err
10095
}
96+
return nil
10197
}
10298

103-
func (options *Text) ListItem(out *bytes.Buffer, text []byte, flags int) {
104-
out.Write(text)
105-
}
106-
107-
func (options *Text) Paragraph(out *bytes.Buffer, text func() bool) {
108-
marker := out.Len()
109-
doubleSpace(out)
110-
111-
if !text() {
112-
out.Truncate(marker)
113-
return
114-
}
115-
}
116-
117-
func (options *Text) AutoLink(out *bytes.Buffer, link []byte, kind int) {
118-
out.Write(link)
119-
}
120-
121-
func (options *Text) CodeSpan(out *bytes.Buffer, text []byte) {
122-
out.Write(text)
123-
}
124-
125-
func (options *Text) DoubleEmphasis(out *bytes.Buffer, text []byte) {
126-
out.Write(text)
127-
}
128-
129-
func (options *Text) Emphasis(out *bytes.Buffer, text []byte) {
130-
if len(text) == 0 {
131-
return
132-
}
133-
out.Write(text)
134-
}
135-
136-
func (options *Text) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {}
137-
138-
func (options *Text) LineBreak(out *bytes.Buffer) {}
139-
140-
func (options *Text) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
141-
out.Write(content)
142-
if !isRelativeLink(link) {
143-
out.WriteString(" ")
144-
out.Write(link)
145-
}
146-
}
147-
148-
func (options *Text) RawHtmlTag(out *bytes.Buffer, text []byte) {}
149-
150-
func (options *Text) TripleEmphasis(out *bytes.Buffer, text []byte) {
151-
out.Write(text)
152-
}
153-
154-
func (options *Text) StrikeThrough(out *bytes.Buffer, text []byte) {
155-
out.Write(text)
156-
}
157-
158-
func (options *Text) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {}
159-
160-
func (options *Text) Entity(out *bytes.Buffer, entity []byte) {
161-
out.Write(entity)
162-
}
163-
164-
func (options *Text) NormalText(out *bytes.Buffer, text []byte) {
165-
out.Write(text)
166-
}
167-
168-
func (options *Text) Smartypants(out *bytes.Buffer, text []byte) {}
169-
170-
func (options *Text) DocumentHeader(out *bytes.Buffer) {}
171-
172-
func (options *Text) DocumentFooter(out *bytes.Buffer) {}
173-
174-
func (options *Text) TocHeader(text []byte, level int) {}
175-
176-
func (options *Text) TocFinalize() {}
99+
func (r *TextRender) AddOptions(...renderer.Option) {}
177100

178101
func doubleSpace(out *bytes.Buffer) {
179102
if out.Len() > 0 {

0 commit comments

Comments
 (0)