Skip to content

Commit

Permalink
Refactor mdplain package to use yuin/goldmark library for markdow…
Browse files Browse the repository at this point in the history
…n processing (#332)

* Add test for `PlainMarkdown()`

* Refactor `mdplain` package implementation to use `yuin/goldmark` library
  • Loading branch information
SBGoods authored Jan 26, 2024
1 parent 3b3c4c7 commit 6b686ee
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 172 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ require (
github.com/hashicorp/terraform-json v0.21.0
github.com/mattn/go-colorable v0.1.13
github.com/rogpeppe/go-internal v1.12.0
github.com/russross/blackfriday v1.6.0
github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-meta v1.1.0
github.com/zclconf/go-cty v1.14.2
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
Expand Down
26 changes: 19 additions & 7 deletions internal/mdplain/mdplain.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@

package mdplain

import "github.com/russross/blackfriday"
import (
"bytes"

// Clean runs a VERY naive cleanup of markdown text to make it more palatable as plain text.
func PlainMarkdown(md string) (string, error) {
pt := &Text{}

html := blackfriday.MarkdownOptions([]byte(md), pt, blackfriday.Options{})
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)

return string(html), nil
// Clean runs a VERY naive cleanup of markdown text to make it more palatable as plain text.
func PlainMarkdown(markdown string) (string, error) {
var buf bytes.Buffer
extensions := []goldmark.Extender{
extension.Linkify,
}
md := goldmark.New(
goldmark.WithExtensions(extensions...),
goldmark.WithRenderer(NewTextRenderer()),
)
if err := md.Convert([]byte(markdown), &buf); err != nil {
return "", err
}
return buf.String(), nil
}
38 changes: 38 additions & 0 deletions internal/mdplain/mdplain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package mdplain

import (
"os"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestPlainMarkdown(t *testing.T) {
t.Parallel()

input, err := os.ReadFile("testdata/markdown.md")
if err != nil {
t.Errorf("Error opening file: %s", err.Error())
return
}

expectedFile, err := os.ReadFile("testdata/mdplain.txt")
if err != nil {
t.Errorf("Error opening file: %s", err.Error())
return
}

expected := string(expectedFile)
actual, err := PlainMarkdown(string(input))
if err != nil {
t.Errorf("Error rendering markdown: %s", err.Error())
return
}
if !cmp.Equal(expected, actual) {
t.Errorf(cmp.Diff(expected, actual))
}

}
247 changes: 85 additions & 162 deletions internal/mdplain/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,175 +5,98 @@ package mdplain

import (
"bytes"
"io"

"github.com/russross/blackfriday"
"github.com/yuin/goldmark/ast"
extAST "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
)

type Text struct{}

func TextRenderer() blackfriday.Renderer {
return &Text{}
}

func (options *Text) GetFlags() int {
return 0
}

func (options *Text) TitleBlock(out *bytes.Buffer, text []byte) {
text = bytes.TrimPrefix(text, []byte("% "))
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
out.Write(text)
out.WriteString("\n")
}

func (options *Text) Header(out *bytes.Buffer, text func() bool, level int, id string) {
marker := out.Len()
doubleSpace(out)

if !text() {
out.Truncate(marker)
return
type TextRender struct{}

func NewTextRenderer() *TextRender {
return &TextRender{}
}

func (r *TextRender) Render(w io.Writer, source []byte, n ast.Node) error {
out := bytes.NewBuffer([]byte{})
err := ast.Walk(n, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering || node.Type() == ast.TypeDocument {
return ast.WalkContinue, nil
}

switch node := node.(type) {
case *ast.Blockquote, *ast.Heading:
doubleSpace(out)
out.Write(node.Text(source))
return ast.WalkSkipChildren, nil
case *ast.ThematicBreak:
doubleSpace(out)
return ast.WalkSkipChildren, nil
case *ast.CodeBlock:
doubleSpace(out)
for i := 0; i < node.Lines().Len(); i++ {
line := node.Lines().At(i)
out.Write(line.Value(source))
}
return ast.WalkSkipChildren, nil
case *ast.FencedCodeBlock:
doubleSpace(out)
doubleSpace(out)
for i := 0; i < node.Lines().Len(); i++ {
line := node.Lines().At(i)
_, _ = out.Write(line.Value(source))
}
return ast.WalkSkipChildren, nil
case *ast.List:
doubleSpace(out)
return ast.WalkContinue, nil
case *ast.Paragraph:
doubleSpace(out)
if node.Text(source)[0] == '|' { // Write tables as-is.
for i := 0; i < node.Lines().Len(); i++ {
line := node.Lines().At(i)
out.Write(line.Value(source))
}
return ast.WalkSkipChildren, nil
}
return ast.WalkContinue, nil
case *ast.AutoLink, *extAST.Strikethrough:
out.Write(node.Text(source))
return ast.WalkContinue, nil
case *ast.CodeSpan:
out.Write(node.Text(source))
return ast.WalkSkipChildren, nil
case *ast.Link:
_, err := out.Write(node.Text(source))
if !isRelativeLink(node.Destination) {
out.WriteString(" ")
out.Write(node.Destination)
}
return ast.WalkSkipChildren, err
case *ast.Text:
out.Write(node.Text(source))
if node.SoftLineBreak() {
doubleSpace(out)
}
return ast.WalkContinue, nil
case *ast.Image:
return ast.WalkSkipChildren, nil

}
return ast.WalkContinue, nil
})
if err != nil {
return err
}
}

func (options *Text) BlockHtml(out *bytes.Buffer, text []byte) {
doubleSpace(out)
out.Write(text)
out.WriteByte('\n')
}

func (options *Text) HRule(out *bytes.Buffer) {
doubleSpace(out)
}

func (options *Text) BlockCode(out *bytes.Buffer, text []byte, lang string) {
options.BlockCodeNormal(out, text, lang)
}

func (options *Text) BlockCodeNormal(out *bytes.Buffer, text []byte, lang string) {
doubleSpace(out)
out.Write(text)
}

func (options *Text) BlockQuote(out *bytes.Buffer, text []byte) {
doubleSpace(out)
out.Write(text)
}

func (options *Text) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
doubleSpace(out)
out.Write(header)
out.Write(body)
}

func (options *Text) TableRow(out *bytes.Buffer, text []byte) {
doubleSpace(out)
out.Write(text)
}

func (options *Text) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
doubleSpace(out)
out.Write(text)
}

func (options *Text) TableCell(out *bytes.Buffer, text []byte, align int) {
doubleSpace(out)
out.Write(text)
}

func (options *Text) Footnotes(out *bytes.Buffer, text func() bool) {
options.HRule(out)
options.List(out, text, 0)
}

func (options *Text) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
out.Write(text)
}

func (options *Text) List(out *bytes.Buffer, text func() bool, flags int) {
marker := out.Len()
doubleSpace(out)

if !text() {
out.Truncate(marker)
return
_, err = w.Write(out.Bytes())
if err != nil {
return err
}
return nil
}

func (options *Text) ListItem(out *bytes.Buffer, text []byte, flags int) {
out.Write(text)
}

func (options *Text) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len()
doubleSpace(out)

if !text() {
out.Truncate(marker)
return
}
}

func (options *Text) AutoLink(out *bytes.Buffer, link []byte, kind int) {
out.Write(link)
}

func (options *Text) CodeSpan(out *bytes.Buffer, text []byte) {
out.Write(text)
}

func (options *Text) DoubleEmphasis(out *bytes.Buffer, text []byte) {
out.Write(text)
}

func (options *Text) Emphasis(out *bytes.Buffer, text []byte) {
if len(text) == 0 {
return
}
out.Write(text)
}

func (options *Text) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {}

func (options *Text) LineBreak(out *bytes.Buffer) {}

func (options *Text) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
out.Write(content)
if !isRelativeLink(link) {
out.WriteString(" ")
out.Write(link)
}
}

func (options *Text) RawHtmlTag(out *bytes.Buffer, text []byte) {}

func (options *Text) TripleEmphasis(out *bytes.Buffer, text []byte) {
out.Write(text)
}

func (options *Text) StrikeThrough(out *bytes.Buffer, text []byte) {
out.Write(text)
}

func (options *Text) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {}

func (options *Text) Entity(out *bytes.Buffer, entity []byte) {
out.Write(entity)
}

func (options *Text) NormalText(out *bytes.Buffer, text []byte) {
out.Write(text)
}

func (options *Text) Smartypants(out *bytes.Buffer, text []byte) {}

func (options *Text) DocumentHeader(out *bytes.Buffer) {}

func (options *Text) DocumentFooter(out *bytes.Buffer) {}

func (options *Text) TocHeader(text []byte, level int) {}

func (options *Text) TocFinalize() {}
func (r *TextRender) AddOptions(...renderer.Option) {}

func doubleSpace(out *bytes.Buffer) {
if out.Len() > 0 {
Expand Down
Loading

0 comments on commit 6b686ee

Please sign in to comment.