Compare commits

...

10 commits

Author SHA1 Message Date
Yusuke Inuzuka
04410ff159
Merge pull request #487 from n-peugnet/patch-1
Fix GitHub actions badge URL
2025-02-19 03:38:00 +09:00
Yusuke Inuzuka
f7b2d24c09
Merge pull request #489 from lyricat/master
chore: update `goldmark-enclave` repo URL and information
2025-02-19 03:37:26 +09:00
Lyric Wai
ba49c5c69d
Merge pull request #1 from lyricat/lyricat-patch-enclave
Update README.md
2025-02-18 23:34:09 +09:00
Lyric Wai
c05fb087a4
Update README.md
Update `goldmark-enclave` url and information
2025-02-18 23:33:14 +09:00
Nicolas Peugnet
b39daae79e
Fix GitHub actions badge URL 2025-02-02 15:58:36 +01:00
yuin
d9c03f07f0 Deprecate Node.Text
Node.Text was intended to get a text value from some inline nodes.
A 'text value' of a Text node is clear.

But

- BaseNode had a default implementation of Node.Text
- Lacks of GoDoc description that Node.Text is valid only for
  some inline nodes

So, some users are using Node.Text for BlockNodes.

A 'text value' for a BlockNode is not clear.

e.g. : Text value of a ListNode

- It should be contains list markers?
- What do characters concatinate List items with? newlines? spaces?
- If it contains codeblocks, codeblocks should be fenced or indented?

Now we would like to avoid such ambiguous method.
2024-10-16 20:47:35 +09:00
yuin
65dcf6cd0a Add warning to Node.Text GoDoc 2024-10-15 19:22:00 +09:00
yuin
ad1565131a Fix #470 2024-10-15 19:19:41 +09:00
yuin
bc993b4f59 Fix testcases 2024-10-12 23:12:47 +09:00
yuin
41273a4d07 Fix EOF rendering 2024-10-12 23:05:37 +09:00
14 changed files with 463 additions and 106 deletions

View file

@ -2,7 +2,7 @@ goldmark
========================================== ==========================================
[![https://pkg.go.dev/github.com/yuin/goldmark](https://pkg.go.dev/badge/github.com/yuin/goldmark.svg)](https://pkg.go.dev/github.com/yuin/goldmark) [![https://pkg.go.dev/github.com/yuin/goldmark](https://pkg.go.dev/badge/github.com/yuin/goldmark.svg)](https://pkg.go.dev/github.com/yuin/goldmark)
[![https://github.com/yuin/goldmark/actions?query=workflow:test](https://github.com/yuin/goldmark/workflows/test/badge.svg?branch=master&event=push)](https://github.com/yuin/goldmark/actions?query=workflow:test) [![https://github.com/yuin/goldmark/actions?query=workflow:test](https://github.com/yuin/goldmark/actions/workflows/test.yaml/badge.svg?branch=master&event=push)](https://github.com/yuin/goldmark/actions?query=workflow:test)
[![https://coveralls.io/github/yuin/goldmark](https://coveralls.io/repos/github/yuin/goldmark/badge.svg?branch=master)](https://coveralls.io/github/yuin/goldmark) [![https://coveralls.io/github/yuin/goldmark](https://coveralls.io/repos/github/yuin/goldmark/badge.svg?branch=master)](https://coveralls.io/github/yuin/goldmark)
[![https://goreportcard.com/report/github.com/yuin/goldmark](https://goreportcard.com/badge/github.com/yuin/goldmark)](https://goreportcard.com/report/github.com/yuin/goldmark) [![https://goreportcard.com/report/github.com/yuin/goldmark](https://goreportcard.com/badge/github.com/yuin/goldmark)](https://goreportcard.com/report/github.com/yuin/goldmark)
@ -493,7 +493,7 @@ Extensions
- [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams. - [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams.
- [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations. - [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations.
- [goldmark-img64](https://github.com/tenkoh/goldmark-img64): Adds support for embedding images into the document as DataURL (base64 encoded). - [goldmark-img64](https://github.com/tenkoh/goldmark-img64): Adds support for embedding images into the document as DataURL (base64 encoded).
- [goldmark-enclave](https://github.com/quail-ink/goldmark-enclave): Adds support for embedding youtube/bilibili video, X's [oembed tweet](https://publish.twitter.com/), [tradingview](https://www.tradingview.com/widget/)'s chart, [quail](https://quail.ink)'s widget into the document. - [goldmark-enclave](https://github.com/quailyquaily/goldmark-enclave): Adds support for embedding youtube/bilibili video, X's [oembed X](https://publish.x.com/), [tradingview chart](https://www.tradingview.com/widget/)'s chart, [quaily widget](https://quaily.com), [spotify embeds](https://developer.spotify.com/documentation/embeds), [dify embed](https://dify.ai/) and html audio into the document.
- [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables. - [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables.
- [goldmark-tgmd](https://github.com/Mad-Pixels/goldmark-tgmd): A Telegram markdown renderer that can be passed to `goldmark.WithRenderer()`. - [goldmark-tgmd](https://github.com/Mad-Pixels/goldmark-tgmd): A Telegram markdown renderer that can be passed to `goldmark.WithRenderer()`.

View file

@ -385,7 +385,8 @@ a* b c d *e*
//- - - - - - - - -// //- - - - - - - - -//
x x
//- - - - - - - - -// //- - - - - - - - -//
<pre><code> x</code></pre> <pre><code> x
</code></pre>
//= = = = = = = = = = = = = = = = = = = = = = = =// //= = = = = = = = = = = = = = = = = = = = = = = =//
26: NUL bytes must be replaced with U+FFFD 26: NUL bytes must be replaced with U+FFFD
@ -821,7 +822,7 @@ text" /></p>
</blockquote> </blockquote>
//= = = = = = = = = = = = = = = = = = = = = = = =// //= = = = = = = = = = = = = = = = = = = = = = = =//
66: EOF should be rendered as a newline with an unclosed block 66: EOF should be rendered as a newline with an unclosed block(w/ TAB)
//- - - - - - - - -// //- - - - - - - - -//
> ``` > ```
> 0 > 0
@ -831,3 +832,14 @@ text" /></p>
</code></pre> </code></pre>
</blockquote> </blockquote>
//= = = = = = = = = = = = = = = = = = = = = = = =// //= = = = = = = = = = = = = = = = = = = = = = = =//
67: EOF should be rendered as a newline with an unclosed block
//- - - - - - - - -//
> ```
> 0
//- - - - - - - - -//
<blockquote>
<pre><code> 0
</code></pre>
</blockquote>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -123,6 +123,12 @@ type Node interface {
Dump(source []byte, level int) Dump(source []byte, level int)
// Text returns text values of this node. // Text returns text values of this node.
// This method is valid only for some inline nodes.
// If this node is a block node, Text returns a text value as reasonable as possible.
// Notice that there are no 'correct' text values for the block nodes.
// Result for the block nodes may be different from your expectation.
//
// Deprecated: Use other properties of the node to get the text value(i.e. Pragraph.Lines, Text.Value).
Text(source []byte) []byte Text(source []byte) []byte
// HasBlankPreviousLines returns true if the row before this node is blank, // HasBlankPreviousLines returns true if the row before this node is blank,
@ -374,11 +380,18 @@ func (n *BaseNode) OwnerDocument() *Document {
return nil return nil
} }
// Text implements Node.Text . // Text implements Node.Text .
//
// Deprecated: Use other properties of the node to get the text value(i.e. Pragraph.Lines, Text.Value).
func (n *BaseNode) Text(source []byte) []byte { func (n *BaseNode) Text(source []byte) []byte {
var buf bytes.Buffer var buf bytes.Buffer
for c := n.firstChild; c != nil; c = c.NextSibling() { for c := n.firstChild; c != nil; c = c.NextSibling() {
buf.Write(c.Text(source)) buf.Write(c.Text(source))
if sb, ok := c.(interface {
SoftLineBreak() bool
}); ok && sb.SoftLineBreak() {
buf.WriteByte('\n')
}
} }
return buf.Bytes() return buf.Bytes()
} }

View file

@ -1,28 +1,10 @@
package ast package ast
import ( import (
"bytes"
"reflect" "reflect"
"testing" "testing"
"github.com/yuin/goldmark/text"
) )
func TestRemoveChildren(t *testing.T) {
root := NewDocument()
node1 := NewDocument()
node2 := NewDocument()
root.AppendChild(root, node1)
root.AppendChild(root, node2)
root.RemoveChildren(root)
t.Logf("%+v", node2.PreviousSibling())
}
func TestWalk(t *testing.T) { func TestWalk(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -76,48 +58,3 @@ func node(n Node, children ...Node) Node {
} }
return n return n
} }
func TestBaseBlock_Text(t *testing.T) {
source := []byte(`# Heading
code block here
and also here
A paragraph
` + "```" + `somelang
fenced code block
` + "```" + `
The end`)
t.Run("fetch text from code block", func(t *testing.T) {
block := NewCodeBlock()
block.lines = text.NewSegments()
block.lines.Append(text.Segment{Start: 15, Stop: 31})
block.lines.Append(text.Segment{Start: 32, Stop: 46})
expected := []byte("code block here\nand also here\n")
if !bytes.Equal(expected, block.Text(source)) {
t.Errorf("Expected: %q, got: %q", string(expected), string(block.Text(source)))
}
})
t.Run("fetch text from fenced code block", func(t *testing.T) {
block := NewFencedCodeBlock(&Text{
Segment: text.Segment{Start: 63, Stop: 71},
})
block.lines = text.NewSegments()
block.lines.Append(text.Segment{Start: 72, Stop: 90})
expectedLang := []byte("somelang")
if !bytes.Equal(expectedLang, block.Language(source)) {
t.Errorf("Expected: %q, got: %q", string(expectedLang), string(block.Language(source)))
}
expected := []byte("fenced code block\n")
if !bytes.Equal(expected, block.Text(source)) {
t.Errorf("Expected: %q, got: %q", string(expected), string(block.Text(source)))
}
})
}

View file

@ -1,7 +1,6 @@
package ast package ast
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
@ -48,15 +47,6 @@ func (b *BaseBlock) SetLines(v *textm.Segments) {
b.lines = v b.lines = v
} }
// Text implements Node.Text.
func (b *BaseBlock) Text(source []byte) []byte {
var buf bytes.Buffer
for _, line := range b.Lines().Sliced(0, b.Lines().Len()) {
buf.Write(line.Value(source))
}
return buf.Bytes()
}
// A Document struct is a root node of Markdown text. // A Document struct is a root node of Markdown text.
type Document struct { type Document struct {
BaseBlock BaseBlock
@ -140,6 +130,13 @@ func (n *TextBlock) Kind() NodeKind {
return KindTextBlock return KindTextBlock
} }
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. TextBlock.Lines).
func (n *TextBlock) Text(source []byte) []byte {
return n.Lines().Value(source)
}
// NewTextBlock returns a new TextBlock node. // NewTextBlock returns a new TextBlock node.
func NewTextBlock() *TextBlock { func NewTextBlock() *TextBlock {
return &TextBlock{ return &TextBlock{
@ -165,6 +162,13 @@ func (n *Paragraph) Kind() NodeKind {
return KindParagraph return KindParagraph
} }
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. Paragraph.Lines).
func (n *Paragraph) Text(source []byte) []byte {
return n.Lines().Value(source)
}
// NewParagraph returns a new Paragraph node. // NewParagraph returns a new Paragraph node.
func NewParagraph() *Paragraph { func NewParagraph() *Paragraph {
return &Paragraph{ return &Paragraph{
@ -259,6 +263,13 @@ func (n *CodeBlock) Kind() NodeKind {
return KindCodeBlock return KindCodeBlock
} }
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. CodeBlock.Lines).
func (n *CodeBlock) Text(source []byte) []byte {
return n.Lines().Value(source)
}
// NewCodeBlock returns a new CodeBlock node. // NewCodeBlock returns a new CodeBlock node.
func NewCodeBlock() *CodeBlock { func NewCodeBlock() *CodeBlock {
return &CodeBlock{ return &CodeBlock{
@ -314,6 +325,13 @@ func (n *FencedCodeBlock) Kind() NodeKind {
return KindFencedCodeBlock return KindFencedCodeBlock
} }
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. FencedCodeBlock.Lines).
func (n *FencedCodeBlock) Text(source []byte) []byte {
return n.Lines().Value(source)
}
// NewFencedCodeBlock return a new FencedCodeBlock node. // NewFencedCodeBlock return a new FencedCodeBlock node.
func NewFencedCodeBlock(info *Text) *FencedCodeBlock { func NewFencedCodeBlock(info *Text) *FencedCodeBlock {
return &FencedCodeBlock{ return &FencedCodeBlock{
@ -508,6 +526,17 @@ func (n *HTMLBlock) Kind() NodeKind {
return KindHTMLBlock return KindHTMLBlock
} }
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. HTMLBlock.Lines).
func (n *HTMLBlock) Text(source []byte) []byte {
ret := n.Lines().Value(source)
if n.HasClosure() {
ret = append(ret, n.ClosureLine.Value(source)...)
}
return ret
}
// NewHTMLBlock returns a new HTMLBlock node. // NewHTMLBlock returns a new HTMLBlock node.
func NewHTMLBlock(typ HTMLBlockType) *HTMLBlock { func NewHTMLBlock(typ HTMLBlockType) *HTMLBlock {
return &HTMLBlock{ return &HTMLBlock{

View file

@ -143,17 +143,25 @@ func (n *Text) Merge(node Node, source []byte) bool {
} }
// Text implements Node.Text. // Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. Text.Value).
func (n *Text) Text(source []byte) []byte { func (n *Text) Text(source []byte) []byte {
return n.Segment.Value(source) return n.Segment.Value(source)
} }
// Value returns a value of this node.
// SoftLineBreaks are not included in the returned value.
func (n *Text) Value(source []byte) []byte {
return n.Segment.Value(source)
}
// Dump implements Node.Dump. // Dump implements Node.Dump.
func (n *Text) Dump(source []byte, level int) { func (n *Text) Dump(source []byte, level int) {
fs := textFlagsString(n.flags) fs := textFlagsString(n.flags)
if len(fs) != 0 { if len(fs) != 0 {
fs = "(" + fs + ")" fs = "(" + fs + ")"
} }
fmt.Printf("%sText%s: \"%s\"\n", strings.Repeat(" ", level), fs, strings.TrimRight(string(n.Text(source)), "\n")) fmt.Printf("%sText%s: \"%s\"\n", strings.Repeat(" ", level), fs, strings.TrimRight(string(n.Value(source)), "\n"))
} }
// KindText is a NodeKind of the Text node. // KindText is a NodeKind of the Text node.
@ -258,6 +266,8 @@ func (n *String) SetCode(v bool) {
} }
// Text implements Node.Text. // Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. String.Value).
func (n *String) Text(source []byte) []byte { func (n *String) Text(source []byte) []byte {
return n.Value return n.Value
} }
@ -492,15 +502,22 @@ func (n *AutoLink) URL(source []byte) []byte {
ret := make([]byte, 0, len(n.Protocol)+s.Len()+3) ret := make([]byte, 0, len(n.Protocol)+s.Len()+3)
ret = append(ret, n.Protocol...) ret = append(ret, n.Protocol...)
ret = append(ret, ':', '/', '/') ret = append(ret, ':', '/', '/')
ret = append(ret, n.value.Text(source)...) ret = append(ret, n.value.Value(source)...)
return ret return ret
} }
return n.value.Text(source) return n.value.Value(source)
} }
// Label returns a label of this node. // Label returns a label of this node.
func (n *AutoLink) Label(source []byte) []byte { func (n *AutoLink) Label(source []byte) []byte {
return n.value.Text(source) return n.value.Value(source)
}
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. AutoLink.Label).
func (n *AutoLink) Text(source []byte) []byte {
return n.value.Value(source)
} }
// NewAutoLink returns a new AutoLink node. // NewAutoLink returns a new AutoLink node.
@ -541,6 +558,13 @@ func (n *RawHTML) Kind() NodeKind {
return KindRawHTML return KindRawHTML
} }
// Text implements Node.Text.
//
// Deprecated: Use other properties of the node to get the text value(i.e. RawHTML.Segments).
func (n *RawHTML) Text(source []byte) []byte {
return n.Segments.Value(source)
}
// NewRawHTML returns a new RawHTML node. // NewRawHTML returns a new RawHTML node.
func NewRawHTML() *RawHTML { func NewRawHTML() *RawHTML {
return &RawHTML{ return &RawHTML{

204
ast_test.go Normal file
View file

@ -0,0 +1,204 @@
package goldmark_test
import (
"bytes"
"testing"
. "github.com/yuin/goldmark"
"github.com/yuin/goldmark/testutil"
"github.com/yuin/goldmark/text"
)
func TestASTBlockNodeText(t *testing.T) {
var cases = []struct {
Name string
Source string
T1 string
T2 string
C bool
}{
{
Name: "AtxHeading",
Source: `# l1
a
# l2`,
T1: `l1`,
T2: `l2`,
},
{
Name: "SetextHeading",
Source: `l1
l2
===============
a
l3
l4
==============`,
T1: `l1
l2`,
T2: `l3
l4`,
},
{
Name: "CodeBlock",
Source: ` l1
l2
a
l3
l4`,
T1: `l1
l2
`,
T2: `l3
l4
`,
},
{
Name: "FencedCodeBlock",
Source: "```" + `
l1
l2
` + "```" + `
a
` + "```" + `
l3
l4`,
T1: `l1
l2
`,
T2: `l3
l4
`,
},
{
Name: "Blockquote",
Source: `> l1
> l2
a
> l3
> l4`,
T1: `l1
l2`,
T2: `l3
l4`,
},
{
Name: "List",
Source: `- l1
l2
a
- l3
l4`,
T1: `l1
l2`,
T2: `l3
l4`,
C: true,
},
{
Name: "HTMLBlock",
Source: `<div>
l1
l2
</div>
a
<div>
l3
l4`,
T1: `<div>
l1
l2
</div>
`,
T2: `<div>
l3
l4`,
},
}
for _, cs := range cases {
t.Run(cs.Name, func(t *testing.T) {
s := []byte(cs.Source)
md := New()
n := md.Parser().Parse(text.NewReader(s))
c1 := n.FirstChild()
c2 := c1.NextSibling().NextSibling()
if cs.C {
c1 = c1.FirstChild()
c2 = c2.FirstChild()
}
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
t.Errorf("%s unmatch: %s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
}
if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
}
})
}
}
func TestASTInlineNodeText(t *testing.T) {
var cases = []struct {
Name string
Source string
T1 string
}{
{
Name: "CodeSpan",
Source: "`c1`",
T1: `c1`,
},
{
Name: "Emphasis",
Source: `*c1 **c2***`,
T1: `c1 c2`,
},
{
Name: "Link",
Source: `[label](url)`,
T1: `label`,
},
{
Name: "AutoLink",
Source: `<http://url>`,
T1: `http://url`,
},
{
Name: "RawHTML",
Source: `<span>c1</span>`,
T1: `<span>`,
},
}
for _, cs := range cases {
t.Run(cs.Name, func(t *testing.T) {
s := []byte(cs.Source)
md := New()
n := md.Parser().Parse(text.NewReader(s))
c1 := n.FirstChild().FirstChild()
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
}
})
}
}

View file

@ -150,7 +150,8 @@ on two lines.</p>
//- - - - - - - - -// //- - - - - - - - -//
<dl> <dl>
<dt>0</dt> <dt>0</dt>
<dd><pre><code> 0</code></pre> <dd><pre><code> 0
</code></pre>
</dd> </dd>
</dl> </dl>
//= = = = = = = = = = = = = = = = = = = = = = = =// //= = = = = = = = = = = = = = = = = = = = = = = =//

123
extension/ast_test.go Normal file
View file

@ -0,0 +1,123 @@
package extension
import (
"bytes"
"testing"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/testutil"
"github.com/yuin/goldmark/text"
)
func TestASTBlockNodeText(t *testing.T) {
var cases = []struct {
Name string
Source string
T1 string
T2 string
C bool
}{
{
Name: "DefinitionList",
Source: `c1
: c2
c3
a
c4
: c5
c6`,
T1: `c1c2
c3`,
T2: `c4c5
c6`,
},
{
Name: "Table",
Source: `| h1 | h2 |
| -- | -- |
| c1 | c2 |
a
| h3 | h4 |
| -- | -- |
| c3 | c4 |`,
T1: `h1h2c1c2`,
T2: `h3h4c3c4`,
},
}
for _, cs := range cases {
t.Run(cs.Name, func(t *testing.T) {
s := []byte(cs.Source)
md := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
DefinitionList,
Table,
),
)
n := md.Parser().Parse(text.NewReader(s))
c1 := n.FirstChild()
c2 := c1.NextSibling().NextSibling()
if cs.C {
c1 = c1.FirstChild()
c2 = c2.FirstChild()
}
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
}
if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
}
})
}
}
func TestASTInlineNodeText(t *testing.T) {
var cases = []struct {
Name string
Source string
T1 string
}{
{
Name: "Strikethrough",
Source: `~c1 *c2*~`,
T1: `c1 c2`,
},
}
for _, cs := range cases {
t.Run(cs.Name, func(t *testing.T) {
s := []byte(cs.Source)
md := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
Strikethrough,
),
)
n := md.Parser().Parse(text.NewReader(s))
c1 := n.FirstChild().FirstChild()
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
}
})
}
}

View file

@ -35,6 +35,7 @@ func (b *codeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context)
if segment.Padding != 0 { if segment.Padding != 0 {
preserveLeadingTabInCodeBlock(&segment, reader, 0) preserveLeadingTabInCodeBlock(&segment, reader, 0)
} }
segment.ForceNewline = true
node.Lines().Append(segment) node.Lines().Append(segment)
reader.Advance(segment.Len() - 1) reader.Advance(segment.Len() - 1)
return node, NoChildren return node, NoChildren
@ -59,6 +60,7 @@ func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context
preserveLeadingTabInCodeBlock(&segment, reader, 0) preserveLeadingTabInCodeBlock(&segment, reader, 0)
} }
segment.ForceNewline = true
node.Lines().Append(segment) node.Lines().Append(segment)
reader.Advance(segment.Len() - 1) reader.Advance(segment.Len() - 1)
return Continue | NoChildren return Continue | NoChildren

View file

@ -100,6 +100,7 @@ func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc C
if padding != 0 { if padding != 0 {
preserveLeadingTabInCodeBlock(&seg, reader, fdata.indent) preserveLeadingTabInCodeBlock(&seg, reader, fdata.indent)
} }
seg.ForceNewline = true // EOF as newline
node.Lines().Append(seg) node.Lines().Append(seg)
reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding)
return Continue | NoChildren return Continue | NoChildren

View file

@ -878,12 +878,6 @@ func (p *parser) Parse(reader text.Reader, opts ...ParseOption) ast.Node {
blockReader := text.NewBlockReader(reader.Source(), nil) blockReader := text.NewBlockReader(reader.Source(), nil)
p.walkBlock(root, func(node ast.Node) { p.walkBlock(root, func(node ast.Node) {
p.parseBlock(blockReader, node, pc) p.parseBlock(blockReader, node, pc)
lines := node.Lines()
if lines != nil && lines.Len() != 0 {
s := lines.At(lines.Len() - 1)
s.EOB = true
lines.Set(lines.Len()-1, s)
}
}) })
for _, at := range p.astTransformers { for _, at := range p.astTransformers {
at.Transform(root, reader, pc) at.Transform(root, reader, pc)

View file

@ -680,7 +680,7 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
} }
_, _ = w.WriteString(`" alt="`) _, _ = w.WriteString(`" alt="`)
r.renderAttribute(w, source, n) r.renderTexts(w, source, n)
_ = w.WriteByte('"') _ = w.WriteByte('"')
if n.Title != nil { if n.Title != nil {
_, _ = w.WriteString(` title="`) _, _ = w.WriteString(` title="`)
@ -737,7 +737,7 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en
if r.EastAsianLineBreaks != EastAsianLineBreaksNone && len(value) != 0 { if r.EastAsianLineBreaks != EastAsianLineBreaksNone && len(value) != 0 {
sibling := node.NextSibling() sibling := node.NextSibling()
if sibling != nil && sibling.Kind() == ast.KindText { if sibling != nil && sibling.Kind() == ast.KindText {
if siblingText := sibling.(*ast.Text).Text(source); len(siblingText) != 0 { if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
thisLastRune := util.ToRune(value, len(value)-1) thisLastRune := util.ToRune(value, len(value)-1)
siblingFirstRune, _ := utf8.DecodeRune(siblingText) siblingFirstRune, _ := utf8.DecodeRune(siblingText)
if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) { if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
@ -770,19 +770,14 @@ func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node,
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
func (r *Renderer) renderAttribute(w util.BufWriter, source []byte, n ast.Node) { func (r *Renderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
for c := n.FirstChild(); c != nil; c = c.NextSibling() { for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if s, ok := c.(*ast.String); ok { if s, ok := c.(*ast.String); ok {
_, _ = r.renderString(w, source, s, true) _, _ = r.renderString(w, source, s, true)
} else if t, ok := c.(*ast.String); ok { } else if t, ok := c.(*ast.Text); ok {
_, _ = r.renderText(w, source, t, true) _, _ = r.renderText(w, source, t, true)
} else if !c.HasChildren() {
r.Writer.Write(w, c.Text(source))
if t, ok := c.(*ast.Text); ok && t.SoftLineBreak() {
_ = w.WriteByte('\n')
}
} else { } else {
r.renderAttribute(w, source, c) r.renderTexts(w, source, c)
} }
} }
} }

View file

@ -20,8 +20,19 @@ type Segment struct {
// Padding is a padding length of the segment. // Padding is a padding length of the segment.
Padding int Padding int
// EOB is true if the segment is end of the block. // ForceNewline is true if the segment should be ended with a newline.
EOB bool // Some elements(i.e. CodeBlock, FencedCodeBlock) does not trim trailing
// newlines. Spec defines that EOF is treated as a newline, so we need to
// add a newline to the end of the segment if it is not empty.
//
// i.e.:
//
// ```go
// const test = "test"
//
// This code does not close the code block and ends with EOF. In this case,
// we need to add a newline to the end of the last line like `const test = "test"\n`.
ForceNewline bool
} }
// NewSegment return a new Segment. // NewSegment return a new Segment.
@ -44,13 +55,15 @@ func NewSegmentPadding(start, stop, n int) Segment {
// Value returns a value of the segment. // Value returns a value of the segment.
func (t *Segment) Value(buffer []byte) []byte { func (t *Segment) Value(buffer []byte) []byte {
var result []byte
if t.Padding == 0 { if t.Padding == 0 {
return buffer[t.Start:t.Stop] result = buffer[t.Start:t.Stop]
} else {
result = make([]byte, 0, t.Padding+t.Stop-t.Start+1)
result = append(result, bytes.Repeat(space, t.Padding)...)
result = append(result, buffer[t.Start:t.Stop]...)
} }
result := make([]byte, 0, t.Padding+t.Stop-t.Start+1) if t.ForceNewline && len(result) > 0 && result[len(result)-1] != '\n' {
result = append(result, bytes.Repeat(space, t.Padding)...)
result = append(result, buffer[t.Start:t.Stop]...)
if t.EOB && len(result) > 0 && result[len(result)-1] != '\n' {
result = append(result, '\n') result = append(result, '\n')
} }
return result return result
@ -215,3 +228,12 @@ func (s *Segments) Unshift(v Segment) {
s.values = append(s.values[0:1], s.values[0:]...) s.values = append(s.values[0:1], s.values[0:]...)
s.values[0] = v s.values[0] = v
} }
// Value returns a string value of the collection.
func (s *Segments) Value(buffer []byte) []byte {
var result []byte
for _, v := range s.values {
result = append(result, v.Value(buffer)...)
}
return result
}