Add extension tests, Fix bugs in extensions

This commit is contained in:
yuin 2019-05-16 19:46:36 +09:00
parent 1963434c50
commit 2ddc99baff
29 changed files with 912 additions and 144 deletions

View file

@ -93,7 +93,6 @@ Parser and Renderer options
| `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. | | `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. |
| `parser.WithAutoHeadingID` | `-` | Enables auto heading ids. | | `parser.WithAutoHeadingID` | `-` | Enables auto heading ids. |
| `parser.WithAttribute` | `-` | Enables custom attributes. Currently only headings supports attributes. | | `parser.WithAttribute` | `-` | Enables custom attributes. Currently only headings supports attributes. |
| `parser.WithFilterTags` | `...string` | HTML tag names forbidden in HTML blocks and Raw HTMLs. |
### HTML Renderer options ### HTML Renderer options
@ -116,7 +115,8 @@ Parser and Renderer options
- [Gitmark Flavored Markdown: Task list items](https://github.github.com/gfm/#task-list-items-extension-) - [Gitmark Flavored Markdown: Task list items](https://github.github.com/gfm/#task-list-items-extension-)
- `extension.GFM` - `extension.GFM`
- This extension enables Table, Strikethrough, Linkify and TaskList. - This extension enables Table, Strikethrough, Linkify and TaskList.
In addition, this extension sets some tags to `parser.FilterTags` . - This extension does not filter tags defined in [6.11Disallowed Raw HTML (extension)](https://github.github.com/gfm/#disallowed-raw-html-extension-).
If you need to filter HTML tags, see [Security](#security)
- `extension.DefinitionList` - `extension.DefinitionList`
- [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list) - [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list)
- `extension.Footnote` - `extension.Footnote`

27
_test/options.txt Normal file
View file

@ -0,0 +1,27 @@
1
//- - - - - - - - -//
## Title 0
## Title1 # {#id_1 .class-1}
## Title2 {#id_2}
## Title3 ## {#id_3 .class-3}
## Title4 ## {attr3=value3}
## Title5 ## {#id_5 attr5=value5}
## Title6 ## {#id_6 .class6 attr6=value6}
## Title7 ## {#id_7 attr7="value \"7"}
//- - - - - - - - -//
<h2 id="title-0">Title 0</h2>
<h2 id="id_1" class="class-1">Title1</h2>
<h2 id="id_2">Title2</h2>
<h2 id="id_3" class="class-3">Title3</h2>
<h2 attr3="value3" id="title4">Title4</h2>
<h2 id="id_5" attr5="value5">Title5</h2>
<h2 id="id_6" class="class6" attr6="value6">Title6</h2>
<h2 id="id_7" attr7="value &quot;7">Title7</h2>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -362,10 +362,13 @@ const (
// An AutoLink struct represents an autolink of the Markdown text. // An AutoLink struct represents an autolink of the Markdown text.
type AutoLink struct { type AutoLink struct {
BaseInline BaseInline
// Value is a link text of this node.
Value *Text
// Type is a type of this autolink. // Type is a type of this autolink.
AutoLinkType AutoLinkType AutoLinkType AutoLinkType
// Protocol specified a protocol of the link.
Protocol []byte
value *Text
} }
// Inline implements Inline.Inline. // Inline implements Inline.Inline.
@ -373,7 +376,7 @@ func (n *AutoLink) Inline() {}
// Dump implenets Node.Dump // Dump implenets Node.Dump
func (n *AutoLink) Dump(source []byte, level int) { func (n *AutoLink) Dump(source []byte, level int) {
segment := n.Value.Segment segment := n.value.Segment
m := map[string]string{ m := map[string]string{
"Value": string(segment.Value(source)), "Value": string(segment.Value(source)),
} }
@ -388,11 +391,29 @@ func (n *AutoLink) Kind() NodeKind {
return KindAutoLink return KindAutoLink
} }
// URL returns an url of this node.
func (n *AutoLink) URL(source []byte) []byte {
if n.Protocol != nil {
s := n.value.Segment
ret := make([]byte, 0, len(n.Protocol)+s.Len()+3)
ret = append(ret, n.Protocol...)
ret = append(ret, ':', '/', '/')
ret = append(ret, n.value.Text(source)...)
return ret
}
return n.value.Text(source)
}
// Label returns a label of this node.
func (n *AutoLink) Label(source []byte) []byte {
return n.value.Text(source)
}
// NewAutoLink returns a new AutoLink node. // NewAutoLink returns a new AutoLink node.
func NewAutoLink(typ AutoLinkType, value *Text) *AutoLink { func NewAutoLink(typ AutoLinkType, value *Text) *AutoLink {
return &AutoLink{ return &AutoLink{
BaseInline: BaseInline{}, BaseInline: BaseInline{},
Value: value, value: value,
AutoLinkType: typ, AutoLinkType: typ,
} }
} }

View file

@ -1,7 +1,6 @@
package goldmark package goldmark
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"testing" "testing"
@ -27,30 +26,17 @@ func TestSpec(t *testing.T) {
if err := json.Unmarshal(bs, &testCases); err != nil { if err := json.Unmarshal(bs, &testCases); err != nil {
panic(err) panic(err)
} }
cases := []MarkdownTestCase{}
for _, c := range testCases {
cases = append(cases, MarkdownTestCase{
No: c.Example,
Markdown: c.Markdown,
Expected: c.HTML,
})
}
markdown := New(WithRendererOptions( markdown := New(WithRendererOptions(
html.WithXHTML(), html.WithXHTML(),
html.WithUnsafe(), html.WithUnsafe(),
)) ))
for _, testCase := range testCases { DoTestCases(markdown, cases, t)
var out bytes.Buffer
if err := markdown.Convert([]byte(testCase.Markdown), &out); err != nil {
panic(err)
}
if !bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(testCase.HTML))) {
format := `============= case %d ================
Markdown:
-----------
%s
Expected:
----------
%s
Actual
---------
%s
`
t.Errorf(format, testCase.Example, testCase.Markdown, testCase.HTML, out.Bytes())
}
}
} }

View file

@ -0,0 +1,143 @@
1
//- - - - - - - - -//
Apple
: Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.
Orange
: The fruit of an evergreen tree of the genus Citrus.
//- - - - - - - - -//
<dl>
<dt>Apple</dt>
<dd>Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.</dd>
<dt>Orange</dt>
<dd>The fruit of an evergreen tree of the genus Citrus.</dd>
</dl>
//= = = = = = = = = = = = = = = = = = = = = = = =//
2
//- - - - - - - - -//
Apple
: Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.
: An American computer company.
Orange
: The fruit of an evergreen tree of the genus Citrus.
//- - - - - - - - -//
<dl>
<dt>Apple</dt>
<dd>Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.</dd>
<dd>An American computer company.</dd>
<dt>Orange</dt>
<dd>The fruit of an evergreen tree of the genus Citrus.</dd>
</dl>
//= = = = = = = = = = = = = = = = = = = = = = = =//
3
//- - - - - - - - -//
Term 1
Term 2
: Definition a
Term 3
: Definition b
//- - - - - - - - -//
<dl>
<dt>Term 1</dt>
<dt>Term 2</dt>
<dd>Definition a</dd>
<dt>Term 3</dt>
<dd>Definition b</dd>
</dl>
//= = = = = = = = = = = = = = = = = = = = = = = =//
4
//- - - - - - - - -//
Apple
: Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.
Orange
: The fruit of an evergreen tree of the genus Citrus.
//- - - - - - - - -//
<dl>
<dt>Apple</dt>
<dd>
<p>Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.</p>
</dd>
<dt>Orange</dt>
<dd>
<p>The fruit of an evergreen tree of the genus Citrus.</p>
</dd>
</dl>
//= = = = = = = = = = = = = = = = = = = = = = = =//
5
//- - - - - - - - -//
Term 1
: This is a definition with two paragraphs. Lorem ipsum
dolor sit amet, consectetuer adipiscing elit. Aliquam
hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus.
: Second definition for term 1, also wrapped in a paragraph
because of the blank line preceding it.
Term 2
: This definition has a code block, a blockquote and a list.
code block.
> block quote
> on two lines.
1. first list item
2. second list item
//- - - - - - - - -//
<dl>
<dt>Term 1</dt>
<dd>
<p>This is a definition with two paragraphs. Lorem ipsum
dolor sit amet, consectetuer adipiscing elit. Aliquam
hendrerit mi posuere lectus.</p>
<p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus.</p>
</dd>
<dd>
<p>Second definition for term 1, also wrapped in a paragraph
because of the blank line preceding it.</p>
</dd>
<dt>Term 2</dt>
<dd>
<p>This definition has a code block, a blockquote and a list.</p>
<pre><code>code block.
</code></pre>
<blockquote>
<p>block quote
on two lines.</p>
</blockquote>
<ol>
<li>first list item</li>
<li>second list item</li>
</ol>
</dd>
</dl>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -0,0 +1,22 @@
1
//- - - - - - - - -//
That's some text with a footnote.[^1]
[^1]: And that's the footnote.
That's the second paragraph.
//- - - - - - - - -//
<p>That's some text with a footnote.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>And that's the footnote.</p>
<p>That's the second paragraph.</p>
</li>
</ol>
<section>
//= = = = = = = = = = = = = = = = = = = = = = = =//

114
extension/_test/linkify.txt Normal file
View file

@ -0,0 +1,114 @@
1
//- - - - - - - - -//
www.commonmark.org
//- - - - - - - - -//
<p><a href="http://www.commonmark.org">www.commonmark.org</a></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
2
//- - - - - - - - -//
Visit www.commonmark.org/help for more information.
//- - - - - - - - -//
<p>Visit <a href="http://www.commonmark.org/help">www.commonmark.org/help</a> for more information.</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
3
//- - - - - - - - -//
www.google.com/search?q=Markup+(business)
www.google.com/search?q=Markup+(business)))
(www.google.com/search?q=Markup+(business))
(www.google.com/search?q=Markup+(business)
//- - - - - - - - -//
<p><a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a></p>
<p><a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a>))</p>
<p>(<a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a>)</p>
<p>(<a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
4
//- - - - - - - - -//
www.google.com/search?q=(business))+ok
//- - - - - - - - -//
<p><a href="http://www.google.com/search?q=(business))+ok">www.google.com/search?q=(business))+ok</a></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
5
//- - - - - - - - -//
www.google.com/search?q=commonmark&hl=en
www.google.com/search?q=commonmark&hl;
//- - - - - - - - -//
<p><a href="http://www.google.com/search?q=commonmark&amp;hl=en">www.google.com/search?q=commonmark&amp;hl=en</a></p>
<p><a href="http://www.google.com/search?q=commonmark">www.google.com/search?q=commonmark</a>&amp;hl;</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
6
//- - - - - - - - -//
www.commonmark.org/he<lp
//- - - - - - - - -//
<p><a href="http://www.commonmark.org/he">www.commonmark.org/he</a>&lt;lp</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
7
//- - - - - - - - -//
http://commonmark.org
(Visit https://encrypted.google.com/search?q=Markup+(business))
Anonymous FTP is available at ftp://foo.bar.baz.
//- - - - - - - - -//
<p><a href="http://commonmark.org">http://commonmark.org</a></p>
<p>(Visit <a href="https://encrypted.google.com/search?q=Markup+(business)">https://encrypted.google.com/search?q=Markup+(business)</a>)</p>
<p>Anonymous FTP is available at <a href="ftp://foo.bar.baz">ftp://foo.bar.baz</a>.</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
8
//- - - - - - - - -//
foo@bar.baz
//- - - - - - - - -//
<p><a href="mailto:foo@bar.baz">foo@bar.baz</a></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
9
//- - - - - - - - -//
hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.
//- - - - - - - - -//
<p>hello@mail+xyz.example isn't valid, but <a href="mailto:hello+xyz@mail.example">hello+xyz@mail.example</a> is.</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
10
//- - - - - - - - -//
a.b-c_d@a.b
a.b-c_d@a.b.
a.b-c_d@a.b-
a.b-c_d@a.b_
//- - - - - - - - -//
<p><a href="mailto:a.b-c_d@a.b">a.b-c_d@a.b</a></p>
<p><a href="mailto:a.b-c_d@a.b">a.b-c_d@a.b</a>.</p>
<p>a.b-c_d@a.b-</p>
<p>a.b-c_d@a.b_</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -0,0 +1,18 @@
1
//- - - - - - - - -//
~~Hi~~ Hello, world!
//- - - - - - - - -//
<p><del>Hi</del> Hello, world!</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
2
//- - - - - - - - -//
This ~~has a
new paragraph~~.
//- - - - - - - - -//
<p>This ~~has a</p>
<p>new paragraph~~.</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

190
extension/_test/table.txt Normal file
View file

@ -0,0 +1,190 @@
1
//- - - - - - - - -//
| foo | bar |
| --- | --- |
| baz | bim |
//- - - - - - - - -//
<table>
<thead>
<tr>
<th>foo</th>
<th>bar</th>
</tr>
</thead>
<tbody>
<tr>
<td>baz</td>
<td>bim</td>
</tr>
</tbody>
</table>
//= = = = = = = = = = = = = = = = = = = = = = = =//
2
//- - - - - - - - -//
| abc | defghi |
:-: | -----------:
bar | baz
//- - - - - - - - -//
<table>
<thead>
<tr>
<th align="center">abc</th>
<th align="right">defghi</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">bar</td>
<td align="right">baz</td>
</tr>
</tbody>
</table>
//= = = = = = = = = = = = = = = = = = = = = = = =//
3
//- - - - - - - - -//
| f\|oo |
| ------ |
| b `\|` az |
| b **\|** im |
//- - - - - - - - -//
<table>
<thead>
<tr>
<th>f|oo</th>
</tr>
</thead>
<tbody>
<tr>
<td>b <code>\|</code> az</td>
</tr>
<tr>
<td>b <strong>|</strong> im</td>
</tr>
</tbody>
</table>
//= = = = = = = = = = = = = = = = = = = = = = = =//
4
//- - - - - - - - -//
| abc | def |
| --- | --- |
| bar | baz |
> bar
//- - - - - - - - -//
<table>
<thead>
<tr>
<th>abc</th>
<th>def</th>
</tr>
</thead>
<tbody>
<tr>
<td>bar</td>
<td>baz</td>
</tr>
</tbody>
</table>
<blockquote>
<p>bar</p>
</blockquote>
//= = = = = = = = = = = = = = = = = = = = = = = =//
5
//- - - - - - - - -//
| abc | def |
| --- | --- |
| bar | baz |
bar
bar
//- - - - - - - - -//
<table>
<thead>
<tr>
<th>abc</th>
<th>def</th>
</tr>
</thead>
<tbody>
<tr>
<td>bar</td>
<td>baz</td>
</tr>
<tr>
<td>bar</td>
<td></td>
</tr>
</tbody>
</table>
<p>bar</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
6
//- - - - - - - - -//
| abc | def |
| --- |
| bar |
//- - - - - - - - -//
<p>| abc | def |
| --- |
| bar |</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
7
//- - - - - - - - -//
| abc | def |
| --- | --- |
| bar |
| bar | baz | boo |
//- - - - - - - - -//
<table>
<thead>
<tr>
<th>abc</th>
<th>def</th>
</tr>
</thead>
<tbody>
<tr>
<td>bar</td>
<td></td>
</tr>
<tr>
<td>bar</td>
<td>baz</td>
</tr>
</tbody>
</table>
//= = = = = = = = = = = = = = = = = = = = = = = =//
8
//- - - - - - - - -//
| abc | def |
| --- | --- |
//- - - - - - - - -//
<table>
<thead>
<tr>
<th>abc</th>
<th>def</th>
</tr>
</thead>
</table>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -0,0 +1,30 @@
1
//- - - - - - - - -//
- [ ] foo
- [x] bar
//- - - - - - - - -//
<ul>
<li><input disabled="" type="checkbox">foo</li>
<li><input checked="" disabled="" type="checkbox">bar</li>
</ul>
//= = = = = = = = = = = = = = = = = = = = = = = =//
2
//- - - - - - - - -//
- [x] foo
- [ ] bar
- [x] baz
- [ ] bim
//- - - - - - - - -//
<ul>
<li><input checked="" disabled="" type="checkbox">foo
<ul>
<li><input disabled="" type="checkbox">bar</li>
<li><input checked="" disabled="" type="checkbox">baz</li>
</ul>
</li>
<li><input disabled="" type="checkbox">bim</li>
</ul>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -0,0 +1,20 @@
1
//- - - - - - - - -//
This should 'be' replaced
//- - - - - - - - -//
<p>This should &lsquo;be&rsquo; replaced</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
2
//- - - - - - - - -//
This should "be" replaced
//- - - - - - - - -//
<p>This should &ldquo;be&rdquo; replaced</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
3
//- - - - - - - - -//
**--** *---* a...<< b>>
//- - - - - - - - -//
<p><strong>&ndash;</strong> <em>&mdash;</em> a&hellip;&laquo; b&raquo;</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

View file

@ -102,7 +102,8 @@ func NewTableRow(alignments []Alignment) *TableRow {
// A TableHeader struct represents a table header of Markdown(GFM) text. // A TableHeader struct represents a table header of Markdown(GFM) text.
type TableHeader struct { type TableHeader struct {
*TableRow gast.BaseBlock
Alignments []Alignment
} }
// KindTableHeader is a NodeKind of the TableHeader node. // KindTableHeader is a NodeKind of the TableHeader node.
@ -113,9 +114,20 @@ func (n *TableHeader) Kind() gast.NodeKind {
return KindTableHeader return KindTableHeader
} }
// Dump implements Node.Dump.
func (n *TableHeader) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// NewTableHeader returns a new TableHeader node. // NewTableHeader returns a new TableHeader node.
func NewTableHeader(row *TableRow) *TableHeader { func NewTableHeader(row *TableRow) *TableHeader {
return &TableHeader{row} n := &TableHeader{}
for c := row.FirstChild(); c != nil; {
next := c.NextSibling()
n.AppendChild(n, c)
c = next
}
return n
} }
// A TableCell struct represents a table cell of a Markdown(GFM) text. // A TableCell struct represents a table cell of a Markdown(GFM) text.

View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestDefinitionList(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
DefinitionList,
),
)
goldmark.DoTestCaseFile(markdown, "_test/definition_list.txt", t)
}

View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestFootnote(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
Footnote,
),
)
goldmark.DoTestCaseFile(markdown, "_test/footnote.txt", t)
}

View file

@ -2,7 +2,6 @@ package extension
import ( import (
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
) )
type gfm struct { type gfm struct {
@ -11,20 +10,7 @@ type gfm struct {
// GFM is an extension that provides Github Flavored markdown functionalities. // GFM is an extension that provides Github Flavored markdown functionalities.
var GFM = &gfm{} var GFM = &gfm{}
var filterTags = []string{
"title",
"textarea",
"style",
"xmp",
"iframe",
"noembed",
"noframes",
"script",
"plaintext",
}
func (e *gfm) Extend(m goldmark.Markdown) { func (e *gfm) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithFilterTags(filterTags...))
Linkify.Extend(m) Linkify.Extend(m)
Table.Extend(m) Table.Extend(m)
Strikethrough.Extend(m) Strikethrough.Extend(m)

View file

@ -48,13 +48,14 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
} }
var m []int var m []int
typ := ast.AutoLinkType(ast.AutoLinkEmail) var protocol []byte
typ = ast.AutoLinkURL var typ ast.AutoLinkType = ast.AutoLinkURL
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) { if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
m = urlRegexp.FindSubmatchIndex(line) m = urlRegexp.FindSubmatchIndex(line)
} }
if m == nil && bytes.HasPrefix(line, domainWWW) { if m == nil && bytes.HasPrefix(line, domainWWW) {
m = wwwURLRegxp.FindSubmatchIndex(line) m = wwwURLRegxp.FindSubmatchIndex(line)
protocol = []byte("http")
} }
if m != nil { if m != nil {
lastChar := line[m[1]-1] lastChar := line[m[1]-1]
@ -70,7 +71,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
} }
} }
if closing > 0 { if closing > 0 {
m[1]-- m[1] -= closing
} }
} else if lastChar == ';' { } else if lastChar == ';' {
i := m[1] - 2 i := m[1] - 2
@ -119,7 +120,9 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
consumes += m[1] consumes += m[1]
block.Advance(consumes) block.Advance(consumes)
n := ast.NewTextSegment(text.NewSegment(start, start+m[1])) n := ast.NewTextSegment(text.NewSegment(start, start+m[1]))
return ast.NewAutoLink(typ, n) link := ast.NewAutoLink(typ, n)
link.Protocol = protocol
return link
} }
func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) { func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {

19
extension/linkify_test.go Normal file
View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestLinkify(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
Linkify,
),
)
goldmark.DoTestCaseFile(markdown, "_test/linkify.txt", t)
}

View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestStrikethrough(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
Strikethrough,
),
)
goldmark.DoTestCaseFile(markdown, "_test/strikethrough.txt", t)
}

View file

@ -41,7 +41,7 @@ func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.
if alignments == nil { if alignments == nil {
return return
} }
header := b.parseRow(lines.At(0), alignments, reader) header := b.parseRow(lines.At(0), alignments, true, reader)
if header == nil || len(alignments) != header.ChildCount() { if header == nil || len(alignments) != header.ChildCount() {
return return
} }
@ -50,15 +50,14 @@ func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.
table.AppendChild(table, ast.NewTableHeader(header)) table.AppendChild(table, ast.NewTableHeader(header))
if lines.Len() > 2 { if lines.Len() > 2 {
for i := 2; i < lines.Len(); i++ { for i := 2; i < lines.Len(); i++ {
table.AppendChild(table, b.parseRow(lines.At(i), alignments, reader)) table.AppendChild(table, b.parseRow(lines.At(i), alignments, false, reader))
} }
} }
node.Parent().InsertBefore(node.Parent(), node, table) node.Parent().InsertBefore(node.Parent(), node, table)
node.Parent().RemoveChild(node.Parent(), node) node.Parent().RemoveChild(node.Parent(), node)
return
} }
func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, reader text.Reader) *ast.TableRow { func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, isHeader bool, reader text.Reader) *ast.TableRow {
source := reader.Source() source := reader.Source()
line := segment.Value(source) line := segment.Value(source)
pos := 0 pos := 0
@ -72,7 +71,16 @@ func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []
if len(line) > 0 && line[limit-1] == '|' { if len(line) > 0 && line[limit-1] == '|' {
limit-- limit--
} }
for i := 0; pos < limit; i++ { i := 0
for ; pos < limit; i++ {
alignment := ast.AlignNone
if i >= len(alignments) {
if !isHeader {
return row
}
} else {
alignment = alignments[i]
}
closure := util.FindClosure(line[pos:], byte(0), '|', true, false) closure := util.FindClosure(line[pos:], byte(0), '|', true, false)
if closure < 0 { if closure < 0 {
closure = len(line[pos:]) closure = len(line[pos:])
@ -82,10 +90,13 @@ func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []
segment = segment.TrimLeftSpace(source) segment = segment.TrimLeftSpace(source)
segment = segment.TrimRightSpace(source) segment = segment.TrimRightSpace(source)
node.Lines().Append(segment) node.Lines().Append(segment)
node.Alignment = alignments[i] node.Alignment = alignment
row.AppendChild(row, node) row.AppendChild(row, node)
pos += closure + 1 pos += closure + 1
} }
for ; i < len(alignments); i++ {
row.AppendChild(row, ast.NewTableCell())
}
return row return row
} }
@ -175,9 +186,6 @@ func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n
if n.NextSibling() != nil { if n.NextSibling() != nil {
w.WriteString("<tbody>\n") w.WriteString("<tbody>\n")
} }
if n.Parent().LastChild() == n {
w.WriteString("</tbody>\n")
}
} }
return gast.WalkContinue, nil return gast.WalkContinue, nil
} }
@ -197,7 +205,7 @@ func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n ga
func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
n := node.(*ast.TableCell) n := node.(*ast.TableCell)
tag := "td" tag := "td"
if n.Parent().Parent().FirstChild() == n.Parent() { if n.Parent().Kind() == ast.KindTableHeader {
tag = "th" tag = "th"
} }
if entering { if entering {

19
extension/table_test.go Normal file
View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestTable(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
Table,
),
)
goldmark.DoTestCaseFile(markdown, "_test/table.txt", t)
}

View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestTaskList(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
TaskList,
),
)
goldmark.DoTestCaseFile(markdown, "_test/tasklist.txt", t)
}

View file

@ -0,0 +1,19 @@
package extension
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
"testing"
)
func TestTypographer(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
Typographer,
),
)
goldmark.DoTestCaseFile(markdown, "_test/typographer.txt", t)
}

16
options_test.go Normal file
View file

@ -0,0 +1,16 @@
package goldmark
import (
"github.com/yuin/goldmark/parser"
"testing"
)
func TestDefinitionList(t *testing.T) {
markdown := New(
WithParserOptions(
parser.WithAttribute(),
parser.WithAutoHeadingID(),
),
)
DoTestCaseFile(markdown, "_test/options.txt", t)
}

View file

@ -125,7 +125,7 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context)
break break
} }
node.SetAttribute(line[as+ai[0]:as+ai[1]], node.SetAttribute(line[as+ai[0]:as+ai[1]],
line[as+ai[2]:as+ai[3]]) util.UnescapePunctuations(line[as+ai[2]:as+ai[3]]))
as += ai[3] + skip as += ai[3] + skip
} }
for ; as < stop && util.IsSpace(line[as]); as++ { for ; as < stop && util.IsSpace(line[as]); as++ {
@ -208,7 +208,8 @@ func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) {
indicies := util.FindAttributeIndiciesReverse(line, true) indicies := util.FindAttributeIndiciesReverse(line, true)
if indicies != nil { if indicies != nil {
for _, index := range indicies { for _, index := range indicies {
node.SetAttribute(line[index[0]:index[1]], line[index[2]:index[3]]) node.SetAttribute(line[index[0]:index[1]],
util.UnescapePunctuations(line[index[2]:index[3]]))
} }
lastLine.Stop = lastLine.Start + indicies[0][0] - 1 lastLine.Stop = lastLine.Start + indicies[0][0] - 1
lastLine.TrimRightSpace(reader.Source()) lastLine.TrimRightSpace(reader.Source())

View file

@ -9,48 +9,6 @@ import (
"strings" "strings"
) )
// An HTMLConfig struct is a data structure that holds configuration of the renderers related to raw htmls.
type HTMLConfig struct {
FilterTags map[string]bool
}
// SetOption implements SetOptioner.
func (b *HTMLConfig) SetOption(name OptionName, value interface{}) {
switch name {
case optFilterTags:
b.FilterTags = value.(map[string]bool)
}
}
// A HTMLOption interface sets options for the raw HTML parsers.
type HTMLOption interface {
Option
SetHTMLOption(*HTMLConfig)
}
const optFilterTags OptionName = "FilterTags"
type withFilterTags struct {
value map[string]bool
}
func (o *withFilterTags) SetParserOption(c *Config) {
c.Options[optFilterTags] = o.value
}
func (o *withFilterTags) SetHTMLOption(p *HTMLConfig) {
p.FilterTags = o.value
}
// WithFilterTags is a functional otpion that specify forbidden tag names.
func WithFilterTags(names ...string) HTMLOption {
m := map[string]bool{}
for _, name := range names {
m[name] = true
}
return &withFilterTags{m}
}
var allowedBlockTags = map[string]bool{ var allowedBlockTags = map[string]bool{
"address": true, "address": true,
"article": true, "article": true,
@ -137,17 +95,14 @@ var htmlBlockType6Regexp = regexp.MustCompile(`^[ ]{0,3}</?([a-zA-Z0-9]+)(?:\s.*
var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/)?([a-zA-Z0-9]+)(` + attributePattern + `*)(:?>|/>)\s*\n?$`) var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/)?([a-zA-Z0-9]+)(` + attributePattern + `*)(:?>|/>)\s*\n?$`)
type htmlBlockParser struct { type htmlBlockParser struct {
HTMLConfig
} }
var defaultHtmlBlockParser = &htmlBlockParser{}
// NewHTMLBlockParser return a new BlockParser that can parse html // NewHTMLBlockParser return a new BlockParser that can parse html
// blocks. // blocks.
func NewHTMLBlockParser(opts ...HTMLOption) BlockParser { func NewHTMLBlockParser() BlockParser {
p := &htmlBlockParser{} return defaultHtmlBlockParser
for _, o := range opts {
o.SetHTMLOption(&p.HTMLConfig)
}
return p
} }
func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
@ -191,11 +146,6 @@ func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context)
} }
} }
if node != nil { if node != nil {
if b.FilterTags != nil {
if _, ok := b.FilterTags[tagName]; ok {
return nil, NoChildren
}
}
reader.Advance(segment.Len() - 1) reader.Advance(segment.Len() - 1)
node.Lines().Append(segment) node.Lines().Append(segment)
return node, NoChildren return node, NoChildren

View file

@ -84,6 +84,9 @@ func (s *ids) Generate(value, prefix []byte) []byte {
continue continue
} }
if util.IsAlphaNumeric(v) { if util.IsAlphaNumeric(v) {
if 'A' <= v && v <= 'Z' {
v += 'a' - 'A'
}
result = append(result, v) result = append(result, v)
} else if util.IsSpace(v) { } else if util.IsSpace(v) {
result = append(result, '-') result = append(result, '-')

View file

@ -9,17 +9,14 @@ import (
) )
type rawHTMLParser struct { type rawHTMLParser struct {
HTMLConfig
} }
var defaultRawHTMLParser = &rawHTMLParser{}
// NewRawHTMLParser return a new InlineParser that can parse // NewRawHTMLParser return a new InlineParser that can parse
// inline htmls // inline htmls
func NewRawHTMLParser(opts ...HTMLOption) InlineParser { func NewRawHTMLParser() InlineParser {
p := &rawHTMLParser{} return defaultRawHTMLParser
for _, o := range opts {
o.SetHTMLOption(&p.HTMLConfig)
}
return p
} }
func (s *rawHTMLParser) Trigger() []byte { func (s *rawHTMLParser) Trigger() []byte {
@ -74,22 +71,7 @@ var dummyMatch = [][]byte{}
func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node { func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node {
sline, ssegment := block.Position() sline, ssegment := block.Position()
var m [][]byte
if s.FilterTags != nil {
m = block.FindSubMatch(reg)
} else {
if block.Match(reg) { if block.Match(reg) {
m = dummyMatch
}
}
if m != nil {
if s.FilterTags != nil && len(m) > 1 {
tagName := string(m[1])
if _, ok := s.FilterTags[tagName]; ok {
return nil
}
}
node := ast.NewRawHTML() node := ast.NewRawHTML()
eline, esegment := block.Position() eline, esegment := block.Position()
block.SetPosition(sline, ssegment) block.SetPosition(sline, ssegment)

View file

@ -352,14 +352,14 @@ func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
w.WriteString(`<a href="`) w.WriteString(`<a href="`)
segment := n.Value.Segment url := n.URL(source)
value := segment.Value(source) label := n.Label(source)
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(value), []byte("mailto:")) { if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
w.WriteString("mailto:") w.WriteString("mailto:")
} }
w.Write(util.EscapeHTML(util.URLEscape(value, false))) w.Write(util.EscapeHTML(util.URLEscape(url, false)))
w.WriteString(`">`) w.WriteString(`">`)
w.Write(util.EscapeHTML(value)) w.Write(util.EscapeHTML(label))
w.WriteString(`</a>`) w.WriteString(`</a>`)
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
@ -493,7 +493,7 @@ func (r *Renderer) RenderAttributes(w util.BufWriter, node ast.Node) {
w.WriteString(" ") w.WriteString(" ")
w.Write(attr.Name) w.Write(attr.Name)
w.WriteString(`="`) w.WriteString(`="`)
w.Write(attr.Value) w.Write(util.EscapeHTML(attr.Value))
w.WriteByte('"') w.WriteByte('"')
} }
} }

103
testutil.go Normal file
View file

@ -0,0 +1,103 @@
package goldmark
import (
"bufio"
"bytes"
"fmt"
"github.com/yuin/goldmark/util"
"os"
"strconv"
"strings"
testing "testing"
)
type MarkdownTestCase struct {
No int
Markdown string
Expected string
}
const attributeSeparator = "//- - - - - - - - -//"
const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
func DoTestCaseFile(m Markdown, filename string, t *testing.T) {
fp, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fp.Close()
scanner := bufio.NewScanner(fp)
c := MarkdownTestCase{
No: -1,
Markdown: "",
Expected: "",
}
cases := []MarkdownTestCase{}
line := 0
for scanner.Scan() {
line++
if util.IsBlank([]byte(scanner.Text())) {
continue
}
c.No, err = strconv.Atoi(scanner.Text())
if err != nil {
panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
}
if !scanner.Scan() {
panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
}
line++
if scanner.Text() != attributeSeparator {
panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
}
buf := []string{}
for scanner.Scan() {
line++
text := scanner.Text()
if text == attributeSeparator {
break
}
buf = append(buf, text)
}
c.Markdown = strings.Join(buf, "\n")
buf = []string{}
for scanner.Scan() {
line++
text := scanner.Text()
if text == caseSeparator {
break
}
buf = append(buf, text)
}
c.Expected = strings.Join(buf, "\n")
cases = append(cases, c)
}
DoTestCases(m, cases, t)
}
func DoTestCases(m Markdown, cases []MarkdownTestCase, t *testing.T) {
for _, testCase := range cases {
var out bytes.Buffer
if err := m.Convert([]byte(testCase.Markdown), &out); err != nil {
panic(err)
}
if !bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(testCase.Expected))) {
format := `============= case %d ================
Markdown:
-----------
%s
Expected:
----------
%s
Actual
---------
%s
`
t.Errorf(format, testCase.No, testCase.Markdown, testCase.Expected, out.Bytes())
}
}
}