mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 13:21:00 +00:00
feat(releasepr): release PRs can be updated
- PR Description - Read prefix+suffix from PR description and put into changelog - Keep those overrides on PR description changes - Add pending level to new PRs
This commit is contained in:
parent
cb529f4760
commit
fe871a0213
17 changed files with 1442 additions and 22 deletions
32
internal/markdown/extensions/ast/section.go
Normal file
32
internal/markdown/extensions/ast/section.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// A Section struct represents a section of elements.
|
||||
type Section struct {
|
||||
gast.BaseBlock
|
||||
Name string
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Section) Dump(source []byte, level int) {
|
||||
m := map[string]string{
|
||||
"Name": n.Name,
|
||||
}
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindSection is a NodeKind of the Section node.
|
||||
var KindSection = gast.NewNodeKind("Section")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Section) Kind() gast.NodeKind {
|
||||
return KindSection
|
||||
}
|
||||
|
||||
// NewSection returns a new Section node.
|
||||
func NewSection(name string) *Section {
|
||||
return &Section{Name: name}
|
||||
}
|
||||
88
internal/markdown/extensions/section.go
Normal file
88
internal/markdown/extensions/section.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"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/ast"
|
||||
)
|
||||
|
||||
var sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
|
||||
var sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
|
||||
|
||||
const (
|
||||
sectionTrigger = "<!--"
|
||||
SectionStartFormat = "<!-- section-start %s -->"
|
||||
SectionEndFormat = "<!-- section-end %s -->"
|
||||
)
|
||||
|
||||
type sectionParser struct {
|
||||
}
|
||||
|
||||
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
|
||||
line, _ := reader.PeekLine()
|
||||
|
||||
if result := sectionStartRegex.FindSubmatch(line); result != nil {
|
||||
reader.AdvanceLine()
|
||||
return ast.NewSection(string(result[1])), parser.HasChildren
|
||||
}
|
||||
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
func (s *sectionParser) Continue(node gast.Node, reader text.Reader, _ parser.Context) parser.State {
|
||||
n := node.(*ast.Section)
|
||||
|
||||
line, _ := reader.PeekLine()
|
||||
|
||||
if result := sectionEndRegex.FindSubmatch(line); result != nil {
|
||||
if string(result[1]) == n.Name {
|
||||
reader.AdvanceLine()
|
||||
return parser.Close
|
||||
}
|
||||
}
|
||||
|
||||
return parser.Continue | parser.HasChildren
|
||||
}
|
||||
|
||||
func (s *sectionParser) Close(_ gast.Node, _ text.Reader, _ parser.Context) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
func (s *sectionParser) CanInterruptParagraph() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *sectionParser) CanAcceptIndentedLine() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var defaultSectionParser = §ionParser{}
|
||||
|
||||
// NewSectionParser returns a new BlockParser that can parse
|
||||
// a section block. Section blocks can be used to group various nodes under a parent ast node.
|
||||
// This parser must take precedence over the parser.HTMLParser.
|
||||
func NewSectionParser() parser.BlockParser {
|
||||
return defaultSectionParser
|
||||
}
|
||||
|
||||
func (s *sectionParser) Trigger() []byte {
|
||||
return []byte(sectionTrigger)
|
||||
}
|
||||
|
||||
type section struct {
|
||||
}
|
||||
|
||||
// Section is an extension that allow you to use group content under a shared parent ast node.
|
||||
var Section = §ion{}
|
||||
|
||||
func (e *section) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithBlockParsers(
|
||||
util.Prioritized(NewSectionParser(), 0),
|
||||
))
|
||||
}
|
||||
17
internal/markdown/goldmark.go
Normal file
17
internal/markdown/goldmark.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
|
||||
"github.com/apricote/releaser-pleaser/internal/markdown/renderer/markdown"
|
||||
)
|
||||
|
||||
func New() goldmark.Markdown {
|
||||
return goldmark.New(
|
||||
goldmark.WithExtensions(extensions.Section),
|
||||
goldmark.WithRenderer(renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(markdown.NewRenderer(), 1)))),
|
||||
)
|
||||
}
|
||||
21
internal/markdown/renderer/markdown/LICENSE
Normal file
21
internal/markdown/renderer/markdown/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Rolf Lewis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
internal/markdown/renderer/markdown/README.md
Normal file
4
internal/markdown/renderer/markdown/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
This directory is a vendored copy of https://github.com/RolfLewis/goldmark-down/blob/main/markdown.go.
|
||||
The original repository is set to a `main` package which can not be imported.
|
||||
|
||||
It is under the MIT license.
|
||||
836
internal/markdown/renderer/markdown/renderer.go
Normal file
836
internal/markdown/renderer/markdown/renderer.go
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
exast "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
rpexast "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||
)
|
||||
|
||||
type blockState struct {
|
||||
node ast.Node
|
||||
fresh bool
|
||||
}
|
||||
|
||||
type listState struct {
|
||||
marker byte
|
||||
ordered bool
|
||||
index int
|
||||
}
|
||||
|
||||
type Renderer struct {
|
||||
listStack []listState
|
||||
openBlocks []blockState
|
||||
prefixStack []string
|
||||
prefix []byte
|
||||
atNewline bool
|
||||
}
|
||||
|
||||
// NewRenderer returns a new Renderer with given options.
|
||||
func NewRenderer() renderer.NodeRenderer {
|
||||
r := &Renderer{}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
// default registrations
|
||||
// blocks
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(ast.KindHeading, r.renderHeading)
|
||||
reg.Register(ast.KindBlockquote, r.renderBlockquote)
|
||||
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
|
||||
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
|
||||
reg.Register(ast.KindList, r.renderList)
|
||||
reg.Register(ast.KindListItem, r.renderListItem)
|
||||
reg.Register(ast.KindParagraph, r.renderParagraph)
|
||||
reg.Register(ast.KindTextBlock, r.renderTextBlock)
|
||||
reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
|
||||
|
||||
// inlines
|
||||
reg.Register(ast.KindAutoLink, r.renderAutoLink)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(ast.KindEmphasis, r.renderEmphasis)
|
||||
reg.Register(ast.KindImage, r.renderImage)
|
||||
reg.Register(ast.KindLink, r.renderLink)
|
||||
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||
reg.Register(ast.KindText, r.renderText)
|
||||
reg.Register(ast.KindString, r.renderString)
|
||||
|
||||
// GFM Extensions
|
||||
// Tables
|
||||
reg.Register(exast.KindTable, r.renderTable)
|
||||
reg.Register(exast.KindTableHeader, r.renderTableHeader)
|
||||
reg.Register(exast.KindTableRow, r.renderTableRow)
|
||||
reg.Register(exast.KindTableCell, r.renderTableCell)
|
||||
// Strikethrough
|
||||
reg.Register(exast.KindStrikethrough, r.renderStrikethrough)
|
||||
// Checkbox
|
||||
reg.Register(exast.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
|
||||
// releaser-pleaser Extensions
|
||||
// Section
|
||||
reg.Register(rpexast.KindSection, r.renderSection)
|
||||
}
|
||||
|
||||
func (r *Renderer) write(w io.Writer, buf []byte) (int, error) {
|
||||
written := 0
|
||||
for len(buf) > 0 {
|
||||
if r.atNewline {
|
||||
if err := r.beginLine(w); err != nil {
|
||||
return 0, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
atNewline := false
|
||||
newline := bytes.IndexByte(buf, '\n')
|
||||
if newline == -1 {
|
||||
newline = len(buf) - 1
|
||||
} else {
|
||||
atNewline = true
|
||||
}
|
||||
|
||||
n, err := w.Write(buf[:newline+1])
|
||||
written += n
|
||||
r.atNewline = n > 0 && atNewline && n == newline+1
|
||||
if len(r.openBlocks) != 0 {
|
||||
r.openBlocks[len(r.openBlocks)-1].fresh = false
|
||||
}
|
||||
if err != nil {
|
||||
return written, fmt.Errorf(": %w", err)
|
||||
}
|
||||
buf = buf[n:]
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) beginLine(w io.Writer) error {
|
||||
if len(r.openBlocks) != 0 {
|
||||
current := r.openBlocks[len(r.openBlocks)-1]
|
||||
if current.node.Kind() == ast.KindParagraph && !current.fresh {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
n, err := w.Write(r.prefix)
|
||||
if n != 0 {
|
||||
r.atNewline = r.prefix[len(r.prefix)-1] == '\n'
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) writeLines(w util.BufWriter, source []byte, lines *text.Segments) error {
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
if _, err := r.write(w, line.Value(source)); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) writeByte(w io.Writer, c byte) error {
|
||||
if _, err := r.write(w, []byte{c}); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the
|
||||
// beginning of each line.
|
||||
func (r *Renderer) writeString(w io.Writer, s string) (int, error) {
|
||||
n, err := r.write(w, []byte(s))
|
||||
if err != nil {
|
||||
return n, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// PushIndent adds the specified amount of indentation to the current line prefix.
|
||||
func (r *Renderer) pushIndent(amount int) {
|
||||
r.pushPrefix(strings.Repeat(" ", amount))
|
||||
}
|
||||
|
||||
// PushPrefix adds the specified string to the current line prefix.
|
||||
func (r *Renderer) pushPrefix(prefix string) {
|
||||
r.prefixStack = append(r.prefixStack, prefix)
|
||||
r.prefix = append(r.prefix, []byte(prefix)...)
|
||||
}
|
||||
|
||||
// PopPrefix removes the last piece added by a call to PushIndent or PushPrefix from the current line prefix.
|
||||
func (r *Renderer) popPrefix() {
|
||||
r.prefix = r.prefix[:len(r.prefix)-len(r.prefixStack[len(r.prefixStack)-1])]
|
||||
r.prefixStack = r.prefixStack[:len(r.prefixStack)-1]
|
||||
}
|
||||
|
||||
// OpenBlock ensures that each block begins on a new line, and that blank lines are inserted before blocks as
|
||||
// indicated by node.HasPreviousBlankLines.
|
||||
func (r *Renderer) openBlock(w util.BufWriter, source []byte, node ast.Node) error {
|
||||
r.openBlocks = append(r.openBlocks, blockState{
|
||||
node: node,
|
||||
fresh: true,
|
||||
})
|
||||
|
||||
hasBlankPreviousLines := node.HasBlankPreviousLines()
|
||||
|
||||
// FIXME: standard goldmark table parser doesn't recognize Blank Previous Lines so we'll always add one
|
||||
if node.Kind() == exast.KindTable {
|
||||
hasBlankPreviousLines = true
|
||||
}
|
||||
|
||||
// Work around the fact that the first child of a node notices the same set of preceding blank lines as its parent.
|
||||
if p := node.Parent(); p != nil && p.FirstChild() == node {
|
||||
if p.Kind() == ast.KindDocument || p.Kind() == ast.KindListItem || p.HasBlankPreviousLines() {
|
||||
hasBlankPreviousLines = false
|
||||
}
|
||||
}
|
||||
|
||||
if hasBlankPreviousLines {
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
r.openBlocks[len(r.openBlocks)-1].fresh = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseBlock marks the current block as closed.
|
||||
func (r *Renderer) closeBlock(w io.Writer) error {
|
||||
if !r.atNewline {
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
r.openBlocks = r.openBlocks[:len(r.openBlocks)-1]
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderDocument renders an *ast.Document node to the given BufWriter.
|
||||
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderHeading renders an *ast.Heading node to the given BufWriter.
|
||||
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.writeString(w, strings.Repeat("#", node.(*ast.Heading).Level)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if err := r.writeByte(w, ' '); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderBlockquote renders an *ast.Blockquote node to the given BufWriter.
|
||||
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.writeString(w, "> "); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
r.pushPrefix("> ")
|
||||
} else {
|
||||
r.popPrefix()
|
||||
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderCodeBlock renders an *ast.CodeBlock node to the given BufWriter.
|
||||
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
r.popPrefix()
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
// // Each line of a code block needs to be aligned at the same offset, and a code block must start with at least four
|
||||
// // spaces. To achieve this, we unconditionally add four spaces to the first line of the code block and indent the
|
||||
// // rest as necessary.
|
||||
// if _, err := r.writeString(w, " "); err != nil {
|
||||
// return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
// }
|
||||
|
||||
r.pushIndent(4)
|
||||
if err := r.writeLines(w, source, node.Lines()); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderFencedCodeBlock renders an *ast.FencedCodeBlock node to the given BufWriter.
|
||||
func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
code := node.(*ast.FencedCodeBlock)
|
||||
|
||||
// Write the start of the fenced code block.
|
||||
fence := []byte("```")
|
||||
if _, err := r.write(w, fence); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
language := code.Language(source)
|
||||
if _, err := r.write(w, language); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return ast.WalkStop, nil
|
||||
}
|
||||
|
||||
// Write the contents of the fenced code block.
|
||||
if err := r.writeLines(w, source, node.Lines()); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
// Write the end of the fenced code block.
|
||||
if err := r.beginLine(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
if _, err := r.write(w, fence); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderHTMLBlock renders an *ast.HTMLBlock node to the given BufWriter.
|
||||
func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
// Write the contents of the HTML block.
|
||||
if err := r.writeLines(w, source, node.Lines()); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
// Write the closure line, if any.
|
||||
html := node.(*ast.HTMLBlock)
|
||||
if html.HasClosure() {
|
||||
if _, err := r.write(w, html.ClosureLine.Value(source)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderList renders an *ast.List node to the given BufWriter.
|
||||
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
list := node.(*ast.List)
|
||||
r.listStack = append(r.listStack, listState{
|
||||
marker: list.Marker,
|
||||
ordered: list.IsOrdered(),
|
||||
index: list.Start,
|
||||
})
|
||||
} else {
|
||||
r.listStack = r.listStack[:len(r.listStack)-1]
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderListItem renders an *ast.ListItem node to the given BufWriter.
|
||||
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
markerWidth := 2 // marker + space
|
||||
|
||||
state := &r.listStack[len(r.listStack)-1]
|
||||
if state.ordered {
|
||||
width, err := r.writeString(w, strconv.FormatInt(int64(state.index), 10))
|
||||
if err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
state.index++
|
||||
markerWidth += width // marker, space, and digits
|
||||
}
|
||||
|
||||
if _, err := r.write(w, []byte{state.marker, ' '}); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
r.pushIndent(markerWidth)
|
||||
} else {
|
||||
r.popPrefix()
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderParagraph renders an *ast.Paragraph node to the given BufWriter.
|
||||
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
// A paragraph that follows another paragraph or a blockquote must be preceded by a blank line.
|
||||
if !node.HasBlankPreviousLines() {
|
||||
if prev := node.PreviousSibling(); prev != nil && (prev.Kind() == ast.KindParagraph || prev.Kind() == ast.KindBlockquote) {
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderTextBlock renders an *ast.TextBlock node to the given BufWriter.
|
||||
func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderThematicBreak renders an *ast.ThematicBreak node to the given BufWriter.
|
||||
func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
// TODO: this prints an extra no line
|
||||
if _, err := r.writeString(w, "--------"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderAutoLink renders an *ast.AutoLink node to the given BufWriter.
|
||||
func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if err := r.writeByte(w, '<'); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
if _, err := r.write(w, node.(*ast.AutoLink).Label(source)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
if err := r.writeByte(w, '>'); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) shouldPadCodeSpan(source []byte, node *ast.CodeSpan) bool {
|
||||
c := node.FirstChild()
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
segment := c.(*ast.Text).Segment
|
||||
text := segment.Value(source)
|
||||
|
||||
var firstChar byte
|
||||
if len(text) > 0 {
|
||||
firstChar = text[0]
|
||||
}
|
||||
|
||||
allWhitespace := true
|
||||
for {
|
||||
if util.FirstNonSpacePosition(text) != -1 {
|
||||
allWhitespace = false
|
||||
break
|
||||
}
|
||||
c = c.NextSibling()
|
||||
if c == nil {
|
||||
break
|
||||
}
|
||||
segment = c.(*ast.Text).Segment
|
||||
text = segment.Value(source)
|
||||
}
|
||||
if allWhitespace {
|
||||
return false
|
||||
}
|
||||
|
||||
var lastChar byte
|
||||
if len(text) > 0 {
|
||||
lastChar = text[len(text)-1]
|
||||
}
|
||||
|
||||
return firstChar == '`' || firstChar == ' ' || lastChar == '`' || lastChar == ' '
|
||||
}
|
||||
|
||||
// RenderCodeSpan renders an *ast.CodeSpan node to the given BufWriter.
|
||||
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
code := node.(*ast.CodeSpan)
|
||||
delimiter := []byte{'`'}
|
||||
pad := r.shouldPadCodeSpan(source, code)
|
||||
|
||||
if _, err := r.write(w, delimiter); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
if pad {
|
||||
if err := r.writeByte(w, ' '); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
text := c.(*ast.Text).Segment
|
||||
if _, err := r.write(w, text.Value(source)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
if pad {
|
||||
if err := r.writeByte(w, ' '); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
if _, err := r.write(w, delimiter); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
// RenderEmphasis renders an *ast.Emphasis node to the given BufWriter.
|
||||
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
em := node.(*ast.Emphasis)
|
||||
if _, err := r.writeString(w, strings.Repeat("*", em.Level)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) escapeLinkDest(dest []byte) []byte {
|
||||
requiresEscaping := false
|
||||
for _, c := range dest {
|
||||
if c <= 32 || c == '(' || c == ')' || c == 127 {
|
||||
requiresEscaping = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !requiresEscaping {
|
||||
return dest
|
||||
}
|
||||
|
||||
escaped := make([]byte, 0, len(dest)+2)
|
||||
escaped = append(escaped, '<')
|
||||
for _, c := range dest {
|
||||
if c == '<' || c == '>' {
|
||||
escaped = append(escaped, '\\')
|
||||
}
|
||||
escaped = append(escaped, c)
|
||||
}
|
||||
escaped = append(escaped, '>')
|
||||
return escaped
|
||||
}
|
||||
|
||||
func (r *Renderer) linkTitleDelimiter(title []byte) byte {
|
||||
for i, c := range title {
|
||||
if c == '"' && (i == 0 || title[i-1] != '\\') {
|
||||
return '\''
|
||||
}
|
||||
}
|
||||
return '"'
|
||||
}
|
||||
|
||||
func (r *Renderer) renderLinkOrImage(w util.BufWriter, open string, dest, title []byte, enter bool) error {
|
||||
if enter {
|
||||
if _, err := r.writeString(w, open); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if _, err := r.writeString(w, "]("); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.write(w, r.escapeLinkDest(dest)); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
if len(title) != 0 {
|
||||
delimiter := r.linkTitleDelimiter(title)
|
||||
if _, err := fmt.Fprintf(w, ` %c%s%c`, delimiter, string(title), delimiter); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.writeByte(w, ')'); err != nil {
|
||||
return fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderImage renders an *ast.Image node to the given BufWriter.
|
||||
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
img := node.(*ast.Image)
|
||||
if err := r.renderLinkOrImage(w, "![", img.Destination, img.Title, enter); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderLink renders an *ast.Link node to the given BufWriter.
|
||||
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
link := node.(*ast.Link)
|
||||
if err := r.renderLinkOrImage(w, "[", link.Destination, link.Title, enter); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderRawHTML renders an *ast.RawHTML node to the given BufWriter.
|
||||
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
raw := node.(*ast.RawHTML)
|
||||
for i := 0; i < raw.Segments.Len(); i++ {
|
||||
segment := raw.Segments.At(i)
|
||||
if _, err := r.write(w, segment.Value(source)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
// RenderText renders an *ast.Text node to the given BufWriter.
|
||||
func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
text := node.(*ast.Text)
|
||||
value := text.Segment.Value(source)
|
||||
|
||||
if _, err := r.write(w, value); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
switch {
|
||||
case text.HardLineBreak():
|
||||
if _, err := r.writeString(w, "\\\n"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
case text.SoftLineBreak():
|
||||
if err := r.writeByte(w, '\n'); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderString renders an *ast.String node to the given BufWriter.
|
||||
func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
str := node.(*ast.String)
|
||||
if _, err := r.write(w, str.Value); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTable(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTableHeader(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.writeString(w, "| "); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if _, err := r.writeString(w, " |\n|"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
for x := 0; x < node.ChildCount(); x++ { // use as column count
|
||||
if _, err := r.writeString(w, " --- |"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.writeString(w, "| "); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if _, err := r.writeString(w, " |"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if !enter {
|
||||
if node.NextSibling() != nil {
|
||||
if _, err := r.writeString(w, " | "); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if _, err := r.writeString(w, "~~"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
if enter {
|
||||
var fill byte = ' '
|
||||
if task := node.(*exast.TaskCheckBox); task.IsChecked {
|
||||
fill = 'x'
|
||||
}
|
||||
|
||||
if _, err := r.write(w, []byte{'[', fill, ']', ' '}); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
35
internal/markdown/renderer/markdown/section.go
Normal file
35
internal/markdown/renderer/markdown/section.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
|
||||
rpexast "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||
)
|
||||
|
||||
func (r *Renderer) renderSection(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
n := node.(*rpexast.Section)
|
||||
|
||||
if enter {
|
||||
if err := r.openBlock(w, source, node); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if _, err := r.writeString(w, fmt.Sprintf(extensions.SectionStartFormat, n.Name)+"\n"); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
} else {
|
||||
if _, err := r.writeString(w, "\n"+fmt.Sprintf(extensions.SectionEndFormat, n.Name)); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
|
||||
if err := r.closeBlock(w); err != nil {
|
||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue