mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-02-07 02:07:04 +00:00
refactor: releasepr markdown handling (#42)
This commit is contained in:
parent
0750bd6b46
commit
36a0b90bcd
6 changed files with 466 additions and 83 deletions
|
|
@ -7,7 +7,8 @@ import (
|
|||
// A Section struct represents a section of elements.
|
||||
type Section struct {
|
||||
gast.BaseBlock
|
||||
Name string
|
||||
Name string
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
|
|
@ -26,6 +27,10 @@ func (n *Section) Kind() gast.NodeKind {
|
|||
return KindSection
|
||||
}
|
||||
|
||||
func (n *Section) HideInOutput() {
|
||||
n.Hidden = true
|
||||
}
|
||||
|
||||
// NewSection returns a new Section node.
|
||||
func NewSection(name string) *Section {
|
||||
return &Section{Name: name}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ var (
|
|||
|
||||
const (
|
||||
sectionTrigger = "<!--"
|
||||
SectionStartFormat = "<!-- section-start %s -->"
|
||||
SectionEndFormat = "<!-- section-end %s -->"
|
||||
SectionStartFormat = "<!-- section-start %s -->\n"
|
||||
SectionEndFormat = "\n<!-- section-end %s -->"
|
||||
)
|
||||
|
||||
type sectionParser struct{}
|
||||
|
|
@ -91,6 +91,10 @@ func (s SectionMarkdownRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegi
|
|||
func (s SectionMarkdownRenderer) renderSection(w util.BufWriter, _ []byte, node gast.Node, enter bool) (gast.WalkStatus, error) {
|
||||
n := node.(*ast.Section)
|
||||
|
||||
if n.Hidden {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if enter {
|
||||
// Add blank previous line if applicable
|
||||
if node.PreviousSibling() != nil && node.HasBlankPreviousLines() {
|
||||
|
|
@ -107,12 +111,10 @@ func (s SectionMarkdownRenderer) renderSection(w util.BufWriter, _ []byte, node
|
|||
return gast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := w.WriteRune('\n'); err != nil {
|
||||
return gast.WalkStop, err
|
||||
}
|
||||
}
|
||||
|
||||
return gast.WalkContinue, nil
|
||||
// Somehow the goldmark-markdown renderer does not flush this properly on its own
|
||||
return gast.WalkContinue, w.Flush()
|
||||
}
|
||||
|
||||
type section struct{}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ package markdown
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
markdown "github.com/teekennedy/goldmark-markdown"
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
|
||||
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||
)
|
||||
|
||||
func New() goldmark.Markdown {
|
||||
|
|
@ -34,3 +38,82 @@ func Format(input string) (string, error) {
|
|||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func GetCodeBlockText(source []byte, language string, output *string) gast.Walker {
|
||||
return func(n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if n.Kind() != gast.KindFencedCodeBlock {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
codeBlock := n.(*gast.FencedCodeBlock)
|
||||
|
||||
if string(codeBlock.Language(source)) != language {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
*output = textFromLines(source, codeBlock)
|
||||
// Stop looking after we find the first result
|
||||
return gast.WalkStop, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetSectionText(source []byte, name string, output *string) gast.Walker {
|
||||
return func(n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if n.Kind() != ast.KindSection {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
section := n.(*ast.Section)
|
||||
|
||||
if section.Name != name {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// Do not show section markings in output, we only care about the content
|
||||
section.HideInOutput()
|
||||
|
||||
// Found the right section
|
||||
outputBuffer := new(bytes.Buffer)
|
||||
err := New().Renderer().Render(outputBuffer, source, section)
|
||||
if err != nil {
|
||||
return gast.WalkStop, err
|
||||
}
|
||||
|
||||
*output = outputBuffer.String()
|
||||
// Stop looking after we find the first result
|
||||
return gast.WalkStop, nil
|
||||
}
|
||||
}
|
||||
|
||||
func textFromLines(source []byte, n gast.Node) string {
|
||||
content := make([]byte, 0)
|
||||
|
||||
l := n.Lines().Len()
|
||||
for i := 0; i < l; i++ {
|
||||
line := n.Lines().At(i)
|
||||
content = append(content, line.Value(source)...)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(content))
|
||||
}
|
||||
|
||||
func WalkAST(source []byte, walkers ...gast.Walker) (err error) {
|
||||
doc := New().Parser().Parse(text.NewReader(source))
|
||||
|
||||
for _, walker := range walkers {
|
||||
err = gast.Walk(doc, walker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
252
internal/markdown/goldmark_test.go
Normal file
252
internal/markdown/goldmark_test.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "heading spacing",
|
||||
input: "# Foo\n## Bar\n### Baz",
|
||||
want: "# Foo\n\n## Bar\n\n### Baz\n",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "no empty lines for list items",
|
||||
input: "# Foo\n- 1\n- 2\n",
|
||||
want: "# Foo\n\n- 1\n- 2\n",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "sections",
|
||||
input: "# Foo\n<!-- section-start foobar -->\n- 1\n- 2\n<!-- section-end foobar -->\n",
|
||||
want: "# Foo\n\n<!-- section-start foobar -->\n- 1\n- 2\n\n<!-- section-end foobar -->\n",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Format(tt.input)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCodeBlockText(t *testing.T) {
|
||||
type args struct {
|
||||
source []byte
|
||||
language string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no code block",
|
||||
args: args{
|
||||
source: []byte("# Foo"),
|
||||
language: "missing",
|
||||
},
|
||||
want: "",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "code block",
|
||||
args: args{
|
||||
source: []byte("```test\nContent\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "Content",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "code block with other language",
|
||||
args: args{
|
||||
source: []byte("```unknown\nContent\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "multiple code blocks with different languages",
|
||||
args: args{
|
||||
source: []byte("```unknown\nContent\n```\n\n```test\n1337\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "1337",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "multiple code blocks with same language returns first one",
|
||||
args: args{
|
||||
source: []byte("```test\nContent\n```\n\n```test\n1337\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "Content",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
|
||||
err := WalkAST(tt.args.source,
|
||||
GetCodeBlockText(tt.args.source, tt.args.language, &got),
|
||||
)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSectionText(t *testing.T) {
|
||||
type args struct {
|
||||
source []byte
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no section",
|
||||
args: args{
|
||||
source: []byte("# Foo"),
|
||||
name: "missing",
|
||||
},
|
||||
want: "",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "section",
|
||||
args: args{
|
||||
source: []byte("<!-- section-start test -->\nContent\n<!-- section-end test -->"),
|
||||
name: "test",
|
||||
},
|
||||
want: "Content\n",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "section with other name",
|
||||
args: args{
|
||||
source: []byte("<!-- section-start unknown -->\nContent\n<!-- section-end unknown -->"),
|
||||
name: "test",
|
||||
},
|
||||
want: "",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "multiple sections with different names",
|
||||
args: args{
|
||||
source: []byte("<!-- section-start unknown -->\nContent\n<!-- section-end unknown -->\n\n<!-- section-start test -->\n1337\n<!-- section-end test -->"),
|
||||
name: "test",
|
||||
},
|
||||
want: "1337\n",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "multiple sections with same name returns first one",
|
||||
args: args{
|
||||
source: []byte("<!-- section-start test -->\nContent\n<!-- section-end test -->\n\n<!-- section-start test -->\n1337\n<!-- section-end test -->"),
|
||||
name: "test",
|
||||
},
|
||||
want: "Content\n",
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
|
||||
err := WalkAST(tt.args.source,
|
||||
GetSectionText(tt.args.source, tt.args.name, &got),
|
||||
)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkAST(t *testing.T) {
|
||||
type args struct {
|
||||
source []byte
|
||||
walkers []ast.Walker
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "empty walker",
|
||||
args: args{
|
||||
source: []byte("# Foo"),
|
||||
walkers: []ast.Walker{
|
||||
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
|
||||
return ast.WalkStop, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "returns walker error",
|
||||
args: args{
|
||||
source: []byte("# Foo"),
|
||||
walkers: []ast.Walker{
|
||||
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
|
||||
return ast.WalkStop, errors.New("test")
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "runs all walkers",
|
||||
args: args{
|
||||
source: []byte("# Foo"),
|
||||
walkers: []ast.Walker{
|
||||
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
|
||||
return ast.WalkStop, nil
|
||||
},
|
||||
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
|
||||
return ast.WalkStop, errors.New("test")
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := WalkAST(tt.args.source, tt.args.walkers...)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue