Merge commit 'ad273dbe5dba7bd0e901270464e25fc1f030a5b5' as 'backend/goldmark'
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
6: Definition lists indented with tabs
|
||||
//- - - - - - - - -//
|
||||
0
|
||||
: ```
|
||||
0
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>0</dt>
|
||||
<dd><pre><code> 0
|
||||
</code></pre>
|
||||
</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
@@ -0,0 +1,91 @@
|
||||
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>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>And that's the footnote.</p>
|
||||
<p>That's the second paragraph. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
[^000]:0 [^]:
|
||||
//- - - - - - - - -//
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
This[^3] is[^1] text with footnotes[^2].
|
||||
|
||||
[^1]: Footnote one
|
||||
[^2]: Footnote two
|
||||
[^3]: Footnote three
|
||||
//- - - - - - - - -//
|
||||
<p>This<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> is<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> text with footnotes<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>Footnote three <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:2">
|
||||
<p>Footnote one <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:3">
|
||||
<p>Footnote two <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
test![^1]
|
||||
|
||||
[^1]: footnote
|
||||
//- - - - - - - - -//
|
||||
<p>test!<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>footnote <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
6: Multiple references to the same footnotes should have different ids
|
||||
//- - - - - - - - -//
|
||||
something[^fn:1]
|
||||
|
||||
something[^fn:1]
|
||||
|
||||
something[^fn:1]
|
||||
|
||||
[^fn:1]: footnote text
|
||||
//- - - - - - - - -//
|
||||
<p>something<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<p>something<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<p>something<sup id="fnref2:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>footnote text <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a> <a href="#fnref1:1" class="footnote-backref" role="doc-backlink">↩︎</a> <a href="#fnref2:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
@@ -0,0 +1,193 @@
|
||||
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&hl=en">www.google.com/search?q=commonmark&hl=en</a></p>
|
||||
<p><a href="http://www.google.com/search?q=commonmark">www.google.com/search?q=commonmark</a>&hl;</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
6
|
||||
//- - - - - - - - -//
|
||||
www.commonmark.org/he<lp
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://www.commonmark.org/he">www.commonmark.org/he</a><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>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
11
|
||||
//- - - - - - - - -//
|
||||
https://github.com#sun,mon
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://github.com#sun,mon">https://github.com#sun,mon</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
12
|
||||
//- - - - - - - - -//
|
||||
https://github.com/sunday's
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://github.com/sunday's">https://github.com/sunday's</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
13
|
||||
//- - - - - - - - -//
|
||||
https://github.com?q=stars:>1
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://github.com?q=stars:%3E1">https://github.com?q=stars:>1</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
14
|
||||
//- - - - - - - - -//
|
||||
[https://google.com](https://google.com)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://google.com">https://google.com</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
15
|
||||
//- - - - - - - - -//
|
||||
This is a `git@github.com:vim/vim`
|
||||
//- - - - - - - - -//
|
||||
<p>This is a <code>git@github.com:vim/vim</code></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
16
|
||||
//- - - - - - - - -//
|
||||
https://nic.college
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://nic.college">https://nic.college</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
17
|
||||
//- - - - - - - - -//
|
||||
http://server.intranet.acme.com:1313
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://server.intranet.acme.com:1313">http://server.intranet.acme.com:1313</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
18
|
||||
//- - - - - - - - -//
|
||||
https://g.page/foo
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://g.page/foo">https://g.page/foo</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
19: Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink
|
||||
//- - - - - - - - -//
|
||||
__http://test.com/~/a__
|
||||
__http://test.com/~/__
|
||||
__http://test.com/~__
|
||||
__http://test.com/a/~__
|
||||
//- - - - - - - - -//
|
||||
<p><strong><a href="http://test.com/~/a">http://test.com/~/a</a></strong>
|
||||
<strong><a href="http://test.com/~/">http://test.com/~/</a></strong>
|
||||
<strong><a href="http://test.com/">http://test.com/</a>~</strong>
|
||||
<strong><a href="http://test.com/a/">http://test.com/a/</a>~</strong></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
@@ -0,0 +1,39 @@
|
||||
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>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
~Hi~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<p><del>Hi</del> Hello, world!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4: Three or more tildes do not create a strikethrough
|
||||
//- - - - - - - - -//
|
||||
This will ~~~not~~~ strike.
|
||||
//- - - - - - - - -//
|
||||
<p>This will ~~~not~~~ strike.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
5: Leading three or more tildes do not create a strikethrough, create a code block
|
||||
//- - - - - - - - -//
|
||||
~~~Hi~~~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<pre><code class="language-Hi~~~"></code></pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
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>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
9
|
||||
//- - - - - - - - -//
|
||||
Foo|Bar
|
||||
---|---
|
||||
`Yoyo`|Dyne
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Foo</th>
|
||||
<th>Bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Yoyo</code></td>
|
||||
<td>Dyne</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
10
|
||||
//- - - - - - - - -//
|
||||
foo|bar
|
||||
---|---
|
||||
`\` | second column
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>foo</th>
|
||||
<th>bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>\</code></td>
|
||||
<td>second column</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
11: Tables can interrupt paragraph
|
||||
//- - - - - - - - -//
|
||||
**xxx**
|
||||
| hello | hi |
|
||||
| :----: | :----:|
|
||||
//- - - - - - - - -//
|
||||
<p><strong>xxx</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">hello</th>
|
||||
<th align="center">hi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
12: A delimiter can not start with more than 3 spaces
|
||||
//- - - - - - - - -//
|
||||
Foo
|
||||
---
|
||||
//- - - - - - - - -//
|
||||
<p>Foo
|
||||
---</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
13: A delimiter can not start with more than 3 spaces(w/ tabs)
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
- aaa
|
||||
|
||||
Foo
|
||||
\t\t---
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<p>aaa</p>
|
||||
<p>Foo
|
||||
---</p>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
14: Delimiter-like line inside a list item
|
||||
//- - - - - - - - -//
|
||||
- [Marketing](marketing/_index.md)
|
||||
--
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li><a href="marketing/_index.md">Marketing</a>
|
||||
--</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
- test[x]=[x]
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>test[x]=[x]</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
+ [x] [x]
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li><input checked="" disabled="" type="checkbox"> [x]</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
@@ -0,0 +1,143 @@
|
||||
1
|
||||
//- - - - - - - - -//
|
||||
This should 'be' replaced
|
||||
//- - - - - - - - -//
|
||||
<p>This should ‘be’ replaced</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
This should "be" replaced
|
||||
//- - - - - - - - -//
|
||||
<p>This should “be” replaced</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
**--** *---* a...<< b>>
|
||||
//- - - - - - - - -//
|
||||
<p><strong>–</strong> <em>—</em> a…« b»</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
Some say '90s, others say 90's, but I can't say which is best.
|
||||
//- - - - - - - - -//
|
||||
<p>Some say ’90s, others say 90’s, but I can’t say which is best.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
5: contractions
|
||||
//- - - - - - - - -//
|
||||
Alice's, I'm ,Don't, You'd
|
||||
|
||||
I've, I'll, You're
|
||||
|
||||
[Cat][]'s Pajamas
|
||||
|
||||
Yahoo!'s
|
||||
|
||||
[Cat]: http://example.com
|
||||
//- - - - - - - - -//
|
||||
<p>Alice’s, I’m ,Don’t, You’d</p>
|
||||
<p>I’ve, I’ll, You’re</p>
|
||||
<p><a href="http://example.com">Cat</a>’s Pajamas</p>
|
||||
<p>Yahoo!’s</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
6: "" after digits are an inch
|
||||
//- - - - - - - - -//
|
||||
My height is 5'6"".
|
||||
//- - - - - - - - -//
|
||||
<p>My height is 5'6"".</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
7: quote followed by ,.?! and spaces maybe a closer
|
||||
//- - - - - - - - -//
|
||||
reported "issue 1 (IE-only)", "issue 2", 'issue3 (FF-only)', 'issue4'
|
||||
//- - - - - - - - -//
|
||||
<p>reported “issue 1 (IE-only)”, “issue 2”, ‘issue3 (FF-only)’, ‘issue4’</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
8: handle inches in qoutes
|
||||
//- - - - - - - - -//
|
||||
"Monitor 21"" and "Monitor""
|
||||
//- - - - - - - - -//
|
||||
<p>“Monitor 21"” and “Monitor”"</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
9: Closing quotation marks within italics
|
||||
//- - - - - - - - -//
|
||||
*"At first, things were not clear."*
|
||||
//- - - - - - - - -//
|
||||
<p><em>“At first, things were not clear.”</em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
10: Closing quotation marks within boldfacing
|
||||
//- - - - - - - - -//
|
||||
**"At first, things were not clear."**
|
||||
//- - - - - - - - -//
|
||||
<p><strong>“At first, things were not clear.”</strong></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
11: Closing quotation marks within boldfacing and italics
|
||||
//- - - - - - - - -//
|
||||
***"At first, things were not clear."***
|
||||
//- - - - - - - - -//
|
||||
<p><em><strong>“At first, things were not clear.”</strong></em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
12: Closing quotation marks within boldfacing and italics
|
||||
//- - - - - - - - -//
|
||||
***"At first, things were not clear."***
|
||||
//- - - - - - - - -//
|
||||
<p><em><strong>“At first, things were not clear.”</strong></em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
13: Plural possessives
|
||||
//- - - - - - - - -//
|
||||
John's dog is named Sam. The Smiths' dog is named Rover.
|
||||
//- - - - - - - - -//
|
||||
<p>John’s dog is named Sam. The Smiths’ dog is named Rover.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
14: Links within quotation marks and parenthetical phrases
|
||||
//- - - - - - - - -//
|
||||
This is not difficult (see "[Introduction to Hugo Templating](https://gohugo.io/templates/introduction/)").
|
||||
//- - - - - - - - -//
|
||||
<p>This is not difficult (see “<a href="https://gohugo.io/templates/introduction/">Introduction to Hugo Templating</a>”).</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
15: Quotation marks within links
|
||||
//- - - - - - - - -//
|
||||
Apple's early Cairo font gave us ["moof" and the "dogcow."](https://www.macworld.com/article/2926184/we-miss-you-clarus-the-dogcow.html)
|
||||
//- - - - - - - - -//
|
||||
<p>Apple’s early Cairo font gave us <a href="https://www.macworld.com/article/2926184/we-miss-you-clarus-the-dogcow.html">“moof” and the “dogcow.”</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
16: Single closing quotation marks with slang/informalities
|
||||
//- - - - - - - - -//
|
||||
"I'm not doin' that," Bill said with emphasis.
|
||||
//- - - - - - - - -//
|
||||
<p>“I’m not doin’ that,” Bill said with emphasis.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
17: Closing single quotation marks in quotations-within-quotations
|
||||
//- - - - - - - - -//
|
||||
Janet said, "When everything is 'breaking news,' nothing is 'breaking news.'"
|
||||
//- - - - - - - - -//
|
||||
<p>Janet said, “When everything is ‘breaking news,’ nothing is ‘breaking news.’”</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
18: Opening single quotation marks for abbreviations
|
||||
//- - - - - - - - -//
|
||||
We're talking about the internet --- 'net for short. Let's rock 'n roll!
|
||||
//- - - - - - - - -//
|
||||
<p>We’re talking about the internet — ’net for short. Let’s rock ’n roll!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
19: Quotes in alt text
|
||||
//- - - - - - - - -//
|
||||

|
||||
//- - - - - - - - -//
|
||||
<p><img src="https://example.com/image.jpg" alt="Nice & day, isn’t it?"></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
@@ -0,0 +1,99 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// A DefinitionList struct represents a definition list of Markdown
|
||||
// (PHPMarkdownExtra) text.
|
||||
type DefinitionList struct {
|
||||
gast.BaseBlock
|
||||
Offset int
|
||||
TemporaryParagraph *gast.Paragraph
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *DefinitionList) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// Pos implements Node.Pos.
|
||||
func (n *DefinitionList) Pos() int {
|
||||
if n.FirstChild() != nil {
|
||||
return n.FirstChild().Pos()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// KindDefinitionList is a NodeKind of the DefinitionList node.
|
||||
var KindDefinitionList = gast.NewNodeKind("DefinitionList")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *DefinitionList) Kind() gast.NodeKind {
|
||||
return KindDefinitionList
|
||||
}
|
||||
|
||||
// NewDefinitionList returns a new DefinitionList node.
|
||||
func NewDefinitionList(offset int, para *gast.Paragraph) *DefinitionList {
|
||||
return &DefinitionList{
|
||||
Offset: offset,
|
||||
TemporaryParagraph: para,
|
||||
}
|
||||
}
|
||||
|
||||
// A DefinitionTerm struct represents a definition list term of Markdown
|
||||
// (PHPMarkdownExtra) text.
|
||||
type DefinitionTerm struct {
|
||||
gast.BaseBlock
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *DefinitionTerm) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// Pos implements Node.Pos.
|
||||
func (n *DefinitionTerm) Pos() int {
|
||||
if n.Lines().Len() == 0 {
|
||||
return -1
|
||||
}
|
||||
return n.Lines().At(0).Start
|
||||
}
|
||||
|
||||
// KindDefinitionTerm is a NodeKind of the DefinitionTerm node.
|
||||
var KindDefinitionTerm = gast.NewNodeKind("DefinitionTerm")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *DefinitionTerm) Kind() gast.NodeKind {
|
||||
return KindDefinitionTerm
|
||||
}
|
||||
|
||||
// NewDefinitionTerm returns a new DefinitionTerm node.
|
||||
func NewDefinitionTerm() *DefinitionTerm {
|
||||
return &DefinitionTerm{}
|
||||
}
|
||||
|
||||
// A DefinitionDescription struct represents a definition list description of Markdown
|
||||
// (PHPMarkdownExtra) text.
|
||||
type DefinitionDescription struct {
|
||||
gast.BaseBlock
|
||||
IsTight bool
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *DefinitionDescription) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindDefinitionDescription is a NodeKind of the DefinitionDescription node.
|
||||
var KindDefinitionDescription = gast.NewNodeKind("DefinitionDescription")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *DefinitionDescription) Kind() gast.NodeKind {
|
||||
return KindDefinitionDescription
|
||||
}
|
||||
|
||||
// NewDefinitionDescription returns a new DefinitionDescription node.
|
||||
func NewDefinitionDescription() *DefinitionDescription {
|
||||
return &DefinitionDescription{}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// A FootnoteLink struct represents a link to a footnote of Markdown
|
||||
// (PHP Markdown Extra) text.
|
||||
type FootnoteLink struct {
|
||||
gast.BaseInline
|
||||
Index int
|
||||
RefCount int
|
||||
RefIndex int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *FootnoteLink) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Index"] = fmt.Sprintf("%v", n.Index)
|
||||
m["RefCount"] = fmt.Sprintf("%v", n.RefCount)
|
||||
m["RefIndex"] = fmt.Sprintf("%v", n.RefIndex)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnoteLink is a NodeKind of the FootnoteLink node.
|
||||
var KindFootnoteLink = gast.NewNodeKind("FootnoteLink")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *FootnoteLink) Kind() gast.NodeKind {
|
||||
return KindFootnoteLink
|
||||
}
|
||||
|
||||
// NewFootnoteLink returns a new FootnoteLink node.
|
||||
func NewFootnoteLink(index int) *FootnoteLink {
|
||||
return &FootnoteLink{
|
||||
Index: index,
|
||||
RefCount: 0,
|
||||
RefIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// A FootnoteBacklink struct represents a link to a footnote of Markdown
|
||||
// (PHP Markdown Extra) text.
|
||||
type FootnoteBacklink struct {
|
||||
gast.BaseInline
|
||||
Index int
|
||||
RefCount int
|
||||
RefIndex int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *FootnoteBacklink) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Index"] = fmt.Sprintf("%v", n.Index)
|
||||
m["RefCount"] = fmt.Sprintf("%v", n.RefCount)
|
||||
m["RefIndex"] = fmt.Sprintf("%v", n.RefIndex)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnoteBacklink is a NodeKind of the FootnoteBacklink node.
|
||||
var KindFootnoteBacklink = gast.NewNodeKind("FootnoteBacklink")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *FootnoteBacklink) Kind() gast.NodeKind {
|
||||
return KindFootnoteBacklink
|
||||
}
|
||||
|
||||
// NewFootnoteBacklink returns a new FootnoteBacklink node.
|
||||
func NewFootnoteBacklink(index int) *FootnoteBacklink {
|
||||
return &FootnoteBacklink{
|
||||
Index: index,
|
||||
RefCount: 0,
|
||||
RefIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// A Footnote struct represents a footnote of Markdown
|
||||
// (PHP Markdown Extra) text.
|
||||
type Footnote struct {
|
||||
gast.BaseBlock
|
||||
Ref []byte
|
||||
Index int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Footnote) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Index"] = fmt.Sprintf("%v", n.Index)
|
||||
m["Ref"] = string(n.Ref)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnote is a NodeKind of the Footnote node.
|
||||
var KindFootnote = gast.NewNodeKind("Footnote")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Footnote) Kind() gast.NodeKind {
|
||||
return KindFootnote
|
||||
}
|
||||
|
||||
// NewFootnote returns a new Footnote node.
|
||||
func NewFootnote(ref []byte) *Footnote {
|
||||
return &Footnote{
|
||||
Ref: ref,
|
||||
Index: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// A FootnoteList struct represents footnotes of Markdown
|
||||
// (PHP Markdown Extra) text.
|
||||
type FootnoteList struct {
|
||||
gast.BaseBlock
|
||||
Count int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *FootnoteList) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Count"] = fmt.Sprintf("%v", n.Count)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnoteList is a NodeKind of the FootnoteList node.
|
||||
var KindFootnoteList = gast.NewNodeKind("FootnoteList")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *FootnoteList) Kind() gast.NodeKind {
|
||||
return KindFootnoteList
|
||||
}
|
||||
|
||||
// NewFootnoteList returns a new FootnoteList node.
|
||||
func NewFootnoteList() *FootnoteList {
|
||||
return &FootnoteList{
|
||||
Count: 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Package ast defines AST nodes that represents extension's elements
|
||||
package ast
|
||||
|
||||
import (
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// A Strikethrough struct represents a strikethrough of GFM text.
|
||||
type Strikethrough struct {
|
||||
gast.BaseInline
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Strikethrough) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindStrikethrough is a NodeKind of the Strikethrough node.
|
||||
var KindStrikethrough = gast.NewNodeKind("Strikethrough")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Strikethrough) Kind() gast.NodeKind {
|
||||
return KindStrikethrough
|
||||
}
|
||||
|
||||
// NewStrikethrough returns a new Strikethrough node.
|
||||
func NewStrikethrough() *Strikethrough {
|
||||
return &Strikethrough{}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// Alignment is a text alignment of table cells.
|
||||
type Alignment int
|
||||
|
||||
const (
|
||||
// AlignLeft indicates text should be left justified.
|
||||
AlignLeft Alignment = iota + 1
|
||||
|
||||
// AlignRight indicates text should be right justified.
|
||||
AlignRight
|
||||
|
||||
// AlignCenter indicates text should be centered.
|
||||
AlignCenter
|
||||
|
||||
// AlignNone indicates text should be aligned by default manner.
|
||||
AlignNone
|
||||
)
|
||||
|
||||
func (a Alignment) String() string {
|
||||
switch a {
|
||||
case AlignLeft:
|
||||
return "left"
|
||||
case AlignRight:
|
||||
return "right"
|
||||
case AlignCenter:
|
||||
return "center"
|
||||
case AlignNone:
|
||||
return "none"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A Table struct represents a table of Markdown(GFM) text.
|
||||
type Table struct {
|
||||
gast.BaseBlock
|
||||
|
||||
// Alignments returns alignments of the columns.
|
||||
Alignments []Alignment
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Table) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, func(level int) {
|
||||
indent := strings.Repeat(" ", level)
|
||||
fmt.Printf("%sAlignments {\n", indent)
|
||||
for i, alignment := range n.Alignments {
|
||||
indent2 := strings.Repeat(" ", level+1)
|
||||
fmt.Printf("%s%s", indent2, alignment.String())
|
||||
if i != len(n.Alignments)-1 {
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n%s}\n", indent)
|
||||
})
|
||||
}
|
||||
|
||||
// KindTable is a NodeKind of the Table node.
|
||||
var KindTable = gast.NewNodeKind("Table")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Table) Kind() gast.NodeKind {
|
||||
return KindTable
|
||||
}
|
||||
|
||||
// NewTable returns a new Table node.
|
||||
func NewTable() *Table {
|
||||
return &Table{
|
||||
Alignments: []Alignment{},
|
||||
}
|
||||
}
|
||||
|
||||
// A TableRow struct represents a table row of Markdown(GFM) text.
|
||||
type TableRow struct {
|
||||
gast.BaseBlock
|
||||
Alignments []Alignment
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *TableRow) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindTableRow is a NodeKind of the TableRow node.
|
||||
var KindTableRow = gast.NewNodeKind("TableRow")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *TableRow) Kind() gast.NodeKind {
|
||||
return KindTableRow
|
||||
}
|
||||
|
||||
// NewTableRow returns a new TableRow node.
|
||||
func NewTableRow(alignments []Alignment) *TableRow {
|
||||
return &TableRow{Alignments: alignments}
|
||||
}
|
||||
|
||||
// A TableHeader struct represents a table header of Markdown(GFM) text.
|
||||
type TableHeader struct {
|
||||
gast.BaseBlock
|
||||
Alignments []Alignment
|
||||
}
|
||||
|
||||
// KindTableHeader is a NodeKind of the TableHeader node.
|
||||
var KindTableHeader = gast.NewNodeKind("TableHeader")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *TableHeader) Kind() gast.NodeKind {
|
||||
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.
|
||||
func NewTableHeader(row *TableRow) *TableHeader {
|
||||
n := &TableHeader{}
|
||||
n.SetPos(row.Pos())
|
||||
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.
|
||||
type TableCell struct {
|
||||
gast.BaseBlock
|
||||
Alignment Alignment
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *TableCell) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindTableCell is a NodeKind of the TableCell node.
|
||||
var KindTableCell = gast.NewNodeKind("TableCell")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *TableCell) Kind() gast.NodeKind {
|
||||
return KindTableCell
|
||||
}
|
||||
|
||||
// NewTableCell returns a new TableCell node.
|
||||
func NewTableCell() *TableCell {
|
||||
return &TableCell{
|
||||
Alignment: AlignNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// A TaskCheckBox struct represents a checkbox of a task list.
|
||||
type TaskCheckBox struct {
|
||||
gast.BaseInline
|
||||
IsChecked bool
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *TaskCheckBox) Dump(source []byte, level int) {
|
||||
m := map[string]string{
|
||||
"Checked": fmt.Sprintf("%v", n.IsChecked),
|
||||
}
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindTaskCheckBox is a NodeKind of the TaskCheckBox node.
|
||||
var KindTaskCheckBox = gast.NewNodeKind("TaskCheckBox")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *TaskCheckBox) Kind() gast.NodeKind {
|
||||
return KindTaskCheckBox
|
||||
}
|
||||
|
||||
// NewTaskCheckBox returns a new TaskCheckBox node.
|
||||
func NewTaskCheckBox(checked bool) *TaskCheckBox {
|
||||
return &TaskCheckBox{
|
||||
IsChecked: checked,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
// A CJKOption sets options for CJK support mostly for HTML based renderers.
|
||||
type CJKOption func(*cjk)
|
||||
|
||||
// A EastAsianLineBreaks is a style of east asian line breaks.
|
||||
type EastAsianLineBreaks int
|
||||
|
||||
const (
|
||||
//EastAsianLineBreaksNone renders line breaks as it is.
|
||||
EastAsianLineBreaksNone EastAsianLineBreaks = iota
|
||||
// EastAsianLineBreaksSimple is a style where soft line breaks are ignored
|
||||
// if both sides of the break are east asian wide characters.
|
||||
EastAsianLineBreaksSimple
|
||||
// EastAsianLineBreaksCSS3Draft is a style where soft line breaks are ignored
|
||||
// even if only one side of the break is an east asian wide character.
|
||||
EastAsianLineBreaksCSS3Draft
|
||||
)
|
||||
|
||||
// WithEastAsianLineBreaks is a functional option that indicates whether softline breaks
|
||||
// between east asian wide characters should be ignored.
|
||||
// style defauts to [EastAsianLineBreaksSimple] .
|
||||
func WithEastAsianLineBreaks(style ...EastAsianLineBreaks) CJKOption {
|
||||
return func(c *cjk) {
|
||||
if len(style) == 0 {
|
||||
c.EastAsianLineBreaks = EastAsianLineBreaksSimple
|
||||
return
|
||||
}
|
||||
c.EastAsianLineBreaks = style[0]
|
||||
}
|
||||
}
|
||||
|
||||
// WithEscapedSpace is a functional option that indicates that a '\' escaped half-space(0x20) should not be rendered.
|
||||
func WithEscapedSpace() CJKOption {
|
||||
return func(c *cjk) {
|
||||
c.EscapedSpace = true
|
||||
}
|
||||
}
|
||||
|
||||
type cjk struct {
|
||||
EastAsianLineBreaks EastAsianLineBreaks
|
||||
EscapedSpace bool
|
||||
}
|
||||
|
||||
// CJK is a goldmark extension that provides functionalities for CJK languages.
|
||||
var CJK = NewCJK(WithEastAsianLineBreaks(), WithEscapedSpace())
|
||||
|
||||
// NewCJK returns a new extension with given options.
|
||||
func NewCJK(opts ...CJKOption) goldmark.Extender {
|
||||
e := &cjk{
|
||||
EastAsianLineBreaks: EastAsianLineBreaksNone,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *cjk) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(html.WithEastAsianLineBreaks(
|
||||
html.EastAsianLineBreaks(e.EastAsianLineBreaks)))
|
||||
if e.EscapedSpace {
|
||||
m.Renderer().AddOptions(html.WithWriter(html.NewWriter(html.WithEscapedSpace())))
|
||||
m.Parser().AddOptions(parser.WithEscapedSpace())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestEscapedSpace(t *testing.T) {
|
||||
markdown := goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
no := 1
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec)",
|
||||
Markdown: "太郎は**「こんにちわ」**と言った\nんです",
|
||||
Expected: "<p>太郎は**「こんにちわ」**と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 2
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "With spaces around an emphasis started with east asian punctuations, it is interpreted as an emphasis(but remains unnecessary spaces)",
|
||||
Markdown: "太郎は **「こんにちわ」** と言った\nんです",
|
||||
Expected: "<p>太郎は <strong>「こんにちわ」</strong> と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Enables EscapedSpace
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(NewCJK(WithEscapedSpace())),
|
||||
)
|
||||
|
||||
no = 3
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "With spaces around an emphasis started with east asian punctuations,it is interpreted as an emphasis",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は<strong>「こんにちわ」</strong>と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// ' ' triggers Linkify extension inline parser.
|
||||
// Escaped spaces should not trigger the inline parser.
|
||||
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewCJK(WithEscapedSpace()),
|
||||
Linkify,
|
||||
),
|
||||
)
|
||||
|
||||
no = 4
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Escaped space and linkfy extension",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は<strong>「こんにちわ」</strong>と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestEastAsianLineBreaks(t *testing.T) {
|
||||
markdown := goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
no := 1
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks are rendered as a newline, so some asian users will see it as an unnecessary space",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Enables EastAsianLineBreaks
|
||||
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(NewCJK(WithEastAsianLineBreaks())),
|
||||
)
|
||||
|
||||
no = 2
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between east asian wide characters are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 3
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between western characters are rendered as a newline",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nbんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったa\nbんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 4
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between a western character and an east asian wide character are rendered as a newline",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったa\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 5
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are rendered as a newline",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nbんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言った\nbんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// WithHardWraps take precedence over WithEastAsianLineBreaks
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(NewCJK(WithEastAsianLineBreaks())),
|
||||
)
|
||||
no = 6
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "WithHardWraps take precedence over WithEastAsianLineBreaks",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言った<br />\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Tests with EastAsianLineBreaksStyleSimple
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewCJK(WithEastAsianLineBreaks()),
|
||||
Linkify,
|
||||
),
|
||||
)
|
||||
no = 7
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "WithEastAsianLineBreaks and linkfy extension",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\r\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
no = 8
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between east asian wide characters or punctuations are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と、\r\n言った\r\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と、言ったんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
no = 9
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are ignored",
|
||||
Markdown: "私はプログラマーです。\n東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。",
|
||||
Expected: "<p>私はプログラマーです。東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Tests with EastAsianLineBreaksCSS3Draft
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewCJK(WithEastAsianLineBreaks(EastAsianLineBreaksCSS3Draft)),
|
||||
),
|
||||
)
|
||||
no = 10
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between a western character and an east asian wide character are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったaんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 11
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nbんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったbんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 12
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are ignored",
|
||||
Markdown: "私はプログラマーです。\n東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。",
|
||||
Expected: "<p>私はプログラマーです。東京の会社に勤めています。GoでWebアプリケーションを開発しています。</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type definitionListParser struct {
|
||||
}
|
||||
|
||||
var defaultDefinitionListParser = &definitionListParser{}
|
||||
|
||||
// NewDefinitionListParser return a new parser.BlockParser that
|
||||
// can parse PHP Markdown Extra Definition lists.
|
||||
func NewDefinitionListParser() parser.BlockParser {
|
||||
return defaultDefinitionListParser
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Trigger() []byte {
|
||||
return []byte{':'}
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
if _, ok := parent.(*ast.DefinitionList); ok {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
line, _ := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
indent := pc.BlockIndent()
|
||||
if pos < 0 || line[pos] != ':' || indent != 0 {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
last := parent.LastChild()
|
||||
// need 1 or more spaces after ':'
|
||||
w, _ := util.IndentWidth(line[pos+1:], pos+1)
|
||||
if w < 1 {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
if w >= 8 { // starts with indented code
|
||||
w = 5
|
||||
}
|
||||
w += pos + 1 /* 1 = ':' */
|
||||
|
||||
para, lastIsParagraph := last.(*gast.Paragraph)
|
||||
var list *ast.DefinitionList
|
||||
status := parser.HasChildren
|
||||
var ok bool
|
||||
if lastIsParagraph {
|
||||
list, ok = last.PreviousSibling().(*ast.DefinitionList)
|
||||
if ok { // is not first item
|
||||
list.Offset = w
|
||||
list.TemporaryParagraph = para
|
||||
} else { // is first item
|
||||
list = ast.NewDefinitionList(w, para)
|
||||
status |= parser.RequireParagraph
|
||||
}
|
||||
} else if list, ok = last.(*ast.DefinitionList); ok { // multiple description
|
||||
list.Offset = w
|
||||
list.TemporaryParagraph = nil
|
||||
} else {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
return list, status
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
|
||||
line, _ := reader.PeekLine()
|
||||
if util.IsBlank(line) {
|
||||
return parser.Continue | parser.HasChildren
|
||||
}
|
||||
list, _ := node.(*ast.DefinitionList)
|
||||
w, _ := util.IndentWidth(line, reader.LineOffset())
|
||||
if w < list.Offset {
|
||||
return parser.Close
|
||||
}
|
||||
pos, padding := util.IndentPosition(line, reader.LineOffset(), list.Offset)
|
||||
reader.AdvanceAndSetPadding(pos, padding)
|
||||
return parser.Continue | parser.HasChildren
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
func (b *definitionListParser) CanInterruptParagraph() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *definitionListParser) CanAcceptIndentedLine() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type definitionDescriptionParser struct {
|
||||
}
|
||||
|
||||
var defaultDefinitionDescriptionParser = &definitionDescriptionParser{}
|
||||
|
||||
// NewDefinitionDescriptionParser return a new parser.BlockParser that
|
||||
// can parse definition description starts with ':'.
|
||||
func NewDefinitionDescriptionParser() parser.BlockParser {
|
||||
return defaultDefinitionDescriptionParser
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) Trigger() []byte {
|
||||
return []byte{':'}
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) Open(
|
||||
parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
line, _ := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
indent := pc.BlockIndent()
|
||||
if pos < 0 || line[pos] != ':' || indent != 0 {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
list, _ := parent.(*ast.DefinitionList)
|
||||
if list == nil {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
para := list.TemporaryParagraph
|
||||
list.TemporaryParagraph = nil
|
||||
if para != nil {
|
||||
lines := para.Lines()
|
||||
l := lines.Len()
|
||||
for i := range l {
|
||||
term := ast.NewDefinitionTerm()
|
||||
segment := lines.At(i)
|
||||
term.Lines().Append(segment.TrimRightSpace(reader.Source()))
|
||||
list.AppendChild(list, term)
|
||||
}
|
||||
para.Parent().RemoveChild(para.Parent(), para)
|
||||
}
|
||||
cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
|
||||
reader.AdvanceAndSetPadding(cpos+1, padding)
|
||||
|
||||
return ast.NewDefinitionDescription(), parser.HasChildren
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
|
||||
// definitionListParser detects end of the description.
|
||||
// so this method will never be called.
|
||||
return parser.Continue | parser.HasChildren
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
|
||||
desc := node.(*ast.DefinitionDescription)
|
||||
desc.IsTight = !desc.HasBlankPreviousLines()
|
||||
if desc.IsTight {
|
||||
for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() {
|
||||
paragraph, ok := gc.(*gast.Paragraph)
|
||||
if ok {
|
||||
textBlock := gast.NewTextBlock()
|
||||
textBlock.SetLines(paragraph.Lines())
|
||||
desc.ReplaceChild(desc, paragraph, textBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) CanInterruptParagraph() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders DefinitionList nodes.
|
||||
type DefinitionListHTMLRenderer struct {
|
||||
html.Config
|
||||
}
|
||||
|
||||
// NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer.
|
||||
func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &DefinitionListHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindDefinitionList, r.renderDefinitionList)
|
||||
reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm)
|
||||
reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
|
||||
}
|
||||
|
||||
// DefinitionListAttributeFilter defines attribute names which dl elements can have.
|
||||
var DefinitionListAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionList(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<dl")
|
||||
html.RenderAttributes(w, n, DefinitionListAttributeFilter)
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("<dl>\n")
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("</dl>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
|
||||
var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<dt")
|
||||
html.RenderAttributes(w, n, DefinitionTermAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<dt>")
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("</dt>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
|
||||
var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.DefinitionDescription)
|
||||
_, _ = w.WriteString("<dd")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, DefinitionDescriptionAttributeFilter)
|
||||
}
|
||||
if n.IsTight {
|
||||
_, _ = w.WriteString(">")
|
||||
} else {
|
||||
_, _ = w.WriteString(">\n")
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("</dd>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
type definitionList struct {
|
||||
}
|
||||
|
||||
// DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists.
|
||||
var DefinitionList = &definitionList{}
|
||||
|
||||
func (e *definitionList) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithBlockParsers(
|
||||
util.Prioritized(NewDefinitionListParser(), 101),
|
||||
util.Prioritized(NewDefinitionDescriptionParser(), 102),
|
||||
))
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewDefinitionListHTMLRenderer(), 500),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestDefinitionList(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
DefinitionList,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/definition_list.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var footnoteListKey = parser.NewContextKey()
|
||||
var footnoteLinkListKey = parser.NewContextKey()
|
||||
|
||||
type footnoteBlockParser struct {
|
||||
}
|
||||
|
||||
var defaultFootnoteBlockParser = &footnoteBlockParser{}
|
||||
|
||||
// NewFootnoteBlockParser returns a new parser.BlockParser that can parse
|
||||
// footnotes of the Markdown(PHP Markdown Extra) text.
|
||||
func NewFootnoteBlockParser() parser.BlockParser {
|
||||
return defaultFootnoteBlockParser
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) Trigger() []byte {
|
||||
return []byte{'['}
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
line, segment := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
if pos < 0 || line[pos] != '[' {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
pos++
|
||||
if pos > len(line)-1 || line[pos] != '^' {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
open := pos + 1
|
||||
var closes int
|
||||
closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint:staticcheck
|
||||
closes = pos + 1 + closure
|
||||
next := closes + 1
|
||||
if closure > -1 {
|
||||
if next >= len(line) || line[next] != ':' {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
} else {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
padding := segment.Padding
|
||||
label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding))
|
||||
if util.IsBlank(label) {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
item := ast.NewFootnote(label)
|
||||
|
||||
pos = next + 1 - padding
|
||||
if pos >= len(line) {
|
||||
reader.Advance(pos)
|
||||
return item, parser.NoChildren
|
||||
}
|
||||
reader.AdvanceAndSetPadding(pos, padding)
|
||||
return item, parser.HasChildren
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
|
||||
line, _ := reader.PeekLine()
|
||||
if util.IsBlank(line) {
|
||||
return parser.Continue | parser.HasChildren
|
||||
}
|
||||
childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
|
||||
if childpos < 0 {
|
||||
return parser.Close
|
||||
}
|
||||
reader.AdvanceAndSetPadding(childpos, padding)
|
||||
return parser.Continue | parser.HasChildren
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
|
||||
var list *ast.FootnoteList
|
||||
if tlist := pc.Get(footnoteListKey); tlist != nil {
|
||||
list = tlist.(*ast.FootnoteList)
|
||||
} else {
|
||||
list = ast.NewFootnoteList()
|
||||
pc.Set(footnoteListKey, list)
|
||||
node.Parent().InsertBefore(node.Parent(), node, list)
|
||||
}
|
||||
node.Parent().RemoveChild(node.Parent(), node)
|
||||
list.AppendChild(list, node)
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) CanInterruptParagraph() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) CanAcceptIndentedLine() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type footnoteParser struct {
|
||||
}
|
||||
|
||||
var defaultFootnoteParser = &footnoteParser{}
|
||||
|
||||
// NewFootnoteParser returns a new parser.InlineParser that can parse
|
||||
// footnote links of the Markdown(PHP Markdown Extra) text.
|
||||
func NewFootnoteParser() parser.InlineParser {
|
||||
return defaultFootnoteParser
|
||||
}
|
||||
|
||||
func (s *footnoteParser) Trigger() []byte {
|
||||
// footnote syntax probably conflict with the image syntax.
|
||||
// So we need trigger this parser with '!'.
|
||||
return []byte{'!', '['}
|
||||
}
|
||||
|
||||
func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
line, segment := block.PeekLine()
|
||||
pos := 1
|
||||
if len(line) > 0 && line[0] == '!' {
|
||||
pos++
|
||||
}
|
||||
if pos >= len(line) || line[pos] != '^' {
|
||||
return nil
|
||||
}
|
||||
pos++
|
||||
if pos >= len(line) {
|
||||
return nil
|
||||
}
|
||||
open := pos
|
||||
closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint:staticcheck
|
||||
if closure < 0 {
|
||||
return nil
|
||||
}
|
||||
closes := pos + closure
|
||||
value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
|
||||
block.Advance(closes + 1)
|
||||
|
||||
var list *ast.FootnoteList
|
||||
if tlist := pc.Get(footnoteListKey); tlist != nil {
|
||||
list = tlist.(*ast.FootnoteList)
|
||||
}
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
index := 0
|
||||
for def := list.FirstChild(); def != nil; def = def.NextSibling() {
|
||||
d := def.(*ast.Footnote)
|
||||
if bytes.Equal(d.Ref, value) {
|
||||
if d.Index < 0 {
|
||||
list.Count++
|
||||
d.Index = list.Count
|
||||
}
|
||||
index = d.Index
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fnlink := ast.NewFootnoteLink(index)
|
||||
var fnlist []*ast.FootnoteLink
|
||||
if tmp := pc.Get(footnoteLinkListKey); tmp != nil {
|
||||
fnlist = tmp.([]*ast.FootnoteLink)
|
||||
} else {
|
||||
fnlist = []*ast.FootnoteLink{}
|
||||
pc.Set(footnoteLinkListKey, fnlist)
|
||||
}
|
||||
pc.Set(footnoteLinkListKey, append(fnlist, fnlink))
|
||||
if line[0] == '!' {
|
||||
parent.AppendChild(parent, gast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+1)))
|
||||
}
|
||||
|
||||
return fnlink
|
||||
}
|
||||
|
||||
type footnoteASTTransformer struct {
|
||||
}
|
||||
|
||||
var defaultFootnoteASTTransformer = &footnoteASTTransformer{}
|
||||
|
||||
// NewFootnoteASTTransformer returns a new parser.ASTTransformer that
|
||||
// insert a footnote list to the last of the document.
|
||||
func NewFootnoteASTTransformer() parser.ASTTransformer {
|
||||
return defaultFootnoteASTTransformer
|
||||
}
|
||||
|
||||
func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
|
||||
var list *ast.FootnoteList
|
||||
var fnlist []*ast.FootnoteLink
|
||||
if tmp := pc.Get(footnoteListKey); tmp != nil {
|
||||
list = tmp.(*ast.FootnoteList)
|
||||
}
|
||||
if tmp := pc.Get(footnoteLinkListKey); tmp != nil {
|
||||
fnlist = tmp.([]*ast.FootnoteLink)
|
||||
}
|
||||
|
||||
pc.Set(footnoteListKey, nil)
|
||||
pc.Set(footnoteLinkListKey, nil)
|
||||
|
||||
if list == nil {
|
||||
return
|
||||
}
|
||||
|
||||
counter := map[int]int{}
|
||||
if fnlist != nil {
|
||||
for _, fnlink := range fnlist {
|
||||
if fnlink.Index >= 0 {
|
||||
counter[fnlink.Index]++
|
||||
}
|
||||
}
|
||||
refCounter := map[int]int{}
|
||||
for _, fnlink := range fnlist {
|
||||
fnlink.RefCount = counter[fnlink.Index]
|
||||
if _, ok := refCounter[fnlink.Index]; !ok {
|
||||
refCounter[fnlink.Index] = 0
|
||||
}
|
||||
fnlink.RefIndex = refCounter[fnlink.Index]
|
||||
refCounter[fnlink.Index]++
|
||||
}
|
||||
}
|
||||
for footnote := list.FirstChild(); footnote != nil; {
|
||||
var container gast.Node = footnote
|
||||
next := footnote.NextSibling()
|
||||
if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) {
|
||||
container = fc
|
||||
}
|
||||
fn := footnote.(*ast.Footnote)
|
||||
index := fn.Index
|
||||
if index < 0 {
|
||||
list.RemoveChild(list, footnote)
|
||||
} else {
|
||||
refCount := counter[index]
|
||||
backLink := ast.NewFootnoteBacklink(index)
|
||||
backLink.RefCount = refCount
|
||||
backLink.RefIndex = 0
|
||||
container.AppendChild(container, backLink)
|
||||
if refCount > 1 {
|
||||
for i := 1; i < refCount; i++ {
|
||||
backLink := ast.NewFootnoteBacklink(index)
|
||||
backLink.RefCount = refCount
|
||||
backLink.RefIndex = i
|
||||
container.AppendChild(container, backLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
footnote = next
|
||||
}
|
||||
list.SortChildren(func(n1, n2 gast.Node) int {
|
||||
if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
if list.Count <= 0 {
|
||||
list.Parent().RemoveChild(list.Parent(), list)
|
||||
return
|
||||
}
|
||||
|
||||
node.AppendChild(node, list)
|
||||
}
|
||||
|
||||
// FootnoteConfig holds configuration values for the footnote extension.
|
||||
//
|
||||
// Link* and Backlink* configurations have some variables:
|
||||
// Occurrences of “^^” in the string will be replaced by the
|
||||
// corresponding footnote number in the HTML output.
|
||||
// Occurrences of “%%” will be replaced by a number for the
|
||||
// reference (footnotes can have multiple references).
|
||||
type FootnoteConfig struct {
|
||||
html.Config
|
||||
|
||||
// IDPrefix is a prefix for the id attributes generated by footnotes.
|
||||
IDPrefix []byte
|
||||
|
||||
// IDPrefix is a function that determines the id attribute for given Node.
|
||||
IDPrefixFunction func(gast.Node) []byte
|
||||
|
||||
// LinkTitle is an optional title attribute for footnote links.
|
||||
LinkTitle []byte
|
||||
|
||||
// BacklinkTitle is an optional title attribute for footnote backlinks.
|
||||
BacklinkTitle []byte
|
||||
|
||||
// LinkClass is a class for footnote links.
|
||||
LinkClass []byte
|
||||
|
||||
// BacklinkClass is a class for footnote backlinks.
|
||||
BacklinkClass []byte
|
||||
|
||||
// BacklinkHTML is an HTML content for footnote backlinks.
|
||||
BacklinkHTML []byte
|
||||
}
|
||||
|
||||
// FootnoteOption interface is a functional option interface for the extension.
|
||||
type FootnoteOption interface {
|
||||
renderer.Option
|
||||
// SetFootnoteOption sets given option to the extension.
|
||||
SetFootnoteOption(*FootnoteConfig)
|
||||
}
|
||||
|
||||
// NewFootnoteConfig returns a new Config with defaults.
|
||||
func NewFootnoteConfig() FootnoteConfig {
|
||||
return FootnoteConfig{
|
||||
Config: html.NewConfig(),
|
||||
LinkTitle: []byte(""),
|
||||
BacklinkTitle: []byte(""),
|
||||
LinkClass: []byte("footnote-ref"),
|
||||
BacklinkClass: []byte("footnote-backref"),
|
||||
BacklinkHTML: []byte("↩︎"),
|
||||
}
|
||||
}
|
||||
|
||||
// SetOption implements renderer.SetOptioner.
|
||||
func (c *FootnoteConfig) SetOption(name renderer.OptionName, value any) {
|
||||
switch name {
|
||||
case optFootnoteIDPrefixFunction:
|
||||
c.IDPrefixFunction = value.(func(gast.Node) []byte)
|
||||
case optFootnoteIDPrefix:
|
||||
c.IDPrefix = value.([]byte)
|
||||
case optFootnoteLinkTitle:
|
||||
c.LinkTitle = value.([]byte)
|
||||
case optFootnoteBacklinkTitle:
|
||||
c.BacklinkTitle = value.([]byte)
|
||||
case optFootnoteLinkClass:
|
||||
c.LinkClass = value.([]byte)
|
||||
case optFootnoteBacklinkClass:
|
||||
c.BacklinkClass = value.([]byte)
|
||||
case optFootnoteBacklinkHTML:
|
||||
c.BacklinkHTML = value.([]byte)
|
||||
default:
|
||||
c.Config.SetOption(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
type withFootnoteHTMLOptions struct {
|
||||
value []html.Option
|
||||
}
|
||||
|
||||
func (o *withFootnoteHTMLOptions) SetConfig(c *renderer.Config) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.(renderer.Option).SetConfig(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *withFootnoteHTMLOptions) SetFootnoteOption(c *FootnoteConfig) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.SetHTMLOption(&c.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFootnoteHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
|
||||
func WithFootnoteHTMLOptions(opts ...html.Option) FootnoteOption {
|
||||
return &withFootnoteHTMLOptions{opts}
|
||||
}
|
||||
|
||||
const optFootnoteIDPrefix renderer.OptionName = "FootnoteIDPrefix"
|
||||
|
||||
type withFootnoteIDPrefix struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefix) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteIDPrefix] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.IDPrefix = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes.
|
||||
func WithFootnoteIDPrefix[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteIDPrefix{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction"
|
||||
|
||||
type withFootnoteIDPrefixFunction struct {
|
||||
value func(gast.Node) []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefixFunction) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteIDPrefixFunction] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefixFunction) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.IDPrefixFunction = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteIDPrefixFunction is a functional option that is a prefix for the id attributes generated by footnotes.
|
||||
func WithFootnoteIDPrefixFunction(a func(gast.Node) []byte) FootnoteOption {
|
||||
return &withFootnoteIDPrefixFunction{a}
|
||||
}
|
||||
|
||||
const optFootnoteLinkTitle renderer.OptionName = "FootnoteLinkTitle"
|
||||
|
||||
type withFootnoteLinkTitle struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkTitle) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteLinkTitle] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.LinkTitle = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links.
|
||||
func WithFootnoteLinkTitle[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteLinkTitle{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle"
|
||||
|
||||
type withFootnoteBacklinkTitle struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkTitle) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteBacklinkTitle] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.BacklinkTitle = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks.
|
||||
func WithFootnoteBacklinkTitle[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkTitle{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass"
|
||||
|
||||
type withFootnoteLinkClass struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkClass) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteLinkClass] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.LinkClass = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteLinkClass is a functional option that is a class for footnote links.
|
||||
func WithFootnoteLinkClass[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteLinkClass{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass"
|
||||
|
||||
type withFootnoteBacklinkClass struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkClass) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteBacklinkClass] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.BacklinkClass = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks.
|
||||
func WithFootnoteBacklinkClass[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkClass{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML"
|
||||
|
||||
type withFootnoteBacklinkHTML struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkHTML) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteBacklinkHTML] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.BacklinkHTML = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks.
|
||||
func WithFootnoteBacklinkHTML[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkHTML{[]byte(a)}
|
||||
}
|
||||
|
||||
// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders FootnoteLink nodes.
|
||||
type FootnoteHTMLRenderer struct {
|
||||
FootnoteConfig
|
||||
}
|
||||
|
||||
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
|
||||
func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer {
|
||||
r := &FootnoteHTMLRenderer{
|
||||
FootnoteConfig: NewFootnoteConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetFootnoteOption(&r.FootnoteConfig)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink)
|
||||
reg.Register(ast.KindFootnoteBacklink, r.renderFootnoteBacklink)
|
||||
reg.Register(ast.KindFootnote, r.renderFootnote)
|
||||
reg.Register(ast.KindFootnoteList, r.renderFootnoteList)
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteLink(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.FootnoteLink)
|
||||
is := strconv.Itoa(n.Index)
|
||||
_, _ = w.WriteString(`<sup id="`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fnref`)
|
||||
if n.RefIndex > 0 {
|
||||
_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex))
|
||||
}
|
||||
_ = w.WriteByte(':')
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`"><a href="#`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fn:`)
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`" class="`)
|
||||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass,
|
||||
n.Index, n.RefCount))
|
||||
if len(r.FootnoteConfig.LinkTitle) > 0 {
|
||||
_, _ = w.WriteString(`" title="`)
|
||||
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, n.Index, n.RefCount)))
|
||||
}
|
||||
_, _ = w.WriteString(`" role="doc-noteref">`)
|
||||
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`</a></sup>`)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.FootnoteBacklink)
|
||||
is := strconv.Itoa(n.Index)
|
||||
_, _ = w.WriteString(` <a href="#`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fnref`)
|
||||
if n.RefIndex > 0 {
|
||||
_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex))
|
||||
}
|
||||
_ = w.WriteByte(':')
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`" class="`)
|
||||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, n.Index, n.RefCount))
|
||||
if len(r.FootnoteConfig.BacklinkTitle) > 0 {
|
||||
_, _ = w.WriteString(`" title="`)
|
||||
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, n.Index, n.RefCount)))
|
||||
}
|
||||
_, _ = w.WriteString(`" role="doc-backlink">`)
|
||||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount))
|
||||
_, _ = w.WriteString(`</a>`)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnote(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
n := node.(*ast.Footnote)
|
||||
is := strconv.Itoa(n.Index)
|
||||
if entering {
|
||||
_, _ = w.WriteString(`<li id="`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fn:`)
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`"`)
|
||||
if node.Attributes() != nil {
|
||||
html.RenderAttributes(w, node, html.ListItemAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("</li>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteList(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
_, _ = w.WriteString(`<div class="footnotes" role="doc-endnotes"`)
|
||||
if node.Attributes() != nil {
|
||||
html.RenderAttributes(w, node, html.GlobalAttributeFilter)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
if r.Config.XHTML {
|
||||
_, _ = w.WriteString("\n<hr />\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("\n<hr>\n")
|
||||
}
|
||||
_, _ = w.WriteString("<ol>\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("</ol>\n")
|
||||
_, _ = w.WriteString("</div>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) idPrefix(node gast.Node) []byte {
|
||||
if r.FootnoteConfig.IDPrefix != nil {
|
||||
return r.FootnoteConfig.IDPrefix
|
||||
}
|
||||
if r.FootnoteConfig.IDPrefixFunction != nil {
|
||||
return r.FootnoteConfig.IDPrefixFunction(node)
|
||||
}
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
func applyFootnoteTemplate(b []byte, index, refCount int) []byte {
|
||||
fast := true
|
||||
for i, c := range b {
|
||||
if i != 0 {
|
||||
if b[i-1] == '^' && c == '^' {
|
||||
fast = false
|
||||
break
|
||||
}
|
||||
if b[i-1] == '%' && c == '%' {
|
||||
fast = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if fast {
|
||||
return b
|
||||
}
|
||||
is := []byte(strconv.Itoa(index))
|
||||
rs := []byte(strconv.Itoa(refCount))
|
||||
ret := bytes.Replace(b, []byte("^^"), is, -1)
|
||||
return bytes.Replace(ret, []byte("%%"), rs, -1)
|
||||
}
|
||||
|
||||
type footnote struct {
|
||||
options []FootnoteOption
|
||||
}
|
||||
|
||||
// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes.
|
||||
var Footnote = &footnote{
|
||||
options: []FootnoteOption{},
|
||||
}
|
||||
|
||||
// NewFootnote returns a new extension with given options.
|
||||
func NewFootnote(opts ...FootnoteOption) goldmark.Extender {
|
||||
return &footnote{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *footnote) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(
|
||||
parser.WithBlockParsers(
|
||||
util.Prioritized(NewFootnoteBlockParser(), 999),
|
||||
),
|
||||
parser.WithInlineParsers(
|
||||
util.Prioritized(NewFootnoteParser(), 101),
|
||||
),
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(NewFootnoteASTTransformer(), 999),
|
||||
),
|
||||
)
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewFootnoteHTMLRenderer(e.options...), 500),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func TestFootnote(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Footnote,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/footnote.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
type footnoteID struct {
|
||||
}
|
||||
|
||||
func (a *footnoteID) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
|
||||
node.Meta()["footnote-prefix"] = "article12-"
|
||||
}
|
||||
|
||||
func TestFootnoteOptions(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefix("article12-"),
|
||||
WithFootnoteLinkClass("link-class"),
|
||||
WithFootnoteBacklinkClass("backlink-class"),
|
||||
WithFootnoteLinkTitle("link-title-%%-^^"),
|
||||
WithFootnoteBacklinkTitle("backlink-title"),
|
||||
WithFootnoteBacklinkHTML("^"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Footnote with options",
|
||||
Markdown: `That's some text with a footnote.[^1]
|
||||
|
||||
Same footnote.[^1]
|
||||
|
||||
Another one.[^2]
|
||||
|
||||
[^1]: And that's the footnote.
|
||||
[^2]: Another footnote.
|
||||
`,
|
||||
Expected: `<p>That's some text with a footnote.<sup id="article12-fnref:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Same footnote.<sup id="article12-fnref1:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Another one.<sup id="article12-fnref:2"><a href="#article12-fn:2" class="link-class" title="link-title-1-2" role="doc-noteref">2</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="article12-fn:1">
|
||||
<p>And that's the footnote. <a href="#article12-fnref:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a> <a href="#article12-fnref1:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
<li id="article12-fn:2">
|
||||
<p>Another footnote. <a href="#article12-fnref:2" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&footnoteID{}, 100),
|
||||
),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefixFunction(func(n gast.Node) []byte {
|
||||
v, ok := n.OwnerDocument().Meta()["footnote-prefix"]
|
||||
if ok {
|
||||
return util.StringToReadOnlyBytes(v.(string))
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
WithFootnoteLinkClass([]byte("link-class")),
|
||||
WithFootnoteBacklinkClass([]byte("backlink-class")),
|
||||
WithFootnoteLinkTitle([]byte("link-title-%%-^^")),
|
||||
WithFootnoteBacklinkTitle([]byte("backlink-title")),
|
||||
WithFootnoteBacklinkHTML([]byte("^")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Footnote with an id prefix function",
|
||||
Markdown: `That's some text with a footnote.[^1]
|
||||
|
||||
Same footnote.[^1]
|
||||
|
||||
Another one.[^2]
|
||||
|
||||
[^1]: And that's the footnote.
|
||||
[^2]: Another footnote.
|
||||
`,
|
||||
Expected: `<p>That's some text with a footnote.<sup id="article12-fnref:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Same footnote.<sup id="article12-fnref1:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Another one.<sup id="article12-fnref:2"><a href="#article12-fn:2" class="link-class" title="link-title-1-2" role="doc-noteref">2</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="article12-fn:1">
|
||||
<p>And that's the footnote. <a href="#article12-fnref:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a> <a href="#article12-fnref1:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
<li id="article12-fn:2">
|
||||
<p>Another footnote. <a href="#article12-fnref:2" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
|
||||
type gfm struct {
|
||||
}
|
||||
|
||||
// GFM is an extension that provides Github Flavored markdown functionalities.
|
||||
var GFM = &gfm{}
|
||||
|
||||
func (e *gfm) Extend(m goldmark.Markdown) {
|
||||
Linkify.Extend(m)
|
||||
Table.Extend(m)
|
||||
Strikethrough.Extend(m)
|
||||
TaskList.Extend(m)
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?:[/#?][-a-zA-Z0-9@:%_\+.~#!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
|
||||
|
||||
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
|
||||
|
||||
// An LinkifyConfig struct is a data structure that holds configuration of the
|
||||
// Linkify extension.
|
||||
type LinkifyConfig struct {
|
||||
AllowedProtocols [][]byte
|
||||
URLRegexp *regexp.Regexp
|
||||
WWWRegexp *regexp.Regexp
|
||||
EmailRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
const (
|
||||
optLinkifyAllowedProtocols parser.OptionName = "LinkifyAllowedProtocols"
|
||||
optLinkifyURLRegexp parser.OptionName = "LinkifyURLRegexp"
|
||||
optLinkifyWWWRegexp parser.OptionName = "LinkifyWWWRegexp"
|
||||
optLinkifyEmailRegexp parser.OptionName = "LinkifyEmailRegexp"
|
||||
)
|
||||
|
||||
// SetOption implements SetOptioner.
|
||||
func (c *LinkifyConfig) SetOption(name parser.OptionName, value any) {
|
||||
switch name {
|
||||
case optLinkifyAllowedProtocols:
|
||||
c.AllowedProtocols = value.([][]byte)
|
||||
case optLinkifyURLRegexp:
|
||||
c.URLRegexp = value.(*regexp.Regexp)
|
||||
case optLinkifyWWWRegexp:
|
||||
c.WWWRegexp = value.(*regexp.Regexp)
|
||||
case optLinkifyEmailRegexp:
|
||||
c.EmailRegexp = value.(*regexp.Regexp)
|
||||
}
|
||||
}
|
||||
|
||||
// A LinkifyOption interface sets options for the LinkifyOption.
|
||||
type LinkifyOption interface {
|
||||
parser.Option
|
||||
SetLinkifyOption(*LinkifyConfig)
|
||||
}
|
||||
|
||||
type withLinkifyAllowedProtocols struct {
|
||||
value [][]byte
|
||||
}
|
||||
|
||||
func (o *withLinkifyAllowedProtocols) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyAllowedProtocols] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.AllowedProtocols = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyAllowedProtocols is a functional option that specify allowed
|
||||
// protocols in autolinks. Each protocol must end with ':' like
|
||||
// 'http:' .
|
||||
func WithLinkifyAllowedProtocols[T []byte | string](value []T) LinkifyOption {
|
||||
opt := &withLinkifyAllowedProtocols{}
|
||||
for _, v := range value {
|
||||
opt.value = append(opt.value, []byte(v))
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
type withLinkifyURLRegexp struct {
|
||||
value *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *withLinkifyURLRegexp) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyURLRegexp] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyURLRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.URLRegexp = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyURLRegexp is a functional option that specify
|
||||
// a pattern of the URL including a protocol.
|
||||
func WithLinkifyURLRegexp(value *regexp.Regexp) LinkifyOption {
|
||||
return &withLinkifyURLRegexp{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type withLinkifyWWWRegexp struct {
|
||||
value *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *withLinkifyWWWRegexp) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyWWWRegexp] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyWWWRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.WWWRegexp = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyWWWRegexp is a functional option that specify
|
||||
// a pattern of the URL without a protocol.
|
||||
// This pattern must start with 'www.' .
|
||||
func WithLinkifyWWWRegexp(value *regexp.Regexp) LinkifyOption {
|
||||
return &withLinkifyWWWRegexp{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type withLinkifyEmailRegexp struct {
|
||||
value *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *withLinkifyEmailRegexp) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyEmailRegexp] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyEmailRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.EmailRegexp = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyEmailRegexp is a functional otpion that specify
|
||||
// a pattern of the email address.
|
||||
func WithLinkifyEmailRegexp(value *regexp.Regexp) LinkifyOption {
|
||||
return &withLinkifyEmailRegexp{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type linkifyParser struct {
|
||||
LinkifyConfig
|
||||
}
|
||||
|
||||
// NewLinkifyParser return a new InlineParser can parse
|
||||
// text that seems like a URL.
|
||||
func NewLinkifyParser(opts ...LinkifyOption) parser.InlineParser {
|
||||
p := &linkifyParser{
|
||||
LinkifyConfig: LinkifyConfig{
|
||||
AllowedProtocols: nil,
|
||||
URLRegexp: urlRegexp,
|
||||
WWWRegexp: wwwURLRegxp,
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.SetLinkifyOption(&p.LinkifyConfig)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *linkifyParser) Trigger() []byte {
|
||||
// ' ' indicates any white spaces and a line head
|
||||
return []byte{' ', '*', '_', '~', '('}
|
||||
}
|
||||
|
||||
var (
|
||||
protoHTTP = []byte("http:")
|
||||
protoHTTPS = []byte("https:")
|
||||
protoFTP = []byte("ftp:")
|
||||
domainWWW = []byte("www.")
|
||||
)
|
||||
|
||||
func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||
if pc.IsInLinkLabel() {
|
||||
return nil
|
||||
}
|
||||
line, segment := block.PeekLine()
|
||||
consumes := 0
|
||||
start := segment.Start
|
||||
c := line[0]
|
||||
// advance if current position is not a line head.
|
||||
if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' {
|
||||
consumes++
|
||||
start++
|
||||
line = line[1:]
|
||||
}
|
||||
|
||||
var m []int
|
||||
var protocol []byte
|
||||
var typ ast.AutoLinkType = ast.AutoLinkURL
|
||||
if s.LinkifyConfig.AllowedProtocols == nil {
|
||||
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
|
||||
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
|
||||
}
|
||||
} else {
|
||||
for _, prefix := range s.LinkifyConfig.AllowedProtocols {
|
||||
if bytes.HasPrefix(line, prefix) {
|
||||
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if m == nil && bytes.HasPrefix(line, domainWWW) {
|
||||
m = s.LinkifyConfig.WWWRegexp.FindSubmatchIndex(line)
|
||||
protocol = []byte("http")
|
||||
}
|
||||
if m != nil && m[0] != 0 {
|
||||
m = nil
|
||||
}
|
||||
if m != nil && m[0] == 0 {
|
||||
lastChar := line[m[1]-1]
|
||||
if lastChar == '.' {
|
||||
m[1]--
|
||||
} else if lastChar == ')' {
|
||||
closing := 0
|
||||
for i := m[1] - 1; i >= m[0]; i-- {
|
||||
switch line[i] {
|
||||
case ')':
|
||||
closing++
|
||||
case '(':
|
||||
closing--
|
||||
}
|
||||
}
|
||||
if closing > 0 {
|
||||
m[1] -= closing
|
||||
}
|
||||
} else if lastChar == ';' {
|
||||
i := m[1] - 2
|
||||
for ; i >= m[0]; i-- {
|
||||
if util.IsAlphaNumeric(line[i]) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i != m[1]-2 {
|
||||
if line[i] == '&' {
|
||||
m[1] -= m[1] - i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if m == nil {
|
||||
if len(line) > 0 && util.IsPunct(line[0]) {
|
||||
return nil
|
||||
}
|
||||
typ = ast.AutoLinkEmail
|
||||
stop := -1
|
||||
if s.LinkifyConfig.EmailRegexp == nil {
|
||||
stop = util.FindEmailIndex(line)
|
||||
} else {
|
||||
m := s.LinkifyConfig.EmailRegexp.FindSubmatchIndex(line)
|
||||
if m != nil && m[0] == 0 {
|
||||
stop = m[1]
|
||||
}
|
||||
}
|
||||
if stop < 0 {
|
||||
return nil
|
||||
}
|
||||
at := bytes.IndexByte(line, '@')
|
||||
m = []int{0, stop, at, stop - 1}
|
||||
if bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
|
||||
return nil
|
||||
}
|
||||
lastChar := line[m[1]-1]
|
||||
if lastChar == '.' {
|
||||
m[1]--
|
||||
}
|
||||
if m[1] < len(line) {
|
||||
nextChar := line[m[1]]
|
||||
if nextChar == '-' || nextChar == '_' {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if consumes != 0 {
|
||||
s := segment.WithStop(segment.Start + 1)
|
||||
ast.MergeOrAppendTextSegment(parent, s)
|
||||
}
|
||||
i := m[1] - 1
|
||||
for ; i > 0; i-- {
|
||||
c := line[i]
|
||||
switch c {
|
||||
case '?', '!', '.', ',', ':', '*', '_', '~':
|
||||
default:
|
||||
goto endfor
|
||||
}
|
||||
}
|
||||
endfor:
|
||||
i++
|
||||
consumes += i
|
||||
block.Advance(consumes)
|
||||
n := ast.NewTextSegment(text.NewSegment(start, start+i))
|
||||
link := ast.NewAutoLink(typ, n)
|
||||
link.Protocol = protocol
|
||||
return link
|
||||
}
|
||||
|
||||
func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
type linkify struct {
|
||||
options []LinkifyOption
|
||||
}
|
||||
|
||||
// Linkify is an extension that allow you to parse text that seems like a URL.
|
||||
var Linkify = &linkify{}
|
||||
|
||||
// NewLinkify creates a new [goldmark.Extender] that
|
||||
// allow you to parse text that seems like a URL.
|
||||
func NewLinkify(opts ...LinkifyOption) goldmark.Extender {
|
||||
return &linkify{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *linkify) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(
|
||||
parser.WithInlineParsers(
|
||||
util.Prioritized(NewLinkifyParser(e.options...), 999),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestLinkify(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Linkify,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/linkify.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
func TestLinkifyWithAllowedProtocols(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyAllowedProtocols([]string{
|
||||
"ssh:",
|
||||
}),
|
||||
WithLinkifyURLRegexp(
|
||||
regexp.MustCompile(`\w+://[^\s]+`),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Markdown: `hoge ssh://user@hoge.com. http://example.com/`,
|
||||
Expected: `<p>hoge <a href="ssh://user@hoge.com">ssh://user@hoge.com</a>. http://example.com/</p>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestLinkifyWithWWWRegexp(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyWWWRegexp(
|
||||
regexp.MustCompile(`www\.example\.com`),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Markdown: `www.google.com www.example.com`,
|
||||
Expected: `<p>www.google.com <a href="http://www.example.com">www.example.com</a></p>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestLinkifyWithEmailRegexp(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyEmailRegexp(
|
||||
regexp.MustCompile(`user@example\.com`),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Markdown: `hoge@example.com user@example.com`,
|
||||
Expected: `<p>hoge@example.com <a href="mailto:user@example.com">user@example.com</a></p>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package extension is a collection of builtin extensions.
|
||||
package extension
|
||||
@@ -0,0 +1,118 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type strikethroughDelimiterProcessor struct {
|
||||
}
|
||||
|
||||
func (p *strikethroughDelimiterProcessor) IsDelimiter(b byte) bool {
|
||||
return b == '~'
|
||||
}
|
||||
|
||||
func (p *strikethroughDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
|
||||
return opener.Char == closer.Char
|
||||
}
|
||||
|
||||
func (p *strikethroughDelimiterProcessor) OnMatch(consumes int) gast.Node {
|
||||
return ast.NewStrikethrough()
|
||||
}
|
||||
|
||||
var defaultStrikethroughDelimiterProcessor = &strikethroughDelimiterProcessor{}
|
||||
|
||||
type strikethroughParser struct {
|
||||
}
|
||||
|
||||
var defaultStrikethroughParser = &strikethroughParser{}
|
||||
|
||||
// NewStrikethroughParser return a new InlineParser that parses
|
||||
// strikethrough expressions.
|
||||
func NewStrikethroughParser() parser.InlineParser {
|
||||
return defaultStrikethroughParser
|
||||
}
|
||||
|
||||
func (s *strikethroughParser) Trigger() []byte {
|
||||
return []byte{'~'}
|
||||
}
|
||||
|
||||
func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
before := block.PrecendingCharacter()
|
||||
line, segment := block.PeekLine()
|
||||
node := parser.ScanDelimiter(line, before, 1, defaultStrikethroughDelimiterProcessor)
|
||||
if node == nil || node.OriginalLength > 2 || before == '~' {
|
||||
return nil
|
||||
}
|
||||
|
||||
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
|
||||
block.Advance(node.OriginalLength)
|
||||
pc.PushDelimiter(node)
|
||||
return node
|
||||
}
|
||||
|
||||
func (s *strikethroughParser) CloseBlock(parent gast.Node, pc parser.Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
// StrikethroughHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders Strikethrough nodes.
|
||||
type StrikethroughHTMLRenderer struct {
|
||||
html.Config
|
||||
}
|
||||
|
||||
// NewStrikethroughHTMLRenderer returns a new StrikethroughHTMLRenderer.
|
||||
func NewStrikethroughHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &StrikethroughHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *StrikethroughHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindStrikethrough, r.renderStrikethrough)
|
||||
}
|
||||
|
||||
// StrikethroughAttributeFilter defines attribute names which dd elements can have.
|
||||
var StrikethroughAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *StrikethroughHTMLRenderer) renderStrikethrough(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<del")
|
||||
html.RenderAttributes(w, n, StrikethroughAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<del>")
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("</del>")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
type strikethrough struct {
|
||||
}
|
||||
|
||||
// Strikethrough is an extension that allow you to use strikethrough expression like '~~text~~' .
|
||||
var Strikethrough = &strikethrough{}
|
||||
|
||||
func (e *strikethrough) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(NewStrikethroughParser(), 500),
|
||||
))
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewStrikethroughHTMLRenderer(), 500),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestStrikethrough(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Strikethrough,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/strikethrough.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var escapedPipeCellListKey = parser.NewContextKey()
|
||||
|
||||
type escapedPipeCell struct {
|
||||
Cell *ast.TableCell
|
||||
Pos []int
|
||||
Transformed bool
|
||||
}
|
||||
|
||||
// TableCellAlignMethod indicates how are table cells aligned in HTML format.
|
||||
type TableCellAlignMethod int
|
||||
|
||||
const (
|
||||
// TableCellAlignDefault renders alignments by default method.
|
||||
// With XHTML, alignments are rendered as an align attribute.
|
||||
// With HTML5, alignments are rendered as a style attribute.
|
||||
TableCellAlignDefault TableCellAlignMethod = iota
|
||||
|
||||
// TableCellAlignAttribute renders alignments as an align attribute.
|
||||
TableCellAlignAttribute
|
||||
|
||||
// TableCellAlignStyle renders alignments as a style attribute.
|
||||
TableCellAlignStyle
|
||||
|
||||
// TableCellAlignNone does not care about alignments.
|
||||
// If you using classes or other styles, you can add these attributes
|
||||
// in an ASTTransformer.
|
||||
TableCellAlignNone
|
||||
)
|
||||
|
||||
// TableConfig struct holds options for the extension.
|
||||
type TableConfig struct {
|
||||
html.Config
|
||||
|
||||
// TableCellAlignMethod indicates how are table celss aligned.
|
||||
TableCellAlignMethod TableCellAlignMethod
|
||||
}
|
||||
|
||||
// TableOption interface is a functional option interface for the extension.
|
||||
type TableOption interface {
|
||||
renderer.Option
|
||||
// SetTableOption sets given option to the extension.
|
||||
SetTableOption(*TableConfig)
|
||||
}
|
||||
|
||||
// NewTableConfig returns a new Config with defaults.
|
||||
func NewTableConfig() TableConfig {
|
||||
return TableConfig{
|
||||
Config: html.NewConfig(),
|
||||
TableCellAlignMethod: TableCellAlignDefault,
|
||||
}
|
||||
}
|
||||
|
||||
// SetOption implements renderer.SetOptioner.
|
||||
func (c *TableConfig) SetOption(name renderer.OptionName, value any) {
|
||||
switch name {
|
||||
case optTableCellAlignMethod:
|
||||
c.TableCellAlignMethod = value.(TableCellAlignMethod)
|
||||
default:
|
||||
c.Config.SetOption(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
type withTableHTMLOptions struct {
|
||||
value []html.Option
|
||||
}
|
||||
|
||||
func (o *withTableHTMLOptions) SetConfig(c *renderer.Config) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.(renderer.Option).SetConfig(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *withTableHTMLOptions) SetTableOption(c *TableConfig) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.SetHTMLOption(&c.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithTableHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
|
||||
func WithTableHTMLOptions(opts ...html.Option) TableOption {
|
||||
return &withTableHTMLOptions{opts}
|
||||
}
|
||||
|
||||
const optTableCellAlignMethod renderer.OptionName = "TableTableCellAlignMethod"
|
||||
|
||||
type withTableCellAlignMethod struct {
|
||||
value TableCellAlignMethod
|
||||
}
|
||||
|
||||
func (o *withTableCellAlignMethod) SetConfig(c *renderer.Config) {
|
||||
c.Options[optTableCellAlignMethod] = o.value
|
||||
}
|
||||
|
||||
func (o *withTableCellAlignMethod) SetTableOption(c *TableConfig) {
|
||||
c.TableCellAlignMethod = o.value
|
||||
}
|
||||
|
||||
// WithTableCellAlignMethod is a functional option that indicates how are table cells aligned in HTML format.
|
||||
func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption {
|
||||
return &withTableCellAlignMethod{a}
|
||||
}
|
||||
|
||||
func isTableDelim(bs []byte) bool {
|
||||
if w, _ := util.IndentWidth(bs, 0); w > 3 {
|
||||
return false
|
||||
}
|
||||
allSep := true
|
||||
for _, b := range bs {
|
||||
if b != '-' {
|
||||
allSep = false
|
||||
}
|
||||
if !(util.IsSpace(b) || b == '-' || b == '|' || b == ':') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return !allSep
|
||||
}
|
||||
|
||||
var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
|
||||
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
|
||||
var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
|
||||
var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`)
|
||||
|
||||
type tableParagraphTransformer struct {
|
||||
}
|
||||
|
||||
var defaultTableParagraphTransformer = &tableParagraphTransformer{}
|
||||
|
||||
// NewTableParagraphTransformer returns a new ParagraphTransformer
|
||||
// that can transform paragraphs into tables.
|
||||
func NewTableParagraphTransformer() parser.ParagraphTransformer {
|
||||
return defaultTableParagraphTransformer
|
||||
}
|
||||
|
||||
func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.Reader, pc parser.Context) {
|
||||
ppos := node.Pos()
|
||||
lines := node.Lines()
|
||||
if lines.Len() < 2 {
|
||||
return
|
||||
}
|
||||
for i := 1; i < lines.Len(); i++ {
|
||||
alignments := b.parseDelimiter(lines.At(i), reader)
|
||||
if alignments == nil {
|
||||
continue
|
||||
}
|
||||
header := b.parseRow(lines.At(i-1), alignments, true, reader, pc)
|
||||
if header == nil || len(alignments) != header.ChildCount() {
|
||||
return
|
||||
}
|
||||
table := ast.NewTable()
|
||||
table.Alignments = alignments
|
||||
table.SetPos(ppos)
|
||||
table.AppendChild(table, ast.NewTableHeader(header))
|
||||
for j := i + 1; j < lines.Len(); j++ {
|
||||
table.AppendChild(table, b.parseRow(lines.At(j), alignments, false, reader, pc))
|
||||
}
|
||||
node.Lines().SetSliced(0, i-1)
|
||||
node.Parent().InsertAfter(node.Parent(), node, table)
|
||||
if node.Lines().Len() == 0 {
|
||||
node.Parent().RemoveChild(node.Parent(), node)
|
||||
} else {
|
||||
last := node.Lines().At(i - 2)
|
||||
last.Stop = last.Stop - 1 // trim last newline(\n)
|
||||
node.Lines().Set(i-2, last)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *tableParagraphTransformer) parseRow(segment text.Segment,
|
||||
alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow {
|
||||
npos := segment
|
||||
source := reader.Source()
|
||||
segment = segment.TrimLeftSpace(source)
|
||||
segment = segment.TrimRightSpace(source)
|
||||
line := segment.Value(source)
|
||||
pos := 0
|
||||
limit := len(line)
|
||||
row := ast.NewTableRow(alignments)
|
||||
row.SetPos(npos.Start)
|
||||
if len(line) > 0 && line[pos] == '|' {
|
||||
pos++
|
||||
}
|
||||
if len(line) > 0 && line[limit-1] == '|' {
|
||||
limit--
|
||||
}
|
||||
i := 0
|
||||
for ; pos < limit; i++ {
|
||||
alignment := ast.AlignNone
|
||||
if i >= len(alignments) {
|
||||
if !isHeader {
|
||||
return row
|
||||
}
|
||||
} else {
|
||||
alignment = alignments[i]
|
||||
}
|
||||
|
||||
var escapedCell *escapedPipeCell
|
||||
node := ast.NewTableCell()
|
||||
node.SetPos(npos.Start + pos - npos.Padding)
|
||||
node.Alignment = alignment
|
||||
hasBacktick := false
|
||||
closure := pos
|
||||
for ; closure < limit; closure++ {
|
||||
if line[closure] == '`' {
|
||||
hasBacktick = true
|
||||
}
|
||||
if line[closure] == '|' {
|
||||
if closure == 0 || line[closure-1] != '\\' {
|
||||
break
|
||||
} else if hasBacktick {
|
||||
if escapedCell == nil {
|
||||
escapedCell = &escapedPipeCell{node, []int{}, false}
|
||||
escapedList := pc.ComputeIfAbsent(escapedPipeCellListKey,
|
||||
func() any {
|
||||
return []*escapedPipeCell{}
|
||||
}).([]*escapedPipeCell)
|
||||
escapedList = append(escapedList, escapedCell)
|
||||
pc.Set(escapedPipeCellListKey, escapedList)
|
||||
}
|
||||
escapedCell.Pos = append(escapedCell.Pos, segment.Start+closure-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
seg := text.NewSegment(segment.Start+pos, segment.Start+closure)
|
||||
seg = seg.TrimLeftSpace(source)
|
||||
seg = seg.TrimRightSpace(source)
|
||||
node.Lines().Append(seg)
|
||||
row.AppendChild(row, node)
|
||||
pos = closure + 1
|
||||
}
|
||||
for ; i < len(alignments); i++ {
|
||||
row.AppendChild(row, ast.NewTableCell())
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment {
|
||||
|
||||
line := segment.Value(reader.Source())
|
||||
if !isTableDelim(line) {
|
||||
return nil
|
||||
}
|
||||
cols := bytes.Split(line, []byte{'|'})
|
||||
if util.IsBlank(cols[0]) {
|
||||
cols = cols[1:]
|
||||
}
|
||||
if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) {
|
||||
cols = cols[:len(cols)-1]
|
||||
}
|
||||
|
||||
var alignments []ast.Alignment
|
||||
for _, col := range cols {
|
||||
if tableDelimLeft.Match(col) {
|
||||
alignments = append(alignments, ast.AlignLeft)
|
||||
} else if tableDelimRight.Match(col) {
|
||||
alignments = append(alignments, ast.AlignRight)
|
||||
} else if tableDelimCenter.Match(col) {
|
||||
alignments = append(alignments, ast.AlignCenter)
|
||||
} else if tableDelimNone.Match(col) {
|
||||
alignments = append(alignments, ast.AlignNone)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return alignments
|
||||
}
|
||||
|
||||
type tableASTTransformer struct {
|
||||
}
|
||||
|
||||
var defaultTableASTTransformer = &tableASTTransformer{}
|
||||
|
||||
// NewTableASTTransformer returns a parser.ASTTransformer for tables.
|
||||
func NewTableASTTransformer() parser.ASTTransformer {
|
||||
return defaultTableASTTransformer
|
||||
}
|
||||
|
||||
func (a *tableASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
|
||||
lst := pc.Get(escapedPipeCellListKey)
|
||||
if lst == nil {
|
||||
return
|
||||
}
|
||||
pc.Set(escapedPipeCellListKey, nil)
|
||||
for _, v := range lst.([]*escapedPipeCell) {
|
||||
if v.Transformed {
|
||||
continue
|
||||
}
|
||||
_ = gast.Walk(v.Cell, func(n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering || n.Kind() != gast.KindCodeSpan {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
for c := n.FirstChild(); c != nil; {
|
||||
next := c.NextSibling()
|
||||
if c.Kind() != gast.KindText {
|
||||
c = next
|
||||
continue
|
||||
}
|
||||
parent := c.Parent()
|
||||
ts := &c.(*gast.Text).Segment
|
||||
n := c
|
||||
for _, v := range lst.([]*escapedPipeCell) {
|
||||
for _, pos := range v.Pos {
|
||||
if ts.Start <= pos && pos < ts.Stop {
|
||||
segment := n.(*gast.Text).Segment
|
||||
n1 := gast.NewRawTextSegment(segment.WithStop(pos))
|
||||
n2 := gast.NewRawTextSegment(segment.WithStart(pos + 1))
|
||||
parent.InsertAfter(parent, n, n1)
|
||||
parent.InsertAfter(parent, n1, n2)
|
||||
parent.RemoveChild(parent, n)
|
||||
n = n2
|
||||
v.Transformed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
c = next
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TableHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders Table nodes.
|
||||
type TableHTMLRenderer struct {
|
||||
TableConfig
|
||||
}
|
||||
|
||||
// NewTableHTMLRenderer returns a new TableHTMLRenderer.
|
||||
func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
|
||||
r := &TableHTMLRenderer{
|
||||
TableConfig: NewTableConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetTableOption(&r.TableConfig)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindTable, r.renderTable)
|
||||
reg.Register(ast.KindTableHeader, r.renderTableHeader)
|
||||
reg.Register(ast.KindTableRow, r.renderTableRow)
|
||||
reg.Register(ast.KindTableCell, r.renderTableCell)
|
||||
}
|
||||
|
||||
// TableAttributeFilter defines attribute names which table elements can have.
|
||||
//
|
||||
// - align: Deprecated
|
||||
// - bgcolor: Deprecated
|
||||
// - border: Deprecated
|
||||
// - cellpadding: Deprecated
|
||||
// - cellspacing: Deprecated
|
||||
// - frame: Deprecated
|
||||
// - rules: Deprecated
|
||||
// - summary: Deprecated
|
||||
// - width: Deprecated.
|
||||
var TableAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,bgcolor,border,cellpadding,cellspacing,frame,rules,summary,width`) // nolint: lll
|
||||
|
||||
func (r *TableHTMLRenderer) renderTable(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
_, _ = w.WriteString("<table")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, TableAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("</table>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// TableHeaderAttributeFilter defines attribute names which <thead> elements can have.
|
||||
//
|
||||
// - align: Deprecated since HTML4, Obsolete since HTML5
|
||||
// - bgcolor: Not Standardized
|
||||
// - char: Deprecated since HTML4, Obsolete since HTML5
|
||||
// - charoff: Deprecated since HTML4, Obsolete since HTML5
|
||||
// - valign: Deprecated since HTML4, Obsolete since HTML5.
|
||||
var TableHeaderAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,bgcolor,char,charoff,valign`)
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableHeader(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
_, _ = w.WriteString("<thead")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, TableHeaderAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
_, _ = w.WriteString("<tr>\n") // Header <tr> has no separate handle
|
||||
} else {
|
||||
_, _ = w.WriteString("</tr>\n")
|
||||
_, _ = w.WriteString("</thead>\n")
|
||||
if n.NextSibling() != nil {
|
||||
_, _ = w.WriteString("<tbody>\n")
|
||||
}
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// TableRowAttributeFilter defines attribute names which <tr> elements can have.
|
||||
//
|
||||
// - align: Obsolete since HTML5
|
||||
// - bgcolor: Obsolete since HTML5
|
||||
// - char: Obsolete since HTML5
|
||||
// - charoff: Obsolete since HTML5
|
||||
// - valign: Obsolete since HTML5.
|
||||
var TableRowAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,bgcolor,char,charoff,valign`)
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableRow(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
_, _ = w.WriteString("<tr")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, TableRowAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("</tr>\n")
|
||||
if n.Parent().LastChild() == n {
|
||||
_, _ = w.WriteString("</tbody>\n")
|
||||
}
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// TableThCellAttributeFilter defines attribute names which table <th> cells can have.
|
||||
//
|
||||
// - abbr: [OK] Contains a short abbreviated description of the cell's content [NOT OK in <td>]
|
||||
// - align: Obsolete since HTML5
|
||||
// - axis: Obsolete since HTML5
|
||||
// - bgcolor: Not Standardized
|
||||
// - char: Obsolete since HTML5
|
||||
// - charoff: Obsolete since HTML5
|
||||
// - colspan: [OK] Number of columns that the cell is to span
|
||||
// - headers: [OK] This attribute contains a list of space-separated strings,
|
||||
// each corresponding to the id attribute of the <th> elements that apply to this element
|
||||
// - height: Deprecated since HTML4. Obsolete since HTML5
|
||||
// - rowspan: [OK] Number of rows that the cell is to span
|
||||
// - scope: [OK] This enumerated attribute defines the cells that the header
|
||||
// (defined in the <th>) element relates to [NOT OK in <td>]
|
||||
// - valign: Obsolete since HTML5
|
||||
// - width: Deprecated since HTML4. Obsolete since HTML5.
|
||||
var TableThCellAttributeFilter = html.GlobalAttributeFilter.ExtendString(`abbr,align,axis,bgcolor,char,charoff,colspan,headers,height,rowspan,scope,valign,width`) // nolint:lll
|
||||
|
||||
// TableTdCellAttributeFilter defines attribute names which table <td> cells can have.
|
||||
//
|
||||
// - abbr: Obsolete since HTML5. [OK in <th>]
|
||||
// - align: Obsolete since HTML5
|
||||
// - axis: Obsolete since HTML5
|
||||
// - bgcolor: Not Standardized
|
||||
// - char: Obsolete since HTML5
|
||||
// - charoff: Obsolete since HTML5
|
||||
// - colspan: [OK] Number of columns that the cell is to span
|
||||
// - headers: [OK] This attribute contains a list of space-separated strings, each corresponding
|
||||
// to the id attribute of the <th> elements that apply to this element
|
||||
// - height: Deprecated since HTML4. Obsolete since HTML5
|
||||
// - rowspan: [OK] Number of rows that the cell is to span
|
||||
// - scope: Obsolete since HTML5. [OK in <th>]
|
||||
// - valign: Obsolete since HTML5
|
||||
// - width: Deprecated since HTML4. Obsolete since HTML5.
|
||||
var TableTdCellAttributeFilter = html.GlobalAttributeFilter.ExtendString(`abbr,align,axis,bgcolor,char,charoff,colspan,headers,height,rowspan,scope,valign,width`) // nolint: lll
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableCell(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
n := node.(*ast.TableCell)
|
||||
tag := "td"
|
||||
if n.Parent().Kind() == ast.KindTableHeader {
|
||||
tag = "th"
|
||||
}
|
||||
if entering {
|
||||
_, _ = fmt.Fprintf(w, "<%s", tag)
|
||||
if n.Alignment != ast.AlignNone {
|
||||
amethod := r.TableConfig.TableCellAlignMethod
|
||||
if amethod == TableCellAlignDefault {
|
||||
if r.Config.XHTML {
|
||||
amethod = TableCellAlignAttribute
|
||||
} else {
|
||||
amethod = TableCellAlignStyle
|
||||
}
|
||||
}
|
||||
switch amethod {
|
||||
case TableCellAlignAttribute:
|
||||
if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
|
||||
_, _ = fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
|
||||
}
|
||||
case TableCellAlignStyle:
|
||||
v, ok := n.AttributeString("style")
|
||||
var cob util.CopyOnWriteBuffer
|
||||
if ok {
|
||||
switch v := v.(type) {
|
||||
case []byte:
|
||||
cob = util.NewCopyOnWriteBuffer(v)
|
||||
case string:
|
||||
cob = util.NewCopyOnWriteBuffer([]byte(v))
|
||||
}
|
||||
cob.AppendByte(';')
|
||||
}
|
||||
style := fmt.Sprintf("text-align:%s", n.Alignment.String())
|
||||
cob.AppendString(style)
|
||||
n.SetAttributeString("style", cob.Bytes())
|
||||
}
|
||||
}
|
||||
if n.Attributes() != nil {
|
||||
if tag == "td" {
|
||||
html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <td>
|
||||
} else {
|
||||
html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th>
|
||||
}
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "</%s>\n", tag)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
type table struct {
|
||||
options []TableOption
|
||||
}
|
||||
|
||||
// Table is an extension that allow you to use GFM tables .
|
||||
var Table = &table{
|
||||
options: []TableOption{},
|
||||
}
|
||||
|
||||
// NewTable returns a new extension with given options.
|
||||
func NewTable(opts ...TableOption) goldmark.Extender {
|
||||
return &table{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *table) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(
|
||||
parser.WithParagraphTransformers(
|
||||
util.Prioritized(NewTableParagraphTransformer(), 200),
|
||||
),
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(defaultTableASTTransformer, 0),
|
||||
),
|
||||
)
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func TestTable(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
html.WithXHTML(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Table,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/table.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
func TestTableWithAlignDefault(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignDefault),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignDefault and XHTML should be rendered as an align attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<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>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignDefault),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Cell with TableCellAlignDefault and HTML5 should be rendered as a style attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableWithAlignAttribute(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignAttribute),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignAttribute and XHTML should be rendered as an align attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<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>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignAttribute),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Cell with TableCellAlignAttribute and HTML5 should be rendered as an align attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<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>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
type tableStyleTransformer struct {
|
||||
}
|
||||
|
||||
func (a *tableStyleTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
||||
cell := node.FirstChild().FirstChild().FirstChild().(*east.TableCell)
|
||||
cell.SetAttributeString("style", []byte("font-size:1em"))
|
||||
}
|
||||
|
||||
func TestTableWithAlignStyle(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignStyle and XHTML should be rendered as a style attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Cell with TableCellAlignStyle and HTML5 should be rendered as a style attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&tableStyleTransformer{}, 0),
|
||||
),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 3,
|
||||
Description: "Styled cell should not be broken the style by the alignments",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="font-size:1em;text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableWithAlignNone(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignNone),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignStyle and XHTML should not be rendered",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td>baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableFuzzedPanics(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "This should not panic",
|
||||
Markdown: "* 0\n-|\n\t0",
|
||||
Expected: `<ul>
|
||||
<li>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>0</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`)
|
||||
|
||||
type taskCheckBoxParser struct {
|
||||
}
|
||||
|
||||
var defaultTaskCheckBoxParser = &taskCheckBoxParser{}
|
||||
|
||||
// NewTaskCheckBoxParser returns a new InlineParser that can parse
|
||||
// checkboxes in list items.
|
||||
// This parser must take precedence over the parser.LinkParser.
|
||||
func NewTaskCheckBoxParser() parser.InlineParser {
|
||||
return defaultTaskCheckBoxParser
|
||||
}
|
||||
|
||||
func (s *taskCheckBoxParser) Trigger() []byte {
|
||||
return []byte{'['}
|
||||
}
|
||||
|
||||
func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
// Given AST structure must be like
|
||||
// - List
|
||||
// - ListItem : parent.Parent
|
||||
// - TextBlock : parent
|
||||
// (current line)
|
||||
if parent.Parent() == nil || parent.Parent().FirstChild() != parent {
|
||||
return nil
|
||||
}
|
||||
|
||||
if parent.HasChildren() {
|
||||
return nil
|
||||
}
|
||||
if _, ok := parent.Parent().(*gast.ListItem); !ok {
|
||||
return nil
|
||||
}
|
||||
line, _ := block.PeekLine()
|
||||
m := taskListRegexp.FindSubmatchIndex(line)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
value := line[m[2]:m[3]][0]
|
||||
block.Advance(m[1])
|
||||
checked := value == 'x' || value == 'X'
|
||||
return ast.NewTaskCheckBox(checked)
|
||||
}
|
||||
|
||||
func (s *taskCheckBoxParser) CloseBlock(parent gast.Node, pc parser.Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders checkboxes in list items.
|
||||
type TaskCheckBoxHTMLRenderer struct {
|
||||
html.Config
|
||||
}
|
||||
|
||||
// NewTaskCheckBoxHTMLRenderer returns a new TaskCheckBoxHTMLRenderer.
|
||||
func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &TaskCheckBoxHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
}
|
||||
|
||||
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.TaskCheckBox)
|
||||
|
||||
if n.IsChecked {
|
||||
_, _ = w.WriteString(`<input checked="" disabled="" type="checkbox"`)
|
||||
} else {
|
||||
_, _ = w.WriteString(`<input disabled="" type="checkbox"`)
|
||||
}
|
||||
if r.XHTML {
|
||||
_, _ = w.WriteString(" /> ")
|
||||
} else {
|
||||
_, _ = w.WriteString("> ")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
type taskList struct {
|
||||
}
|
||||
|
||||
// TaskList is an extension that allow you to use GFM task lists.
|
||||
var TaskList = &taskList{}
|
||||
|
||||
func (e *taskList) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(NewTaskCheckBoxParser(), 0),
|
||||
))
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 500),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestTaskList(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
TaskList,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/tasklist.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var uncloseCounterKey = parser.NewContextKey()
|
||||
|
||||
type unclosedCounter struct {
|
||||
Single int
|
||||
Double int
|
||||
}
|
||||
|
||||
func (u *unclosedCounter) Reset() {
|
||||
u.Single = 0
|
||||
u.Double = 0
|
||||
}
|
||||
|
||||
func getUnclosedCounter(pc parser.Context) *unclosedCounter {
|
||||
v := pc.Get(uncloseCounterKey)
|
||||
if v == nil {
|
||||
v = &unclosedCounter{}
|
||||
pc.Set(uncloseCounterKey, v)
|
||||
}
|
||||
return v.(*unclosedCounter)
|
||||
}
|
||||
|
||||
// TypographicPunctuation is a key of the punctuations that can be replaced with
|
||||
// typographic entities.
|
||||
type TypographicPunctuation int
|
||||
|
||||
const (
|
||||
// LeftSingleQuote is ' .
|
||||
LeftSingleQuote TypographicPunctuation = iota + 1
|
||||
// RightSingleQuote is ' .
|
||||
RightSingleQuote
|
||||
// LeftDoubleQuote is " .
|
||||
LeftDoubleQuote
|
||||
// RightDoubleQuote is " .
|
||||
RightDoubleQuote
|
||||
// EnDash is -- .
|
||||
EnDash
|
||||
// EmDash is --- .
|
||||
EmDash
|
||||
// Ellipsis is ... .
|
||||
Ellipsis
|
||||
// LeftAngleQuote is << .
|
||||
LeftAngleQuote
|
||||
// RightAngleQuote is >> .
|
||||
RightAngleQuote
|
||||
// Apostrophe is ' .
|
||||
Apostrophe
|
||||
|
||||
typographicPunctuationMax
|
||||
)
|
||||
|
||||
// An TypographerConfig struct is a data structure that holds configuration of the
|
||||
// Typographer extension.
|
||||
type TypographerConfig struct {
|
||||
Substitutions [][]byte
|
||||
}
|
||||
|
||||
func newDefaultSubstitutions() [][]byte {
|
||||
replacements := make([][]byte, typographicPunctuationMax)
|
||||
replacements[LeftSingleQuote] = []byte("‘")
|
||||
replacements[RightSingleQuote] = []byte("’")
|
||||
replacements[LeftDoubleQuote] = []byte("“")
|
||||
replacements[RightDoubleQuote] = []byte("”")
|
||||
replacements[EnDash] = []byte("–")
|
||||
replacements[EmDash] = []byte("—")
|
||||
replacements[Ellipsis] = []byte("…")
|
||||
replacements[LeftAngleQuote] = []byte("«")
|
||||
replacements[RightAngleQuote] = []byte("»")
|
||||
replacements[Apostrophe] = []byte("’")
|
||||
|
||||
return replacements
|
||||
}
|
||||
|
||||
// SetOption implements SetOptioner.
|
||||
func (b *TypographerConfig) SetOption(name parser.OptionName, value any) {
|
||||
switch name {
|
||||
case optTypographicSubstitutions:
|
||||
b.Substitutions = value.([][]byte)
|
||||
}
|
||||
}
|
||||
|
||||
// A TypographerOption interface sets options for the TypographerParser.
|
||||
type TypographerOption interface {
|
||||
parser.Option
|
||||
SetTypographerOption(*TypographerConfig)
|
||||
}
|
||||
|
||||
const optTypographicSubstitutions parser.OptionName = "TypographicSubstitutions"
|
||||
|
||||
// TypographicSubstitutions is a list of the substitutions for the Typographer extension.
|
||||
type TypographicSubstitutions map[TypographicPunctuation][]byte
|
||||
|
||||
type withTypographicSubstitutions struct {
|
||||
value [][]byte
|
||||
}
|
||||
|
||||
func (o *withTypographicSubstitutions) SetParserOption(c *parser.Config) {
|
||||
c.Options[optTypographicSubstitutions] = o.value
|
||||
}
|
||||
|
||||
func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig) {
|
||||
p.Substitutions = o.value
|
||||
}
|
||||
|
||||
// WithTypographicSubstitutions is a functional otpion that specify replacement text
|
||||
// for punctuations.
|
||||
func WithTypographicSubstitutions[T []byte | string](values map[TypographicPunctuation]T) TypographerOption {
|
||||
replacements := newDefaultSubstitutions()
|
||||
for k, v := range values {
|
||||
replacements[k] = []byte(v)
|
||||
}
|
||||
|
||||
return &withTypographicSubstitutions{replacements}
|
||||
}
|
||||
|
||||
type typographerDelimiterProcessor struct {
|
||||
}
|
||||
|
||||
func (p *typographerDelimiterProcessor) IsDelimiter(b byte) bool {
|
||||
return b == '\'' || b == '"'
|
||||
}
|
||||
|
||||
func (p *typographerDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
|
||||
return opener.Char == closer.Char
|
||||
}
|
||||
|
||||
func (p *typographerDelimiterProcessor) OnMatch(consumes int) gast.Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultTypographerDelimiterProcessor = &typographerDelimiterProcessor{}
|
||||
|
||||
type typographerParser struct {
|
||||
TypographerConfig
|
||||
}
|
||||
|
||||
// NewTypographerParser return a new InlineParser that parses
|
||||
// typographer expressions.
|
||||
func NewTypographerParser(opts ...TypographerOption) parser.InlineParser {
|
||||
p := &typographerParser{
|
||||
TypographerConfig: TypographerConfig{
|
||||
Substitutions: newDefaultSubstitutions(),
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.SetTypographerOption(&p.TypographerConfig)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *typographerParser) Trigger() []byte {
|
||||
return []byte{'\'', '"', '-', '.', ',', '<', '>', '*', '['}
|
||||
}
|
||||
|
||||
func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
line, _ := block.PeekLine()
|
||||
c := line[0]
|
||||
if len(line) > 2 {
|
||||
if c == '-' {
|
||||
if s.Substitutions[EmDash] != nil && line[1] == '-' && line[2] == '-' { // ---
|
||||
node := gast.NewString(s.Substitutions[EmDash])
|
||||
node.SetCode(true)
|
||||
block.Advance(3)
|
||||
return node
|
||||
}
|
||||
} else if c == '.' {
|
||||
if s.Substitutions[Ellipsis] != nil && line[1] == '.' && line[2] == '.' { // ...
|
||||
node := gast.NewString(s.Substitutions[Ellipsis])
|
||||
node.SetCode(true)
|
||||
block.Advance(3)
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(line) > 1 {
|
||||
if c == '<' {
|
||||
if s.Substitutions[LeftAngleQuote] != nil && line[1] == '<' { // <<
|
||||
node := gast.NewString(s.Substitutions[LeftAngleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(2)
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
} else if c == '>' {
|
||||
if s.Substitutions[RightAngleQuote] != nil && line[1] == '>' { // >>
|
||||
node := gast.NewString(s.Substitutions[RightAngleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(2)
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
} else if s.Substitutions[EnDash] != nil && c == '-' && line[1] == '-' { // --
|
||||
node := gast.NewString(s.Substitutions[EnDash])
|
||||
node.SetCode(true)
|
||||
block.Advance(2)
|
||||
return node
|
||||
}
|
||||
}
|
||||
if c == '\'' || c == '"' {
|
||||
before := block.PrecendingCharacter()
|
||||
d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor)
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
counter := getUnclosedCounter(pc)
|
||||
if c == '\'' {
|
||||
if s.Substitutions[Apostrophe] != nil {
|
||||
// Handle decade abbrevations such as '90s
|
||||
if d.CanOpen && !d.CanClose && len(line) > 3 &&
|
||||
util.IsNumeric(line[1]) && util.IsNumeric(line[2]) && line[3] == 's' {
|
||||
after := rune(' ')
|
||||
if len(line) > 4 {
|
||||
after = util.ToRune(line, 4)
|
||||
}
|
||||
if len(line) == 3 || util.IsSpaceRune(after) || util.IsPunctRune(after) {
|
||||
node := gast.NewString(s.Substitutions[Apostrophe])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
}
|
||||
// special cases: 'twas, 'em, 'net
|
||||
if len(line) > 1 && (unicode.IsPunct(before) || unicode.IsSpace(before)) &&
|
||||
(line[1] == 't' || line[1] == 'e' || line[1] == 'n' || line[1] == 'l') {
|
||||
node := gast.NewString(s.Substitutions[Apostrophe])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
// Convert normal apostrophes. This is probably more flexible than necessary but
|
||||
// converts any apostrophe in between two alphanumerics.
|
||||
if len(line) > 1 && (unicode.IsDigit(before) || unicode.IsLetter(before)) &&
|
||||
(unicode.IsLetter(util.ToRune(line, 1))) {
|
||||
node := gast.NewString(s.Substitutions[Apostrophe])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
}
|
||||
if s.Substitutions[LeftSingleQuote] != nil && d.CanOpen && !d.CanClose {
|
||||
nt := LeftSingleQuote
|
||||
// special cases: Alice's, I'm, Don't, You'd
|
||||
if len(line) > 1 && (line[1] == 's' || line[1] == 'm' || line[1] == 't' || line[1] == 'd') &&
|
||||
(len(line) < 3 || util.IsPunct(line[2]) || util.IsSpace(line[2])) {
|
||||
nt = RightSingleQuote
|
||||
}
|
||||
// special cases: I've, I'll, You're
|
||||
if len(line) > 2 && ((line[1] == 'v' && line[2] == 'e') ||
|
||||
(line[1] == 'l' && line[2] == 'l') || (line[1] == 'r' && line[2] == 'e')) &&
|
||||
(len(line) < 4 || util.IsPunct(line[3]) || util.IsSpace(line[3])) {
|
||||
nt = RightSingleQuote
|
||||
}
|
||||
if nt == LeftSingleQuote {
|
||||
counter.Single++
|
||||
}
|
||||
|
||||
node := gast.NewString(s.Substitutions[nt])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
if s.Substitutions[RightSingleQuote] != nil {
|
||||
// plural possesive and abbreviations: Smiths', doin'
|
||||
if len(line) > 1 && unicode.IsSpace(util.ToRune(line, 0)) || unicode.IsPunct(util.ToRune(line, 0)) &&
|
||||
(len(line) > 2 && !unicode.IsDigit(util.ToRune(line, 1))) {
|
||||
node := gast.NewString(s.Substitutions[RightSingleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
}
|
||||
if s.Substitutions[RightSingleQuote] != nil && counter.Single > 0 {
|
||||
isClose := d.CanClose && !d.CanOpen
|
||||
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && unicode.IsPunct(util.ToRune(line, 1)) &&
|
||||
(len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
|
||||
if isClose || maybeClose {
|
||||
node := gast.NewString(s.Substitutions[RightSingleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
counter.Single--
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
if c == '"' {
|
||||
if s.Substitutions[LeftDoubleQuote] != nil && d.CanOpen && !d.CanClose {
|
||||
node := gast.NewString(s.Substitutions[LeftDoubleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
counter.Double++
|
||||
return node
|
||||
}
|
||||
if s.Substitutions[RightDoubleQuote] != nil && counter.Double > 0 {
|
||||
isClose := d.CanClose && !d.CanOpen
|
||||
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && (unicode.IsPunct(util.ToRune(line, 1))) &&
|
||||
(len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
|
||||
if isClose || maybeClose {
|
||||
// special case: "Monitor 21""
|
||||
if len(line) > 1 && line[1] == '"' && unicode.IsDigit(before) {
|
||||
return nil
|
||||
}
|
||||
node := gast.NewString(s.Substitutions[RightDoubleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
counter.Double--
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) {
|
||||
getUnclosedCounter(pc).Reset()
|
||||
}
|
||||
|
||||
type typographer struct {
|
||||
options []TypographerOption
|
||||
}
|
||||
|
||||
// Typographer is an extension that replaces punctuations with typographic entities.
|
||||
var Typographer = &typographer{}
|
||||
|
||||
// NewTypographer returns a new Extender that replaces punctuations with typographic entities.
|
||||
func NewTypographer(opts ...TypographerOption) goldmark.Extender {
|
||||
return &typographer{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *typographer) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(NewTypographerParser(e.options...), 9999),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestTypographer(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Typographer,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/typographer.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
Reference in New Issue
Block a user