diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 707377f..3f4fe38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,26 +16,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ env.go_version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run linters - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.56.0 + version: v1.62.2 test: runs-on: ubuntu-latest steps: - name: Install Go if: success() - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ env.go_version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run tests run: go test -v -covermode=count -coverprofile=coverage.out - name: Send coverage diff --git a/renderer.go b/renderer.go index 5ec724c..caa8c0f 100644 --- a/renderer.go +++ b/renderer.go @@ -5,7 +5,9 @@ import ( "bytes" "fmt" "io" + "slices" "sync" + "unicode" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" @@ -243,7 +245,7 @@ func (r *Renderer) renderFencedCodeBlock(node ast.Node, entering bool) ast.WalkS r.rc.writer.WriteBytes([]byte("```")) if entering { if info := n.Info; info != nil { - r.rc.writer.WriteBytes(info.Text(r.rc.source)) + r.rc.writer.WriteBytes(info.Value(r.rc.source)) } r.rc.writer.FlushLine() r.renderLines(node, entering) @@ -310,7 +312,7 @@ func (r *Renderer) renderRawHTML(node ast.Node, entering bool) ast.WalkStatus { func (r *Renderer) renderText(node ast.Node, entering bool) ast.WalkStatus { n := node.(*ast.Text) if entering { - text := n.Text(r.rc.source) + text := n.Value(r.rc.source) r.rc.writer.WriteBytes(text) if n.SoftLineBreak() { @@ -369,10 +371,64 @@ func (r *Renderer) renderLinkCommon(title, destination []byte, entering bool) as } func (r *Renderer) renderCodeSpan(node ast.Node, entering bool) ast.WalkStatus { - if bytes.Count(node.Text(r.rc.source), []byte("`"))%2 != 0 { - r.rc.writer.WriteBytes([]byte("``")) + if entering { + // get contents of codespan + var contentBytes []byte + for c := node.FirstChild(); c != nil; c = c.NextSibling() { + text := c.(*ast.Text).Segment + contentBytes = append(contentBytes, text.Value(r.rc.source)...) + } + contents := string(contentBytes) + + // + var beginsWithSpace bool + var endsWithSpace bool + var beginsWithBackTick bool + var endsWithBackTick bool + isOnlySpace := true + backtickLengths := []int{} + count := 0 + for i, c := range contents { + if i == 0 { + beginsWithSpace = unicode.IsSpace(c) + beginsWithBackTick = c == '`' + } else if i == len(contents)-1 { + endsWithSpace = unicode.IsSpace(c) + endsWithBackTick = c == '`' + } + if !unicode.IsSpace(c) { + isOnlySpace = false + } + if c == '`' { + count++ + } else if count > 0 { + backtickLengths = append(backtickLengths, count) + count = 0 + } + } + if count > 0 { + backtickLengths = append(backtickLengths, count) + } + + // Surround the codespan with the minimum number of backticks required to contain the span. + for i := 1; i <= len(contentBytes); i++ { + if !slices.Contains(backtickLengths, i) { + r.rc.codeSpanContext.backtickLength = i + break + } + } + r.rc.writer.WriteBytes(bytes.Repeat([]byte("`"), r.rc.codeSpanContext.backtickLength)) + + // Check if the code span needs to be padded with spaces + if beginsWithSpace && endsWithSpace && !isOnlySpace || beginsWithBackTick || endsWithBackTick { + r.rc.codeSpanContext.padSpace = true + r.rc.writer.WriteBytes([]byte(" ")) + } } else { - r.rc.writer.WriteBytes([]byte("`")) + if r.rc.codeSpanContext.padSpace { + r.rc.writer.WriteBytes([]byte(" ")) + } + r.rc.writer.WriteBytes(bytes.Repeat([]byte("`"), r.rc.codeSpanContext.backtickLength)) } return ast.WalkContinue @@ -389,7 +445,8 @@ type renderContext struct { // source is the markdown source source []byte // listMarkers is the marker character used for the current list - lists []listContext + lists []listContext + codeSpanContext codeSpanContext } type listContext struct { @@ -397,6 +454,14 @@ type listContext struct { num int } +// codeSpanContext holds state about how the current codespan should be rendererd. +type codeSpanContext struct { + // number of backticks to use + backtickLength int + // whether to surround the codespan with spaces + padSpace bool +} + // newRenderContext returns a new renderContext object func newRenderContext(writer io.Writer, source []byte, config *Config) renderContext { return renderContext{ diff --git a/renderer_test.go b/renderer_test.go index 20aae24..1cd66b9 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -193,6 +193,12 @@ func TestRenderedOutput(t *testing.T) { "`foo`", "`foo`\n", }, + { + "Multiline code span", + []Option{}, + "`foo\nbar`", + "`foo\nbar`\n", + }, { "Two-backtick code span", []Option{}, @@ -200,16 +206,22 @@ func TestRenderedOutput(t *testing.T) { "``foo ` bar``\n", }, { - "Code span stripping leading and trailing spaces", + "Reduced backtick code span", + []Option{}, + "``foo bar``", + "`foo bar`\n", + }, + { + "Code span preserving leading and trailing spaces", []Option{}, "` `` `", - "````\n", + "` `` `\n", }, { - "Code span stripping one space", + "Code span preserving surrounding spaces", []Option{}, "` `` `", - "` `` `\n", + "` `` `\n", }, { "Unstrippable left space only",